Bump copyright year
[mailman-postorious.git] / src / postorius / forms / list_forms.py
blob237aad6978e558e61618ad9dc58b8448da6eb2a1
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2017-2022 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 re
22 from django import forms
23 from django.apps import apps
24 from django.core.exceptions import ValidationError
25 from django.core.validators import validate_email
26 from django.utils.translation import gettext_lazy as _
28 from django_mailman3.lib.mailman import get_mailman_client
30 from postorius.forms.fields import (
31 ACTION_CHOICES, ListOfStringsField, delivery_mode_field,
32 delivery_status_field, moderation_action_field)
33 from postorius.forms.validators import validate_uuid_or_email
34 from postorius.models import EmailTemplate, _email_template_help_text
35 from postorius.utils import LANGUAGES
38 DIGEST_FREQUENCY_CHOICES = (
39 ("daily", _("Daily")),
40 ("weekly", _("Weekly")),
41 ("quarterly", _("Quarterly")),
42 ("monthly", _("Monthly")),
43 ("yearly", _("Yearly"))
46 ROSTER_VISIBILITY_CHOICES = (
47 ("moderators", _("Only mailinglist moderators")),
48 ("members", _("Only mailinglist members")),
49 ("public", _("Anyone")),
53 EMPTY_STRING = ''
56 class ListNew(forms.Form):
58 """
59 Form fields to add a new list. Languages are hard coded which should
60 be replaced by a REST lookup of available languages.
61 """
62 listname = forms.CharField(
63 label=_('List Name'),
64 required=True,
65 error_messages={'required': _('Please enter a name for your list.'),
66 'invalid': _('Please enter a valid list name.')})
67 mail_host = forms.ChoiceField()
68 list_owner = forms.EmailField(
69 label=_('Initial list owner address'),
70 error_messages={
71 'required': _("Please enter the list owner's email address.")},
72 required=True)
73 advertised = forms.ChoiceField(
74 widget=forms.RadioSelect(),
75 label=_('Advertise this list?'),
76 error_messages={
77 'required': _("Please choose a list type.")},
78 required=True,
79 choices=(
80 (True, _("Advertise this list in list index")),
81 (False, _("Hide this list in list index"))))
82 list_style = forms.ChoiceField()
83 description = forms.CharField(
84 label=_('Description'),
85 required=False)
87 def __init__(self, domain_choices, style_choices, *args, **kwargs):
88 super(ListNew, self).__init__(*args, **kwargs)
89 self.fields["mail_host"] = forms.ChoiceField(
90 widget=forms.Select(),
91 label=_('Mail Host'),
92 required=True,
93 choices=domain_choices,
94 error_messages={'required': _("Choose an existing Domain."),
95 'invalid': _("Choose a valid Mail Host")})
96 self.fields["list_style"] = forms.ChoiceField(
97 widget=forms.Select(),
98 label=_('List Style'),
99 required=True,
100 choices=style_choices,
101 error_messages={'required': _("Choose a List Style."),
102 'invalid': _("Choose a valid List Style.")})
103 if len(domain_choices) < 2:
104 self.fields["mail_host"].help_text = _(
105 "Site admin has not created any domains")
106 # if len(choices) < 2:
107 # help_text=_("No domains available: " +
108 # "The site admin must create new domains " +
109 # "before you will be able to create a list")
111 def clean_listname(self):
112 try:
113 validate_email(self.cleaned_data['listname'] + '@example.net')
114 except ValidationError:
115 # TODO (maxking): Error should atleast point to what is a valid
116 # listname. It may not always be obvious which characters aren't
117 # allowed in a listname.
118 raise forms.ValidationError(_("Please enter a valid listname"))
119 return self.cleaned_data['listname']
121 class Meta:
124 Class to handle the automatic insertion of fieldsets and divs.
126 To use it: add a list for each wished fieldset. The first item in
127 the list should be the wished name of the fieldset, the following
128 the fields that should be included in the fieldset.
130 layout = [["List Details",
131 "listname",
132 "mail_host",
133 "list_style",
134 "list_owner",
135 "description",
136 "advertised"], ]
139 class ListSubscribe(forms.Form):
140 """Form fields to join an existing list.
143 DELIVERY_STATUS_CHOICES = (("enabled", _('Enabled')),
144 ("by_user", _('Disabled')))
146 subscriber = forms.ChoiceField(
147 label=_('Your email address'),
148 widget=forms.Select(),
149 validators=[validate_uuid_or_email, ],
150 help_text=_(
151 'Subscribing via "Primary Address" will change subscription'
152 ' address when you change your primary address.'),
153 error_messages={
154 'required': _('Please enter an email address.'),
155 'invalid': _('Please enter a valid email address.')})
157 display_name = forms.CharField(
158 label=_('Your name (optional)'), required=False)
160 delivery_mode = delivery_mode_field(default='regular')
161 delivery_status = delivery_status_field(choices=DELIVERY_STATUS_CHOICES,
162 widget=forms.RadioSelect)
164 def __init__(self, user_emails, user_id, primary_email, *args, **kwargs):
165 super(ListSubscribe, self).__init__(*args, **kwargs)
166 choices = list((address, address)
167 for address in user_emails)
168 if primary_email and user_id:
169 choices.insert(
171 (user_id, _('Primary Address ({})').format(primary_email)))
172 self.fields['subscriber'].choices = choices
175 class ListAnonymousSubscribe(forms.Form):
176 """Form fields to join an existing list as an anonymous user.
179 email = forms.CharField(
180 label=_('Your email address'),
181 validators=[validate_email],
182 error_messages={
183 'required': _('Please enter an email address.'),
184 'invalid': _('Please enter a valid email address.')})
186 display_name = forms.CharField(
187 label=_('Your name (optional)'), required=False)
190 class ListSettingsForm(forms.Form):
192 Base class for list settings forms.
194 mlist_properties = []
196 def __init__(self, *args, **kwargs):
197 self._mlist = kwargs.pop('mlist')
198 super(ListSettingsForm, self).__init__(*args, **kwargs)
201 SUBSCRIPTION_POLICY_CHOICES = (
202 ('open', _('Open')),
203 ('confirm', _('Confirm')),
204 ('moderate', _('Moderate')),
205 ('confirm_then_moderate', _('Confirm, then moderate')),
209 class MemberPolicyForm(ListSettingsForm):
211 Policies related to members.
213 subscription_policy = forms.ChoiceField(
214 label=_('Subscription Policy'),
215 choices=SUBSCRIPTION_POLICY_CHOICES,
216 help_text=_('Open: Subscriptions are added automatically\n'
217 'Confirm: Subscribers need to confirm the subscription '
218 'using an email sent to them\n'
219 'Moderate: Moderators will have to authorize '
220 'each subscription manually.\n'
221 'Confirm then Moderate: First subscribers have to confirm,'
222 ' then a moderator needs to authorize.'))
224 unsubscription_policy = forms.ChoiceField(
225 label=_('Un-Subscription Policy'),
226 choices=SUBSCRIPTION_POLICY_CHOICES,
227 help_text=_('Open: Un-Subscriptions happen automatically\n'
228 'Confirm: Subscribers need to confirm the un-subscription '
229 'using an email sent to them\n'
230 'Moderate: Moderators will have to authorize '
231 'each un-subscription manually.\n'
232 'Confirm then Moderate: First subscribers have to confirm,'
233 ' then a moderator needs to authorize.'))
236 class BounceProcessingForm(ListSettingsForm):
237 """List's bounce processing settings."""
239 forward_unrecognized_choices = (
240 ('discard', _('Discard')),
241 ('administrators', _('List Admins')),
242 ('site_owner', _('Site Admin')),
245 process_bounces = forms.BooleanField(
246 widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
247 required=False,
248 label=_('Process Bounces'),
249 help_text=_(
250 'Specifies whether or not this list should do automatic'
251 ' bounce processing.'))
253 bounce_score_threshold = forms.IntegerField(
254 min_value=0,
255 label=_('Bounce score threshold'),
256 required=False,
257 help_text=_(
258 'This is the bounce score above which a member\'s subscription '
259 ' will be automatically disabled. When the subscription is '
260 ' re-enabled, their bounce score will be reset to zero.'))
262 bounce_info_stale_after = forms.CharField(
263 label=_('Bounce info stale after'),
264 required=False,
265 help_text=_(
266 'The number of days after which a member\'s bounce information'
267 ' is considered stale. If no new bounces have been received in'
268 ' the interim, the bounce score is reset to zero.'
269 ' This value must be an integer. '))
271 bounce_notify_owner_on_bounce_increment = forms.BooleanField(
272 widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
273 required=False,
274 label=_('Notify owner on bounce increment'),
275 help_text=_(
276 'This option controls whether or not the list owner is notified'
277 ' when a member\'s bounce score is incremented, but to a value'
278 ' less than their bounce threshold. '))
280 bounce_notify_owner_on_disable = forms.BooleanField(
281 widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
282 required=False,
283 label=_('Notify owner on disable'),
284 help_text=_(
285 'This option controls whether or not the list owner is notified'
286 ' when a member\'s subscription is automatically disabled due'
287 ' to their bounce threshold being reached. '))
289 bounce_notify_owner_on_removal = forms.BooleanField(
290 widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
291 required=False,
292 label=_('Notify owner on removal'),
293 help_text=_(
294 'This option controls whether or not the list owner is '
295 'notified when a member is removed from the list after '
296 'their disabled notifications have been exhausted. '))
298 forward_unrecognized_bounces_to = forms.ChoiceField(
299 choices=forward_unrecognized_choices,
300 widget=forms.RadioSelect,
301 label=_('Forward unrecognized bounces'),
302 help_text=_('Discard: Unrecognized bounces will be discarded\n'
303 'List Admins: Send to the list owners and moderators\n'
304 'Site Admin: Send to the site\'s configured site_owner'))
306 bounce_you_are_disabled_warnings_interval = forms.CharField(
307 label=_('Bounce disabled warnings interval'),
308 required=False,
309 help_text=_(
310 'The number of days between each disabled notification.'))
312 bounce_you_are_disabled_warnings = forms.IntegerField(
313 min_value=0,
314 label=_('Bounce disable warnings'),
315 required=False,
316 help_text=_(
317 'The number of notices a disabled member will receive before'
318 ' their address is removed from the mailing list\'s roster. '
319 'Set this to 0 to immediately remove an address from the list'
320 ' once their bounce score exceeds the threshold. '
321 'This value must be an integer. '))
324 class ArchiveSettingsForm(ListSettingsForm):
326 Set the general archive policy.
328 mlist_properties = ['archivers']
330 archive_policy_choices = (
331 ("public", _("Public archives")),
332 ("private", _("Private archives")),
333 ("never", _("Do not archive this list")),
336 archive_rendering_mode_choices = (
337 ("text", _("Plain text")),
338 ("markdown", _("Markdown text")),
341 archive_policy = forms.ChoiceField(
342 choices=archive_policy_choices,
343 widget=forms.RadioSelect,
344 label=_('Archive policy'),
345 help_text=_('Policy for archiving messages for this list'),
348 archivers = forms.MultipleChoiceField(
349 widget=forms.CheckboxSelectMultiple,
350 label=_('Active archivers'),
351 required=False) # May be empty if no archivers are desired.
353 archive_rendering_mode = forms.ChoiceField(
354 choices=archive_rendering_mode_choices,
355 widget=forms.RadioSelect,
356 required=False,
357 label=_('Archive Rendering mode'),
358 help_text=_('This option enables rendering of emails in archiver as '
359 'rich text with formatting based on markup in the email.'
360 '\nCurrently, this option is only supported by Hyperkitty.'
364 def __init__(self, *args, **kwargs):
365 super(ArchiveSettingsForm, self).__init__(*args, **kwargs)
366 archiver_opts = sorted(self._mlist.archivers.keys())
367 self.fields['archivers'].choices = sorted(
368 [(key, key) for key in archiver_opts])
369 if self.initial:
370 self.initial['archivers'] = [
371 key for key in archiver_opts if self._mlist.archivers[key] is True] # noqa
372 # If Hyperkitty isn't installed, do not show the archive_rendering_mode
373 # field since it doesn't apply to other archivers.
374 if not apps.is_installed('hyperkitty'):
375 del self.fields['archive_rendering_mode']
377 def clean_archivers(self):
378 result = {}
379 for archiver, etc in self.fields['archivers'].choices:
380 result[archiver] = archiver in self.cleaned_data['archivers']
381 self.cleaned_data['archivers'] = result
382 return result
385 class MessageAcceptanceForm(ListSettingsForm):
387 List messages acceptance settings.
389 acceptable_aliases = ListOfStringsField(
390 label=_("Acceptable aliases"),
391 required=False,
392 help_text=_(
393 'This is a list, one per line, of addresses and regexps matching '
394 'addresses that are acceptable in To: or Cc: in lieu of the list '
395 'posting address when `require_explicit_destination\' is enabled. '
396 ' Entries are either email addresses or regexps matching email '
397 'addresses. Regexps are entries beginning with `^\' and are '
398 'matched against every recipient address in the message. The '
399 'matching is performed with Python\'s re.match() function, meaning'
400 ' they are anchored to the start of the string.'))
401 require_explicit_destination = forms.BooleanField(
402 widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
403 required=False,
404 label=_('Require Explicit Destination'),
405 help_text=_(
406 'This checks to ensure that the list posting address or an '
407 'acceptable alias explicitly appears in a To: or Cc: header in '
408 'the post.'))
409 administrivia = forms.BooleanField(
410 widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
411 required=False,
412 label=_('Administrivia'),
413 help_text=_(
414 'Administrivia tests will check postings to see whether it\'s '
415 'really meant as an administrative request (like subscribe, '
416 'unsubscribe, etc), and will add it to the the administrative '
417 'requests queue, notifying the administrator of the new request, '
418 'in the process.'))
419 default_member_action = forms.ChoiceField(
420 widget=forms.RadioSelect(),
421 label=_('Default action to take when a member posts to the list'),
422 error_messages={
423 'required': _("Please choose a default member action.")},
424 required=True,
425 choices=ACTION_CHOICES,
426 help_text=_(
427 'Default action to take when a member posts to the list.\n'
428 'Hold: This holds the message for approval by the list '
429 'moderators.\n'
430 'Reject: this automatically rejects the message by sending a '
431 'bounce notice to the post\'s author. The text of the bounce '
432 'notice can be configured by you.\n'
433 'Discard: this simply discards the message, with no notice '
434 'sent to the post\'s author.\n'
435 'Accept: accepts any postings without any further checks.\n'
436 'Default Processing: run additional checks and accept '
437 'the message.'))
438 default_nonmember_action = forms.ChoiceField(
439 widget=forms.RadioSelect(),
440 label=_('Default action to take when a non-member posts to the list'),
441 error_messages={
442 'required': _("Please choose a default non-member action.")},
443 required=True,
444 choices=ACTION_CHOICES,
445 help_text=_(
446 'When a post from a non-member is received, the message\'s sender '
447 'is matched against the list of explicitly accepted, held, '
448 'rejected (bounced), and discarded addresses. '
449 'If no match is found, then this action is taken.'))
450 emergency = forms.BooleanField(
451 widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
452 required=False,
453 label=_('Emergency Moderation'),
454 help_text=_(
455 'When this option is enabled, all list traffic is emergency'
456 ' moderated, i.e. held for moderation. Turn this option on when'
457 ' your list is experiencing a flamewar and you want a cooling off'
458 ' period. '),
460 max_message_size = forms.IntegerField(
461 min_value=0,
462 label=_('Maximum message size'),
463 required=False,
464 help_text=_(
465 'The maximum allowed message size in KB. '
466 'This can be used to prevent emails with large attachments. '
467 'A size of 0 disables the check.'))
468 max_num_recipients = forms.IntegerField(
469 min_value=0,
470 label=_('Maximum number of recipients'),
471 required=False,
472 help_text=_(
473 'If a post has this many or more explicit recipients (To: and '
474 'Cc:), the post will be held for moderation. '
475 'This can be used to prevent mass mailings from being accepted. '
476 'A value of 0 disables the check.'))
478 # TODO: Expose after this functionality actually works in Core.
479 # max_days_to_hold = forms.IntegerField(
480 # min_value=0,
481 # label=_('Discard held posts after'),
482 # required=False,
483 # help_text=_(
484 # 'No. of days after which held messages will be automatically'
485 # ' discarded.'))
487 accept_these_nonmembers = ListOfStringsField(
488 label=_("Accept these non-members"),
489 required=False,
490 help_text=_(
491 'This is a list, one per line, of regexps matching '
492 'addresses that are allowed to post to this mailing list without'
493 ' subscribing to the list.'
494 ' Entries are regexps beginning with `^\' and are matched against'
495 ' the sender addresses in the message.'
496 ' While non-regexp addresses can be entered here, it is preferred'
497 ' to add the address as a nonmember and set the nonmember\'s '
498 'Moderation to Default Processing.'))
500 hold_these_nonmembers = ListOfStringsField(
501 label=_("Hold these non-members"),
502 required=False,
503 help_text=_(
504 'This is a list, one per line, of regexps matching '
505 'nonmember addresses, posts from which are held automatically.'
506 ' Entries are regexps beginning with `^\' and are matched against'
507 ' the sender addresses in the message.'
508 ' While non-regexp addresses can be entered here, it is preferred'
509 ' to add the address as a nonmember and set the nonmember\'s '
510 'Moderation to Hold.'))
512 reject_these_nonmembers = ListOfStringsField(
513 label=_("Reject these non-members"),
514 required=False,
515 help_text=_(
516 'This is a list, one per line, of regexps matching '
517 'nonmember addresses, posts from which are rejected with notice to'
518 ' the sender.'
519 ' Entries are regexps beginning with `^\' and are matched against'
520 ' the sender addresses in the message.'
521 ' While non-regexp addresses can be entered here, it is preferred'
522 ' to add the address as a nonmember and set the nonmember\'s '
523 'Moderation to Reject.'))
525 discard_these_nonmembers = ListOfStringsField(
526 label=_("Discard these non-members"),
527 required=False,
528 help_text=_(
529 'This is a list, one per line, of regexps matching '
530 'nonmember addresses, posts from which are discarded automatically'
531 '. Entries are regexps beginning with `^\' and are matched against'
532 ' the sender addresses in the message.'
533 ' While non-regexp addresses can be entered here, it is preferred'
534 ' to add the address as a nonmember and set the nonmember\'s '
535 'Moderation to Discard.'))
537 def clean_acceptable_aliases(self):
538 # python's urlencode will drop this attribute completely if an empty
539 # list is passed with doseq=True. To make it work for us, we instead
540 # use an empty string to signify an empty value. In turn, Core will
541 # also consider an empty value to be empty list for list-of-strings
542 # field.
543 if not self.cleaned_data['acceptable_aliases']:
544 return EMPTY_STRING
545 for alias in self.cleaned_data['acceptable_aliases']:
546 if alias.startswith('^'):
547 try:
548 re.compile(alias)
549 except re.error as e:
550 raise forms.ValidationError(
551 _('Invalid alias regexp: {}: {}').format(alias, e.msg))
552 else:
553 try:
554 validate_email(alias)
555 except ValidationError:
556 raise forms.ValidationError(
557 _('Invalid alias email: {}').format(alias))
558 return self.cleaned_data['acceptable_aliases']
561 class DigestSettingsForm(ListSettingsForm):
563 List digest settings.
565 digests_enabled = forms.ChoiceField(
566 choices=((True, _('Yes')), (False, _('No'))),
567 widget=forms.RadioSelect,
568 required=False,
569 label=_('Enable Digests'),
570 help_text=_('Should Mailman enable digests for this MailingList?'),
572 digest_send_periodic = forms.ChoiceField(
573 choices=((True, _('Yes')), (False, _('No'))),
574 widget=forms.RadioSelect,
575 required=False,
576 label=_('Send Digest Periodically'),
577 help_text=_('Should Mailman send out digests periodically?'),
579 digest_volume_frequency = forms.ChoiceField(
580 choices=DIGEST_FREQUENCY_CHOICES,
581 widget=forms.RadioSelect,
582 required=False,
583 label=_('Digest Volume Frequency'),
584 help_text=_('At what frequency should Mailman increment the digest '
585 'volume number and reset the issue number?'),
587 digest_size_threshold = forms.DecimalField(
588 label=_('Digest size threshold'),
589 help_text=_('How big in Kb should a digest be before '
590 'it gets sent out?'))
593 class DMARCMitigationsForm(ListSettingsForm):
595 DMARC Mitigations list settings.
597 dmarc_mitigate_action = forms.ChoiceField(
598 label=_('DMARC mitigation action'),
599 widget=forms.Select(),
600 required=False,
601 error_messages={
602 'required': _("Please choose a DMARC mitigation action.")},
603 choices=(
604 ('no_mitigation', _('No DMARC mitigations')),
605 ('munge_from', _('Replace From: with list address')),
606 ('wrap_message',
607 _('Wrap the message in an outer message From: the list.')),
608 ('reject', _('Reject the message')),
609 ('discard', _('Discard the message'))),
610 help_text=_(
611 'The action to apply to messages From: a domain publishing a '
612 'DMARC policy of reject or quarantine or to all messages if '
613 'DMARC Mitigate unconditionally is True.'))
614 dmarc_mitigate_unconditionally = forms.ChoiceField(
615 choices=((True, _('Yes')), (False, _('No'))),
616 widget=forms.RadioSelect,
617 required=False,
618 label=_('DMARC Mitigate unconditionally'),
619 help_text=_(
620 'If DMARC mitigation action is munge_from or wrap_message, '
621 'should it apply to all messages regardless of the DMARC policy '
622 'of the From: domain.'))
623 dmarc_moderation_notice = forms.CharField(
624 label=_('DMARC rejection notice'),
625 required=False,
626 widget=forms.Textarea(),
627 help_text=_(
628 'Text to replace the default reason in any rejection notice to '
629 'be sent when DMARC mitigation action of reject applies.'))
630 dmarc_wrapped_message_text = forms.CharField(
631 label=_('DMARC wrapped message text'),
632 required=False,
633 widget=forms.Textarea(),
634 help_text=_(
635 'Text to be added as a separate text/plain MIME part preceding '
636 'the original message part in the wrapped message when DMARC '
637 'mitigation action of wrap message applies.'))
640 PERSONALIZATION_CHOICES = (
641 ('none', _('None')),
642 ('individual', _('Individual')),
643 ('full', _('Full'))
646 PERSONALIZATION_CHOICES_HELP = _(
648 None: No personalization.
650 Individual: Everyone gets a unique copy of the message, and there are a \
651 few more substitution variables, but no headers are modified.
653 Full: All of the 'individual' personalization plus recipient header \
654 modification. """)
656 FILTER_ACTION_CHOICES = (
657 ('discard', _('Discard')),
658 ('reject', _('Reject')),
659 ('forward', _('Forward')),
660 ('preserve', _('Preserve')),
663 FILTER_ACTION_HELP = _("""Action to take on messages which have no content
664 after filtering.
665 Discard = silently discard the message.
666 Reject = discard the message and notify the sender.
667 Forward = forward the message to the list owner(s).
668 Preserve = save the message in qfiles/bad.
669 """)
672 class AlterMessagesForm(ListSettingsForm):
674 Alter messages list settings.
676 personalize = forms.ChoiceField(
677 choices=PERSONALIZATION_CHOICES,
678 widget=forms.RadioSelect,
679 required=False,
680 label=_('Personalize'),
681 help_text=PERSONALIZATION_CHOICES_HELP)
682 filter_content = forms.ChoiceField(
683 choices=((True, _('Yes')), (False, _('No'))),
684 widget=forms.RadioSelect,
685 required=False,
686 label=_('Filter content'),
687 help_text=_('Should Mailman filter the content of list traffic '
688 'according to the settings below?'))
689 filter_types = ListOfStringsField(
690 label=_('Filter types'),
691 required=False,
692 help_text=_(
693 'MIME types to filter from the incoming posts. A list of common '
694 'types can be found '
695 '<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types">here </a>' # noqa# E501
697 filter_extensions = ListOfStringsField(
698 label=_('Filter extensions'),
699 required=False,
700 help_text=_(
701 'Extensions to filter from the incoming posts.'
703 pass_types = ListOfStringsField(
704 label=_('Pass types'),
705 required=False,
706 help_text=_(
707 'MIME types to allow in the incoming posts. A list of common '
708 'types can be found '
709 '<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types">here </a>' # noqa# E501
711 pass_extensions = ListOfStringsField(
712 label=_('Pass extensions'),
713 required=False,
714 help_text=_(
715 'Extensions to allow in the incoming posts.'
717 collapse_alternatives = forms.ChoiceField(
718 choices=((True, _('Yes')), (False, _('No'))),
719 widget=forms.RadioSelect,
720 required=False,
721 label=_('Collapse alternatives'),
722 help_text=_('Should Mailman collapse multipart/alternative to '
723 'its first part content?'))
724 filter_action = forms.ChoiceField(
725 choices=FILTER_ACTION_CHOICES,
726 widget=forms.RadioSelect,
727 required=False,
728 label=_('Filter Action'),
729 help_text=FILTER_ACTION_HELP)
730 convert_html_to_plaintext = forms.ChoiceField(
731 choices=((True, _('Yes')), (False, _('No'))),
732 widget=forms.RadioSelect,
733 required=False,
734 label=_('Convert html to plaintext'),
735 help_text=_('Should Mailman convert text/html parts to plain text? '
736 'This conversion happens after MIME attachments '
737 'have been stripped.'))
738 anonymous_list = forms.ChoiceField(
739 choices=((True, _('Yes')), (False, _('No'))),
740 widget=forms.RadioSelect,
741 required=False,
742 label=_('Anonymous list'),
743 help_text=_('Hide the sender of a message, '
744 'replacing it with the list address '
745 '(Removes From, Sender and Reply-To fields)'))
746 include_rfc2369_headers = forms.ChoiceField(
747 choices=((True, _('Yes')), (False, _('No'))),
748 widget=forms.RadioSelect,
749 required=False,
750 label=_('Include RFC2369 headers'),
751 help_text=_(
752 'Yes is highly recommended. RFC 2369 defines a set of List-* '
753 'headers that are normally added to every message sent to the '
754 'list membership. These greatly aid end-users who are using '
755 'standards compliant mail readers. They should normally always '
756 'be enabled. However, not all mail readers are standards '
757 'compliant yet, and if you have a large number of members who are '
758 'using non-compliant mail readers, they may be annoyed at these '
759 'headers. You should first try to educate your members as to why '
760 'these headers exist, and how to hide them in their mail clients. '
761 'As a last resort you can disable these headers, but this is not '
762 'recommended (and in fact, your ability to disable these headers '
763 'may eventually go away).'))
764 allow_list_posts = forms.ChoiceField(
765 choices=((True, _('Yes')), (False, _('No'))),
766 widget=forms.RadioSelect,
767 required=False,
768 label=_("Include the list post header"),
769 help_text=_(
770 "This can be set to no for announce lists that do not wish to "
771 "include the List-Post header because posting to the list is "
772 "discouraged."))
773 reply_to_address = forms.CharField(
774 label=_('Explicit reply-to address'),
775 required=False,
776 help_text=_(
777 'This option allows admins to set an explicit Reply-to address. '
778 'It is only used if the reply-to is set to use an explicitly set '
779 'header'))
780 first_strip_reply_to = forms.ChoiceField(
781 choices=((True, _('Yes')), (False, _('No'))),
782 widget=forms.RadioSelect,
783 required=False,
784 help_text=_(
785 'Should any existing Reply-To: header found in the original '
786 'message be stripped? If so, this will be done regardless of '
787 'whether an explict Reply-To: header is added by Mailman or not.'))
788 reply_goes_to_list = forms.ChoiceField(
789 label=_('Reply goes to list'),
790 widget=forms.Select(),
791 required=False,
792 error_messages={
793 'required': _("Please choose a reply-to action.")},
794 choices=(
795 ('no_munging', _('No Munging')),
796 ('point_to_list', _('Reply goes to list')),
797 ('explicit_header', _('Explicit Reply-to header set')),
798 ('explicit_header_only', _('Explicit Reply-to set; no Cc added'))),
799 help_text=_(
800 'Where are replies to list messages directed? No Munging is '
801 'strongly recommended for most mailing lists. \nThis option '
802 'controls what Mailman does to the Reply-To: header in messages '
803 'flowing through this mailing list. When set to No Munging, no '
804 'Reply-To: header is '
805 'added by Mailman, although if one is present in the original '
806 'message, it is not stripped. Setting this value to either Reply '
807 'to List, Explicit Reply, or Reply Only causes Mailman to insert '
808 'a specific Reply-To: header in all messages, overriding the '
809 'header in the original message if necessary '
810 '(Explicit Reply inserts the value of reply_to_address). '
811 'Explicit Reply-to set; no Cc added is useful for'
812 'announce-only lists where you want to avoid someone replying '
813 'to the list address. There are many reasons not to introduce or '
814 'override the Reply-To: header. One is that some posters depend '
815 'on their own Reply-To: settings to convey their valid return '
816 'address. Another is that modifying Reply-To: makes it much more '
817 'difficult to send private replies. See <a href="'
818 'http://marc.merlins.org/netrants/reply-to-harmful.html">'
819 '`Reply-To\' Munging Considered Harmful</a> for a general '
820 'discussion of this issue. See <a href="'
821 'http://marc.merlins.org/netrants/reply-to-useful.html">'
822 '`Reply-To\' Munging Considered Useful</a> for a dissenting '
823 'opinion. '
824 'Some mailing lists have restricted '
825 'posting privileges, with a parallel list devoted to discussions. '
826 'Examples are `patches\' or `checkin\' lists, where software '
827 'changes are posted by a revision control system, but discussion '
828 'about the changes occurs on a developers mailing list. To '
829 'support these types of mailing lists, select Explicit Reply and '
830 'set the Reply-To: address option to point to the parallel list.'))
831 posting_pipeline = forms.ChoiceField(
832 label=_('Pipeline'),
833 widget=forms.Select(),
834 required=False,
835 choices=lambda: ((p, p) for p in get_mailman_client()
836 .pipelines['pipelines']),
837 help_text=_('Type of pipeline you want to use for this mailing list'))
840 class ListAutomaticResponsesForm(ListSettingsForm):
842 List settings for automatic responses.
844 autorespond_choices = (
845 ("respond_and_continue", _("Respond and continue processing")),
846 ("respond_and_discard", _("Respond and discard message")),
847 ("none", _("No automatic response")))
848 autorespond_owner = forms.ChoiceField(
849 choices=autorespond_choices,
850 widget=forms.RadioSelect,
851 label=_('Autorespond to list owner'),
852 help_text=_('Should Mailman send an auto-response to '
853 'emails sent to the -owner address?'))
854 autoresponse_owner_text = forms.CharField(
855 label=_('Autoresponse owner text'),
856 widget=forms.Textarea(),
857 required=False,
858 help_text=_('Auto-response text to send to -owner emails.'))
859 autorespond_postings = forms.ChoiceField(
860 choices=autorespond_choices,
861 widget=forms.RadioSelect,
862 label=_('Autorespond postings'),
863 help_text=_('Should Mailman send an auto-response to '
864 'mailing list posters?'))
865 autoresponse_postings_text = forms.CharField(
866 label=_('Autoresponse postings text'),
867 widget=forms.Textarea(),
868 required=False,
869 help_text=_('Auto-response text to send to mailing list posters.'))
870 autorespond_requests = forms.ChoiceField(
871 choices=autorespond_choices,
872 widget=forms.RadioSelect,
873 label=_('Autorespond requests'),
874 help_text=_(
875 'Should Mailman send an auto-response to emails sent to the '
876 '-request address? If you choose yes, decide whether you want '
877 'Mailman to discard the original email, or forward it on to the '
878 'system as a normal mail command.'))
879 autoresponse_request_text = forms.CharField(
880 label=_('Autoresponse request text'),
881 widget=forms.Textarea(),
882 required=False,
883 help_text=_('Auto-response text to send to -request emails.'))
884 autoresponse_grace_period = forms.CharField(
885 label=_('Autoresponse grace period'),
886 help_text=_(
887 'Number of days between auto-responses to either the mailing list '
888 'or -request/-owner address from the same poster. Set to zero '
889 '(or negative) for no grace period (i.e. auto-respond to every '
890 'message).'))
891 respond_to_post_requests = forms.ChoiceField(
892 choices=((True, _('Yes')), (False, _('No'))),
893 widget=forms.RadioSelect,
894 required=False,
895 label=_('Notify users of held messages'),
896 help_text=_(
897 'Should Mailman notify users about their messages held for '
898 'approval. If you say \'No\', no notifications will be sent '
899 'to users about the pending approval on their messages.'))
900 send_welcome_message = forms.ChoiceField(
901 choices=((True, _('Yes')), (False, _('No'))),
902 widget=forms.RadioSelect,
903 required=False,
904 label=_('Send welcome message'),
905 help_text=_(
906 'Send welcome message to newly subscribed members? '
907 'Turn this off only if you plan on subscribing people manually '
908 'and don\'t want them to know that you did so. Setting this to No '
909 'is most useful for transparently migrating lists from some other '
910 'mailing list manager to Mailman.\n'
911 'The text of Welcome message can be set via the Templates tab.'))
912 send_goodbye_message = forms.ChoiceField(
913 choices=((True, _('Yes')), (False, _('No'))),
914 widget=forms.RadioSelect,
915 required=False,
916 label=_('Send goodbye message'),
917 help_text=_(
918 'Send goodbye message to newly unsubscribed members? '
919 'Turn this off only if you plan on unsubscribing people manually '
920 'and don\'t want them to know that you did so.\n'
921 'The text of Goodbye message can be set via the Templates tab.'))
922 admin_immed_notify = forms.BooleanField(
923 widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
924 required=False,
925 label=_('Admin immed notify'),
926 help_text=_(
927 'Should the list moderators get immediate notice of new requests, '
928 'as well as daily notices about collected ones? List moderators '
929 '(and list administrators) are sent daily reminders of requests '
930 'pending approval, like subscriptions to a moderated list, '
931 'or postings that are being held for one reason or another. '
932 'Setting this option causes notices to be sent immediately on the '
933 'arrival of new requests as well. '))
934 admin_notify_mchanges = forms.BooleanField(
935 widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
936 required=False,
937 label=_('Notify admin of membership changes'),
938 help_text=_('Should administrator get notices of '
939 'subscribes and unsubscribes?'))
942 NEWSGROUP_MODERATION_CHOICES = (
943 ('none', _('Not Moderated')),
944 ('open_moderated', _('Moderated but allows for open posting')),
945 ('moderated', _('Moderated')),
949 class ListIdentityForm(ListSettingsForm):
951 List identity settings.
953 advertised = forms.ChoiceField(
954 choices=((True, _('Yes')), (False, _('No'))),
955 widget=forms.RadioSelect,
956 label=_('Show list on index page'),
957 help_text=_('Choose whether to include this list '
958 'on the list of all lists'))
959 description = forms.CharField(
960 label=_('Description'),
961 required=False,
962 help_text=_(
963 'This description is used when the mailing list is listed with '
964 'other mailing lists, or in headers, and so forth. It should be '
965 'as succinct as you can get it, while still identifying what the '
966 'list is.'),
968 info = forms.CharField(
969 label=_('Information'),
970 help_text=_('A longer description of this mailing list.'),
971 required=False,
972 widget=forms.Textarea())
973 display_name = forms.CharField(
974 label=_('Display name'),
975 required=False,
976 help_text=_('Display name is the name shown in the web interface.')
978 subject_prefix = forms.CharField(
979 label=_('Subject prefix'),
980 strip=False,
981 required=False,
983 preferred_language = forms.ChoiceField(
984 label=_('Preferred Language'),
985 required=False,
986 widget=forms.Select(),
987 choices=LANGUAGES,
989 member_roster_visibility = forms.ChoiceField(
990 label=_('Members List Visibility'),
991 required=False,
992 widget=forms.Select(),
993 choices=ROSTER_VISIBILITY_CHOICES,
994 help_text=_('Who is allowed to see members list for this MailingList?')
996 gateway_to_mail = forms.BooleanField(
997 widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
998 required=False,
999 label=_('Gateway to mail'),
1000 help_text=_('Flag indicating that posts to the linked newsgroup should'
1001 ' be gated to the list')
1003 gateway_to_news = forms.BooleanField(
1004 widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
1005 required=False,
1006 label=_('Gateway to news'),
1007 help_text=_('Flag indicating that posts to the list should be gated to'
1008 ' the linked newsgroup.')
1010 linked_newsgroup = forms.CharField(
1011 label=_('Linked Newsgroup'),
1012 required=False,
1013 help_text=_(
1014 'The name of the linked newsgroup.')
1016 newsgroup_moderation = forms.ChoiceField(
1017 label=_('Newsgroup moderation'),
1018 required=False,
1019 widget=forms.Select(),
1020 choices=NEWSGROUP_MODERATION_CHOICES,
1021 help_text=_('The moderation policy for the linked newsgroup,'
1022 ' if there is one.')
1024 nntp_prefix_subject_too = forms.BooleanField(
1025 widget=forms.RadioSelect(choices=((True, _('Yes')), (False, _('No')))),
1026 required=False,
1027 label=_('NNTP Include subject prefix '),
1028 help_text=_('Flag indicating whether the list\'s "Subject Prefix"'
1029 ' should be included in posts gated to usenet.')
1032 def clean_subject_prefix(self):
1034 Strip the leading whitespaces from the subject_prefix form field.
1036 return self.cleaned_data.get('subject_prefix', '').lstrip()
1039 class ListMassSubscription(forms.Form):
1040 """Form fields to masssubscribe users to a list.
1042 emails = ListOfStringsField(
1043 label=_('Emails to mass subscribe'),
1044 help_text=_(
1045 'The following formats are accepted:\n'
1046 'jdoe@example.com\n'
1047 '&lt;jdoe@example.com&gt;\n'
1048 'John Doe &lt;jdoe@example.com&gt;\n'
1049 '"John Doe" &lt;jdoe@example.com&gt;\n'
1050 'jdoe@example.com (John Doe)\n'
1051 'Use the last three to associate a display name with the address\n'
1055 pre_confirmed = forms.BooleanField(
1056 label=_('Pre confirm'),
1057 initial=True,
1058 required=False,
1059 help_text=_(
1060 'If checked, users will not have to confirm their subscription.'),
1061 widget=forms.CheckboxInput()
1064 pre_approved = forms.BooleanField(
1065 label=_('Pre approved'),
1066 initial=True,
1067 required=False,
1068 help_text=_(
1069 'If checked, moderators will not have to approve the subscription'
1070 ' request.',),
1071 widget=forms.CheckboxInput()
1074 pre_verified = forms.BooleanField(
1075 label=_('Pre Verified'),
1076 initial=False,
1077 required=False,
1078 help_text=_(
1079 'If checked, users will not have to verify that their '
1080 'email address is valid.'),
1081 widget=forms.CheckboxInput()
1084 invitation = forms.BooleanField(
1085 label=_('Invitation'),
1086 initial=False,
1087 required=False,
1088 help_text=_(
1089 'If checked, the other checkboxes are ignored and the users will '
1090 'be sent an invitation to join the list and will be subscribed '
1091 'upon acceptance thereof.'),
1092 widget=forms.CheckboxInput()
1095 send_welcome_message = forms.ChoiceField(
1096 choices=((True, _('Yes')),
1097 (False, _('No')),
1098 ('default', _('List default'))),
1099 widget=forms.RadioSelect,
1100 initial='default',
1101 required=False,
1102 label=_('Send welcome message'),
1103 help_text=_(
1104 'If set to "Yes" or "No", List\'s default setting of '
1105 'send_welcome_message will be ignored for these subscribers and a'
1106 ' welcome message will be sent or not sent based on the choice.'),
1109 def clean_send_welcome_message(self):
1110 """Choose from True or False. Any other value is equivalent to None."""
1111 data = self.cleaned_data['send_welcome_message']
1112 if data in ('True', 'False'):
1113 return data
1114 # None implies this value is unset and isn't passed on to Core in API
1115 # call.
1116 return None
1119 class ListMassRemoval(forms.Form):
1121 """Form fields to remove multiple list users.
1123 emails = ListOfStringsField(
1124 label=_('Emails to Unsubscribe'),
1125 help_text=_('Add one email address on each line'),
1128 class Meta:
1131 Class to define the name of the fieldsets and what should be
1132 included in each.
1134 layout = [["Mass Removal", "emails"]]
1137 class ListHeaderMatchForm(forms.Form):
1138 """Edit a list's header match."""
1140 HM_ACTION_CHOICES = [(None, _("Default antispam action"))] + \
1141 [a for a in ACTION_CHOICES if a[0] != 'defer']
1143 header = forms.CharField(
1144 label=_('Header'),
1145 help_text=_('Email header to filter on (case-insensitive).'),
1146 error_messages={
1147 'required': _('Please enter a header.'),
1148 'invalid': _('Please enter a valid header.')})
1149 pattern = forms.CharField(
1150 label=_('Pattern'),
1151 help_text=_('Regular expression matching the header\'s value.'),
1152 error_messages={
1153 'required': _('Please enter a pattern.'),
1154 'invalid': _('Please enter a valid pattern.')})
1155 action = forms.ChoiceField(
1156 label=_('Action'),
1157 error_messages={'invalid': _('Please enter a valid action.')},
1158 required=False,
1159 choices=HM_ACTION_CHOICES,
1160 help_text=_('Action to take when a header matches')
1164 class ListHeaderMatchFormset(forms.BaseFormSet):
1165 def clean(self):
1166 """Checks that no two header matches have the same order."""
1167 if any(self.errors):
1168 # Don't bother validating the formset unless
1169 # each form is valid on its own
1170 return
1171 orders = []
1172 for form in self.forms:
1173 try:
1174 order = form.cleaned_data['ORDER']
1175 except KeyError:
1176 continue
1177 if order in orders:
1178 raise forms.ValidationError('Header matches must have'
1179 ' distinct orders.')
1180 orders.append(order)
1183 class MemberModeration(forms.Form):
1185 Form handling the member's moderation_action.
1187 moderation_action = moderation_action_field()
1190 class ChangeSubscriptionForm(forms.Form):
1192 subscriber = forms.ChoiceField(
1193 label=_('Select Email'),
1194 required=False,
1195 widget=forms.Select(),
1196 validators=[validate_uuid_or_email, ],)
1198 member_id = forms.CharField(
1199 required=True,
1200 label="",
1201 widget=forms.TextInput(attrs={'readonly': True, 'hidden': True}))
1203 def __init__(self, user_emails, user_id, primary_email,
1204 *args, **kwargs):
1205 super(ChangeSubscriptionForm, self).__init__(*args, **kwargs)
1206 choices = list((address, address)
1207 for address in user_emails)
1208 if primary_email and user_id:
1209 choices.insert(
1211 (user_id, _('Primary Address ({})').format(primary_email)))
1212 self.fields['subscriber'].choices = choices
1215 class TemplateUpdateForm(forms.ModelForm):
1216 data = forms.CharField(
1217 label=_('Data'),
1218 required=False,
1219 strip=False,
1220 widget=forms.Textarea(),
1221 help_text=_email_template_help_text)
1223 class Meta:
1224 model = EmailTemplate
1225 fields = ['data']
1228 class TokenConfirmForm(forms.Form):
1229 """Form to confirm pending (un)subscription requests from User."""
1230 token = forms.CharField(
1231 required=True,
1232 label="",
1233 widget=forms.TextInput(attrs={'readonly': True, 'hidden': True}))