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)
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/>.
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")),
56 class ListNew(forms
.Form
):
59 Form fields to add a new list. Languages are hard coded which should
60 be replaced by a REST lookup of available languages.
62 listname
= forms
.CharField(
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'),
71 'required': _("Please enter the list owner's email address.")},
73 advertised
= forms
.ChoiceField(
74 widget
=forms
.RadioSelect(),
75 label
=_('Advertise this list?'),
77 'required': _("Please choose a list type.")},
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
=_('Short Description'),
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(),
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'),
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
):
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']
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",
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
, ],
151 'Subscribing via "Primary Address" will change subscription'
152 ' address when you change your primary address.'),
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
:
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
],
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
= (
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')))),
248 label
=_('Process Bounces'),
250 'Specifies whether or not this list should do automatic'
251 ' bounce processing.'))
253 bounce_score_threshold
= forms
.IntegerField(
255 label
=_('Bounce score threshold'),
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'),
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')))),
274 label
=_('Notify owner on bounce increment'),
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')))),
283 label
=_('Notify owner on disable'),
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')))),
292 label
=_('Notify owner on removal'),
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'),
310 'The number of days between each disabled notification.'))
312 bounce_you_are_disabled_warnings
= forms
.IntegerField(
314 label
=_('Bounce disable warnings'),
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
,
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
])
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
):
379 for archiver
, etc
in self
.fields
['archivers'].choices
:
380 result
[archiver
] = archiver
in self
.cleaned_data
['archivers']
381 self
.cleaned_data
['archivers'] = result
385 class MessageAcceptanceForm(ListSettingsForm
):
387 List messages acceptance settings.
389 acceptable_aliases
= ListOfStringsField(
390 label
=_("Acceptable aliases"),
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')))),
404 label
=_('Require Explicit Destination'),
406 'This checks to ensure that the list posting address or an '
407 'acceptable alias explicitly appears in a To: or Cc: header in '
409 administrivia
= forms
.BooleanField(
410 widget
=forms
.RadioSelect(choices
=((True, _('Yes')), (False, _('No')))),
412 label
=_('Administrivia'),
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, '
419 default_member_action
= forms
.ChoiceField(
420 widget
=forms
.RadioSelect(),
421 label
=_('Default action to take when a member posts to the list'),
423 'required': _("Please choose a default member action.")},
425 choices
=ACTION_CHOICES
,
427 'Default action to take when a member posts to the list.\n'
428 'Hold: This holds the message for approval by the list '
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 '
438 default_nonmember_action
= forms
.ChoiceField(
439 widget
=forms
.RadioSelect(),
440 label
=_('Default action to take when a non-member posts to the list'),
442 'required': _("Please choose a default non-member action.")},
444 choices
=ACTION_CHOICES
,
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')))),
453 label
=_('Emergency Moderation'),
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'
460 max_message_size
= forms
.IntegerField(
462 label
=_('Maximum message size'),
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(
470 label
=_('Maximum number of recipients'),
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(
481 # label=_('Discard held posts after'),
484 # 'No. of days after which held messages will be automatically'
487 accept_these_nonmembers
= ListOfStringsField(
488 label
=_("Accept these non-members"),
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"),
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"),
516 'This is a list, one per line, of regexps matching '
517 'nonmember addresses, posts from which are rejected with notice to'
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"),
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
543 if not self
.cleaned_data
['acceptable_aliases']:
545 for alias
in self
.cleaned_data
['acceptable_aliases']:
546 if alias
.startswith('^'):
549 except re
.error
as e
:
550 raise forms
.ValidationError(
551 _('Invalid alias regexp: {}: {}').format(alias
, e
.msg
))
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
,
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
,
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
,
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(),
602 'required': _("Please choose a DMARC mitigation action.")},
604 ('no_mitigation', _('No DMARC mitigations')),
605 ('munge_from', _('Replace From: with list address')),
607 _('Wrap the message in an outer message From: the list.')),
608 ('reject', _('Reject the message')),
609 ('discard', _('Discard the message'))),
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
,
618 label
=_('DMARC Mitigate unconditionally'),
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'),
626 widget
=forms
.Textarea(),
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'),
633 widget
=forms
.Textarea(),
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
= (
642 ('individual', _('Individual')),
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 \
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
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.
672 class AlterMessagesForm(ListSettingsForm
):
674 Alter messages list settings.
676 personalize
= forms
.ChoiceField(
677 choices
=PERSONALIZATION_CHOICES
,
678 widget
=forms
.RadioSelect
,
680 label
=_('Personalize'),
681 help_text
=PERSONALIZATION_CHOICES_HELP
)
682 filter_content
= forms
.ChoiceField(
683 choices
=((True, _('Yes')), (False, _('No'))),
684 widget
=forms
.RadioSelect
,
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'),
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'),
701 'Extensions to filter from the incoming posts.'
703 pass_types
= ListOfStringsField(
704 label
=_('Pass types'),
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'),
715 'Extensions to allow in the incoming posts.'
717 collapse_alternatives
= forms
.ChoiceField(
718 choices
=((True, _('Yes')), (False, _('No'))),
719 widget
=forms
.RadioSelect
,
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
,
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
,
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
,
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
,
750 label
=_('Include RFC2369 headers'),
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
,
768 label
=_("Include the list post header"),
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 "
773 reply_to_address
= forms
.CharField(
774 label
=_('Explicit reply-to address'),
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 '
780 first_strip_reply_to
= forms
.ChoiceField(
781 choices
=((True, _('Yes')), (False, _('No'))),
782 widget
=forms
.RadioSelect
,
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(),
793 'required': _("Please choose a reply-to action.")},
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'))),
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 '
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(
833 widget
=forms
.Select(),
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(),
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(),
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'),
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(),
883 help_text
=_('Auto-response text to send to -request emails.'))
884 autoresponse_grace_period
= forms
.CharField(
885 label
=_('Autoresponse grace period'),
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 '
891 respond_to_post_requests
= forms
.ChoiceField(
892 choices
=((True, _('Yes')), (False, _('No'))),
893 widget
=forms
.RadioSelect
,
895 label
=_('Notify users of held messages'),
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
,
904 label
=_('Send welcome message'),
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
,
916 label
=_('Send goodbye message'),
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')))),
925 label
=_('Admin immed notify'),
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')))),
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
=_('Short Description'),
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 '
968 info
= forms
.CharField(
969 label
=_('Long Description'),
970 help_text
=_('A longer description of this mailing list.'),
972 widget
=forms
.Textarea())
973 display_name
= forms
.CharField(
974 label
=_('Display name'),
976 help_text
=_('Display name is the name shown in the web interface.')
978 subject_prefix
= forms
.CharField(
979 label
=_('Subject prefix'),
983 preferred_language
= forms
.ChoiceField(
984 label
=_('Preferred Language'),
986 widget
=forms
.Select(),
989 member_roster_visibility
= forms
.ChoiceField(
990 label
=_('Members List Visibility'),
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')))),
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')))),
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'),
1014 'The name of the linked newsgroup.')
1016 newsgroup_moderation
= forms
.ChoiceField(
1017 label
=_('Newsgroup moderation'),
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')))),
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'),
1045 'The following formats are accepted:\n'
1046 'jdoe@example.com\n'
1047 '<jdoe@example.com>\n'
1048 'John Doe <jdoe@example.com>\n'
1049 '"John Doe" <jdoe@example.com>\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'),
1060 'If checked, users will not have to confirm their subscription.'),
1061 widget
=forms
.CheckboxInput()
1064 pre_approved
= forms
.BooleanField(
1065 label
=_('Pre approved'),
1069 'If checked, moderators will not have to approve the subscription'
1071 widget
=forms
.CheckboxInput()
1074 pre_verified
= forms
.BooleanField(
1075 label
=_('Pre Verified'),
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'),
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')),
1098 ('default', _('List default'))),
1099 widget
=forms
.RadioSelect
,
1102 label
=_('Send welcome message'),
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'):
1114 # None implies this value is unset and isn't passed on to Core in API
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'),
1131 Class to define the name of the fieldsets and what should be
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(
1145 help_text
=_('Email header to filter on (case-insensitive).'),
1147 'required': _('Please enter a header.'),
1148 'invalid': _('Please enter a valid header.')})
1149 pattern
= forms
.CharField(
1151 help_text
=_('Regular expression matching the header\'s value.'),
1153 'required': _('Please enter a pattern.'),
1154 'invalid': _('Please enter a valid pattern.')})
1155 action
= forms
.ChoiceField(
1157 error_messages
={'invalid': _('Please enter a valid action.')},
1159 choices
=HM_ACTION_CHOICES
,
1160 help_text
=_('Action to take when a header matches')
1164 class ListHeaderMatchFormset(forms
.BaseFormSet
):
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
1172 for form
in self
.forms
:
1174 order
= form
.cleaned_data
['ORDER']
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'),
1195 widget
=forms
.Select(),
1196 validators
=[validate_uuid_or_email
, ],)
1198 member_id
= forms
.CharField(
1201 widget
=forms
.TextInput(attrs
={'readonly': True, 'hidden': True}))
1203 def __init__(self
, user_emails
, user_id
, primary_email
,
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
:
1211 (user_id
, _('Primary Address ({})').format(primary_email
)))
1212 self
.fields
['subscriber'].choices
= choices
1215 class TemplateUpdateForm(forms
.ModelForm
):
1216 data
= forms
.CharField(
1220 widget
=forms
.Textarea(),
1221 help_text
=_email_template_help_text
)
1224 model
= EmailTemplate
1228 class TokenConfirmForm(forms
.Form
):
1229 """Form to confirm pending (un)subscription requests from User."""
1230 token
= forms
.CharField(
1233 widget
=forms
.TextInput(attrs
={'readonly': True, 'hidden': True}))