Prepare for 1.3.3 release.
[mailman-postorious.git] / src / postorius / models.py
blob134ca576e8e6551431f3658fb04104fe629e9199
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)
9 # any later version.
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
14 # more details.
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)
22 import logging
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')
67 try:
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.
76 """
77 pass
80 class Mailman404Error(Exception):
81 """Proxy exception. Raised if the API returns 404."""
82 pass
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.
88 """
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
94 def all(self):
95 try:
96 return getattr(get_mailman_client(), self.resource_name_plural)
97 except AttributeError:
98 raise MailmanApiError
99 except MailmanConnectionError as e:
100 raise MailmanApiError(e)
102 def get(self, *args, **kwargs):
103 try:
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:
109 if e.code == 404:
110 raise Mailman404Error('Mailman resource could not be found.')
111 else:
112 raise
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.
119 try:
120 return self.get(*args, **kwargs)
121 except Mailman404Error:
122 raise Http404
123 except MailmanConnectionError as e:
124 raise MailmanApiError(e)
126 def create(self, *args, **kwargs):
127 try:
128 method = getattr(
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:
134 if e.code == 409:
135 raise MailmanApiError
136 else:
137 raise
138 except MailmanConnectionError:
139 raise MailmanApiError
141 def delete(self):
142 """Not implemented since the objects returned from the API
143 have a `delete` method of their own.
145 pass
148 class MailmanListManager(MailmanRestManager):
150 def __init__(self):
151 super(MailmanListManager, self).__init__('list', 'lists')
153 def all(self, advertised=False):
154 try:
155 method = getattr(
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)
165 host_objects = []
166 for obj in objects:
167 if obj.mail_host == mail_host:
168 host_objects.append(obj)
169 return host_objects
172 class MailmanUserManager(MailmanRestManager):
174 def __init__(self):
175 super(MailmanUserManager, self).__init__('user', 'users')
177 def create_from_django(self, user):
178 return self.create(
179 email=user.email, password=None, display_name=user.get_full_name())
181 def get_or_create_from_django(self, user):
182 try:
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):
195 self.args = args
196 self.kwargs = kwargs
198 def save(self):
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):
212 """List model class.
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,
261 blank=True,
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
266 blank=True)
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)
272 class Meta:
273 unique_together = ('name', 'identifier', 'language')
275 def __str__(self):
276 return '<EmailTemplate {0} for {1}>'.format(self.name, self.context)
278 @property
279 def description(self):
280 """Return the long description of template that is human readable."""
281 return dict(TEMPLATES_LIST)[self.name]
283 @property
284 def api_url(self):
285 """API url is the remote url that Core can use to fetch templates"""
286 base_url = getattr(settings, 'POSTORIUS_TEMPLATE_BASE_URL', None)
287 if not base_url:
288 raise ImproperlyConfigured(
289 'Setting "POSTORIUS_TEMPLATE_BASE_URL" is not configured.')
290 resource_url = reverse(
291 'rest_template',
292 kwargs=dict(context=self.context,
293 identifier=self.identifier,
294 name=self.name)
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()
305 else:
306 obj = None
307 return obj
309 def _update_core(self, deleted=False):
310 obj = self._get_context_obj()
311 if obj is None:
312 return
314 if deleted:
315 # POST'ing an empty string will delete this record in Core.
316 api_url = ''
317 else:
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)