1 # -*- coding: utf-8 -*-
2 # Copyright (C) 1998-2019 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
.contrib
.auth
.models
import User
26 from django
.core
.exceptions
import ImproperlyConfigured
27 from django
.db
import models
28 from django
.db
.models
.signals
import post_delete
, post_save
29 from django
.dispatch
import receiver
30 from django
.http
import Http404
31 from django
.urls
import reverse
32 from django
.utils
.translation
import gettext_lazy
as _
34 from mailmanclient
import MailmanConnectionError
36 from postorius
.template_list
import TEMPLATES_LIST
37 from postorius
.utils
import LANGUAGES
, get_mailman_client
40 logger
= logging
.getLogger(__name__
)
42 _email_template_help_text
= _(
43 'Note: Do not add any secret content in templates as they are '
44 'publicly accessible.\n'
45 'You can use these variables in the templates. \n'
46 '$hyperkitty_url: Permalink to archived message in Hyperkitty\n'
47 '$listname: Name of the Mailing List e.g. ant@example.com \n'
48 '$list_id: The List-ID header e.g. ant.example.com \n'
49 '$display_name: Display name of the mailing list e.g. Ant \n'
50 '$short_listname: Local part of the listname e.g. ant \n'
51 '$domain: The domain part of the listname e.g. example.com \n'
52 '$info: The mailing list\'s longer descriptive text \n'
53 '$request_email: The email address for -request address \n'
54 '$owner_email: The email address for -owner address \n'
55 '$site_email: The email address to reach the owners of the site \n'
56 '$language: The two letter language code for list\'s preferred language e.g. fr, en, de \n' # noqa: E501
60 class SubscriptionMode(Enum
):
61 """Valid values for Member.subscription_mode"""
67 @receiver(post_save
, sender
=User
)
68 def create_mailman_user(sender
, **kwargs
):
69 if kwargs
.get('created'):
70 if getattr(settings
, 'AUTOCREATE_MAILMAN_USER', False):
71 user
= kwargs
.get('instance')
73 MailmanUser
.objects
.create_from_django(user
)
74 except (MailmanApiError
, HTTPError
):
75 logger
.error('Mailman user not created for {}'.format(user
))
76 logger
.error('Mailman Core API is not reachable.')
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')
182 def create_from_django(self
, user
):
184 email
=user
.email
, password
=None, display_name
=user
.get_full_name())
186 def get_or_create_from_django(self
, user
):
188 return self
.get(address
=user
.email
)
189 except Mailman404Error
:
190 return self
.create_from_django(user
)
193 class MailmanRestModel(object):
194 """Simple REST Model class to make REST API calls Django style.
196 MailmanApiError
= MailmanApiError
197 DoesNotExist
= Mailman404Error
199 def __init__(self
, *args
, **kwargs
):
204 """Proxy function for `objects.create`.
205 (REST API uses `create`, while Django uses `save`.)
207 self
.objects
.create(*self
.args
, **self
.kwargs
)
210 class Domain(MailmanRestModel
):
211 """Domain model class.
213 objects
= MailmanRestManager('domain', 'domains')
216 class List(MailmanRestModel
):
219 objects
= MailmanListManager()
222 class MailmanUser(MailmanRestModel
):
223 """MailmanUser model class.
225 objects
= MailmanUserManager()
228 class Member(MailmanRestModel
):
229 """Member model class.
231 objects
= MailmanRestManager('member', 'members')
234 class Style(MailmanRestModel
):
237 objects
= MailmanRestManager(None, 'styles')
240 TEMPLATE_CONTEXT_CHOICES
= (
241 ('site', 'Site Wide'),
242 ('domain', 'Domain Wide'),
243 ('list', 'MailingList Wide')
247 class EmailTemplate(models
.Model
):
248 """A Template represents contents of partial or complete emails sent out by
249 Mailman Core on various events or when an action is required. Headers and
250 Footers on emails for decorations are also repsented as templates.
253 # Ease differentiating the various Mailman templates by providing the
254 # template file's name (key) prepended in square brackets to the
255 # template's purpose (value).
256 _templates_list_choices
= [
257 (t
[0], "[{key}] - {value}".format(key
=t
[0], value
=t
[1]))
258 for t
in TEMPLATES_LIST
261 name
= models
.CharField(
262 max_length
=100, choices
=_templates_list_choices
,
263 help_text
=_('Choose the template you want to customize.'))
264 data
= models
.TextField(
265 help_text
=_email_template_help_text
,
268 language
= models
.CharField(
269 max_length
=5, choices
=LANGUAGES
,
270 help_text
=_('Language for the template, this should be the list\'s preferred language.'), # noqa: E501
272 created_at
= models
.DateTimeField(auto_now_add
=True)
273 modified_at
= models
.DateTimeField(auto_now
=True)
274 context
= models
.CharField(max_length
=50, choices
=TEMPLATE_CONTEXT_CHOICES
)
275 identifier
= models
.CharField(blank
=True, max_length
=100)
278 unique_together
= ('name', 'identifier', 'language')
281 return '<EmailTemplate {0} for {1}>'.format(self
.name
, self
.context
)
284 def description(self
):
285 """Return the long description of template that is human readable."""
286 return dict(TEMPLATES_LIST
)[self
.name
]
290 """API url is the remote url that Core can use to fetch templates"""
291 base_url
= getattr(settings
, 'POSTORIUS_TEMPLATE_BASE_URL', None)
293 raise ImproperlyConfigured(
294 'Setting "POSTORIUS_TEMPLATE_BASE_URL" is not configured.')
295 resource_url
= reverse(
297 kwargs
=dict(context
=self
.context
,
298 identifier
=self
.identifier
,
301 return urljoin(base_url
, resource_url
)
303 def _get_context_obj(self
):
304 if self
.context
== 'list':
305 obj
= List
.objects
.get_or_404(fqdn_listname
=self
.identifier
)
306 elif self
.context
== 'domain':
307 obj
= Domain
.objects
.get_or_404(mail_host
=self
.identifier
)
308 elif self
.context
== 'site':
309 obj
= get_mailman_client()
314 def _update_core(self
, deleted
=False):
315 obj
= self
._get
_context
_obj
()
320 # POST'ing an empty string will delete this record in Core.
323 # Use the API endpoint of self that Core can use to fetch this.
324 api_url
= self
.api_url
325 obj
.set_template(self
.name
, api_url
)
328 @receiver(post_save
, sender
=EmailTemplate
)
329 def update_core_post_update(sender
, **kwargs
):
330 kwargs
['instance']._update
_core
()
333 @receiver(post_delete
, sender
=EmailTemplate
)
334 def update_core_post_delete(sender
, **kwargs
):
335 kwargs
['instance']._update
_core
(deleted
=True)