Check for MailingList exists error.
[mailman-postorious.git] / src / postorius / views / list.py
blob6dd9f25bef4fc298fe44d72d769118773925e944
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 1998-2018 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
24 from allauth.account.models import EmailAddress
25 from django.http import HttpResponse, HttpResponseNotAllowed, Http404
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.validators import validate_email
30 from django.forms import formset_factory
31 from django.shortcuts import render, redirect
32 from django.core.exceptions import ValidationError
33 from django.utils.decorators import method_decorator
34 from django.utils.translation import gettext as _
35 from django_mailman3.lib.mailman import get_mailman_client
36 from django_mailman3.lib.paginator import paginate, MailmanPaginator
37 from django.utils.six.moves.urllib.error import HTTPError
39 from postorius.forms import (
40 ListNew, MemberForm, ListSubscribe, MultipleChoiceForm, UserPreferences,
41 ListSubscriptionPolicyForm, ArchiveSettingsForm, MessageAcceptanceForm,
42 DigestSettingsForm, AlterMessagesForm, ListAutomaticResponsesForm,
43 ListIdentityForm, ListMassSubscription, ListMassRemoval, ListAddBanForm,
44 ListHeaderMatchForm, ListHeaderMatchFormset, MemberModeration,
45 DMARCMitigationsForm, ListAnonymousSubscribe)
46 from postorius.models import Domain, List, Mailman404Error, Style
47 from postorius.auth.decorators import (
48 list_owner_required, list_moderator_required, superuser_required)
49 from postorius.views.generic import MailingListView
51 try:
52 from django.core.urlresolvers import reverse
53 except ImportError:
54 from django.urls import reverse
57 logger = logging.getLogger(__name__)
60 @login_required
61 @list_owner_required
62 def list_members_view(request, list_id, role=None):
63 """Display all members of a given list."""
64 if role not in ['owner', 'moderator', 'subscriber']:
65 return redirect('list_members', list_id, 'subscriber')
66 mailing_list = List.objects.get_or_404(fqdn_listname=list_id)
67 if request.method == 'POST':
68 if role == 'subscriber':
69 form = MultipleChoiceForm(request.POST)
70 if form.is_valid():
71 members = form.cleaned_data['choices']
72 for member in members:
73 mailing_list.unsubscribe(member)
74 messages.success(request, _('The selected members'
75 ' have been unsubscribed'))
76 return redirect('list_members', list_id, role)
77 else:
78 member_form = MemberForm(request.POST)
79 if member_form.is_valid():
80 try:
81 getattr(mailing_list, 'add_%s' % role)(
82 member_form.cleaned_data['email'])
83 messages.success(
84 request, _('%(email)s has been added'
85 ' with the role %(role)s')
86 % {'email': member_form.cleaned_data['email'],
87 'role': role})
88 return redirect('list_members', list_id, role)
89 except HTTPError as e:
90 messages.error(request, _(e.msg))
91 else:
92 form = MultipleChoiceForm()
93 member_form = MemberForm()
94 context = {
95 'list': mailing_list,
96 'role': role,
98 if role == 'subscriber':
99 context['page_title'] = _('List subscribers')
100 if request.GET.get('q'):
101 query = context['query'] = request.GET['q']
102 if "*" not in query:
103 query = '*{}*'.format(query)
105 # Proxy the find_members method to insert the query
106 def find_method(count, page):
107 return mailing_list.find_members(query, count=count, page=page)
108 else:
109 find_method = mailing_list.get_member_page
110 context['members'] = paginate(
111 find_method,
112 request.GET.get('page'),
113 request.GET.get('count', 25),
114 paginator_class=MailmanPaginator)
115 if mailing_list.member_count == 0:
116 context['empty_error'] = _('List has no Subscribers')
117 else:
118 context['empty_error'] =\
119 _('No member was found matching the search')
120 context['form'] = form
121 else:
122 context['member_form'] = member_form
123 if role == 'owner':
124 context['page_title'] = _('List owners')
125 context['members'] = mailing_list.owners
126 context['form_action'] = _('Add owner')
127 elif role == 'moderator':
128 context['page_title'] = _('List moderators')
129 context['members'] = mailing_list.moderators
130 context['empty_error'] = _('List has no moderators')
131 context['form_action'] = _('Add moderator')
132 return render(request, 'postorius/lists/members.html', context)
135 @login_required
136 @list_owner_required
137 def list_member_options(request, list_id, email):
138 template_name = 'postorius/lists/memberoptions.html'
139 client = get_mailman_client()
140 mm_list = List.objects.get_or_404(fqdn_listname=list_id)
141 try:
142 mm_member = client.get_member(list_id, email)
143 member_prefs = mm_member.preferences
144 except ValueError:
145 raise Http404(_('Member does not exist'))
146 except Mailman404Error:
147 return render(request, template_name, {'nolists': 'true'})
148 initial_moderation = dict([
149 (key, getattr(mm_member, key)) for key in MemberModeration.base_fields
151 if request.method == 'POST':
152 if request.POST.get("formname") == 'preferences':
153 moderation_form = MemberModeration(initial=initial_moderation)
154 preferences_form = UserPreferences(
155 request.POST, initial=member_prefs)
156 if preferences_form.is_valid():
157 if not preferences_form.has_changed():
158 messages.info(request,
159 _("No change to the member's preferences."))
160 return redirect('list_member_options', list_id, email)
161 for key in list(preferences_form.fields.keys()):
162 member_prefs[key] = preferences_form.cleaned_data[key]
163 try:
164 member_prefs.save()
165 except HTTPError as e:
166 messages.error(request, e.msg)
167 else:
168 messages.success(request, _("The member's preferences have"
169 " been updated."))
170 return redirect('list_member_options', list_id, email)
171 elif request.POST.get("formname") == 'moderation':
172 preferences_form = UserPreferences(initial=member_prefs)
173 moderation_form = MemberModeration(
174 request.POST, initial=initial_moderation)
175 if moderation_form.is_valid():
176 if not moderation_form.has_changed():
177 messages.info(request,
178 _("No change to the member's moderation."))
179 return redirect('list_member_options', list_id, email)
180 for key in list(moderation_form.fields.keys()):
181 setattr(mm_member, key, moderation_form.cleaned_data[key])
182 try:
183 mm_member.save()
184 except HTTPError as e:
185 messages.error(request, e.msg)
186 else:
187 messages.success(request, _("The member's moderation "
188 "settings have been updated."))
189 return redirect('list_member_options', list_id, email)
190 else:
191 preferences_form = UserPreferences(initial=member_prefs)
192 moderation_form = MemberModeration(initial=initial_moderation)
193 return render(request, template_name, {
194 'mm_member': mm_member,
195 'list': mm_list,
196 'preferences_form': preferences_form,
197 'moderation_form': moderation_form,
201 class ListSummaryView(MailingListView):
202 """Shows common list metrics.
205 def get(self, request, list_id):
206 data = {'list': self.mailing_list,
207 'user_subscribed': False,
208 'subscribed_address': None,
209 'public_archive': False,
210 'hyperkitty_enabled': False}
211 if self.mailing_list.settings['archive_policy'] == 'public':
212 data['public_archive'] = True
213 if getattr(settings, 'TESTING', False) and \
214 'hyperkitty' not in settings.INSTALLED_APPS:
215 # avoid systematic test failure when HyperKitty is installed
216 # (missing VCR request, see below).
217 list(self.mailing_list.archivers)
218 if ('hyperkitty' in settings.INSTALLED_APPS and
219 'hyperkitty' in self.mailing_list.archivers and
220 self.mailing_list.archivers['hyperkitty']):
221 data['hyperkitty_enabled'] = True
222 if request.user.is_authenticated:
223 user_emails = EmailAddress.objects.filter(
224 user=request.user, verified=True).order_by(
225 "email").values_list("email", flat=True)
226 pending_requests = [r['email'] for r in self.mailing_list.requests]
227 for address in user_emails:
228 if address in pending_requests:
229 data['user_request_pending'] = True
230 break
231 try:
232 self.mailing_list.get_member(address)
233 except ValueError:
234 pass
235 else:
236 data['user_subscribed'] = True
237 data['subscribed_address'] = address
238 break # no need to test more addresses
239 data['subscribe_form'] = ListSubscribe(user_emails)
240 else:
241 user_emails = None
242 data['anonymous_subscription_form'] = ListAnonymousSubscribe()
243 return render(request, 'postorius/lists/summary.html', data)
246 class ChangeSubscriptionView(MailingListView):
247 """Change mailing list subscription
250 @method_decorator(login_required)
251 def post(self, request, list_id):
252 try:
253 user_emails = EmailAddress.objects.filter(
254 user=request.user, verified=True).order_by(
255 "email").values_list("email", flat=True)
256 form = ListSubscribe(user_emails, request.POST)
257 # Find the currently subscribed email
258 old_email = None
259 for address in user_emails:
260 try:
261 self.mailing_list.get_member(address)
262 except ValueError:
263 pass
264 else:
265 old_email = address
266 break # no need to test more addresses
267 assert old_email is not None
268 if form.is_valid():
269 email = form.cleaned_data['email']
270 if old_email == email:
271 messages.error(request, _('You are already subscribed'))
272 else:
273 self.mailing_list.unsubscribe(old_email)
274 # Since the action is done via the web UI, no email
275 # confirmation is needed.
276 response = self.mailing_list.subscribe(
277 email, pre_confirmed=True)
278 if (type(response) == dict and
279 response.get('token_owner') == 'moderator'):
280 messages.success(
281 request, _('Your request to change the email for'
282 ' this subscription was submitted and'
283 ' is waiting for moderator approval.'))
284 else:
285 messages.success(request,
286 _('Subscription changed to %s') %
287 email)
288 else:
289 messages.error(request,
290 _('Something went wrong. Please try again.'))
291 except HTTPError as e:
292 messages.error(request, e.msg)
293 return redirect('list_summary', self.mailing_list.list_id)
296 class ListSubscribeView(MailingListView):
298 view name: `list_subscribe`
301 @method_decorator(login_required)
302 def post(self, request, list_id):
304 Subscribes an email address to a mailing list via POST and
305 redirects to the `list_summary` view.
307 try:
308 user_emails = EmailAddress.objects.filter(
309 user=request.user, verified=True).order_by(
310 "email").values_list("email", flat=True)
311 form = ListSubscribe(user_emails, request.POST)
312 if form.is_valid():
313 email = request.POST.get('email')
314 response = self.mailing_list.subscribe(
315 email, pre_verified=True, pre_confirmed=True)
316 if (type(response) == dict and
317 response.get('token_owner') == 'moderator'):
318 messages.success(
319 request, _('Your subscription request has been'
320 ' submitted and is waiting for moderator'
321 ' approval.'))
322 else:
323 messages.success(request,
324 _('You are subscribed to %s.') %
325 self.mailing_list.fqdn_listname)
326 else:
327 messages.error(request,
328 _('Something went wrong. Please try again.'))
329 except HTTPError as e:
330 messages.error(request, e.msg)
331 return redirect('list_summary', self.mailing_list.list_id)
334 class ListAnonymousSubscribeView(MailingListView):
336 view name: `list_anonymous_subscribe`
339 def post(self, request, list_id):
341 Subscribes an email address to a mailing list via POST and
342 redirects to the `list_summary` view.
343 This view is used for unauthenticated users and asks Mailman core to
344 verify the supplied email address.
346 try:
347 form = ListAnonymousSubscribe(request.POST)
348 if form.is_valid():
349 email = form.cleaned_data.get('email')
350 self.mailing_list.subscribe(email, pre_verified=False,
351 pre_confirmed=False)
352 messages.success(request, _('Please check your inbox for '
353 'further instructions'))
354 else:
355 messages.error(request,
356 _('Something went wrong. Please try again.'))
357 except HTTPError as e:
358 messages.error(request, e.msg)
359 return redirect('list_summary', self.mailing_list.list_id)
362 class ListUnsubscribeView(MailingListView):
364 """Unsubscribe from a mailing list."""
366 @method_decorator(login_required)
367 def post(self, request, *args, **kwargs):
368 email = request.POST['email']
369 try:
370 self.mailing_list.unsubscribe(email)
371 messages.success(request, _('%s has been unsubscribed'
372 ' from this list.') % email)
373 except ValueError as e:
374 messages.error(request, e)
375 return redirect('list_summary', self.mailing_list.list_id)
378 @login_required
379 @list_owner_required
380 def list_mass_subscribe(request, list_id):
381 mailing_list = List.objects.get_or_404(fqdn_listname=list_id)
382 if request.method == 'POST':
383 form = ListMassSubscription(request.POST)
384 if form.is_valid():
385 for data in form.cleaned_data['emails']:
386 try:
387 # Parse the data to get the address and the display name
388 display_name, address = email.utils.parseaddr(data)
389 validate_email(address)
390 mailing_list.subscribe(address=address,
391 display_name=display_name,
392 pre_verified=True,
393 pre_confirmed=True,
394 pre_approved=True)
395 messages.success(
396 request, _('The address %(address)s has been'
397 ' subscribed to %(list)s.') %
398 {'address': address,
399 'list': mailing_list.fqdn_listname})
400 except HTTPError as e:
401 messages.error(request, e)
402 except ValidationError:
403 messages.error(request, _('The email address %s'
404 ' is not valid.') % address)
405 else:
406 form = ListMassSubscription()
407 return render(request, 'postorius/lists/mass_subscribe.html',
408 {'form': form, 'list': mailing_list})
411 class ListMassRemovalView(MailingListView):
413 """Class For Mass Removal"""
415 @method_decorator(login_required)
416 @method_decorator(list_owner_required)
417 def get(self, request, *args, **kwargs):
418 form = ListMassRemoval()
419 return render(request, 'postorius/lists/mass_removal.html',
420 {'form': form, 'list': self.mailing_list})
422 @method_decorator(list_owner_required)
423 def post(self, request, *args, **kwargs):
424 form = ListMassRemoval(request.POST)
425 if not form.is_valid():
426 messages.error(request, _('Please fill out the form correctly.'))
427 else:
428 for address in form.cleaned_data['emails']:
429 try:
430 validate_email(address)
431 self.mailing_list.unsubscribe(address.lower())
432 messages.success(
433 request, _('The address %(address)s has been'
434 ' unsubscribed from %(list)s.') %
435 {'address': address,
436 'list': self.mailing_list.fqdn_listname})
437 except (HTTPError, ValueError) as e:
438 messages.error(request, e)
439 except ValidationError:
440 messages.error(request, _('The email address %s'
441 ' is not valid.') % address)
442 return redirect('mass_removal', self.mailing_list.list_id)
445 def _perform_action(message_ids, action):
446 for message_id in message_ids:
447 action(message_id)
450 @login_required
451 @list_moderator_required
452 def list_moderation(request, list_id, held_id=-1):
453 mailing_list = List.objects.get_or_404(fqdn_listname=list_id)
454 if request.method == 'POST':
455 form = MultipleChoiceForm(request.POST)
456 if form.is_valid():
457 message_ids = form.cleaned_data['choices']
458 try:
459 if 'accept' in request.POST:
460 _perform_action(message_ids, mailing_list.accept_message)
461 messages.success(request,
462 _('The selected messages were accepted'))
463 elif 'reject' in request.POST:
464 _perform_action(message_ids, mailing_list.reject_message)
465 messages.success(request,
466 _('The selected messages were rejected'))
467 elif 'discard' in request.POST:
468 _perform_action(message_ids, mailing_list.discard_message)
469 messages.success(request,
470 _('The selected messages were discarded'))
471 except HTTPError:
472 messages.error(request, _('Message could not be found'))
473 else:
474 form = MultipleChoiceForm()
475 held_messages = paginate(
476 mailing_list.get_held_page,
477 request.GET.get('page'), request.GET.get('count'),
478 paginator_class=MailmanPaginator)
479 context = {
480 'list': mailing_list,
481 'held_messages': held_messages,
482 'form': form,
484 return render(request, 'postorius/lists/held_messages.html', context)
487 @login_required
488 @list_moderator_required
489 def moderate_held_message(request, list_id):
490 if request.method != 'POST':
491 return HttpResponseNotAllowed(['POST'])
492 msg_id = request.POST['msgid']
493 mailing_list = List.objects.get_or_404(fqdn_listname=list_id)
494 if 'accept' in request.POST:
495 mailing_list.accept_message(msg_id)
496 messages.success(request, _('The message was accepted'))
497 elif 'reject' in request.POST:
498 mailing_list.reject_message(msg_id)
499 messages.success(request, _('The message was rejected'))
500 elif 'discard' in request.POST:
501 mailing_list.discard_message(msg_id)
502 messages.success(request, _('The message was discarded'))
503 return redirect('list_held_messages', list_id)
506 @login_required
507 @list_owner_required
508 def csv_view(request, list_id):
509 """Export all the subscriber in csv
511 mm_lists = List.objects.get_or_404(fqdn_listname=list_id)
513 response = HttpResponse(content_type='text/csv')
514 response['Content-Disposition'] = (
515 'attachment; filename="Subscribers.csv"')
517 writer = csv.writer(response)
518 if mm_lists:
519 for i in mm_lists.members:
520 writer.writerow([i.email])
522 return response
525 def _get_choosable_domains(request):
526 domains = Domain.objects.all()
527 return [(d.mail_host, d.mail_host) for d in domains]
530 def _get_choosable_styles(request):
531 styles = Style.objects.all()
532 options = [(style['name'], style['description'])
533 for style in styles['styles']]
534 # Reorder to put the default at the beginning
535 for style_option in options:
536 if style_option[0] == styles['default']:
537 options.remove(style_option)
538 options.insert(0, style_option)
539 break
540 return options
543 @login_required
544 @superuser_required
545 def list_new(request, template='postorius/lists/new.html'):
547 Add a new mailing list.
548 If the request to the function is a GET request an empty form for
549 creating a new list will be displayed. If the request method is
550 POST the form will be evaluated. If the form is considered
551 correct the list gets created and otherwise the form with the data
552 filled in before the last POST request is returned. The user must
553 be logged in to create a new list.
555 mailing_list = None
556 choosable_domains = [('', _('Choose a Domain'))]
557 choosable_domains += _get_choosable_domains(request)
558 choosable_styles = _get_choosable_styles(request)
559 if request.method == 'POST':
560 form = ListNew(choosable_domains, choosable_styles, request.POST)
561 if form.is_valid():
562 # grab domain
563 domain = Domain.objects.get_or_404(
564 mail_host=form.cleaned_data['mail_host'])
565 # creating the list
566 try:
567 mailing_list = domain.create_list(
568 form.cleaned_data['listname'],
569 style_name=form.cleaned_data['list_style'])
570 mailing_list.add_owner(form.cleaned_data['list_owner'])
571 list_settings = mailing_list.settings
572 if form.cleaned_data['description']:
573 list_settings["description"] = \
574 form.cleaned_data['description']
575 list_settings["advertised"] = form.cleaned_data['advertised']
576 list_settings.save()
577 messages.success(request, _("List created"))
578 return redirect("list_summary",
579 list_id=mailing_list.list_id)
580 # TODO catch correct Error class:
581 except HTTPError as e:
582 # Right now, there is no good way to detect that this is a
583 # duplicate mailing list request other than checking the
584 # reason for 400 error.
585 if e.reason == b'Mailing list exists':
586 form.add_error(
587 'listname', _('Mailing List already exists.'))
588 return render(request, template, {'form': form})
589 # Otherwise just render the generic error page.
590 return render(request, 'postorius/errors/generic.html',
591 {'error': e})
592 else:
593 messages.error(request, _('Please check the errors below'))
594 else:
595 form = ListNew(choosable_domains, choosable_styles, initial={
596 'list_owner': request.user.email,
597 'advertised': True,
599 return render(request, template, {'form': form})
602 def list_index(request, template='postorius/index.html'):
603 """Show a table of all public mailing lists.
605 # TODO maxking: Figure out why does this view accept POST request and why
606 # can't it be just a GET with list parameter.
607 if request.method == 'POST':
608 return redirect("list_summary", list_id=request.POST["list"])
610 def _get_list_page(count, page):
611 client = get_mailman_client()
612 advertised = not request.user.is_superuser
613 return client.get_list_page(
614 advertised=advertised, count=count, page=page)
615 lists = paginate(
616 _get_list_page, request.GET.get('page'), request.GET.get('count'),
617 paginator_class=MailmanPaginator)
618 choosable_domains = _get_choosable_domains(request)
619 return render(request, template,
620 {'lists': lists,
621 'domain_count': len(choosable_domains)})
624 @login_required
625 @list_owner_required
626 def list_delete(request, list_id):
627 """Deletes a list but asks for confirmation first.
629 the_list = List.objects.get_or_404(fqdn_listname=list_id)
630 if request.method == 'POST':
631 the_list.delete()
632 return redirect("list_index")
633 else:
634 submit_url = reverse('list_delete',
635 kwargs={'list_id': list_id})
636 cancel_url = reverse('list_index',)
637 return render(request, 'postorius/lists/confirm_delete.html',
638 {'submit_url': submit_url, 'cancel_url': cancel_url,
639 'list': the_list})
642 @login_required
643 @list_moderator_required
644 def list_subscription_requests(request, list_id):
645 """Shows a list of subscription requests.
647 m_list = List.objects.get_or_404(fqdn_listname=list_id)
648 return render(request, 'postorius/lists/subscription_requests.html',
649 {'list': m_list})
652 @login_required
653 @list_moderator_required
654 def handle_subscription_request(request, list_id, request_id, action):
656 Handle a subscription request. Possible actions:
657 - accept
658 - defer
659 - reject
660 - discard
662 confirmation_messages = {
663 'accept': _('The request has been accepted.'),
664 'reject': _('The request has been rejected.'),
665 'discard': _('The request has been discarded.'),
666 'defer': _('The request has been defered.'),
668 assert action in confirmation_messages
669 try:
670 m_list = List.objects.get_or_404(fqdn_listname=list_id)
671 # Moderate request and add feedback message to session.
672 m_list.moderate_request(request_id, action)
673 messages.success(request, confirmation_messages[action])
674 except HTTPError as e:
675 if e.code == 409:
676 messages.success(request,
677 _('The request was already moderated: %s')
678 % e.reason)
679 else:
680 messages.error(request, _('The request could not be moderated: %s')
681 % e.reason)
682 return redirect('list_subscription_requests', m_list.list_id)
685 SETTINGS_SECTION_NAMES = (
686 ('list_identity', _('List Identity')),
687 ('automatic_responses', _('Automatic Responses')),
688 ('alter_messages', _('Alter Messages')),
689 ('dmarc_mitigations', _('DMARC Mitigations')),
690 ('digest', _('Digest')),
691 ('message_acceptance', _('Message Acceptance')),
692 ('archiving', _('Archiving')),
693 ('subscription_policy', _('Subscription Policy')),
696 SETTINGS_FORMS = {
697 'list_identity': ListIdentityForm,
698 'automatic_responses': ListAutomaticResponsesForm,
699 'alter_messages': AlterMessagesForm,
700 'dmarc_mitigations': DMARCMitigationsForm,
701 'digest': DigestSettingsForm,
702 'message_acceptance': MessageAcceptanceForm,
703 'archiving': ArchiveSettingsForm,
704 'subscription_policy': ListSubscriptionPolicyForm,
708 @login_required
709 @list_owner_required
710 def list_settings(request, list_id=None, visible_section=None,
711 template='postorius/lists/settings.html'):
713 View and edit the settings of a list.
714 The function requires the user to be logged in and have the
715 permissions necessary to perform the action.
717 Use /<NAMEOFTHESECTION>/<NAMEOFTHEOPTION>
718 to show only parts of the settings
719 <param> is optional / is used to differ in between section and option might
720 result in using //option
722 if visible_section is None:
723 visible_section = 'list_identity'
724 try:
725 form_class = SETTINGS_FORMS[visible_section]
726 except KeyError:
727 raise Http404('No such settings section')
728 m_list = List.objects.get_or_404(fqdn_listname=list_id)
729 list_settings = m_list.settings
730 initial_data = dict(
731 (key, str(value)) for key, value in list(list_settings.items()))
732 # List settings are grouped an processed in different forms.
733 if request.method == 'POST':
734 form = form_class(request.POST, mlist=m_list, initial=initial_data)
735 if form.is_valid():
736 try:
737 for key in form.changed_data:
738 if key in form_class.mlist_properties:
739 setattr(m_list, key, form.cleaned_data[key])
740 else:
741 list_settings[key] = form.cleaned_data[key]
742 list_settings.save()
743 messages.success(request, _('The settings have been updated.'))
744 except HTTPError as e:
745 messages.error(request, _('An error occured: %s') % e.reason)
746 return redirect('list_settings', m_list.list_id, visible_section)
747 else:
748 form = form_class(initial=initial_data, mlist=m_list)
750 return render(request, template, {
751 'form': form,
752 'section_names': SETTINGS_SECTION_NAMES,
753 'list': m_list,
754 'visible_section': visible_section,
758 @login_required
759 @list_owner_required
760 def remove_role(request, list_id=None, role=None, address=None,
761 template='postorius/lists/confirm_remove_role.html'):
762 """Removes a list moderator or owner."""
763 the_list = List.objects.get_or_404(fqdn_listname=list_id)
765 redirect_on_success = redirect('list_members', the_list.list_id, role)
767 roster = getattr(the_list, '{}s'.format(role))
768 if address not in roster:
769 messages.error(request,
770 _('The user %(email)s is not in the %(role)s group')
771 % {'email': address, 'role': role})
772 return redirect('list_members', the_list.list_id, role)
774 if role == 'owner':
775 if len(roster) == 1:
776 messages.error(request, _('Removing the last owner is impossible'))
777 return redirect('list_members', the_list.list_id, role)
778 user_emails = EmailAddress.objects.filter(
779 user=request.user, verified=True).order_by(
780 "email").values_list("email", flat=True)
781 if address in user_emails:
782 # The user is removing themselves, redirect to the list info page
783 # because they won't have access to the members page anyway.
784 redirect_on_success = redirect('list_summary', the_list.list_id)
786 if request.method == 'POST':
787 try:
788 the_list.remove_role(role, address)
789 except HTTPError as e:
790 messages.error(request, _('The user could not be removed: %(msg)s')
791 % {'msg': e.msg})
792 return redirect('list_members', the_list.list_id, role)
793 messages.success(request, _('The user %(address)s has been removed'
794 ' from the %(role)s group.')
795 % {'address': address, 'role': role})
796 return redirect_on_success
797 return render(request, template,
798 {'role': role, 'address': address,
799 'list_id': the_list.list_id})
802 @login_required
803 @list_owner_required
804 def remove_all_subscribers(request, list_id):
806 """Empty the list by unsubscribing all members."""
808 mlist = List.objects.get_or_404(fqdn_listname=list_id)
809 if len(mlist.members) == 0:
810 messages.error(request,
811 _('No member is subscribed to the list currently.'))
812 return redirect('mass_removal', mlist.list_id)
813 if request.method == 'POST':
814 try:
815 # TODO maxking: This doesn't scale. Either the Core should provide
816 # an endpoint to remove all subscribers or there should be some
817 # better way to do this. Maybe, Core can take a list of email
818 # addresses in batches of 50 and unsubscribe all of them.
819 for names in mlist.members:
820 mlist.unsubscribe(names.email)
821 messages.success(request, _('All members have been'
822 ' unsubscribed from the list.'))
823 return redirect('list_members', mlist.list_id)
824 except Exception as e:
825 messages.error(request, e)
826 return render(request,
827 'postorius/lists/confirm_removeall_subscribers.html',
828 {'list': mlist})
831 @login_required
832 @list_owner_required
833 def list_bans(request, list_id):
835 Ban or unban email addresses.
837 # Get the list and cache the archivers property.
838 m_list = List.objects.get_or_404(fqdn_listname=list_id)
839 ban_list = m_list.bans
841 # Process form submission.
842 if request.method == 'POST':
843 if 'add' in request.POST:
844 addban_form = ListAddBanForm(request.POST)
845 if addban_form.is_valid():
846 try:
847 ban_list.add(addban_form.cleaned_data['email'])
848 messages.success(request, _(
849 'The email {} has been banned.'.format(
850 addban_form.cleaned_data['email'])))
851 except HTTPError as e:
852 messages.error(
853 request, _('An error occured: %s') % e.reason)
854 except ValueError as e:
855 messages.error(request, _('Invalid data: %s') % e)
856 return redirect('list_bans', list_id)
857 elif 'del' in request.POST:
858 try:
859 ban_list.remove(request.POST['email'])
860 messages.success(request, _(
861 'The email {} has been un-banned'.format(
862 request.POST['email'])))
863 except HTTPError as e:
864 messages.error(request, _('An error occured: %s') % e.reason)
865 except ValueError as e:
866 messages.error(request, _('Invalid data: %s') % e)
867 return redirect('list_bans', list_id)
868 else:
869 addban_form = ListAddBanForm()
870 banned_addresses = paginate(
871 list(ban_list), request.GET.get('page'), request.GET.get('count'))
872 return render(request, 'postorius/lists/bans.html',
873 {'list': m_list,
874 'addban_form': addban_form,
875 'banned_addresses': banned_addresses,
879 @login_required
880 @list_owner_required
881 def list_header_matches(request, list_id):
883 View and edit the list's header matches.
885 m_list = List.objects.get_or_404(fqdn_listname=list_id)
886 header_matches = m_list.header_matches
887 HeaderMatchFormset = formset_factory(
888 ListHeaderMatchForm, extra=1, can_delete=True, can_order=True,
889 formset=ListHeaderMatchFormset)
890 initial_data = [
891 dict([
892 (key, getattr(hm, key)) for key in ListHeaderMatchForm.base_fields
893 ]) for hm in header_matches]
895 # Process form submission.
896 if request.method == 'POST':
897 formset = HeaderMatchFormset(request.POST, initial=initial_data)
898 if formset.is_valid():
899 if not formset.has_changed():
900 return redirect('list_header_matches', list_id)
901 # Purge the existing header_matches
902 header_matches.clear()
903 # Add the ones in the form
905 def form_order(f):
906 # If ORDER is None (new header match), add it last.
907 return f.cleaned_data.get('ORDER') or len(formset.forms)
908 errors = []
909 for form in sorted(formset, key=form_order):
910 if 'header' not in form.cleaned_data:
911 # The new header match form was not filled
912 continue
913 if form.cleaned_data.get('DELETE'):
914 continue
915 try:
916 header_matches.add(
917 header=form.cleaned_data['header'],
918 pattern=form.cleaned_data['pattern'],
919 action=form.cleaned_data['action'],
921 except HTTPError as e:
922 errors.append(e)
923 for e in errors:
924 messages.error(
925 request, _('An error occured: %s') % e.reason)
926 if not errors:
927 messages.success(request, _('The header matches were'
928 ' successfully modified.'))
929 return redirect('list_header_matches', list_id)
930 else:
931 formset = HeaderMatchFormset(initial=initial_data)
932 # Adapt the last form to create new matches
933 form_new = formset.forms[-1]
934 form_new.fields['header'].widget.attrs['placeholder'] = _('New header')
935 form_new.fields['pattern'].widget.attrs['placeholder'] = _('New pattern')
936 del form_new.fields['ORDER']
937 del form_new.fields['DELETE']
939 return render(request, 'postorius/lists/header_matches.html', {
940 'list': m_list,
941 'formset': formset,