fix: Show the RFC2047-decoded name of the user in confirm_token view
[mailman-postorious.git] / src / postorius / views / list.py
blob2aad2206abf6d0f943e5fa9eb91ce138fb2b1662
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 1998-2023 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.utils.translation import gettext_lazy
38 from django.views.decorators.http import require_http_methods
40 from allauth.account.models import EmailAddress
41 from django_mailman3.lib.mailman import (
42 get_mailman_client,
43 get_mailman_user,
44 get_mailman_user_id,
46 from django_mailman3.lib.paginator import MailmanPaginator, paginate
47 from django_mailman3.models import MailDomain
48 from django_mailman3.signals import (
49 mailinglist_created,
50 mailinglist_deleted,
51 mailinglist_modified,
54 from postorius.auth.decorators import (
55 list_moderator_required,
56 list_owner_required,
57 superuser_required,
59 from postorius.auth.mixins import ListOwnerMixin
60 from postorius.forms import (
61 AlterMessagesForm,
62 ArchiveSettingsForm,
63 BounceProcessingForm,
64 ChangeSubscriptionForm,
65 DigestSettingsForm,
66 DMARCMitigationsForm,
67 ListAnonymousSubscribe,
68 ListAutomaticResponsesForm,
69 ListHeaderMatchForm,
70 ListHeaderMatchFormset,
71 ListIdentityForm,
72 ListMassRemoval,
73 ListMassSubscription,
74 ListNew,
75 ListSubscribe,
76 MemberForm,
77 MemberModeration,
78 MemberPolicyForm,
79 MessageAcceptanceForm,
80 MultipleChoiceForm,
81 UserPreferences,
83 from postorius.forms.fields import (
84 DELIVERY_MODE_CHOICES,
85 DELIVERY_STATUS_CHOICES,
87 from postorius.forms.list_forms import ACTION_CHOICES, TokenConfirmForm
88 from postorius.models import (
89 Domain,
90 List,
91 Mailman404Error,
92 Style,
93 SubscriptionMode,
95 from postorius.utils import get_member_or_nonmember, set_preferred
96 from postorius.views.generic import MailingListView, bans_view
99 logger = logging.getLogger(__name__)
102 #: DeliveryStatus field values that an admin cannot set.
103 DISABLED_DELIVERY_STATUS_CHOICES_ADMIN = ['by_bounces']
105 DELIVERY_MODE_DICT = dict(DELIVERY_MODE_CHOICES)
106 DELIVERY_STATUS_DICT = dict(DELIVERY_STATUS_CHOICES)
109 class TokenOwner:
110 """Who 'owns' the token returned from the registrar?"""
112 subscriber = 'subscriber'
113 moderator = 'moderator'
116 class ListMembersViews(ListOwnerMixin, MailingListView):
118 # List of allowed roles for the memberships. The string value matches the
119 # exact value Core's REST API expects.
120 allowed_roles = ['owner', 'moderator', 'member', 'nonmember']
122 def _prepare_query(self, request):
123 """Prepare regex based query to search partial email addresses.
125 Core's `members/find` API allows searching for memberships based on
126 regex. This methods prepares a valid regex to pass on the REST API.
128 if request.GET.get('q'):
129 query = request.GET['q']
130 if '*' not in query:
131 query = '*{}*'.format(query)
132 else:
133 query = ''
135 return query
137 def get(self, request, list_id, role):
138 """Handle GET for Member view.
140 This includes all the membership roles (self.allowed_roles).
142 member_form = MemberForm()
143 # If the role is misspelled, redirect to the default subscribers.
144 if role not in self.allowed_roles:
145 return redirect('list_members', list_id, 'member')
146 context = dict()
147 context['list'] = self.mailing_list
148 # Warning: role not translatable
149 context['role'] = role
150 context['member_form'] = member_form
151 context['page_title'] = _('List {}s'.format(role.capitalize()))
152 context['query'] = self._prepare_query(request)
154 def find_method(count, page):
155 return self.mailing_list.find_members(
156 context['query'], role=role, count=count, page=page
159 context['members'] = paginate(
160 find_method,
161 request.GET.get('page', 1),
162 request.GET.get('count', 25),
163 paginator_class=MailmanPaginator,
165 context['page_subtitle'] = '({})'.format(
166 context['members'].object_list.total_size
168 context['form_action'] = _('Add {}'.format(role))
169 if context['query']:
170 context['empty_error'] = _(
171 'No {}s were found matching the search.'.format(role)
173 else:
174 context['empty_error'] = _('List has no {}s'.format(role))
176 return render(request, 'postorius/lists/members.html', context)
178 def _member_post(self, request, role):
179 """Handle POST for members. Unsubscribe all the members selected."""
180 form = MultipleChoiceForm(request.POST)
181 if form.is_valid():
182 members = form.cleaned_data['choices']
183 for member in members:
184 self.mailing_list.unsubscribe(member)
185 messages.success(
186 request, _('The selected members have been unsubscribed')
188 return redirect('list_members', self.mailing_list.list_id, role)
190 def _non_member_post(self, request, role):
191 """Handle POST for membership roles owner, moderator and non-member.
193 Add memberships if the form is valid otherwise redirect to list_members
194 page with an error message.
196 member_form = MemberForm(request.POST)
197 if member_form.is_valid():
198 try:
199 self.mailing_list.add_role(
200 role=role,
201 address=member_form.cleaned_data['email'],
202 display_name=member_form.cleaned_data['display_name'],
204 messages.success(
205 request,
207 '{email} has been added with the role {role}'.format(
208 email=member_form.cleaned_data['email'], role=role
212 except HTTPError as e:
213 messages.error(request, e.msg)
214 else:
215 messages.error(request, member_form.errors)
216 return redirect('list_members', self.mailing_list.list_id, role)
218 def post(self, request, list_id, role=None):
219 """Handle POST for list members page.
221 List members page have more than one forms, depending on the membership
222 type.
224 - Regular subscribers have a MultipleChoiceForm, which returns a list
225 of emails that needs to be unsubscribed from the MailingList. This is
226 handled by :func:`self._member_post` method.
228 - Owners, moderators and non-members have a MemberForm which allows
229 adding new members with the given roles. This is handled by
230 :func:`self._non_member_post` method.
233 if role not in self.allowed_roles:
234 return redirect('list_members', list_id, 'member')
236 if role in ('member',):
237 return self._member_post(request, role)
238 else:
239 return self._non_member_post(request, role)
242 @login_required
243 @list_owner_required
244 def list_member_options(request, list_id, email):
245 template_name = 'postorius/lists/memberoptions.html'
246 mm_list = List.objects.get_or_404(fqdn_listname=list_id)
247 # If the role is specified explicitly, use that, otherwise the value is
248 # None and is equivalent to the value not being specified and the lookup
249 # happens using only email. This can cause issues when the email is
250 # subscribed as both a Non-Member and Owner/Moderator returning the wrong
251 # Member object.
252 role = request.GET.get('role')
253 try:
254 mm_member = mm_list.find_members(address=email, role=role)[0]
255 member_prefs = mm_member.preferences
256 except (ValueError, IndexError):
257 raise Http404(_('Member does not exist'))
258 except Mailman404Error:
259 return render(request, template_name, {'nolists': 'true'})
261 initial_moderation = dict(
263 (key, getattr(mm_member, key))
264 for key in MemberModeration.base_fields
268 if request.method == 'POST':
269 if request.POST.get('formname') == 'preferences':
270 preferences_form = UserPreferences(
271 request.POST,
272 preferences=member_prefs,
273 disabled_delivery_choices=DISABLED_DELIVERY_STATUS_CHOICES_ADMIN, # noqa: E501
275 if preferences_form.is_valid():
276 try:
277 preferences_form.save()
278 except HTTPError as e:
279 messages.error(request, e.msg)
280 else:
281 messages.success(
282 request,
283 _("The member's preferences" ' have been updated.'),
285 return redirect('list_member_options', list_id, email)
286 elif request.POST.get('formname') == 'moderation':
287 moderation_form = MemberModeration(
288 request.POST, initial=initial_moderation
290 if moderation_form.is_valid():
291 if not moderation_form.has_changed():
292 messages.info(
293 request, _("No change to the member's moderation.")
295 return redirect('list_member_options', list_id, email)
296 for key in list(moderation_form.fields.keys()):
297 # In general, it would be a very bad idea to loop over the
298 # fields and try to set them one by one, However,
299 # moderation form has only one field.
300 setattr(mm_member, key, moderation_form.cleaned_data[key])
301 try:
302 mm_member.save()
303 except HTTPError as e:
304 messages.error(request, e.msg)
305 else:
306 messages.success(
307 request,
309 "The member's moderation "
310 'settings have been updated.'
313 return redirect('list_member_options', list_id, email)
314 else:
315 preferences_form = UserPreferences(
316 preferences=member_prefs,
317 disabled_delivery_choices=DISABLED_DELIVERY_STATUS_CHOICES_ADMIN,
319 moderation_form = MemberModeration(initial=initial_moderation)
320 return render(
321 request,
322 template_name,
324 'mm_member': mm_member,
325 'list': mm_list,
326 'preferences_form': preferences_form,
327 'moderation_form': moderation_form,
332 class ListSummaryView(MailingListView):
333 """Shows common list metrics."""
335 def get(self, request, list_id):
336 data = {
337 'list': self.mailing_list,
338 'user_subscribed': False,
339 'subscribed_address': None,
340 'subscribed_preferred': False,
341 'public_archive': False,
342 'hyperkitty_enabled': False,
344 if self.mailing_list.settings['archive_policy'] == 'public':
345 data['public_archive'] = True
346 if (
347 getattr(settings, 'TESTING', False)
348 and 'hyperkitty' not in settings.INSTALLED_APPS # noqa: W504
350 # avoid systematic test failure when HyperKitty is installed
351 # (missing VCR request, see below).
352 list(self.mailing_list.archivers)
353 archivers = self.mailing_list.archivers
354 if (
355 'hyperkitty' in settings.INSTALLED_APPS
356 and 'hyperkitty' in archivers # noqa: W504
357 and archivers['hyperkitty'] # noqa: W504
358 ): # noqa: W504
359 data['hyperkitty_enabled'] = True
360 if request.user.is_authenticated:
361 user_emails = (
362 EmailAddress.objects.filter(user=request.user, verified=True)
363 .order_by('email')
364 .values_list('email', flat=True)
366 pending_requests = [r['email'] for r in self.mailing_list.requests]
368 subscriptions = []
369 for address in user_emails:
370 if address in pending_requests:
371 data['user_request_pending'] = True
372 continue
373 try:
374 member = self.mailing_list.get_member(address)
375 except ValueError:
376 pass
377 else:
378 subscriptions.append(
380 'subscribed_address': address,
381 'subscriber': member.member_id,
382 'subscribed_preferred': bool(
383 member.subscription_mode
384 == SubscriptionMode.as_user.name
386 'delivery_mode': DELIVERY_MODE_DICT.get(
387 member.preferences.get('delivery_mode')
389 'delivery_status': DELIVERY_STATUS_DICT.get(
390 member.preferences.get('delivery_status')
394 data['user_subscriptions'] = subscriptions
395 data['user_subscribed'] = bool(subscriptions)
397 mm_user = get_mailman_user(request.user)
398 primary_email = None
399 if mm_user.preferred_address is None:
400 primary_email = set_preferred(request.user, mm_user)
401 else:
402 primary_email = mm_user.preferred_address.email
403 data['subscribe_form'] = ListSubscribe(
404 user_emails,
405 user_id=mm_user.user_id,
406 primary_email=primary_email,
408 else:
409 user_emails = None
410 data['anonymous_subscription_form'] = ListAnonymousSubscribe()
411 return render(request, 'postorius/lists/summary.html', data)
414 class ChangeSubscriptionView(MailingListView):
415 """Change mailing list subscription"""
417 @staticmethod
418 def _is_email(subscriber):
419 """Is the subscriber an email address or uuid."""
420 return '@' in subscriber
422 def _change_subscription(self, member, subscriber):
423 """Switch subscriptions to a new email/user.
425 :param member: The currently subscriber Member.
426 :param subscriber: New email or user_id to switch subscription to.
428 # If the Membership is via preferred address and we are switching to an
429 # email or vice versa, we have to do the subscribe-unsubscribe dance.
430 # If the subscription is via an address, then we can simply PATCH the
431 # address in the Member resource.
432 if (
433 member.subscription_mode == SubscriptionMode.as_address.name
434 and self._is_email(subscriber)
436 member.address = subscriber
437 member.save()
438 return
439 # Keep a copy of the preferences so we can restore them after the
440 # Member switches addresses.
441 if member.preferences:
442 prefs = member.preferences
443 member.unsubscribe()
444 # Unless the subscriber is an an email and it is not-verified, we
445 # should get a Member resource here as response. We should never get to
446 # here with subscriber being an un-verified email.
447 new_member = self.mailing_list.subscribe(
448 subscriber,
449 # Since the email is already verified in Postorius.
450 pre_confirmed=True,
451 # Since this user was already a Member, simply switching Email
452 # addresses shouldn't require another approval.
453 pre_approved=True,
455 self._copy_preferences(new_member.preferences, prefs)
457 def _copy_preferences(self, member_pref, old_member_pref):
458 """Copy the preferences of the old member to new one.
460 :param member_pref: The new member's preference to copy preference to.
461 :param old_member_pref: The old_member's preferences to copy
462 preferences from.
464 # We can't simply switch the Preferences object, so we copy values from
465 # previous one to the new one.
466 for prop in old_member_pref._properties:
467 val = old_member_pref.get(prop)
468 if val:
469 member_pref[prop] = val
470 member_pref.save()
472 def _get_membership(self, mm_user, member_id):
473 """Get current memberships of the Mailman user."""
474 for sub in mm_user.subscriptions:
475 if sub.member_id == member_id:
476 return sub
477 return None
479 def _is_subscribed(self, member, subscriber, mm_user):
480 """Check if the member is same as the subscriber.
482 If they are not:
483 - If subscriber is 'user_id' check if member's user_id is same as
484 subscriber.
485 - if subscriber is address, check if member's address is same as
486 subscriber.
488 return (
489 member.subscription_mode == SubscriptionMode.as_address.name
490 and member.email == subscriber
491 ) or (
492 member.subscription_mode == SubscriptionMode.as_user.name
493 and member.user.user_id == subscriber
496 @method_decorator(login_required)
497 def post(self, request, list_id):
498 try:
499 user_emails = (
500 EmailAddress.objects.filter(user=request.user, verified=True)
501 .order_by('email')
502 .values_list('email', flat=True)
504 mm_user = get_mailman_user(request.user)
505 primary_email = None
506 if mm_user.preferred_address is not None:
507 primary_email = mm_user.preferred_address.email
508 form = ChangeSubscriptionForm(
509 user_emails, mm_user.user_id, primary_email, request.POST
512 if form.is_valid():
513 # Step 1: Get the source membership user wants to switch from.
514 member = self._get_membership(
515 mm_user, form.cleaned_data['member_id']
517 if member is None:
518 # Source membership doesn't exist, use should use subscribe
519 # form instead.
520 messages.error(
521 _('You are not subscribed to {}.').format(
522 self.mailing_list.fqdn_listname
525 # Not sure what the right place to return to here, since
526 # there is no on_get resource here. Return to
527 # per-subscription-preferences where the user can choose
528 # subscription to change.
529 return redirect('user_subscription_preferences')
530 subscriber = form.cleaned_data['subscriber']
531 # Step 2: Check if the source and destination subscriber are
532 # the same. This means, if the subscriber is a user_id, check
533 # they match and if the subscriber is a email address, check if
534 # they match.
535 if self._is_subscribed(member, subscriber, mm_user):
536 messages.error(request, _('You are already subscribed'))
537 else:
538 self._change_subscription(member, subscriber)
539 # If the subscriber is user_id (i.e. not an email
540 # address), Show 'Primary Address ()' in the success
541 # message instead of weird user id.
542 if not self._is_email(subscriber):
543 subscriber = _('Primary Address ({})').format(
544 primary_email
546 messages.success(
547 request,
548 _('Subscription changed to {}').format(subscriber),
550 else:
551 messages.error(
552 request,
553 _('Something went wrong. Please try again. {} {}').format(
554 form.errors, form.fields['subscriber'].choices
557 except HTTPError as e:
558 messages.error(request, e.msg)
559 return redirect('list_summary', self.mailing_list.list_id)
562 class ListSubscribeView(MailingListView):
564 view name: `list_subscribe`
567 @method_decorator(login_required)
568 def post(self, request, list_id):
570 Subscribes an email address to a mailing list via POST and
571 redirects to the `list_summary` view.
573 try:
574 user_emails = (
575 EmailAddress.objects.filter(user=request.user, verified=True)
576 .order_by('email')
577 .values_list('email', flat=True)
579 mm_user = get_mailman_user(request.user)
580 primary_email = None
581 if mm_user.preferred_address is not None:
582 primary_email = mm_user.preferred_address.email
583 form = ListSubscribe(
584 user_emails, mm_user.user_id, primary_email, request.POST
586 if form.is_valid():
587 subscriber = form.cleaned_data.get('subscriber')
588 display_name = form.cleaned_data.get('display_name')
589 delivery_mode = form.cleaned_data.get('delivery_mode')
590 delivery_status = form.cleaned_data.get('delivery_status')
591 response = self.mailing_list.subscribe(
592 subscriber,
593 display_name,
594 pre_verified=True,
595 pre_confirmed=True,
596 delivery_mode=delivery_mode,
597 delivery_status=delivery_status,
599 if (
600 type(response) == dict
601 and response.get('token_owner') # noqa: W504
602 == TokenOwner.moderator
604 messages.success(
605 request,
607 'Your subscription request has been'
608 ' submitted and is waiting for moderator'
609 ' approval.'
612 else:
613 messages.success(
614 request,
615 _('You are subscribed to %s.')
616 % self.mailing_list.fqdn_listname,
618 else:
619 messages.error(
620 request, _('Something went wrong. Please try again.')
622 except HTTPError as e:
623 messages.error(request, e.msg)
624 return redirect('list_summary', self.mailing_list.list_id)
627 class ListAnonymousSubscribeView(MailingListView):
629 view name: `list_anonymous_subscribe`
632 def post(self, request, list_id):
634 Subscribes an email address to a mailing list via POST and
635 redirects to the `list_summary` view.
636 This view is used for unauthenticated users and asks Mailman core to
637 verify the supplied email address.
639 try:
640 form = ListAnonymousSubscribe(request.POST)
641 if form.is_valid():
642 email = form.cleaned_data.get('email')
643 display_name = form.cleaned_data.get('display_name')
644 self.mailing_list.subscribe(
645 email,
646 display_name,
647 pre_verified=False,
648 pre_confirmed=False,
650 messages.success(
651 request,
652 _('Please check your inbox for ' 'further instructions'),
654 else:
655 messages.error(
656 request, _('Something went wrong. Please try again.')
658 except HTTPError as e:
659 messages.error(request, e.msg)
660 return redirect('list_summary', self.mailing_list.list_id)
663 class ListUnsubscribeView(MailingListView):
665 """Unsubscribe from a mailing list."""
667 @method_decorator(login_required)
668 def post(self, request, *args, **kwargs):
669 email = request.POST['email']
670 # Verify the user actually controls this email, should
671 # return 1 if the user owns the email, 0 otherwise.
672 found_email = EmailAddress.objects.filter(
673 user=request.user, email=email, verified=True
674 ).count()
675 if found_email == 0:
676 messages.error(request, _('You can only unsubscribe yourself.'))
677 return redirect('list_summary', self.mailing_list.list_id)
678 if self._has_pending_unsub_req(email):
679 messages.error(
680 request,
682 'You have a pending unsubscription request waiting for'
683 ' moderator approval.'
686 return redirect('list_summary', self.mailing_list.list_id)
687 try:
688 response = self.mailing_list.unsubscribe(email)
689 if response is not None and response.get('token') is not None:
690 messages.success(
691 request,
693 'Your unsubscription request has been'
694 ' submitted and is waiting for moderator'
695 ' approval.'
698 else:
699 messages.success(
700 request,
701 _('%s has been unsubscribed' ' from this list.') % email,
703 except ValueError as e:
704 messages.error(request, e)
705 return redirect('list_summary', self.mailing_list.list_id)
708 @login_required
709 @list_owner_required
710 def list_mass_subscribe(request, list_id):
711 mailing_list = List.objects.get_or_404(fqdn_listname=list_id)
712 if request.method == 'POST':
713 form = ListMassSubscription(request.POST)
714 if form.is_valid():
715 for data in form.cleaned_data['emails']:
716 try:
717 # Parse the data to get the address and the display name
718 display_name, address = email.utils.parseaddr(data)
719 validate_email(address)
720 p_v = form.cleaned_data['pre_verified']
721 p_c = form.cleaned_data['pre_confirmed']
722 p_a = form.cleaned_data['pre_approved']
723 inv = form.cleaned_data['invitation']
724 mailing_list.subscribe(
725 address=address,
726 display_name=display_name,
727 pre_verified=p_v,
728 pre_confirmed=p_c,
729 pre_approved=p_a,
730 invitation=inv,
731 send_welcome_message=form.cleaned_data[
732 'send_welcome_message'
735 if inv:
736 message = _(
737 'The address %(address)s has been'
738 ' invited to %(list)s.'
740 else:
741 message = _(
742 'The address %(address)s has been'
743 ' subscribed to %(list)s.'
745 if not (p_v and p_c and p_a):
746 message += _(
747 ' Subscription may be pending address'
748 ' verification, confirmation or'
749 ' approval as appropriate.'
751 messages.success(
752 request,
753 message
755 'address': address,
756 'list': mailing_list.fqdn_listname,
759 except HTTPError as e:
760 messages.error(request, e)
761 except ValidationError:
762 messages.error(
763 request,
764 _('The email address %s' ' is not valid.') % address,
766 # When the email address is bad, set the focus back to the
767 # email field after returning to the same page.
768 form.fields['emails'].widget.attrs['autofocus'] = True
769 else:
770 form = ListMassSubscription()
771 return render(
772 request,
773 'postorius/lists/mass_subscribe.html',
774 {'form': form, 'list': mailing_list},
778 class ListMassRemovalView(MailingListView):
779 """Class For Mass Removal"""
781 @method_decorator(login_required)
782 @method_decorator(list_owner_required)
783 def get(self, request, *args, **kwargs):
784 form = ListMassRemoval()
785 return render(
786 request,
787 'postorius/lists/mass_removal.html',
788 {'form': form, 'list': self.mailing_list},
791 @method_decorator(list_owner_required)
792 def post(self, request, *args, **kwargs):
793 form = ListMassRemoval(request.POST)
794 if not form.is_valid():
795 messages.error(request, _('Please fill out the form correctly.'))
796 else:
797 valid_emails = []
798 invalid_emails = []
799 for data in form.cleaned_data['emails']:
800 try:
801 # Parse the data to get the address.
802 address = email.utils.parseaddr(data)[1]
803 validate_email(address)
804 valid_emails.append(address)
805 except ValidationError:
806 invalid_emails.append(data)
808 try:
809 self.mailing_list.mass_unsubscribe(valid_emails)
810 messages.success(
811 request,
813 'These addresses {address} have been'
814 ' unsubscribed from {list}.'.format(
815 address=', '.join(valid_emails),
816 list=self.mailing_list.fqdn_listname,
820 except (HTTPError, ValueError) as e:
821 messages.error(request, e)
823 if len(invalid_emails) > 0:
824 messages.error(
825 request,
826 _('The email address %s is not valid.') % invalid_emails,
828 return redirect('mass_removal', self.mailing_list.list_id)
831 def _perform_action(message_ids, action):
832 for message_id in message_ids:
833 action(message_id)
836 @login_required
837 @list_moderator_required
838 def list_moderation(request, list_id, held_id=-1):
839 mailing_list = List.objects.get_or_404(fqdn_listname=list_id)
840 if request.method == 'POST':
841 form = MultipleChoiceForm(request.POST)
842 if form.is_valid():
843 message_ids = form.cleaned_data['choices']
844 try:
845 if 'accept' in request.POST:
846 _perform_action(message_ids, mailing_list.accept_message)
847 messages.success(
848 request, _('The selected messages were accepted')
850 elif 'reject' in request.POST:
851 _perform_action(message_ids, mailing_list.reject_message)
852 messages.success(
853 request, _('The selected messages were rejected')
855 elif 'discard' in request.POST:
856 _perform_action(message_ids, mailing_list.discard_message)
857 messages.success(
858 request, _('The selected messages were discarded')
860 except HTTPError:
861 messages.error(request, _('Message could not be found'))
862 else:
863 form = MultipleChoiceForm()
864 held_messages = paginate(
865 mailing_list.get_held_page,
866 request.GET.get('page'),
867 request.GET.get('count'),
868 paginator_class=MailmanPaginator,
870 context = {
871 'list': mailing_list,
872 'held_messages': held_messages,
873 'form': form,
874 'ACTION_CHOICES': ACTION_CHOICES,
876 return render(request, 'postorius/lists/held_messages.html', context)
879 @require_http_methods(['POST'])
880 @login_required
881 @list_moderator_required
882 def moderate_held_message(request, list_id):
883 """Moderate one held message"""
884 mailing_list = List.objects.get_or_404(fqdn_listname=list_id)
885 msg = mailing_list.get_held_message(request.POST['msgid'])
886 moderation_choice = request.POST.get('moderation_choice')
887 reason = request.POST.get('reason')
889 try:
890 if 'accept' in request.POST:
891 mailing_list.accept_message(msg.request_id)
892 messages.success(request, _('The message was accepted'))
893 elif 'reject' in request.POST:
894 mailing_list.reject_message(msg.request_id, reason=reason)
895 messages.success(request, _('The message was rejected'))
896 elif 'discard' in request.POST:
897 mailing_list.discard_message(msg.request_id)
898 messages.success(request, _('The message was discarded'))
899 except HTTPError as e:
900 if e.code == 404:
901 messages.error(request, _('Held message was not found.'))
902 return redirect('list_held_messages', list_id)
903 else:
904 raise
906 moderation_choices = dict(ACTION_CHOICES)
907 if moderation_choice in moderation_choices:
908 member = get_member_or_nonmember(mailing_list, msg.sender)
909 if member is not None:
910 member.moderation_action = moderation_choice
911 member.save()
912 messages.success(
913 request,
915 'Moderation action for {} set to {}'.format(
916 member, moderation_choices[moderation_choice]
920 else:
921 messages.error(
922 request,
923 _('Failed to set moderation action for {}'.format(msg.sender)),
925 return redirect('list_held_messages', list_id)
928 @login_required
929 @list_owner_required
930 def csv_view(request, list_id):
931 """Export all the subscriber in csv"""
932 mm_lists = List.objects.get_or_404(fqdn_listname=list_id)
934 response = HttpResponse(content_type='text/csv')
935 response['Content-Disposition'] = 'attachment; filename="Subscribers.csv"'
937 writer = csv.writer(response)
938 if mm_lists:
939 for i in mm_lists.members:
940 writer.writerow([i.email])
942 return response
945 def _get_choosable_domains(request):
946 domains = Domain.objects.all()
947 return [(d.mail_host, d.mail_host) for d in domains]
950 def _get_choosable_styles(request):
951 styles = Style.objects.all()
952 options = [
953 (style['name'], style['description']) for style in styles['styles']
955 return options
958 def _get_default_style():
959 return Style.objects.all()['default']
962 @login_required
963 @superuser_required
964 def list_new(request, template='postorius/lists/new.html'):
966 Add a new mailing list.
967 If the request to the function is a GET request an empty form for
968 creating a new list will be displayed. If the request method is
969 POST the form will be evaluated. If the form is considered
970 correct the list gets created and otherwise the form with the data
971 filled in before the last POST request is returned. The user must
972 be logged in to create a new list.
974 mailing_list = None
975 choosable_domains = [('', _('Choose a Domain'))]
976 choosable_domains += _get_choosable_domains(request)
977 choosable_styles = _get_choosable_styles(request)
978 if request.method == 'POST':
979 form = ListNew(choosable_domains, choosable_styles, request.POST)
980 if form.is_valid():
981 # grab domain
982 domain = Domain.objects.get_or_404(
983 mail_host=form.cleaned_data['mail_host']
985 # creating the list
986 try:
987 mailing_list = domain.create_list(
988 form.cleaned_data['listname'],
989 style_name=form.cleaned_data['list_style'],
991 mailing_list.add_owner(form.cleaned_data['list_owner'])
992 list_settings = mailing_list.settings
993 if form.cleaned_data['description']:
994 list_settings['description'] = form.cleaned_data[
995 'description'
997 list_settings['advertised'] = form.cleaned_data['advertised']
998 list_settings.save()
999 messages.success(request, _('List created'))
1000 mailinglist_created.send(
1001 sender=List, list_id=mailing_list.list_id
1003 return redirect('list_summary', list_id=mailing_list.list_id)
1004 # TODO catch correct Error class:
1005 except HTTPError as e:
1006 # Right now, there is no good way to detect that this is a
1007 # duplicate mailing list request other than checking the
1008 # reason for 400 error.
1009 if e.reason == 'Mailing list exists':
1010 form.add_error(
1011 'listname', _('Mailing List already exists.')
1013 return render(request, template, {'form': form})
1014 # Otherwise just render the generic error page.
1015 return render(
1016 request, 'postorius/errors/generic.html', {'error': e.msg}
1018 else:
1019 messages.error(request, _('Please check the errors below'))
1020 else:
1021 form = ListNew(
1022 choosable_domains,
1023 choosable_styles,
1024 initial={
1025 'list_owner': request.user.email,
1026 'advertised': True,
1027 'list_style': _get_default_style(),
1030 return render(request, template, {'form': form})
1033 def _unique_lists(lists):
1034 """Return unique lists from a list of mailing lists."""
1035 return {mlist.list_id: mlist for mlist in lists}.values()
1038 def _get_mail_host(web_host):
1039 """Get the mail_host for a web_host if FILTER_VHOST is true and there's
1040 only one mail_host for this web_host.
1042 if not getattr(settings, 'FILTER_VHOST', False):
1043 return None
1044 mail_hosts = []
1045 use_web_host = False
1046 for domain in Domain.objects.all():
1047 try:
1048 if (
1049 MailDomain.objects.get(
1050 mail_domain=domain.mail_host
1051 ).site.domain
1052 == web_host
1054 if domain.mail_host not in mail_hosts:
1055 mail_hosts.append(domain.mail_host)
1056 except MailDomain.DoesNotExist:
1057 use_web_host = True
1058 if len(mail_hosts) == 1:
1059 return mail_hosts[0]
1060 elif len(mail_hosts) == 0 and use_web_host:
1061 return web_host
1062 else:
1063 return None
1066 @login_required
1067 def list_index_authenticated(request):
1068 """Index page for authenticated users.
1070 Index page for authenticated users is slightly different than
1071 un-authenticated ones. Authenticated users will see all their memberships
1072 in the index page.
1074 This view is not paginated and will show all the lists.
1077 role = request.GET.get('role', None)
1078 client = get_mailman_client()
1079 choosable_domains = _get_choosable_domains(request)
1081 # Get the user_id of the current user
1082 user_id = get_mailman_user_id(request.user)
1084 mail_host = _get_mail_host(request.get_host().split(':')[0])
1085 # Get all the mailing lists for the current user.
1086 try:
1087 all_lists = client.find_lists(
1088 user_id, role=role, mail_host=mail_host, count=sys.maxsize
1090 except HTTPError:
1091 # No lists exist with the given role for the given user.
1092 all_lists = []
1093 # If the user has no list that they are subscriber/owner/moderator of, we
1094 # just redirect them to the index page with all lists.
1095 if len(all_lists) == 0 and role is None:
1096 return redirect(reverse('list_index') + '?all-lists')
1097 # Render the list index page with `check_advertised = False` since we don't
1098 # need to check for advertised list given that all the users are related
1099 # and know about the existence of the list anyway.
1100 context = {
1101 'lists': _unique_lists(all_lists),
1102 'domain_count': len(choosable_domains),
1103 'role': role,
1104 'check_advertised': False,
1106 return render(request, 'postorius/index.html', context)
1109 def list_index(request, template='postorius/index.html'):
1110 """Show a table of all public mailing lists."""
1111 # TODO maxking: Figure out why does this view accept POST request and why
1112 # can't it be just a GET with list parameter.
1113 if request.method == 'POST':
1114 return redirect('list_summary', list_id=request.POST['list'])
1115 # If the user is logged-in, show them only related lists in the index,
1116 # except role is present in requests.GET.
1117 if request.user.is_authenticated and 'all-lists' not in request.GET:
1118 return list_index_authenticated(request)
1120 def _get_list_page(count, page):
1121 client = get_mailman_client()
1122 advertised = not request.user.is_superuser
1123 mail_host = _get_mail_host(request.get_host().split(':')[0])
1124 return client.get_list_page(
1125 advertised=advertised, mail_host=mail_host, count=count, page=page
1128 lists = paginate(
1129 _get_list_page,
1130 request.GET.get('page'),
1131 request.GET.get('count'),
1132 paginator_class=MailmanPaginator,
1135 # This is just an optimization to skip un-necessary API
1136 # calls. postorius/index.html page shows the 'Create New Domain' button
1137 # when the logged-in user is a super user. There is no point making those
1138 # API calls if the user isn't a Superuser. So, just call the number 0 if
1139 # the user isn't SU.
1140 if request.user.is_superuser:
1141 domain_count = len(_get_choosable_domains(request))
1142 else:
1143 domain_count = 0
1145 return render(
1146 request,
1147 template,
1149 'lists': lists,
1150 'check_advertised': True,
1151 'all_lists': True,
1152 'domain_count': domain_count,
1157 @login_required
1158 @list_owner_required
1159 def list_delete(request, list_id):
1160 """Deletes a list but asks for confirmation first."""
1161 the_list = List.objects.get_or_404(fqdn_listname=list_id)
1162 if request.method == 'POST':
1163 the_list.delete()
1164 mailinglist_deleted.send(sender=List, list_id=list_id)
1165 return redirect('list_index')
1166 else:
1167 submit_url = reverse('list_delete', kwargs={'list_id': list_id})
1168 cancel_url = reverse(
1169 'list_index',
1171 return render(
1172 request,
1173 'postorius/lists/confirm_delete.html',
1175 'submit_url': submit_url,
1176 'cancel_url': cancel_url,
1177 'list': the_list,
1182 @login_required
1183 @list_moderator_required
1184 def list_pending_confirmations(request, list_id):
1185 """Shows a list of subscription requests."""
1186 return _list_subscriptions(
1187 request=request,
1188 list_id=list_id,
1189 token_owner=TokenOwner.subscriber,
1190 template='postorius/lists/pending_confirmations.html',
1191 page_title=_('Subscriptions pending user confirmation'),
1195 @login_required
1196 @list_moderator_required
1197 def list_subscription_requests(request, list_id):
1198 """Shows a list of subscription requests."""
1199 return _list_subscriptions(
1200 request=request,
1201 list_id=list_id,
1202 token_owner=TokenOwner.moderator,
1203 template='postorius/lists/subscription_requests.html',
1204 page_title=_('Subscriptions pending approval'),
1208 @login_required
1209 @list_moderator_required
1210 def list_unsubscription_requests(request, list_id):
1211 """Shows a list of pending unsubscription requests."""
1212 return _list_subscriptions(
1213 request=request,
1214 list_id=list_id,
1215 token_owner=TokenOwner.moderator,
1216 template='postorius/lists/subscription_requests.html',
1217 page_title=_('Un-subscriptions pending approval'),
1218 request_type='unsubscription',
1222 def _list_subscriptions(
1223 request,
1224 list_id,
1225 token_owner,
1226 template,
1227 page_title,
1228 request_type='subscription',
1230 m_list = List.objects.get_or_404(fqdn_listname=list_id)
1231 requests = [
1233 for req in m_list.get_requests(
1234 token_owner=token_owner, request_type=request_type
1237 paginated_requests = paginate(
1238 requests, request.GET.get('page'), request.GET.get('count', 25)
1240 page_subtitle = '(%d)' % len(requests)
1241 return render(
1242 request,
1243 template,
1245 'list': m_list,
1246 'paginated_requests': paginated_requests,
1247 'page_title': page_title,
1248 'page_subtitle': page_subtitle,
1253 @login_required
1254 @list_moderator_required
1255 @require_http_methods(['POST'])
1256 def handle_subscription_request(request, list_id, request_id):
1258 Handle a subscription request. Possible actions:
1259 - accept
1260 - defer
1261 - reject
1262 - discard
1264 confirmation_messages = {
1265 'accept': _('The request has been accepted.'),
1266 'reject': _('The request has been rejected.'),
1267 'discard': _('The request has been discarded.'),
1268 'defer': _('The request has been defered.'),
1270 # Get the action by the presence of one of the values in POST data.
1271 for each in confirmation_messages:
1272 if each in request.POST:
1273 action = each
1274 # If None of the actions were specified, just raise an error and return.
1275 m_list = List.objects.get_or_404(fqdn_listname=list_id)
1276 if action is None:
1277 messages.error(f'Invalid or missing action {action!r}')
1278 return redirect('list_subscription_requests', m_list.list_id)
1279 reason = request.POST.get('reason')
1280 try:
1281 # Moderate request and add feedback message to session.
1282 m_list.moderate_request(request_id, action, reason)
1283 messages.success(request, confirmation_messages[action])
1284 except HTTPError as e:
1285 if e.code == 409:
1286 messages.success(
1287 request, _('The request was already moderated: %s') % e.reason
1289 else:
1290 messages.error(
1291 request, _('The request could not be moderated: %s') % e.reason
1293 return redirect('list_subscription_requests', m_list.list_id)
1296 SETTINGS_SECTION_NAMES = (
1297 ('list_identity', gettext_lazy('List Identity')),
1298 ('automatic_responses', gettext_lazy('Automatic Responses')),
1299 ('alter_messages', gettext_lazy('Alter Messages')),
1300 ('dmarc_mitigations', gettext_lazy('DMARC Mitigations')),
1301 ('digest', gettext_lazy('Digest')),
1302 ('message_acceptance', gettext_lazy('Message Acceptance')),
1303 ('archiving', gettext_lazy('Archiving')),
1304 ('subscription_policy', gettext_lazy('Member Policy')),
1305 ('bounce_processing', gettext_lazy('Bounce Processing')),
1308 SETTINGS_FORMS = {
1309 'list_identity': ListIdentityForm,
1310 'automatic_responses': ListAutomaticResponsesForm,
1311 'alter_messages': AlterMessagesForm,
1312 'dmarc_mitigations': DMARCMitigationsForm,
1313 'digest': DigestSettingsForm,
1314 'message_acceptance': MessageAcceptanceForm,
1315 'archiving': ArchiveSettingsForm,
1316 'subscription_policy': MemberPolicyForm,
1317 'bounce_processing': BounceProcessingForm,
1321 @login_required
1322 @list_owner_required
1323 def list_settings(
1324 request,
1325 list_id=None,
1326 visible_section=None,
1327 template='postorius/lists/settings.html',
1330 View and edit the settings of a list.
1331 The function requires the user to be logged in and have the
1332 permissions necessary to perform the action.
1334 Use /<NAMEOFTHESECTION>/<NAMEOFTHEOPTION>
1335 to show only parts of the settings
1336 <param> is optional / is used to differ in between section and option might
1337 result in using //option
1339 if visible_section is None:
1340 visible_section = 'list_identity'
1341 try:
1342 form_class = SETTINGS_FORMS[visible_section]
1343 except KeyError:
1344 raise Http404('No such settings section')
1345 m_list = List.objects.get_or_404(fqdn_listname=list_id)
1346 list_settings = m_list.settings
1347 initial_data = dict((key, value) for key, value in list_settings.items())
1348 # List settings are grouped and processed in different forms.
1349 if request.method == 'POST':
1350 form = form_class(request.POST, mlist=m_list, initial=initial_data)
1351 if form.is_valid():
1352 try:
1353 for key in form.changed_data:
1354 if key in form_class.mlist_properties:
1355 setattr(m_list, key, form.cleaned_data[key])
1356 else:
1357 val = form.cleaned_data[key]
1358 # Empty list isn't really a valid value and Core
1359 # interprets empty string as empty value for
1360 # ListOfStringsField. We are doing it here instead of
1361 # ListOfStringsField.to_python() because output from
1362 # list of strings is expected to be a list and the NULL
1363 # value is hence an empty list. The serialization of
1364 # the empty list is for us empty string, hence we are
1365 # doing this here. Technically, it can be done outside
1366 # of this view, but there aren't any other use cases
1367 # where we'd want an empty list of strings.
1368 if val == []:
1369 val = ''
1370 list_settings[key] = val
1371 list_settings.save()
1372 messages.success(request, _('The settings have been updated.'))
1373 mailinglist_modified.send(sender=List, list_id=m_list.list_id)
1374 except HTTPError as e:
1375 messages.error(request, _('An error occurred: ') + e.reason)
1376 return redirect('list_settings', m_list.list_id, visible_section)
1377 else:
1378 form = form_class(initial=initial_data, mlist=m_list)
1380 return render(
1381 request,
1382 template,
1384 'form': form,
1385 'section_names': SETTINGS_SECTION_NAMES,
1386 'list': m_list,
1387 'visible_section': visible_section,
1392 @login_required
1393 @list_owner_required
1394 def remove_role(
1395 request,
1396 list_id=None,
1397 role=None,
1398 address=None,
1399 template='postorius/lists/confirm_remove_role.html',
1401 """Removes a list moderator or owner."""
1402 the_list = List.objects.get_or_404(fqdn_listname=list_id)
1403 redirect_on_success = redirect('list_members', the_list.list_id, role)
1404 roster = getattr(the_list, '{}s'.format(role))
1405 all_emails = [each.email for each in roster]
1406 if address not in all_emails:
1407 messages.error(
1408 request,
1409 _('The user %(email)s is not in the %(role)s group')
1410 % {'email': address, 'role': role},
1412 return redirect('list_members', the_list.list_id, role)
1414 if role == 'owner':
1415 if len(roster) == 1:
1416 messages.error(request, _('Removing the last owner is impossible'))
1417 return redirect('list_members', the_list.list_id, role)
1418 user_emails = (
1419 EmailAddress.objects.filter(user=request.user, verified=True)
1420 .order_by('email')
1421 .values_list('email', flat=True)
1423 if address in user_emails:
1424 # The user is removing themselves, redirect to the list info page
1425 # because they won't have access to the members page anyway.
1426 redirect_on_success = redirect('list_summary', the_list.list_id)
1428 if request.method == 'POST':
1429 try:
1430 the_list.remove_role(role, address)
1431 except HTTPError as e:
1432 messages.error(
1433 request,
1434 _('The user could not be removed: %(msg)s') % {'msg': e.msg},
1436 return redirect('list_members', the_list.list_id, role)
1437 messages.success(
1438 request,
1440 'The user %(address)s has been removed'
1441 ' from the %(role)s group.'
1443 % {'address': address, 'role': role},
1445 return redirect_on_success
1446 return render(
1447 request,
1448 template,
1449 {'role': role, 'address': address, 'list_id': the_list.list_id},
1453 @login_required
1454 @list_owner_required
1455 def remove_all_subscribers(request, list_id):
1456 """Empty the list by unsubscribing all members."""
1458 mlist = List.objects.get_or_404(fqdn_listname=list_id)
1459 if len(mlist.members) == 0:
1460 messages.error(
1461 request, _('No member is subscribed to the list currently.')
1463 return redirect('mass_removal', mlist.list_id)
1464 if request.method == 'POST':
1465 try:
1466 # TODO maxking: This doesn't scale. Either the Core should provide
1467 # an endpoint to remove all subscribers or there should be some
1468 # better way to do this. Maybe, Core can take a list of email
1469 # addresses in batches of 50 and unsubscribe all of them.
1470 for names in mlist.members:
1471 mlist.unsubscribe(names.email)
1472 messages.success(
1473 request,
1474 _('All members have been' ' unsubscribed from the list.'),
1476 return redirect('list_members', mlist.list_id, 'subscriber')
1477 except Exception as e:
1478 messages.error(request, e)
1479 return render(
1480 request,
1481 'postorius/lists/confirm_removeall_subscribers.html',
1482 {'list': mlist},
1486 @login_required
1487 @list_owner_required
1488 def list_bans(request, list_id):
1489 return bans_view(
1490 request, list_id=list_id, template='postorius/lists/bans.html'
1494 @login_required
1495 @list_owner_required
1496 def list_header_matches(request, list_id):
1498 View and edit the list's header matches.
1500 m_list = List.objects.get_or_404(fqdn_listname=list_id)
1501 header_matches = m_list.header_matches
1502 HeaderMatchFormset = formset_factory(
1503 ListHeaderMatchForm,
1504 extra=1,
1505 can_delete=True,
1506 can_order=True,
1507 formset=ListHeaderMatchFormset,
1509 initial_data = [
1510 dict(
1512 (key, getattr(hm, key))
1513 for key in ListHeaderMatchForm.base_fields
1516 for hm in header_matches
1519 # Process form submission.
1520 if request.method == 'POST':
1521 formset = HeaderMatchFormset(request.POST, initial=initial_data)
1522 if formset.is_valid():
1523 if not formset.has_changed():
1524 return redirect('list_header_matches', list_id)
1525 # Purge the existing header_matches
1526 header_matches.clear()
1527 # Add the ones in the form
1529 def form_order(f):
1530 # If ORDER is None (new header match), add it last.
1531 return f.cleaned_data.get('ORDER') or len(formset.forms)
1533 errors = []
1534 for form in sorted(formset, key=form_order):
1535 if 'header' not in form.cleaned_data:
1536 # The new header match form was not filled
1537 continue
1538 if form.cleaned_data.get('DELETE'):
1539 continue
1540 try:
1541 header_matches.add(
1542 header=form.cleaned_data['header'],
1543 pattern=form.cleaned_data['pattern'],
1544 action=form.cleaned_data['action'],
1546 except HTTPError as e:
1547 errors.append(e)
1548 for e in errors:
1549 messages.error(request, _('An error occurred: %s') % e.reason)
1550 if not errors:
1551 messages.success(
1552 request,
1553 _('The header matches were' ' successfully modified.'),
1555 return redirect('list_header_matches', list_id)
1556 else:
1557 formset = HeaderMatchFormset(initial=initial_data)
1558 # Adapt the last form to create new matches
1559 form_new = formset.forms[-1]
1560 form_new.fields['header'].widget.attrs['placeholder'] = _('New header')
1561 form_new.fields['pattern'].widget.attrs['placeholder'] = _('New pattern')
1562 del form_new.fields['ORDER']
1563 del form_new.fields['DELETE']
1565 return render(
1566 request,
1567 'postorius/lists/header_matches.html',
1569 'list': m_list,
1570 'formset': formset,
1575 def confirm_token(request, list_id):
1576 """Confirm token sent via Mailman Core.
1578 This view allows confirming tokens sent via Mailman Core for confirming
1579 subscriptions or verifying email addresses. This is an un-authenticated
1580 view and the authentication is done by assuming the secrecy of the token
1581 sent.
1583 :param request: The Django request object.
1584 :param list_id: The current mailinglist id.
1587 m_list = List.objects.get_or_404(fqdn_listname=list_id)
1588 if request.method == 'POST':
1589 form = TokenConfirmForm(request.POST)
1590 # If the token is None or something, just raise 400 error.
1591 if not form.is_valid():
1592 raise HTTPError(
1593 request.path, 400, _('Invalid confirmation token'), None, None
1595 token = form.cleaned_data.get('token')
1596 try:
1597 pending_req = m_list.get_request(token)
1598 except HTTPError as e:
1599 if e.code == 404:
1600 raise HTTPError(
1601 request.path,
1602 404,
1603 _('Token expired or invalid.'),
1604 None,
1605 None,
1607 raise
1608 # Since we only accept the token there isn't any need for Form data. We
1609 # just need a POST request at this URL to accept the token.
1610 m_list.moderate_request(token, action='accept')
1611 return redirect('list_summary', m_list.list_id)
1612 # Get the token from url parameter.
1613 token = request.GET.get('token')
1614 try:
1615 pending_req = m_list.get_request(token)
1616 except HTTPError as e:
1617 if e.code == 404:
1618 raise HTTPError(
1619 request.path, 404, _('Token expired or invalid.'), None, None
1621 raise
1623 # If this is a token pending moderator approval, they need to login and
1624 # approve it from the pending requests page.
1625 if pending_req.get('token_owner') == 'moderator':
1626 return redirect('list_subscription_requests', m_list.list_id)
1628 token_type = pending_req.get('type')
1629 form = TokenConfirmForm(initial=dict(token=token))
1630 # Show the display_name if it is not None or "".
1631 if pending_req.get('display_name'):
1632 addr = (
1633 pending_req.get('display_name')
1634 + ' <'
1635 + pending_req.get('email')
1636 + '>'
1638 else:
1639 addr = pending_req.get('email')
1641 return render(
1642 request,
1643 'postorius/lists/confirm_token.html',
1645 'mlist': m_list,
1646 'addr': addr,
1647 'token': token,
1648 'type': token_type,
1649 'form': form,