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/>.
19 from __future__
import (
20 absolute_import
, division
, print_function
, unicode_literals
)
23 from urllib
.error
import HTTPError
24 from urllib
.parse
import urljoin
26 from django
.conf
import settings
27 from django
.contrib
.auth
.models
import User
28 from django
.core
.exceptions
import ImproperlyConfigured
29 from django
.db
import models
30 from django
.db
.models
.signals
import post_delete
, post_save
31 from django
.dispatch
import receiver
32 from django
.http
import Http404
33 from django
.urls
import reverse
34 from django
.utils
.translation
import gettext_lazy
as _
36 from mailmanclient
import MailmanConnectionError
38 from postorius
.template_list
import TEMPLATES_LIST
39 from postorius
.utils
import LANGUAGES
, get_mailman_client
42 logger
= logging
.getLogger(__name__
)
44 _email_template_help_text
= _(
45 'Note: Do not add any secret content in templates as they are '
46 'publicly accessible.\n'
47 'You can use these variables in the templates. \n'
48 '$hyperkitty_url: Permalink to archived message in Hyperkitty\n'
49 '$listname: Name of the Mailing List e.g. ant@example.com \n'
50 '$list_id: The List-ID header e.g. ant.example.com \n'
51 '$display_name: Display name of the mailing list e.g. Ant \n'
52 '$short_listname: Local part of the listname e.g. ant \n'
53 '$domain: The domain part of the listname e.g. example.com \n'
54 '$info: The mailing list\'s longer descriptive text \n'
55 '$request_email: The email address for -request address \n'
56 '$owner_email: The email address for -owner address \n'
57 '$site_email: The email address to reach the owners of the site \n'
58 '$language: The two letter language code for list\'s preferred language e.g. fr, en, de \n' # noqa: E501
62 @receiver(post_save
, sender
=User
)
63 def create_mailman_user(sender
, **kwargs
):
64 if kwargs
.get('created'):
65 if getattr(settings
, 'AUTOCREATE_MAILMAN_USER', False):
66 user
= kwargs
.get('instance')
68 MailmanUser
.objects
.create_from_django(user
)
69 except (MailmanApiError
, HTTPError
):
70 logger
.error('Mailman user not created for {}'.format(user
))
71 logger
.error('Mailman Core API is not reachable.')
74 class MailmanApiError(Exception):
75 """Raised if the API is not available.
80 class Mailman404Error(Exception):
81 """Proxy exception. Raised if the API returns 404."""
85 class MailmanRestManager(object):
86 """Manager class to give a model class CRUD access to the API.
87 Returns objects (or lists of objects) retrieved from the API.
90 def __init__(self
, resource_name
, resource_name_plural
, cls_name
=None):
91 self
.resource_name
= resource_name
92 self
.resource_name_plural
= resource_name_plural
96 return getattr(get_mailman_client(), self
.resource_name_plural
)
97 except AttributeError:
99 except MailmanConnectionError
as e
:
100 raise MailmanApiError(e
)
102 def get(self
, *args
, **kwargs
):
104 method
= getattr(get_mailman_client(), 'get_' + self
.resource_name
)
105 return method(*args
, **kwargs
)
106 except AttributeError as e
:
107 raise MailmanApiError(e
)
108 except HTTPError
as e
:
110 raise Mailman404Error('Mailman resource could not be found.')
113 except MailmanConnectionError
as e
:
114 raise MailmanApiError(e
)
116 def get_or_404(self
, *args
, **kwargs
):
117 """Similar to `self.get` but raises standard Django 404 error.
120 return self
.get(*args
, **kwargs
)
121 except Mailman404Error
:
123 except MailmanConnectionError
as e
:
124 raise MailmanApiError(e
)
126 def create(self
, *args
, **kwargs
):
129 get_mailman_client(), 'create_' + self
.resource_name
)
130 return method(*args
, **kwargs
)
131 except AttributeError as e
:
132 raise MailmanApiError(e
)
133 except HTTPError
as e
:
135 raise MailmanApiError
138 except MailmanConnectionError
:
139 raise MailmanApiError
142 """Not implemented since the objects returned from the API
143 have a `delete` method of their own.
148 class MailmanListManager(MailmanRestManager
):
151 super(MailmanListManager
, self
).__init
__('list', 'lists')
153 def all(self
, advertised
=False):
156 get_mailman_client(), 'get_' + self
.resource_name_plural
)
157 return method(advertised
=advertised
)
158 except AttributeError:
159 raise MailmanApiError
160 except MailmanConnectionError
as e
:
161 raise MailmanApiError(e
)
163 def by_mail_host(self
, mail_host
, advertised
=False):
164 objects
= self
.all(advertised
)
167 if obj
.mail_host
== mail_host
:
168 host_objects
.append(obj
)
172 class MailmanUserManager(MailmanRestManager
):
175 super(MailmanUserManager
, self
).__init
__('user', 'users')
177 def create_from_django(self
, user
):
179 email
=user
.email
, password
=None, display_name
=user
.get_full_name())
181 def get_or_create_from_django(self
, user
):
183 return self
.get(address
=user
.email
)
184 except Mailman404Error
:
185 return self
.create_from_django(user
)
188 class MailmanRestModel(object):
189 """Simple REST Model class to make REST API calls Django style.
191 MailmanApiError
= MailmanApiError
192 DoesNotExist
= Mailman404Error
194 def __init__(self
, *args
, **kwargs
):
199 """Proxy function for `objects.create`.
200 (REST API uses `create`, while Django uses `save`.)
202 self
.objects
.create(*self
.args
, **self
.kwargs
)
205 class Domain(MailmanRestModel
):
206 """Domain model class.
208 objects
= MailmanRestManager('domain', 'domains')
211 class List(MailmanRestModel
):
214 objects
= MailmanListManager()
217 class MailmanUser(MailmanRestModel
):
218 """MailmanUser model class.
220 objects
= MailmanUserManager()
223 class Member(MailmanRestModel
):
224 """Member model class.
226 objects
= MailmanRestManager('member', 'members')
229 class Style(MailmanRestModel
):
232 objects
= MailmanRestManager(None, 'styles')
235 TEMPLATE_CONTEXT_CHOICES
= (
236 ('site', 'Site Wide'),
237 ('domain', 'Domain Wide'),
238 ('list', 'MailingList Wide')
242 class EmailTemplate(models
.Model
):
243 """A Template represents contents of partial or complete emails sent out by
244 Mailman Core on various events or when an action is required. Headers and
245 Footers on emails for decorations are also repsented as templates.
248 # Ease differentiating the various Mailman templates by providing the
249 # template file's name (key) prepended in square brackets to the
250 # template's purpose (value).
251 _templates_list_choices
= [
252 (t
[0], "[{key}] - {value}".format(key
=t
[0], value
=t
[1]))
253 for t
in TEMPLATES_LIST
256 name
= models
.CharField(
257 max_length
=100, choices
=_templates_list_choices
,
258 help_text
=_('Choose the template you want to customize.'))
259 data
= models
.TextField(
260 help_text
=_email_template_help_text
,
263 language
= models
.CharField(
264 max_length
=5, choices
=LANGUAGES
,
265 help_text
=_('Language for the template, this should be the list\'s preferred language.'), # noqa: E501
267 created_at
= models
.DateTimeField(auto_now_add
=True)
268 modified_at
= models
.DateTimeField(auto_now
=True)
269 context
= models
.CharField(max_length
=50, choices
=TEMPLATE_CONTEXT_CHOICES
)
270 identifier
= models
.CharField(blank
=True, max_length
=100)
273 unique_together
= ('name', 'identifier', 'language')
276 return '<EmailTemplate {0} for {1}>'.format(self
.name
, self
.context
)
279 def description(self
):
280 """Return the long description of template that is human readable."""
281 return dict(TEMPLATES_LIST
)[self
.name
]
285 """API url is the remote url that Core can use to fetch templates"""
286 base_url
= getattr(settings
, 'POSTORIUS_TEMPLATE_BASE_URL', None)
288 raise ImproperlyConfigured(
289 'Setting "POSTORIUS_TEMPLATE_BASE_URL" is not configured.')
290 resource_url
= reverse(
292 kwargs
=dict(context
=self
.context
,
293 identifier
=self
.identifier
,
296 return urljoin(base_url
, resource_url
)
298 def _get_context_obj(self
):
299 if self
.context
== 'list':
300 obj
= List
.objects
.get_or_404(fqdn_listname
=self
.identifier
)
301 elif self
.context
== 'domain':
302 obj
= Domain
.objects
.get_or_404(mail_host
=self
.identifier
)
303 elif self
.context
== 'site':
304 obj
= get_mailman_client()
309 def _update_core(self
, deleted
=False):
310 obj
= self
._get
_context
_obj
()
315 # POST'ing an empty string will delete this record in Core.
318 # Use the API endpoint of self that Core can use to fetch this.
319 api_url
= self
.api_url
320 obj
.set_template(self
.name
, api_url
)
323 @receiver(post_save
, sender
=EmailTemplate
)
324 def update_core_post_update(sender
, **kwargs
):
325 kwargs
['instance']._update
_core
()
328 @receiver(post_delete
, sender
=EmailTemplate
)
329 def update_core_post_delete(sender
, **kwargs
):
330 kwargs
['instance']._update
_core
(deleted
=True)