Some minor housekeeping fixes.
[mailman-postorious.git] / src / postorius / models.py
blob5b8a6e5d80e3612efd9b38c827b21009937a3c35
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)
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 import logging
20 from enum import Enum
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'
63 as_user = 'as_user'
66 class MailmanApiError(Exception):
67 """Raised if the API is not available.
68 """
69 pass
72 class Mailman404Error(Exception):
73 """Proxy exception. Raised if the API returns 404."""
74 pass
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.
80 """
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
86 def all(self):
87 try:
88 return getattr(get_mailman_client(), self.resource_name_plural)
89 except AttributeError:
90 raise MailmanApiError
91 except MailmanConnectionError as e:
92 raise MailmanApiError(e)
94 def get(self, *args, **kwargs):
95 try:
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:
101 if e.code == 404:
102 raise Mailman404Error('Mailman resource could not be found.')
103 else:
104 raise
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.
111 try:
112 return self.get(*args, **kwargs)
113 except Mailman404Error:
114 raise Http404
115 except MailmanConnectionError as e:
116 raise MailmanApiError(e)
118 def create(self, *args, **kwargs):
119 try:
120 method = getattr(
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:
126 if e.code == 409:
127 raise MailmanApiError
128 else:
129 raise
130 except MailmanConnectionError:
131 raise MailmanApiError
133 def delete(self):
134 """Not implemented since the objects returned from the API
135 have a `delete` method of their own.
137 pass
140 class MailmanListManager(MailmanRestManager):
142 def __init__(self):
143 super(MailmanListManager, self).__init__('list', 'lists')
145 def all(self, advertised=False):
146 try:
147 method = getattr(
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)
157 host_objects = []
158 for obj in objects:
159 if obj.mail_host == mail_host:
160 host_objects.append(obj)
161 return host_objects
164 class MailmanUserManager(MailmanRestManager):
166 def __init__(self):
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):
177 self.args = args
178 self.kwargs = kwargs
180 def save(self):
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):
194 """List model class.
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,
243 blank=True,
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
248 blank=True)
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)
254 class Meta:
255 unique_together = ('name', 'identifier', 'language')
257 def __str__(self):
258 return '<EmailTemplate {0} for {1}>'.format(self.name, self.context)
260 @property
261 def description(self):
262 """Return the long description of template that is human readable."""
263 return dict(TEMPLATES_LIST)[self.name]
265 @property
266 def api_url(self):
267 """API url is the remote url that Core can use to fetch templates"""
268 base_url = getattr(settings, 'POSTORIUS_TEMPLATE_BASE_URL', None)
269 if not base_url:
270 raise ImproperlyConfigured(
271 'Setting "POSTORIUS_TEMPLATE_BASE_URL" is not configured.')
272 resource_url = reverse(
273 'rest_template',
274 kwargs=dict(context=self.context,
275 identifier=self.identifier,
276 name=self.name)
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()
287 else:
288 obj = None
289 return obj
291 def _update_core(self, deleted=False):
292 obj = self._get_context_obj()
293 if obj is None:
294 return
296 if deleted:
297 # POST'ing an empty string will delete this record in Core.
298 api_url = ''
299 else:
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)