1 # -*- coding: utf-8 -*-
2 # Copyright (C) 1998-2021 by the Free Software Foundation, Inc.
4 # This file is part of Postorius.
6 # Postorius is free software: you can redistribute it and/or modify it under
7 # the terms of the GNU General Public License as published by the Free
8 # Software Foundation, either version 3 of the License, or (at your option)
11 # Postorius is distributed in the hope that it will be useful, but WITHOUT
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
16 # You should have received a copy of the GNU General Public License along with
17 # Postorius. If not, see <http://www.gnu.org/licenses/>.
21 from urllib
.error
import HTTPError
22 from urllib
.parse
import urljoin
24 from django
.conf
import settings
25 from django
.core
.exceptions
import ImproperlyConfigured
26 from django
.db
import models
27 from django
.db
.models
.signals
import post_delete
, post_save
28 from django
.dispatch
import receiver
29 from django
.http
import Http404
30 from django
.urls
import reverse
31 from django
.utils
.translation
import gettext_lazy
as _
33 from mailmanclient
import MailmanConnectionError
35 from postorius
.template_list
import TEMPLATES_LIST
36 from postorius
.utils
import LANGUAGES
, get_mailman_client
39 logger
= logging
.getLogger(__name__
)
41 _email_template_help_text
= _(
42 'Note: Do not add any secret content in templates as they are '
43 'publicly accessible.\n'
44 'You can use these variables in the templates. \n'
45 '$hyperkitty_url: Permalink to archived message in Hyperkitty\n'
46 '$listname: Name of the Mailing List e.g. ant@example.com \n'
47 '$list_id: The List-ID header e.g. ant.example.com \n'
48 '$display_name: Display name of the mailing list e.g. Ant \n'
49 '$short_listname: Local part of the listname e.g. ant \n'
50 '$domain: The domain part of the listname e.g. example.com \n'
51 '$info: The mailing list\'s longer descriptive text \n'
52 '$request_email: The email address for -request address \n'
53 '$owner_email: The email address for -owner address \n'
54 '$site_email: The email address to reach the owners of the site \n'
55 '$language: The two letter language code for list\'s preferred language e.g. fr, en, de \n' # noqa: E501
59 class SubscriptionMode(Enum
):
60 """Valid values for Member.subscription_mode"""
62 as_address
= 'as_address'
66 class MailmanApiError(Exception):
67 """Raised if the API is not available.
72 class Mailman404Error(Exception):
73 """Proxy exception. Raised if the API returns 404."""
77 class MailmanRestManager(object):
78 """Manager class to give a model class CRUD access to the API.
79 Returns objects (or lists of objects) retrieved from the API.
82 def __init__(self
, resource_name
, resource_name_plural
, cls_name
=None):
83 self
.resource_name
= resource_name
84 self
.resource_name_plural
= resource_name_plural
88 return getattr(get_mailman_client(), self
.resource_name_plural
)
89 except AttributeError:
91 except MailmanConnectionError
as e
:
92 raise MailmanApiError(e
)
94 def get(self
, *args
, **kwargs
):
96 method
= getattr(get_mailman_client(), 'get_' + self
.resource_name
)
97 return method(*args
, **kwargs
)
98 except AttributeError as e
:
99 raise MailmanApiError(e
)
100 except HTTPError
as e
:
102 raise Mailman404Error('Mailman resource could not be found.')
105 except MailmanConnectionError
as e
:
106 raise MailmanApiError(e
)
108 def get_or_404(self
, *args
, **kwargs
):
109 """Similar to `self.get` but raises standard Django 404 error.
112 return self
.get(*args
, **kwargs
)
113 except Mailman404Error
:
115 except MailmanConnectionError
as e
:
116 raise MailmanApiError(e
)
118 def create(self
, *args
, **kwargs
):
121 get_mailman_client(), 'create_' + self
.resource_name
)
122 return method(*args
, **kwargs
)
123 except AttributeError as e
:
124 raise MailmanApiError(e
)
125 except HTTPError
as e
:
127 raise MailmanApiError
130 except MailmanConnectionError
:
131 raise MailmanApiError
134 """Not implemented since the objects returned from the API
135 have a `delete` method of their own.
140 class MailmanListManager(MailmanRestManager
):
143 super(MailmanListManager
, self
).__init
__('list', 'lists')
145 def all(self
, advertised
=False):
148 get_mailman_client(), 'get_' + self
.resource_name_plural
)
149 return method(advertised
=advertised
)
150 except AttributeError:
151 raise MailmanApiError
152 except MailmanConnectionError
as e
:
153 raise MailmanApiError(e
)
155 def by_mail_host(self
, mail_host
, advertised
=False):
156 objects
= self
.all(advertised
)
159 if obj
.mail_host
== mail_host
:
160 host_objects
.append(obj
)
164 class MailmanUserManager(MailmanRestManager
):
167 super(MailmanUserManager
, self
).__init
__('user', 'users')
170 class MailmanRestModel(object):
171 """Simple REST Model class to make REST API calls Django style.
173 MailmanApiError
= MailmanApiError
174 DoesNotExist
= Mailman404Error
176 def __init__(self
, *args
, **kwargs
):
181 """Proxy function for `objects.create`.
182 (REST API uses `create`, while Django uses `save`.)
184 self
.objects
.create(*self
.args
, **self
.kwargs
)
187 class Domain(MailmanRestModel
):
188 """Domain model class.
190 objects
= MailmanRestManager('domain', 'domains')
193 class List(MailmanRestModel
):
196 objects
= MailmanListManager()
199 class MailmanUser(MailmanRestModel
):
200 """MailmanUser model class.
202 objects
= MailmanUserManager()
205 class Member(MailmanRestModel
):
206 """Member model class.
208 objects
= MailmanRestManager('member', 'members')
211 class Style(MailmanRestModel
):
214 objects
= MailmanRestManager(None, 'styles')
217 TEMPLATE_CONTEXT_CHOICES
= (
218 ('site', 'Site Wide'),
219 ('domain', 'Domain Wide'),
220 ('list', 'MailingList Wide')
224 class EmailTemplate(models
.Model
):
225 """A Template represents contents of partial or complete emails sent out by
226 Mailman Core on various events or when an action is required. Headers and
227 Footers on emails for decorations are also repsented as templates.
230 # Ease differentiating the various Mailman templates by providing the
231 # template file's name (key) prepended in square brackets to the
232 # template's purpose (value).
233 _templates_list_choices
= [
234 (t
[0], "[{key}] - {value}".format(key
=t
[0], value
=t
[1]))
235 for t
in TEMPLATES_LIST
238 name
= models
.CharField(
239 max_length
=100, choices
=_templates_list_choices
,
240 help_text
=_('Choose the template you want to customize.'))
241 data
= models
.TextField(
242 help_text
=_email_template_help_text
,
245 language
= models
.CharField(
246 max_length
=5, choices
=LANGUAGES
,
247 help_text
=_('Language for the template, this should be the list\'s preferred language.'), # noqa: E501
249 created_at
= models
.DateTimeField(auto_now_add
=True)
250 modified_at
= models
.DateTimeField(auto_now
=True)
251 context
= models
.CharField(max_length
=50, choices
=TEMPLATE_CONTEXT_CHOICES
)
252 identifier
= models
.CharField(blank
=True, max_length
=100)
255 unique_together
= ('name', 'identifier', 'language')
258 return '<EmailTemplate {0} for {1}>'.format(self
.name
, self
.context
)
261 def description(self
):
262 """Return the long description of template that is human readable."""
263 return dict(TEMPLATES_LIST
)[self
.name
]
267 """API url is the remote url that Core can use to fetch templates"""
268 base_url
= getattr(settings
, 'POSTORIUS_TEMPLATE_BASE_URL', None)
270 raise ImproperlyConfigured(
271 'Setting "POSTORIUS_TEMPLATE_BASE_URL" is not configured.')
272 resource_url
= reverse(
274 kwargs
=dict(context
=self
.context
,
275 identifier
=self
.identifier
,
278 return urljoin(base_url
, resource_url
)
280 def _get_context_obj(self
):
281 if self
.context
== 'list':
282 obj
= List
.objects
.get_or_404(fqdn_listname
=self
.identifier
)
283 elif self
.context
== 'domain':
284 obj
= Domain
.objects
.get_or_404(mail_host
=self
.identifier
)
285 elif self
.context
== 'site':
286 obj
= get_mailman_client()
291 def _update_core(self
, deleted
=False):
292 obj
= self
._get
_context
_obj
()
297 # POST'ing an empty string will delete this record in Core.
300 # Use the API endpoint of self that Core can use to fetch this.
301 api_url
= self
.api_url
302 obj
.set_template(self
.name
, api_url
)
305 @receiver(post_save
, sender
=EmailTemplate
)
306 def update_core_post_update(sender
, **kwargs
):
307 kwargs
['instance']._update
_core
()
310 @receiver(post_delete
, sender
=EmailTemplate
)
311 def update_core_post_delete(sender
, **kwargs
):
312 kwargs
['instance']._update
_core
(deleted
=True)