1 # -*- coding: utf-8 -*-
2 # Copyright (C) 1998-2022 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 MemberRole(Enum
):
67 """List of roles a Member resource can have.
69 The primary purpose of this enum right now is only to add the
70 role values to translations. At some point, we want to use MemberRole.owner
71 in all the places we are using the literal role as string.
75 nonmember
= _('nonmember')
76 moderator
= _('moderator')
79 class MailmanApiError(Exception):
80 """Raised if the API is not available.
85 class Mailman404Error(Exception):
86 """Proxy exception. Raised if the API returns 404."""
90 class MailmanRestManager(object):
91 """Manager class to give a model class CRUD access to the API.
92 Returns objects (or lists of objects) retrieved from the API.
95 def __init__(self
, resource_name
, resource_name_plural
, cls_name
=None):
96 self
.resource_name
= resource_name
97 self
.resource_name_plural
= resource_name_plural
101 return getattr(get_mailman_client(), self
.resource_name_plural
)
102 except AttributeError:
103 raise MailmanApiError
104 except MailmanConnectionError
as e
:
105 raise MailmanApiError(e
)
107 def get(self
, *args
, **kwargs
):
109 method
= getattr(get_mailman_client(), 'get_' + self
.resource_name
)
110 return method(*args
, **kwargs
)
111 except AttributeError as e
:
112 raise MailmanApiError(e
)
113 except HTTPError
as e
:
115 raise Mailman404Error('Mailman resource could not be found.')
118 except MailmanConnectionError
as e
:
119 raise MailmanApiError(e
)
121 def get_or_404(self
, *args
, **kwargs
):
122 """Similar to `self.get` but raises standard Django 404 error.
125 return self
.get(*args
, **kwargs
)
126 except Mailman404Error
:
128 except MailmanConnectionError
as e
:
129 raise MailmanApiError(e
)
131 def create(self
, *args
, **kwargs
):
134 get_mailman_client(), 'create_' + self
.resource_name
)
135 return method(*args
, **kwargs
)
136 except AttributeError as e
:
137 raise MailmanApiError(e
)
138 except HTTPError
as e
:
140 raise MailmanApiError
143 except MailmanConnectionError
:
144 raise MailmanApiError
147 """Not implemented since the objects returned from the API
148 have a `delete` method of their own.
153 class MailmanListManager(MailmanRestManager
):
156 super(MailmanListManager
, self
).__init
__('list', 'lists')
158 def all(self
, advertised
=False):
161 get_mailman_client(), 'get_' + self
.resource_name_plural
)
162 return method(advertised
=advertised
)
163 except AttributeError:
164 raise MailmanApiError
165 except MailmanConnectionError
as e
:
166 raise MailmanApiError(e
)
168 def by_mail_host(self
, mail_host
, advertised
=False):
169 objects
= self
.all(advertised
)
172 if obj
.mail_host
== mail_host
:
173 host_objects
.append(obj
)
177 class MailmanUserManager(MailmanRestManager
):
180 super(MailmanUserManager
, self
).__init
__('user', 'users')
183 class MailmanRestModel(object):
184 """Simple REST Model class to make REST API calls Django style.
186 MailmanApiError
= MailmanApiError
187 DoesNotExist
= Mailman404Error
189 def __init__(self
, *args
, **kwargs
):
194 """Proxy function for `objects.create`.
195 (REST API uses `create`, while Django uses `save`.)
197 self
.objects
.create(*self
.args
, **self
.kwargs
)
200 class Domain(MailmanRestModel
):
201 """Domain model class.
203 objects
= MailmanRestManager('domain', 'domains')
206 class List(MailmanRestModel
):
209 objects
= MailmanListManager()
212 class MailmanUser(MailmanRestModel
):
213 """MailmanUser model class.
215 objects
= MailmanUserManager()
218 class Member(MailmanRestModel
):
219 """Member model class.
221 objects
= MailmanRestManager('member', 'members')
224 class Style(MailmanRestModel
):
227 objects
= MailmanRestManager(None, 'styles')
230 TEMPLATE_CONTEXT_CHOICES
= (
231 ('site', 'Site Wide'),
232 ('domain', 'Domain Wide'),
233 ('list', 'MailingList Wide')
237 class EmailTemplate(models
.Model
):
238 """A Template represents contents of partial or complete emails sent out by
239 Mailman Core on various events or when an action is required. Headers and
240 Footers on emails for decorations are also repsented as templates.
243 # Ease differentiating the various Mailman templates by providing the
244 # template file's name (key) prepended in square brackets to the
245 # template's purpose (value).
246 _templates_list_choices
= [
247 (t
[0], "[{key}] - {value}".format(key
=t
[0], value
=t
[1]))
248 for t
in TEMPLATES_LIST
251 name
= models
.CharField(
252 max_length
=100, choices
=_templates_list_choices
,
253 help_text
=_('Choose the template you want to customize.'))
254 data
= models
.TextField(
255 help_text
=_email_template_help_text
,
258 language
= models
.CharField(
259 max_length
=5, choices
=LANGUAGES
,
260 help_text
=_('Language for the template, this should be the list\'s preferred language.'), # noqa: E501
262 created_at
= models
.DateTimeField(auto_now_add
=True)
263 modified_at
= models
.DateTimeField(auto_now
=True)
264 context
= models
.CharField(max_length
=50, choices
=TEMPLATE_CONTEXT_CHOICES
)
265 identifier
= models
.CharField(blank
=True, max_length
=100)
268 unique_together
= ('name', 'identifier', 'language')
271 return '<EmailTemplate {0} for {1}>'.format(self
.name
, self
.context
)
274 def description(self
):
275 """Return the long description of template that is human readable."""
276 return dict(TEMPLATES_LIST
)[self
.name
]
280 """API url is the remote url that Core can use to fetch templates"""
281 base_url
= getattr(settings
, 'POSTORIUS_TEMPLATE_BASE_URL', None)
283 raise ImproperlyConfigured(
284 'Setting "POSTORIUS_TEMPLATE_BASE_URL" is not configured.')
285 resource_url
= reverse(
287 kwargs
=dict(context
=self
.context
,
288 identifier
=self
.identifier
,
291 return urljoin(base_url
, resource_url
)
293 def _get_context_obj(self
):
294 if self
.context
== 'list':
295 obj
= List
.objects
.get_or_404(fqdn_listname
=self
.identifier
)
296 elif self
.context
== 'domain':
297 obj
= Domain
.objects
.get_or_404(mail_host
=self
.identifier
)
298 elif self
.context
== 'site':
299 obj
= get_mailman_client()
304 def _update_core(self
, deleted
=False):
305 obj
= self
._get
_context
_obj
()
310 # POST'ing an empty string will delete this record in Core.
313 # Use the API endpoint of self that Core can use to fetch this.
314 api_url
= self
.api_url
315 obj
.set_template(self
.name
, api_url
)
318 @receiver(post_save
, sender
=EmailTemplate
)
319 def update_core_post_update(sender
, **kwargs
):
320 kwargs
['instance']._update
_core
()
323 @receiver(post_delete
, sender
=EmailTemplate
)
324 def update_core_post_delete(sender
, **kwargs
):
325 kwargs
['instance']._update
_core
(deleted
=True)