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