Prepare for 1.3.3 release.
[mailman-postorious.git] / src / postorius / views / list.py
blob117c1462b6e0ba8994c70ffd80fe25274decbcdc
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/>.
20 import csv
21 import email.utils
22 import logging
23 import sys
24 from urllib.error import HTTPError
26 from django.conf import settings
27 from django.contrib import messages
28 from django.contrib.auth.decorators import login_required
29 from django.core.exceptions import ValidationError
30 from django.core.validators import validate_email
31 from django.forms import formset_factory
32 from django.http import Http404, HttpResponse
33 from django.shortcuts import redirect, render
34 from django.urls import reverse
35 from django.utils.decorators import method_decorator
36 from django.utils.translation import gettext as _
37 from django.views.decorators.http import require_http_methods
39 from allauth.account.models import EmailAddress
40 from django_mailman3.lib.mailman import get_mailman_client
41 from django_mailman3.lib.paginator import MailmanPaginator, paginate
42 from django_mailman3.models import MailDomain
43 from django_mailman3.signals import (
44 mailinglist_created, mailinglist_deleted, mailinglist_modified)
46 from postorius.auth.decorators import (
47 list_moderator_required, list_owner_required, superuser_required)
48 from postorius.auth.mixins import ListOwnerMixin
49 from postorius.forms import (
50 AlterMessagesForm, ArchiveSettingsForm, BounceProcessingForm,
51 DigestSettingsForm, DMARCMitigationsForm, ListAnonymousSubscribe,
52 ListAutomaticResponsesForm, ListHeaderMatchForm, ListHeaderMatchFormset,
53 ListIdentityForm, ListMassRemoval, ListMassSubscription, ListNew,
54 ListSubscribe, MemberForm, MemberModeration, MemberPolicyForm,
55 MessageAcceptanceForm, MultipleChoiceForm, UserPreferences)
56 from postorius.forms.list_forms import ACTION_CHOICES
57 from postorius.models import Domain, List, Mailman404Error, Style
58 from postorius.views.generic import MailingListView, bans_view
61 logger = logging.getLogger(__name__)
64 class TokenOwner:
65 """Who 'owns' the token returned from the registrar?"""
66 subscriber = 'subscriber'
67 moderator = 'moderator'
70 class ListMembersViews(ListOwnerMixin, MailingListView):
72 # List of allowed roles for the memberships. The string value matches the
73 # exact value Core's REST API expects.
74 allowed_roles = ['owner', 'moderator', 'member', 'nonmember']
76 def _prepare_query(self, request):
77 """Prepare regex based query to search partial email addresses.
79 Core's `members/find` API allows searching for memberships based on
80 regex. This methods prepares a valid regex to pass on the the REST API.
81 """
82 if request.GET.get('q'):
83 query = request.GET['q']
84 if "*" not in query:
85 query = '*{}*'.format(query)
86 else:
87 query = ''
89 return query
91 def get(self, request, list_id, role):
92 """Handle GET for Member view.
94 This includes all the membership roles (self.allowed_roles).
95 """
96 member_form = MemberForm()
97 # If the role is misspelled, redirect to the default subscribers.
98 if role not in self.allowed_roles:
99 return redirect('list_members', list_id, 'member')
100 context = dict()
101 context['list'] = self.mailing_list
102 context['role'] = role
103 context['member_form'] = member_form
104 context['page_title'] = _('List {}s'.format(role.capitalize()))
105 context['query'] = self._prepare_query(request)
107 def find_method(count, page):
108 return self.mailing_list.find_members(
109 context['query'], role=role, count=count, page=page)
111 context['members'] = paginate(
112 find_method,
113 request.GET.get('page', 1),
114 request.GET.get('count', 25),
115 paginator_class=MailmanPaginator)
116 context['page_subtitle'] = '({})'.format(
117 context['members'].paginator.count)
118 context['form_action'] = _('Add {}'.format(role))
119 if context['query']:
120 context['empty_error'] = _(
121 'No {}s were found matching the search.'.format(role))
122 else:
123 context['empty_error'] = _('List has no {}s'.format(role))
125 return render(request, 'postorius/lists/members.html', context)
127 def _member_post(self, request, role):
128 """Handle POST for members. Unsubscribe all the members selected."""
129 form = MultipleChoiceForm(request.POST)
130 if form.is_valid():
131 members = form.cleaned_data['choices']
132 for member in members:
133 self.mailing_list.unsubscribe(member)
134 messages.success(
135 request,
136 _('The selected members have been unsubscribed'))
137 return redirect('list_members', self.mailing_list.list_id, role)
139 def _non_member_post(self, request, role):
140 """Handle POST for membership roles owner, moderator and non-member.
142 Add memberships if the form is valid otherwise redirect to list_members
143 page with an error message.
145 member_form = MemberForm(request.POST)
146 if member_form.is_valid():
147 try:
148 self.mailing_list.add_role(
149 role=role,
150 address=member_form.cleaned_data['email'],
151 display_name=member_form.cleaned_data['display_name'])
152 messages.success(
153 request,
154 _('{email} has been added with the role {role}'.format(
155 email=member_form.cleaned_data['email'], role=role)))
156 except HTTPError as e:
157 messages.error(request, e.msg)
158 else:
159 messages.error(request, member_form.errors)
160 return redirect('list_members', self.mailing_list.list_id, role)
162 def post(self, request, list_id, role=None):
163 """Handle POST for list members page.
165 List members page have more than one forms, depending on the membership
166 type.
168 - Regular subscribers have a MultipleChoiceForm, which returns a list
169 of emails that needs to be unsubscribed from the MailingList. This is
170 handled by :func:`self._member_post` method.
172 - Owners, moderators and non-members have a MemberForm which allows
173 adding new members with the given roles. This is handled by
174 :func:`self._non_member_post` method.
177 if role not in self.allowed_roles:
178 return redirect('list_members', list_id, 'member')
180 if role in ('member'):
181 return self._member_post(request, role)
182 else:
183 return self._non_member_post(request, role)
186 @login_required
187 @list_owner_required
188 def list_member_options(request, list_id, email):
189 template_name = 'postorius/lists/memberoptions.html'
190 mm_list = List.objects.get_or_404(fqdn_listname=list_id)
191 try:
192 mm_member = mm_list.find_members(address=email)[0]
193 member_prefs = mm_member.preferences
194 except (ValueError, IndexError):
195 raise Http404(_('Member does not exist'))
196 except Mailman404Error:
197 return render(request, template_name, {'nolists': 'true'})
198 initial_moderation = dict([
199 (key, getattr(mm_member, key)) for key in MemberModeration.base_fields
201 if request.method == 'POST':
202 if request.POST.get("formname") == 'preferences':
203 preferences_form = UserPreferences(
204 request.POST, preferences=member_prefs)
205 if preferences_form.is_valid():
206 try:
207 preferences_form.save()
208 except HTTPError as e:
209 messages.error(request, e.msg)
210 else:
211 messages.success(request, _("The member's preferences"
212 " have been updated."))
213 return redirect('list_member_options', list_id, email)
214 elif request.POST.get("formname") == 'moderation':
215 moderation_form = MemberModeration(
216 request.POST, initial=initial_moderation)
217 if moderation_form.is_valid():
218 if not moderation_form.has_changed():
219 messages.info(
220 request, _("No change to the member's moderation."))
221 return redirect('list_member_options', list_id, email)
222 for key in list(moderation_form.fields.keys()):
223 # In general, it would be a very bad idea to loop over the
224 # fields and try to set them one by one, However,
225 # moderation form has only one field.
226 setattr(mm_member, key, moderation_form.cleaned_data[key])
227 try:
228 mm_member.save()
229 except HTTPError as e:
230 messages.error(request, e.msg)
231 else:
232 messages.success(request, _("The member's moderation "
233 "settings have been updated."))
234 return redirect('list_member_options', list_id, email)
235 else:
236 preferences_form = UserPreferences(preferences=member_prefs)
237 moderation_form = MemberModeration(initial=initial_moderation)
238 return render(request, template_name, {
239 'mm_member': mm_member,
240 'list': mm_list,
241 'preferences_form': preferences_form,
242 'moderation_form': moderation_form,
246 class ListSummaryView(MailingListView):
247 """Shows common list metrics.
250 def get(self, request, list_id):
251 data = {'list': self.mailing_list,
252 'user_subscribed': False,
253 'subscribed_address': None,
254 'public_archive': False,
255 'hyperkitty_enabled': False}
256 if self.mailing_list.settings['archive_policy'] == 'public':
257 data['public_archive'] = True
258 if (getattr(settings, 'TESTING', False) and # noqa: W504
259 'hyperkitty' not in settings.INSTALLED_APPS):
260 # avoid systematic test failure when HyperKitty is installed
261 # (missing VCR request, see below).
262 list(self.mailing_list.archivers)
263 if ('hyperkitty' in settings.INSTALLED_APPS and # noqa: W504
264 'hyperkitty' in self.mailing_list.archivers and # noqa: W504
265 self.mailing_list.archivers['hyperkitty']):
266 data['hyperkitty_enabled'] = True
267 if request.user.is_authenticated:
268 user_emails = EmailAddress.objects.filter(
269 user=request.user, verified=True).order_by(
270 "email").values_list("email", flat=True)
271 pending_requests = [r['email'] for r in self.mailing_list.requests]
272 for address in user_emails:
273 if address in pending_requests:
274 data['user_request_pending'] = True
275 break
276 try:
277 self.mailing_list.get_member(address)
278 except ValueError:
279 pass
280 else:
281 data['user_subscribed'] = True
282 data['subscribed_address'] = address
283 break # no need to test more addresses
284 data['subscribe_form'] = ListSubscribe(user_emails)
285 else:
286 user_emails = None
287 data['anonymous_subscription_form'] = ListAnonymousSubscribe()
288 return render(request, 'postorius/lists/summary.html', data)
291 class ChangeSubscriptionView(MailingListView):
292 """Change mailing list subscription
295 @method_decorator(login_required)
296 def post(self, request, list_id):
297 try:
298 user_emails = EmailAddress.objects.filter(
299 user=request.user, verified=True).order_by(
300 "email").values_list("email", flat=True)
301 form = ListSubscribe(user_emails, request.POST)
302 # Find the currently subscribed email
303 old_email = None
304 for address in user_emails:
305 try:
306 self.mailing_list.get_member(address)
307 except ValueError:
308 pass
309 else:
310 old_email = address
311 break # no need to test more addresses
312 assert old_email is not None
313 if form.is_valid():
314 email = form.cleaned_data['email']
315 if old_email == email:
316 messages.error(request, _('You are already subscribed'))
317 else:
318 self.mailing_list.unsubscribe(old_email)
319 # Since the action is done via the web UI, no email
320 # confirmation is needed.
321 response = self.mailing_list.subscribe(
322 email, pre_confirmed=True)
323 if (type(response) == dict and # noqa: W504
324 response.get('token_owner') == TokenOwner.moderator): # noqa: E501
325 messages.success(
326 request, _('Your request to change the email for'
327 ' this subscription was submitted and'
328 ' is waiting for moderator approval.'))
329 else:
330 messages.success(request,
331 _('Subscription changed to %s') %
332 email)
333 else:
334 messages.error(request,
335 _('Something went wrong. Please try again.'))
336 except HTTPError as e:
337 messages.error(request, e.msg)
338 return redirect('list_summary', self.mailing_list.list_id)
341 class ListSubscribeView(MailingListView):
343 view name: `list_subscribe`
346 @method_decorator(login_required)
347 def post(self, request, list_id):
349 Subscribes an email address to a mailing list via POST and
350 redirects to the `list_summary` view.
352 try:
353 user_emails = EmailAddress.objects.filter(
354 user=request.user, verified=True).order_by(
355 "email").values_list("email", flat=True)
356 form = ListSubscribe(user_emails, request.POST)
357 if form.is_valid():
358 email = request.POST.get('email')
359 response = self.mailing_list.subscribe(
360 email, pre_verified=True, pre_confirmed=True)
361 if (type(response) == dict and # noqa: W504
362 response.get('token_owner') == TokenOwner.moderator):
363 messages.success(
364 request, _('Your subscription request has been'
365 ' submitted and is waiting for moderator'
366 ' approval.'))
367 else:
368 messages.success(request,
369 _('You are subscribed to %s.') %
370 self.mailing_list.fqdn_listname)
371 else:
372 messages.error(request,
373 _('Something went wrong. Please try again.'))
374 except HTTPError as e:
375 messages.error(request, e.msg)
376 return redirect('list_summary', self.mailing_list.list_id)
379 class ListAnonymousSubscribeView(MailingListView):
381 view name: `list_anonymous_subscribe`
384 def post(self, request, list_id):
386 Subscribes an email address to a mailing list via POST and
387 redirects to the `list_summary` view.
388 This view is used for unauthenticated users and asks Mailman core to
389 verify the supplied email address.
391 try:
392 form = ListAnonymousSubscribe(request.POST)
393 if form.is_valid():
394 email = form.cleaned_data.get('email')
395 self.mailing_list.subscribe(email, pre_verified=False,
396 pre_confirmed=False)
397 messages.success(request, _('Please check your inbox for '
398 'further instructions'))
399 else:
400 messages.error(request,
401 _('Something went wrong. Please try again.'))
402 except HTTPError as e:
403 messages.error(request, e.msg)
404 return redirect('list_summary', self.mailing_list.list_id)
407 class ListUnsubscribeView(MailingListView):
409 """Unsubscribe from a mailing list."""
411 @method_decorator(login_required)
412 def post(self, request, *args, **kwargs):
413 email = request.POST['email']
414 try:
415 self.mailing_list.unsubscribe(email)
416 messages.success(request, _('%s has been unsubscribed'
417 ' from this list.') % email)
418 except ValueError as e:
419 messages.error(request, e)
420 return redirect('list_summary', self.mailing_list.list_id)
423 @login_required
424 @list_owner_required
425 def list_mass_subscribe(request, list_id):
426 mailing_list = List.objects.get_or_404(fqdn_listname=list_id)
427 if request.method == 'POST':
428 form = ListMassSubscription(request.POST)
429 if form.is_valid():
430 for data in form.cleaned_data['emails']:
431 try:
432 # Parse the data to get the address and the display name
433 display_name, address = email.utils.parseaddr(data)
434 validate_email(address)
435 mailing_list.subscribe(
436 address=address,
437 display_name=display_name,
438 pre_verified=form.cleaned_data['pre_verified'],
439 pre_confirmed=form.cleaned_data['pre_confirmed'],
440 pre_approved=form.cleaned_data['pre_approved'])
441 messages.success(
442 request, _('The address %(address)s has been'
443 ' subscribed to %(list)s.') %
444 {'address': address,
445 'list': mailing_list.fqdn_listname})
446 except HTTPError as e:
447 messages.error(request, e)
448 except ValidationError:
449 messages.error(request, _('The email address %s'
450 ' is not valid.') % address)
451 else:
452 form = ListMassSubscription()
453 return render(request, 'postorius/lists/mass_subscribe.html',
454 {'form': form, 'list': mailing_list})
457 class ListMassRemovalView(MailingListView):
459 """Class For Mass Removal"""
461 @method_decorator(login_required)
462 @method_decorator(list_owner_required)
463 def get(self, request, *args, **kwargs):
464 form = ListMassRemoval()
465 return render(request, 'postorius/lists/mass_removal.html',
466 {'form': form, 'list': self.mailing_list})
468 @method_decorator(list_owner_required)
469 def post(self, request, *args, **kwargs):
470 form = ListMassRemoval(request.POST)
471 if not form.is_valid():
472 messages.error(request, _('Please fill out the form correctly.'))
473 else:
474 for address in form.cleaned_data['emails']:
475 try:
476 validate_email(address)
477 self.mailing_list.unsubscribe(address.lower())
478 messages.success(
479 request, _('The address %(address)s has been'
480 ' unsubscribed from %(list)s.') %
481 {'address': address,
482 'list': self.mailing_list.fqdn_listname})
483 except (HTTPError, ValueError) as e:
484 messages.error(request, e)
485 except ValidationError:
486 messages.error(request, _('The email address %s'
487 ' is not valid.') % address)
488 return redirect('mass_removal', self.mailing_list.list_id)
491 def _perform_action(message_ids, action):
492 for message_id in message_ids:
493 action(message_id)
496 @login_required
497 @list_moderator_required
498 def list_moderation(request, list_id, held_id=-1):
499 mailing_list = List.objects.get_or_404(fqdn_listname=list_id)
500 if request.method == 'POST':
501 form = MultipleChoiceForm(request.POST)
502 if form.is_valid():
503 message_ids = form.cleaned_data['choices']
504 try:
505 if 'accept' in request.POST:
506 _perform_action(message_ids, mailing_list.accept_message)
507 messages.success(request,
508 _('The selected messages were accepted'))
509 elif 'reject' in request.POST:
510 _perform_action(message_ids, mailing_list.reject_message)
511 messages.success(request,
512 _('The selected messages were rejected'))
513 elif 'discard' in request.POST:
514 _perform_action(message_ids, mailing_list.discard_message)
515 messages.success(request,
516 _('The selected messages were discarded'))
517 except HTTPError:
518 messages.error(request, _('Message could not be found'))
519 else:
520 form = MultipleChoiceForm()
521 held_messages = paginate(
522 mailing_list.get_held_page,
523 request.GET.get('page'), request.GET.get('count'),
524 paginator_class=MailmanPaginator)
525 context = {
526 'list': mailing_list,
527 'held_messages': held_messages,
528 'form': form,
529 'ACTION_CHOICES': ACTION_CHOICES,
531 return render(request, 'postorius/lists/held_messages.html', context)
534 @require_http_methods(['POST'])
535 @login_required
536 @list_moderator_required
537 def moderate_held_message(request, list_id):
538 """Moderate one held message"""
539 mailing_list = List.objects.get_or_404(fqdn_listname=list_id)
540 msg = mailing_list.get_held_message(request.POST['msgid'])
541 moderation_choice = request.POST.get('moderation_choice')
542 reason = request.POST.get('reason')
544 try:
545 if 'accept' in request.POST:
546 mailing_list.accept_message(msg.request_id)
547 messages.success(request, _('The message was accepted'))
548 elif 'reject' in request.POST:
549 mailing_list.reject_message(msg.request_id, reason=reason)
550 messages.success(request, _('The message was rejected'))
551 elif 'discard' in request.POST:
552 mailing_list.discard_message(msg.request_id)
553 messages.success(request, _('The message was discarded'))
554 except HTTPError as e:
555 if e.code == 404:
556 messages.error(
557 request,
558 _('Held message was not found.'))
559 return redirect('list_held_messages', list_id)
560 else:
561 raise
563 moderation_choices = dict(ACTION_CHOICES)
564 if moderation_choice in moderation_choices:
565 try:
566 member = mailing_list.get_member(msg.sender)
567 member.moderation_action = moderation_choice
568 member.save()
569 messages.success(
570 request,
571 _('Moderation action for {} set to {}'.format(
572 member, moderation_choices[moderation_choice])))
573 except ValueError as e:
574 messages.error(
575 request,
576 _('Failed to set moderation action: {}'.format(e)))
577 return redirect('list_held_messages', list_id)
580 @login_required
581 @list_owner_required
582 def csv_view(request, list_id):
583 """Export all the subscriber in csv
585 mm_lists = List.objects.get_or_404(fqdn_listname=list_id)
587 response = HttpResponse(content_type='text/csv')
588 response['Content-Disposition'] = (
589 'attachment; filename="Subscribers.csv"')
591 writer = csv.writer(response)
592 if mm_lists:
593 for i in mm_lists.members:
594 writer.writerow([i.email])
596 return response
599 def _get_choosable_domains(request):
600 domains = Domain.objects.all()
601 return [(d.mail_host, d.mail_host) for d in domains]
604 def _get_choosable_styles(request):
605 styles = Style.objects.all()
606 options = [(style['name'], style['description'])
607 for style in styles['styles']]
608 return options
611 def _get_default_style():
612 return Style.objects.all()['default']
615 @login_required
616 @superuser_required
617 def list_new(request, template='postorius/lists/new.html'):
619 Add a new mailing list.
620 If the request to the function is a GET request an empty form for
621 creating a new list will be displayed. If the request method is
622 POST the form will be evaluated. If the form is considered
623 correct the list gets created and otherwise the form with the data
624 filled in before the last POST request is returned. The user must
625 be logged in to create a new list.
627 mailing_list = None
628 choosable_domains = [('', _('Choose a Domain'))]
629 choosable_domains += _get_choosable_domains(request)
630 choosable_styles = _get_choosable_styles(request)
631 if request.method == 'POST':
632 form = ListNew(choosable_domains, choosable_styles, request.POST)
633 if form.is_valid():
634 # grab domain
635 domain = Domain.objects.get_or_404(
636 mail_host=form.cleaned_data['mail_host'])
637 # creating the list
638 try:
639 mailing_list = domain.create_list(
640 form.cleaned_data['listname'],
641 style_name=form.cleaned_data['list_style'])
642 mailing_list.add_owner(form.cleaned_data['list_owner'])
643 list_settings = mailing_list.settings
644 if form.cleaned_data['description']:
645 list_settings["description"] = \
646 form.cleaned_data['description']
647 list_settings["advertised"] = form.cleaned_data['advertised']
648 list_settings.save()
649 messages.success(request, _("List created"))
650 mailinglist_created.send(sender=List,
651 list_id=mailing_list.list_id)
652 return redirect("list_summary",
653 list_id=mailing_list.list_id)
654 # TODO catch correct Error class:
655 except HTTPError as e:
656 # Right now, there is no good way to detect that this is a
657 # duplicate mailing list request other than checking the
658 # reason for 400 error.
659 if e.reason == 'Mailing list exists':
660 form.add_error(
661 'listname', _('Mailing List already exists.'))
662 return render(request, template, {'form': form})
663 # Otherwise just render the generic error page.
664 return render(request, 'postorius/errors/generic.html',
665 {'error': e.msg})
666 else:
667 messages.error(request, _('Please check the errors below'))
668 else:
669 form = ListNew(choosable_domains, choosable_styles, initial={
670 'list_owner': request.user.email,
671 'advertised': True,
672 'list_style': _get_default_style(),
674 return render(request, template, {'form': form})
677 def _unique_lists(lists):
678 """Return unique lists from a list of mailing lists."""
679 return {mlist.list_id: mlist for mlist in lists}.values()
682 def _get_mail_host(web_host):
683 """Get the mail_host for a web_host if FILTER_VHOST is true and there's
684 only one mail_host for this web_host.
686 if not getattr(settings, 'FILTER_VHOST', False):
687 return None
688 mail_hosts = []
689 use_web_host = False
690 for domain in Domain.objects.all():
691 try:
692 if (MailDomain.objects.get(
693 mail_domain=domain.mail_host).site.domain == web_host):
694 if domain.mail_host not in mail_hosts:
695 mail_hosts.append(domain.mail_host)
696 except MailDomain.DoesNotExist:
697 use_web_host = True
698 if len(mail_hosts) == 1:
699 return mail_hosts[0]
700 elif len(mail_hosts) == 0 and use_web_host:
701 return web_host
702 else:
703 return None
706 @login_required
707 def list_index_authenticated(request):
708 """Index page for authenticated users.
710 Index page for authenticated users is slightly different than
711 un-authenticated ones. Authenticated users will see all their memberships
712 in the index page.
714 This view is not paginated and will show all the lists.
717 role = request.GET.get('role', None)
718 client = get_mailman_client()
719 choosable_domains = _get_choosable_domains(request)
721 # Get all the verified addresses of the user.
722 user_emails = EmailAddress.objects.filter(
723 user=request.user, verified=True).order_by(
724 "email").values_list("email", flat=True)
726 # Get all the mailing lists for the current user.
727 all_lists = []
728 mail_host = _get_mail_host(request.get_host().split(':')[0])
729 for user_email in user_emails:
730 try:
731 all_lists.extend(
732 client.find_lists(user_email,
733 role=role,
734 mail_host=mail_host,
735 count=sys.maxsize))
736 except HTTPError:
737 # No lists exist with the given role for the given user.
738 pass
739 # If the user has no list that they are subscriber/owner/moderator of, we
740 # just redirect them to the index page with all lists.
741 if len(all_lists) == 0 and role is None:
742 return redirect(reverse('list_index') + '?all-lists')
743 # Render the list index page with `check_advertised = False` since we don't
744 # need to check for advertised list given that all the users are related
745 # and know about the existence of the list anyway.
746 context = {
747 'lists': _unique_lists(all_lists),
748 'domain_count': len(choosable_domains),
749 'role': role,
750 'check_advertised': False,
752 return render(
753 request,
754 'postorius/index.html',
755 context
759 def list_index(request, template='postorius/index.html'):
760 """Show a table of all public mailing lists."""
761 # TODO maxking: Figure out why does this view accept POST request and why
762 # can't it be just a GET with list parameter.
763 if request.method == 'POST':
764 return redirect("list_summary", list_id=request.POST["list"])
765 # If the user is logged-in, show them only related lists in the index,
766 # except role is present in requests.GET.
767 if request.user.is_authenticated and 'all-lists' not in request.GET:
768 return list_index_authenticated(request)
770 def _get_list_page(count, page):
771 client = get_mailman_client()
772 advertised = not request.user.is_superuser
773 mail_host = _get_mail_host(request.get_host().split(":")[0])
774 return client.get_list_page(
775 advertised=advertised, mail_host=mail_host, count=count, page=page)
777 lists = paginate(
778 _get_list_page, request.GET.get('page'), request.GET.get('count'),
779 paginator_class=MailmanPaginator)
781 choosable_domains = _get_choosable_domains(request)
783 return render(request, template,
784 {'lists': lists,
785 'check_advertised': True,
786 'all_lists': True,
787 'domain_count': len(choosable_domains)})
790 @login_required
791 @list_owner_required
792 def list_delete(request, list_id):
793 """Deletes a list but asks for confirmation first.
795 the_list = List.objects.get_or_404(fqdn_listname=list_id)
796 if request.method == 'POST':
797 the_list.delete()
798 mailinglist_deleted.send(sender=List, list_id=list_id)
799 return redirect("list_index")
800 else:
801 submit_url = reverse('list_delete',
802 kwargs={'list_id': list_id})
803 cancel_url = reverse('list_index',)
804 return render(request, 'postorius/lists/confirm_delete.html',
805 {'submit_url': submit_url, 'cancel_url': cancel_url,
806 'list': the_list})
809 @login_required
810 @list_moderator_required
811 def list_pending_confirmations(request, list_id):
812 """Shows a list of subscription requests.
814 return _list_subscriptions(
815 request=request,
816 list_id=list_id,
817 token_owner=TokenOwner.subscriber,
818 template='postorius/lists/pending_confirmations.html',
819 page_title=_('Subscriptions pending user confirmation'),
823 @login_required
824 @list_moderator_required
825 def list_subscription_requests(request, list_id):
826 """Shows a list of subscription requests."""
827 return _list_subscriptions(
828 request=request,
829 list_id=list_id,
830 token_owner=TokenOwner.moderator,
831 template='postorius/lists/subscription_requests.html',
832 page_title=_('Subscriptions pending approval'),
836 def _list_subscriptions(request, list_id, token_owner, template, page_title):
837 m_list = List.objects.get_or_404(fqdn_listname=list_id)
838 requests = [req
839 for req in m_list.requests
840 if req['token_owner'] == token_owner]
841 paginated_requests = paginate(
842 requests,
843 request.GET.get('page'),
844 request.GET.get('count', 25))
845 page_subtitle = '(%d)' % len(requests)
846 return render(request, template,
847 {'list': m_list,
848 'paginated_requests': paginated_requests,
849 'page_title': page_title,
850 'page_subtitle': page_subtitle})
853 @login_required
854 @list_moderator_required
855 def handle_subscription_request(request, list_id, request_id, action):
857 Handle a subscription request. Possible actions:
858 - accept
859 - defer
860 - reject
861 - discard
863 confirmation_messages = {
864 'accept': _('The request has been accepted.'),
865 'reject': _('The request has been rejected.'),
866 'discard': _('The request has been discarded.'),
867 'defer': _('The request has been defered.'),
869 assert action in confirmation_messages
870 try:
871 m_list = List.objects.get_or_404(fqdn_listname=list_id)
872 # Moderate request and add feedback message to session.
873 m_list.moderate_request(request_id, action)
874 messages.success(request, confirmation_messages[action])
875 except HTTPError as e:
876 if e.code == 409:
877 messages.success(request,
878 _('The request was already moderated: %s')
879 % e.reason)
880 else:
881 messages.error(request, _('The request could not be moderated: %s')
882 % e.reason)
883 return redirect('list_subscription_requests', m_list.list_id)
886 SETTINGS_SECTION_NAMES = (
887 ('list_identity', _('List Identity')),
888 ('automatic_responses', _('Automatic Responses')),
889 ('alter_messages', _('Alter Messages')),
890 ('dmarc_mitigations', _('DMARC Mitigations')),
891 ('digest', _('Digest')),
892 ('message_acceptance', _('Message Acceptance')),
893 ('archiving', _('Archiving')),
894 ('subscription_policy', _('Member Policy')),
895 ('bounce_processing', _('Bounce Processing')),
898 SETTINGS_FORMS = {
899 'list_identity': ListIdentityForm,
900 'automatic_responses': ListAutomaticResponsesForm,
901 'alter_messages': AlterMessagesForm,
902 'dmarc_mitigations': DMARCMitigationsForm,
903 'digest': DigestSettingsForm,
904 'message_acceptance': MessageAcceptanceForm,
905 'archiving': ArchiveSettingsForm,
906 'subscription_policy': MemberPolicyForm,
907 'bounce_processing': BounceProcessingForm,
911 @login_required
912 @list_owner_required
913 def list_settings(request, list_id=None, visible_section=None,
914 template='postorius/lists/settings.html'):
916 View and edit the settings of a list.
917 The function requires the user to be logged in and have the
918 permissions necessary to perform the action.
920 Use /<NAMEOFTHESECTION>/<NAMEOFTHEOPTION>
921 to show only parts of the settings
922 <param> is optional / is used to differ in between section and option might
923 result in using //option
925 if visible_section is None:
926 visible_section = 'list_identity'
927 try:
928 form_class = SETTINGS_FORMS[visible_section]
929 except KeyError:
930 raise Http404('No such settings section')
931 m_list = List.objects.get_or_404(fqdn_listname=list_id)
932 list_settings = m_list.settings
933 initial_data = dict((key, value) for key, value in list_settings.items())
934 # List settings are grouped an processed in different forms.
935 if request.method == 'POST':
936 form = form_class(request.POST, mlist=m_list, initial=initial_data)
937 if form.is_valid():
938 try:
939 for key in form.changed_data:
940 if key in form_class.mlist_properties:
941 setattr(m_list, key, form.cleaned_data[key])
942 else:
943 list_settings[key] = form.cleaned_data[key]
944 list_settings.save()
945 messages.success(request, _('The settings have been updated.'))
946 mailinglist_modified.send(sender=List, list_id=m_list.list_id)
947 except HTTPError as e:
948 messages.error(
949 request,
950 _('An error occurred: ') + e.reason)
951 return redirect('list_settings', m_list.list_id, visible_section)
952 else:
953 form = form_class(initial=initial_data, mlist=m_list)
955 return render(request, template, {
956 'form': form,
957 'section_names': SETTINGS_SECTION_NAMES,
958 'list': m_list,
959 'visible_section': visible_section,
963 @login_required
964 @list_owner_required
965 def remove_role(request, list_id=None, role=None, address=None,
966 template='postorius/lists/confirm_remove_role.html'):
967 """Removes a list moderator or owner."""
968 the_list = List.objects.get_or_404(fqdn_listname=list_id)
969 redirect_on_success = redirect('list_members', the_list.list_id, role)
970 roster = getattr(the_list, '{}s'.format(role))
971 all_emails = [each.email for each in roster]
972 if address not in all_emails:
973 messages.error(request,
974 _('The user %(email)s is not in the %(role)s group')
975 % {'email': address, 'role': role})
976 return redirect('list_members', the_list.list_id, role)
978 if role == 'owner':
979 if len(roster) == 1:
980 messages.error(request, _('Removing the last owner is impossible'))
981 return redirect('list_members', the_list.list_id, role)
982 user_emails = EmailAddress.objects.filter(
983 user=request.user, verified=True).order_by(
984 "email").values_list("email", flat=True)
985 if address in user_emails:
986 # The user is removing themselves, redirect to the list info page
987 # because they won't have access to the members page anyway.
988 redirect_on_success = redirect('list_summary', the_list.list_id)
990 if request.method == 'POST':
991 try:
992 the_list.remove_role(role, address)
993 except HTTPError as e:
994 messages.error(request, _('The user could not be removed: %(msg)s')
995 % {'msg': e.msg})
996 return redirect('list_members', the_list.list_id, role)
997 messages.success(request, _('The user %(address)s has been removed'
998 ' from the %(role)s group.')
999 % {'address': address, 'role': role})
1000 return redirect_on_success
1001 return render(request, template,
1002 {'role': role, 'address': address,
1003 'list_id': the_list.list_id})
1006 @login_required
1007 @list_owner_required
1008 def remove_all_subscribers(request, list_id):
1010 """Empty the list by unsubscribing all members."""
1012 mlist = List.objects.get_or_404(fqdn_listname=list_id)
1013 if len(mlist.members) == 0:
1014 messages.error(request,
1015 _('No member is subscribed to the list currently.'))
1016 return redirect('mass_removal', mlist.list_id)
1017 if request.method == 'POST':
1018 try:
1019 # TODO maxking: This doesn't scale. Either the Core should provide
1020 # an endpoint to remove all subscribers or there should be some
1021 # better way to do this. Maybe, Core can take a list of email
1022 # addresses in batches of 50 and unsubscribe all of them.
1023 for names in mlist.members:
1024 mlist.unsubscribe(names.email)
1025 messages.success(request, _('All members have been'
1026 ' unsubscribed from the list.'))
1027 return redirect('list_members', mlist.list_id, 'subscriber')
1028 except Exception as e:
1029 messages.error(request, e)
1030 return render(request,
1031 'postorius/lists/confirm_removeall_subscribers.html',
1032 {'list': mlist})
1035 @login_required
1036 @list_owner_required
1037 def list_bans(request, list_id):
1038 return bans_view(
1039 request, list_id=list_id, template='postorius/lists/bans.html')
1042 @login_required
1043 @list_owner_required
1044 def list_header_matches(request, list_id):
1046 View and edit the list's header matches.
1048 m_list = List.objects.get_or_404(fqdn_listname=list_id)
1049 header_matches = m_list.header_matches
1050 HeaderMatchFormset = formset_factory(
1051 ListHeaderMatchForm, extra=1, can_delete=True, can_order=True,
1052 formset=ListHeaderMatchFormset)
1053 initial_data = [
1054 dict([
1055 (key, getattr(hm, key)) for key in ListHeaderMatchForm.base_fields
1056 ]) for hm in header_matches]
1058 # Process form submission.
1059 if request.method == 'POST':
1060 formset = HeaderMatchFormset(request.POST, initial=initial_data)
1061 if formset.is_valid():
1062 if not formset.has_changed():
1063 return redirect('list_header_matches', list_id)
1064 # Purge the existing header_matches
1065 header_matches.clear()
1066 # Add the ones in the form
1068 def form_order(f):
1069 # If ORDER is None (new header match), add it last.
1070 return f.cleaned_data.get('ORDER') or len(formset.forms)
1071 errors = []
1072 for form in sorted(formset, key=form_order):
1073 if 'header' not in form.cleaned_data:
1074 # The new header match form was not filled
1075 continue
1076 if form.cleaned_data.get('DELETE'):
1077 continue
1078 try:
1079 header_matches.add(
1080 header=form.cleaned_data['header'],
1081 pattern=form.cleaned_data['pattern'],
1082 action=form.cleaned_data['action'],
1084 except HTTPError as e:
1085 errors.append(e)
1086 for e in errors:
1087 messages.error(
1088 request, _('An error occurred: %s') % e.reason)
1089 if not errors:
1090 messages.success(request, _('The header matches were'
1091 ' successfully modified.'))
1092 return redirect('list_header_matches', list_id)
1093 else:
1094 formset = HeaderMatchFormset(initial=initial_data)
1095 # Adapt the last form to create new matches
1096 form_new = formset.forms[-1]
1097 form_new.fields['header'].widget.attrs['placeholder'] = _('New header')
1098 form_new.fields['pattern'].widget.attrs['placeholder'] = _('New pattern')
1099 del form_new.fields['ORDER']
1100 del form_new.fields['DELETE']
1102 return render(request, 'postorius/lists/header_matches.html', {
1103 'list': m_list,
1104 'formset': formset,