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)
11 # Postorius is distributed in the hope that it will be useful, but WITHOUT
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
16 # You should have received a copy of the GNU General Public License along with
17 # Postorius. If not, see <http://www.gnu.org/licenses/>.
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 (
46 from django_mailman3
.lib
.paginator
import MailmanPaginator
, paginate
47 from django_mailman3
.models
import MailDomain
48 from django_mailman3
.signals
import (
54 from postorius
.auth
.decorators
import (
55 list_moderator_required
,
59 from postorius
.auth
.mixins
import ListOwnerMixin
60 from postorius
.forms
import (
64 ChangeSubscriptionForm
,
67 ListAnonymousSubscribe
,
68 ListAutomaticResponsesForm
,
70 ListHeaderMatchFormset
,
79 MessageAcceptanceForm
,
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 (
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
)
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']
131 query
= '*{}*'.format(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')
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(
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
))
170 context
['empty_error'] = _(
171 'No {}s were found matching the search.'.format(role
)
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
)
182 members
= form
.cleaned_data
['choices']
183 for member
in members
:
184 self
.mailing_list
.unsubscribe(member
)
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():
199 self
.mailing_list
.add_role(
201 address
=member_form
.cleaned_data
['email'],
202 display_name
=member_form
.cleaned_data
['display_name'],
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
)
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
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
)
239 return self
._non
_member
_post
(request
, role
)
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
252 role
= request
.GET
.get('role')
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(
272 preferences
=member_prefs
,
273 disabled_delivery_choices
=DISABLED_DELIVERY_STATUS_CHOICES_ADMIN
, # noqa: E501
275 if preferences_form
.is_valid():
277 preferences_form
.save()
278 except HTTPError
as e
:
279 messages
.error(request
, e
.msg
)
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():
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
])
303 except HTTPError
as e
:
304 messages
.error(request
, e
.msg
)
309 "The member's moderation "
310 'settings have been updated.'
313 return redirect('list_member_options', list_id
, email
)
315 preferences_form
= UserPreferences(
316 preferences
=member_prefs
,
317 disabled_delivery_choices
=DISABLED_DELIVERY_STATUS_CHOICES_ADMIN
,
319 moderation_form
= MemberModeration(initial
=initial_moderation
)
324 'mm_member': mm_member
,
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
):
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
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
355 'hyperkitty' in settings
.INSTALLED_APPS
356 and 'hyperkitty' in archivers
# noqa: W504
357 and archivers
['hyperkitty'] # noqa: W504
359 data
['hyperkitty_enabled'] = True
360 if request
.user
.is_authenticated
:
362 EmailAddress
.objects
.filter(user
=request
.user
, verified
=True)
364 .values_list('email', flat
=True)
366 pending_requests
= [r
['email'] for r
in self
.mailing_list
.requests
]
369 for address
in user_emails
:
370 if address
in pending_requests
:
371 data
['user_request_pending'] = True
374 member
= self
.mailing_list
.get_member(address
)
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
)
399 if mm_user
.preferred_address
is None:
400 primary_email
= set_preferred(request
.user
, mm_user
)
402 primary_email
= mm_user
.preferred_address
.email
403 data
['subscribe_form'] = ListSubscribe(
405 user_id
=mm_user
.user_id
,
406 primary_email
=primary_email
,
410 data
['anonymous_subscription_form'] = ListAnonymousSubscribe()
411 return render(request
, 'postorius/lists/summary.html', data
)
414 class ChangeSubscriptionView(MailingListView
):
415 """Change mailing list subscription"""
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.
433 member
.subscription_mode
== SubscriptionMode
.as_address
.name
434 and self
._is
_email
(subscriber
)
436 member
.address
= subscriber
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
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(
449 # Since the email is already verified in Postorius.
451 # Since this user was already a Member, simply switching Email
452 # addresses shouldn't require another approval.
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
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
)
469 member_pref
[prop
] = val
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
:
479 def _is_subscribed(self
, member
, subscriber
, mm_user
):
480 """Check if the member is same as the subscriber.
483 - If subscriber is 'user_id' check if member's user_id is same as
485 - if subscriber is address, check if member's address is same as
489 member
.subscription_mode
== SubscriptionMode
.as_address
.name
490 and member
.email
== subscriber
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
):
500 EmailAddress
.objects
.filter(user
=request
.user
, verified
=True)
502 .values_list('email', flat
=True)
504 mm_user
= get_mailman_user(request
.user
)
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
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']
518 # Source membership doesn't exist, use should use subscribe
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
535 if self
._is
_subscribed
(member
, subscriber
, mm_user
):
536 messages
.error(request
, _('You are already subscribed'))
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(
548 _('Subscription changed to {}').format(subscriber
),
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.
575 EmailAddress
.objects
.filter(user
=request
.user
, verified
=True)
577 .values_list('email', flat
=True)
579 mm_user
= get_mailman_user(request
.user
)
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
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(
596 delivery_mode
=delivery_mode
,
597 delivery_status
=delivery_status
,
600 type(response
) == dict
601 and response
.get('token_owner') # noqa: W504
602 == TokenOwner
.moderator
607 'Your subscription request has been'
608 ' submitted and is waiting for moderator'
615 _('You are subscribed to %s.')
616 % self
.mailing_list
.fqdn_listname
,
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.
640 form
= ListAnonymousSubscribe(request
.POST
)
642 email
= form
.cleaned_data
.get('email')
643 display_name
= form
.cleaned_data
.get('display_name')
644 self
.mailing_list
.subscribe(
652 _('Please check your inbox for ' 'further instructions'),
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
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
):
682 'You have a pending unsubscription request waiting for'
683 ' moderator approval.'
686 return redirect('list_summary', self
.mailing_list
.list_id
)
688 response
= self
.mailing_list
.unsubscribe(email
)
689 if response
is not None and response
.get('token') is not None:
693 'Your unsubscription request has been'
694 ' submitted and is waiting for moderator'
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
)
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
)
715 for data
in form
.cleaned_data
['emails']:
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(
726 display_name
=display_name
,
731 send_welcome_message
=form
.cleaned_data
[
732 'send_welcome_message'
737 'The address %(address)s has been'
738 ' invited to %(list)s.'
742 'The address %(address)s has been'
743 ' subscribed to %(list)s.'
745 if not (p_v
and p_c
and p_a
):
747 ' Subscription may be pending address'
748 ' verification, confirmation or'
749 ' approval as appropriate.'
756 'list': mailing_list
.fqdn_listname
,
759 except HTTPError
as e
:
760 messages
.error(request
, e
)
761 except ValidationError
:
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
770 form
= ListMassSubscription()
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()
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.'))
799 for data
in form
.cleaned_data
['emails']:
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
)
809 self
.mailing_list
.mass_unsubscribe(valid_emails
)
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:
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
:
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
)
843 message_ids
= form
.cleaned_data
['choices']
845 if 'accept' in request
.POST
:
846 _perform_action(message_ids
, mailing_list
.accept_message
)
848 request
, _('The selected messages were accepted')
850 elif 'reject' in request
.POST
:
851 _perform_action(message_ids
, mailing_list
.reject_message
)
853 request
, _('The selected messages were rejected')
855 elif 'discard' in request
.POST
:
856 _perform_action(message_ids
, mailing_list
.discard_message
)
858 request
, _('The selected messages were discarded')
861 messages
.error(request
, _('Message could not be found'))
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
,
871 'list': mailing_list
,
872 'held_messages': held_messages
,
874 'ACTION_CHOICES': ACTION_CHOICES
,
876 return render(request
, 'postorius/lists/held_messages.html', context
)
879 @require_http_methods(['POST'])
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')
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
:
901 messages
.error(request
, _('Held message was not found.'))
902 return redirect('list_held_messages', list_id
)
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
915 'Moderation action for {} set to {}'.format(
916 member
, moderation_choices
[moderation_choice
]
923 _('Failed to set moderation action for {}'.format(msg
.sender
)),
925 return redirect('list_held_messages', list_id
)
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
)
939 for i
in mm_lists
.members
:
940 writer
.writerow([i
.email
])
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()
953 (style
['name'], style
['description']) for style
in styles
['styles']
958 def _get_default_style():
959 return Style
.objects
.all()['default']
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.
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
)
982 domain
= Domain
.objects
.get_or_404(
983 mail_host
=form
.cleaned_data
['mail_host']
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
[
997 list_settings
['advertised'] = form
.cleaned_data
['advertised']
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':
1011 'listname', _('Mailing List already exists.')
1013 return render(request
, template
, {'form': form
})
1014 # Otherwise just render the generic error page.
1016 request
, 'postorius/errors/generic.html', {'error': e
.msg
}
1019 messages
.error(request
, _('Please check the errors below'))
1025 'list_owner': request
.user
.email
,
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):
1045 use_web_host
= False
1046 for domain
in Domain
.objects
.all():
1049 MailDomain
.objects
.get(
1050 mail_domain
=domain
.mail_host
1054 if domain
.mail_host
not in mail_hosts
:
1055 mail_hosts
.append(domain
.mail_host
)
1056 except MailDomain
.DoesNotExist
:
1058 if len(mail_hosts
) == 1:
1059 return mail_hosts
[0]
1060 elif len(mail_hosts
) == 0 and use_web_host
:
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
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.
1087 all_lists
= client
.find_lists(
1088 user_id
, role
=role
, mail_host
=mail_host
, count
=sys
.maxsize
1091 # No lists exist with the given role for the given user.
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.
1101 'lists': _unique_lists(all_lists
),
1102 'domain_count': len(choosable_domains
),
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
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
))
1150 'check_advertised': True,
1152 'domain_count': domain_count
,
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':
1164 mailinglist_deleted
.send(sender
=List
, list_id
=list_id
)
1165 return redirect('list_index')
1167 submit_url
= reverse('list_delete', kwargs
={'list_id': list_id
})
1168 cancel_url
= reverse(
1173 'postorius/lists/confirm_delete.html',
1175 'submit_url': submit_url
,
1176 'cancel_url': cancel_url
,
1183 @list_moderator_required
1184 def list_pending_confirmations(request
, list_id
):
1185 """Shows a list of subscription requests."""
1186 return _list_subscriptions(
1189 token_owner
=TokenOwner
.subscriber
,
1190 template
='postorius/lists/pending_confirmations.html',
1191 page_title
=_('Subscriptions pending user confirmation'),
1196 @list_moderator_required
1197 def list_subscription_requests(request
, list_id
):
1198 """Shows a list of subscription requests."""
1199 return _list_subscriptions(
1202 token_owner
=TokenOwner
.moderator
,
1203 template
='postorius/lists/subscription_requests.html',
1204 page_title
=_('Subscriptions pending approval'),
1209 @list_moderator_required
1210 def list_unsubscription_requests(request
, list_id
):
1211 """Shows a list of pending unsubscription requests."""
1212 return _list_subscriptions(
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(
1228 request_type
='subscription',
1230 m_list
= List
.objects
.get_or_404(fqdn_listname
=list_id
)
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
)
1246 'paginated_requests': paginated_requests
,
1247 'page_title': page_title
,
1248 'page_subtitle': page_subtitle
,
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:
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
:
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
)
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')
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
:
1287 request
, _('The request was already moderated: %s') % e
.reason
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')),
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
,
1322 @list_owner_required
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'
1342 form_class
= SETTINGS_FORMS
[visible_section
]
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
)
1353 for key
in form
.changed_data
:
1354 if key
in form_class
.mlist_properties
:
1355 setattr(m_list
, key
, form
.cleaned_data
[key
])
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.
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
)
1378 form
= form_class(initial
=initial_data
, mlist
=m_list
)
1385 'section_names': SETTINGS_SECTION_NAMES
,
1387 'visible_section': visible_section
,
1393 @list_owner_required
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
:
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
)
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
)
1419 EmailAddress
.objects
.filter(user
=request
.user
, verified
=True)
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':
1430 the_list
.remove_role(role
, address
)
1431 except HTTPError
as e
:
1434 _('The user could not be removed: %(msg)s') % {'msg': e
.msg
},
1436 return redirect('list_members', the_list
.list_id
, role
)
1440 'The user %(address)s has been removed'
1441 ' from the %(role)s group.'
1443 % {'address': address
, 'role': role
},
1445 return redirect_on_success
1449 {'role': role
, 'address': address
, 'list_id': the_list
.list_id
},
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:
1461 request
, _('No member is subscribed to the list currently.')
1463 return redirect('mass_removal', mlist
.list_id
)
1464 if request
.method
== 'POST':
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
)
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
)
1481 'postorius/lists/confirm_removeall_subscribers.html',
1487 @list_owner_required
1488 def list_bans(request
, list_id
):
1490 request
, list_id
=list_id
, template
='postorius/lists/bans.html'
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
,
1507 formset
=ListHeaderMatchFormset
,
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
1530 # If ORDER is None (new header match), add it last.
1531 return f
.cleaned_data
.get('ORDER') or len(formset
.forms
)
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
1538 if form
.cleaned_data
.get('DELETE'):
1542 header
=form
.cleaned_data
['header'],
1543 pattern
=form
.cleaned_data
['pattern'],
1544 action
=form
.cleaned_data
['action'],
1546 except HTTPError
as e
:
1549 messages
.error(request
, _('An error occurred: %s') % e
.reason
)
1553 _('The header matches were' ' successfully modified.'),
1555 return redirect('list_header_matches', list_id
)
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']
1567 'postorius/lists/header_matches.html',
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
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():
1593 request
.path
, 400, _('Invalid confirmation token'), None, None
1595 token
= form
.cleaned_data
.get('token')
1597 pending_req
= m_list
.get_request(token
)
1598 except HTTPError
as e
:
1603 _('Token expired or invalid.'),
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')
1615 pending_req
= m_list
.get_request(token
)
1616 except HTTPError
as e
:
1619 request
.path
, 404, _('Token expired or invalid.'), None, None
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'):
1633 pending_req
.get('display_name')
1635 + pending_req
.get('email')
1639 addr
= pending_req
.get('email')
1643 'postorius/lists/confirm_token.html',