1# Copyright (C) 2001-2018 by the Free Software Foundation, Inc.
2#
3# This program is free software; you can redistribute it and/or
4# modify it under the terms of the GNU General Public License
5# as published by the Free Software Foundation; either version 2
6# of the License, or (at your option) any later version.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program; if not, write to the Free Software
15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16# USA.
17
18"""MailList mixin class managing the privacy options."""
19
20import os
21import re
22
23from Mailman import mm_cfg
24from Mailman import Utils
25from Mailman.i18n import _
26from Mailman.Gui.GUIBase import GUIBase
27
28try:
29    True, False
30except NameError:
31    True = 1
32    False = 0
33
34
35
36class Privacy(GUIBase):
37    def GetConfigCategory(self):
38        return 'privacy', _('Privacy options...')
39
40    def GetConfigSubCategories(self, category):
41        if category == 'privacy':
42            return [('subscribing', _('Subscription rules')),
43                    ('sender',      _('Sender filters')),
44                    ('recipient',   _('Recipient filters')),
45                    ('spam',        _('Spam filters')),
46                    ]
47        return None
48
49    def GetConfigInfo(self, mlist, category, subcat=None):
50        if category <> 'privacy':
51            return None
52        # Pre-calculate some stuff.  Technically, we shouldn't do the
53        # sub_cfentry calculation here, but it's too ugly to indent it any
54        # further, and besides, that'll mess up i18n catalogs.
55        WIDTH = mm_cfg.TEXTFIELDWIDTH
56        if mm_cfg.ALLOW_OPEN_SUBSCRIBE:
57            sub_cfentry = ('subscribe_policy', mm_cfg.Radio,
58                           # choices
59                           (_('None'),
60                            _('Confirm'),
61                            _('Require approval'),
62                            _('Confirm and approve')),
63                           0,
64                           _('What steps are required for subscription?<br>'),
65                           _("""None - no verification steps (<em>Not
66                           Recommended </em>)<br>
67                           Confirm (*) - email confirmation step required <br>
68                           Require approval - require list administrator
69                           Approval for subscriptions <br>
70                           Confirm and approve - both confirm and approve
71
72                           <p>(*) when someone requests a subscription,
73                           Mailman sends them a notice with a unique
74                           subscription request number that they must reply to
75                           in order to subscribe.<br>
76
77                           This prevents mischievous (or malicious) people
78                           from creating subscriptions for others without
79                           their consent."""))
80        else:
81            sub_cfentry = ('subscribe_policy', mm_cfg.Radio,
82                           # choices
83                           (_('Confirm'),
84                            _('Require approval'),
85                            _('Confirm and approve')),
86                           1,
87                           _('What steps are required for subscription?<br>'),
88                           _("""Confirm (*) - email confirmation required <br>
89                           Require approval - require list administrator
90                           approval for subscriptions <br>
91                           Confirm and approve - both confirm and approve
92
93                           <p>(*) when someone requests a subscription,
94                           Mailman sends them a notice with a unique
95                           subscription request number that they must reply to
96                           in order to subscribe.<br> This prevents
97                           mischievous (or malicious) people from creating
98                           subscriptions for others without their consent."""))
99
100        # some helpful values
101        admin = mlist.GetScriptURL('admin')
102
103        subscribing_rtn = [
104            _("""This section allows you to configure subscription and
105            membership exposure policy.  You can also control whether this
106            list is public or not.  See also the
107            <a href="%(admin)s/archive">Archival Options</a> section for
108            separate archive-related privacy settings."""),
109
110            _('Subscribing'),
111            ('advertised', mm_cfg.Radio, (_('No'), _('Yes')), 0,
112             _("""Advertise this list when people ask what lists are on this
113             machine?""")),
114
115            sub_cfentry,
116
117            ('subscribe_auto_approval', mm_cfg.EmailListEx, (10, WIDTH), 1,
118             _("""List of addresses (or regexps) whose subscriptions do not
119             require approval."""),
120
121             (_("""When subscription requires approval, addresses in this list
122             are allowed to subscribe without administrator approval. Add
123             addresses one per line. You may begin a line with a ^ character
124             to designate a (case insensitive) regular expression match.""")
125             + ' ' +
126             _("""You may also use the @listname notation to designate the
127             members of another list in this installation."""))),
128
129            ('unsubscribe_policy', mm_cfg.Radio, (_('No'), _('Yes')), 0,
130             _("""Is the list moderator's approval required for unsubscription
131             requests?  (<em>No</em> is recommended)"""),
132
133             _("""When members want to leave a list, they will make an
134             unsubscription request, either via the web or via email.
135             Normally it is best for you to allow open unsubscriptions so that
136             users can easily remove themselves from mailing lists (they get
137             really upset if they can't get off lists!).
138
139             <p>For some lists though, you may want to impose moderator
140             approval before an unsubscription request is processed.  Examples
141             of such lists include a corporate mailing list that all employees
142             are required to be members of.""")),
143
144            _('Ban list'),
145            ('ban_list', mm_cfg.EmailListEx, (10, WIDTH), 1,
146             _("""List of addresses which are banned from membership in this
147             mailing list."""),
148
149             _("""Addresses in this list are banned outright from subscribing
150             to this mailing list, with no further moderation required.  Add
151             addresses one per line; start the line with a ^ character to
152             designate a regular expression match.""")),
153
154            _("Membership exposure"),
155            ('private_roster', mm_cfg.Radio,
156             (_('Anyone'), _('List members'), _('List admin only')), 0,
157             _('Who can view subscription list?'),
158
159             _("""When set, the list of subscribers is protected by member or
160             admin password authentication.""")),
161
162            ('obscure_addresses', mm_cfg.Radio, (_('No'), _('Yes')), 0,
163             _("""Show member addresses so they're not directly recognizable
164             as email addresses?"""),
165             _("""Setting this option causes member email addresses to be
166             transformed when they are presented on list web pages (both in
167             text and as links), so they're not trivially recognizable as
168             email addresses.  The intention is to prevent the addresses
169             from being snarfed up by automated web scanners for use by
170             spammers.""")),
171            ]
172
173        adminurl = mlist.GetScriptURL('admin', absolute=1)
174
175        if mlist.dmarc_quarantine_moderation_action:
176            quarantine = _('/Quarantine')
177        else:
178            quarantine = ''
179        sender_rtn = [
180            _("""When a message is posted to the list, a series of
181            moderation steps are taken to decide whether a moderator must
182            first approve the message or not.  This section contains the
183            controls for moderation of both member and non-member postings.
184
185            <p>Member postings are held for moderation if their
186            <b>moderation flag</b> is turned on.  You can control whether
187            member postings are moderated by default or not.
188
189            <p>Non-member postings can be automatically
190            <a href="?VARHELP=privacy/sender/accept_these_nonmembers"
191            >accepted</a>,
192            <a href="?VARHELP=privacy/sender/hold_these_nonmembers">held for
193            moderation</a>,
194            <a href="?VARHELP=privacy/sender/reject_these_nonmembers"
195            >rejected</a> (bounced), or
196            <a href="?VARHELP=privacy/sender/discard_these_nonmembers"
197            >discarded</a>,
198            either individually or as a group.  Any
199            posting from a non-member who is not explicitly accepted,
200            rejected, or discarded, will have their posting filtered by the
201            <a href="?VARHELP=privacy/sender/generic_nonmember_action">general
202            non-member rules</a>.
203
204            <p>In the text boxes below, add one address per line; start the
205            line with a ^ character to designate a <a href=
206            "https://docs.python.org/2/library/re.html"
207            >Python regular expression</a>.  When entering backslashes, do so
208            as if you were using Python raw strings (i.e. you generally just
209            use a single backslash).
210
211            <p>Note that non-regexp matches are always done first."""),
212
213            _('Member filters'),
214
215            ('default_member_moderation', mm_cfg.Radio, (_('No'), _('Yes')),
216             0, _('By default, should new list member postings be moderated?'),
217
218             _("""Each list member has a <em>moderation flag</em> which says
219             whether messages from the list member can be posted directly to
220             the list, or must first be approved by the list moderator.  When
221             the moderation flag is turned on, list member postings must be
222             approved first.  You, the list administrator can decide whether a
223             specific individual's postings will be moderated or not.
224
225             <p>When a new member is subscribed, their initial moderation flag
226             takes its value from this option.  Turn this option off to accept
227             member postings by default.  Turn this option on to, by default,
228             moderate member postings first.  You can always manually set an
229             individual member's moderation bit by using the
230             <a href="%(adminurl)s/members">membership management
231             screens</a>.""")),
232
233            ('member_verbosity_threshold', mm_cfg.Number, 5, 0,
234             _("""Ceiling on acceptable number of member posts, per interval,
235               before automatic moderation."""),
236
237             _("""If a member posts this many times, within a period of time
238               the member is automatically moderated.  Use 0 to disable.  See
239               <a href="?VARHELP=privacy/sender/member_verbosity_interval"
240               >member_verbosity_interval</a> for details on the time period.
241
242               <p>This is intended to stop people who join a list or lists and
243               then use a bot to send many spam messages in a short interval.
244
245               <p>Be careful when using this setting.  If it is set too low,
246               this can be triggered by a single post cross-posted to
247               multiple lists or by a single post to an umbrella list.""")),
248
249            ('member_verbosity_interval', mm_cfg.Number, 5, 0,
250             _("""Number of seconds to remember posts to this list to determine
251               member_verbosity_threshold for automatic moderation of a
252               member."""),
253
254             _("""If a member's total posts to all lists in this installation
255               with member_verbosity_threshold enabled reaches this list's
256               member_verbosity_threshold, the member is automatically
257               moderated on this list.
258
259               <p>Posts which are counted towards this list's
260               member_verbosity_threshold are all posts to any list with
261               member_verbosity_threshold enabled that arrived within that
262               list's member_verbosity_interval.""")),
263
264            ('member_moderation_action', mm_cfg.Radio,
265             (_('Hold'), _('Reject'), _('Discard')), 0,
266             _("""Action to take when a moderated member posts to the
267             list."""),
268             _("""<ul><li><b>Hold</b> -- this holds the message for approval
269             by the list moderators.
270
271             <p><li><b>Reject</b> -- this automatically rejects the message by
272             sending a bounce notice to the post's author.  The text of the
273             bounce notice can be <a
274             href="?VARHELP=privacy/sender/member_moderation_notice"
275             >configured by you</a>.
276
277             <p><li><b>Discard</b> -- this simply discards the message, with
278             no notice sent to the post's author.
279             </ul>""")),
280
281            ('member_moderation_notice', mm_cfg.Text, (10, WIDTH), 1,
282             _("""Text to include in any
283             <a href="?VARHELP/privacy/sender/member_moderation_action"
284             >rejection notice</a> to
285             be sent to moderated members who post to this list.""")),
286
287            ('dmarc_moderation_action', mm_cfg.Radio,
288             (_('Accept'), _('Munge From'), _('Wrap Message'), _('Reject'),
289                 _('Discard')), 0,
290             _("""Action to take when anyone posts to the
291             list from a domain with a DMARC Reject%(quarantine)s Policy."""),
292
293             _("""<ul><li><b>Munge From</b> -- applies the <a
294             href="?VARHELP=general/from_is_list">from_is_list Munge From</a>
295             transformation to these messages.
296
297             <p><li><b>Wrap Message</b> -- applies the <a
298             href="?VARHELP=general/from_is_list">from_is_list Wrap
299             Message</a> transformation to these messages.
300
301             <p><li><b>Reject</b> -- this automatically rejects the message by
302             sending a bounce notice to the post's author.  The text of the
303             bounce notice can be <a
304             href="?VARHELP=privacy/sender/dmarc_moderation_notice"
305             >configured by you</a>.
306
307             <p><li><b>Discard</b> -- this simply discards the message, with
308             no notice sent to the post's author.
309             </ul>
310
311             <p>This setting takes precedence over the <a
312             href="?VARHELP=general/from_is_list"> from_is_list</a> setting
313             if the message is From: an affected domain and the setting is
314             other than Accept.""")),
315
316            ('dmarc_quarantine_moderation_action', mm_cfg.Radio,
317             (_('No'), _('Yes')), 0,
318             _("""Shall the above dmarc_moderation_action apply to messages
319               From: domains with DMARC p=quarantine as well as p=reject"""),
320
321             _("""<ul><li><b>No</b> -- this applies dmarc_moderation_action to
322               only those posts From: a domain with DMARC p=reject.  This is
323               appropriate if you are concerned about bounced messages, but
324               want to apply dmarc_moderation_action to as few messages as
325               possible.
326               <p><li><b>Yes</b> -- this applies dmarc_moderation_action to
327               posts From: a domain with DMARC p=reject or p=quarantine.
328               </ul><p>If a message is From: a domain with DMARC p=quarantine
329               and dmarc_moderation_action is not applied (this set to No)
330               the message will likely not bounce, but will be delivered to
331               recipients' spam folders or other hard to find places.""")),
332
333            ('dmarc_none_moderation_action', mm_cfg.Radio,
334             (_('No'), _('Yes')), 0,
335             _("""Shall the above dmarc_moderation_action apply to messages
336               From: domains with DMARC p=none as well as p=quarantine and
337               p=reject"""),
338
339             _("""<ul><li><b>No</b> -- this applies dmarc_moderation_action to
340               only those posts From: a domain with DMARC p=reject and
341               possibly p=quarantine depending on the setting of
342               dmarc_quarantine_moderation_action.
343               <p><li><b>Yes</b> -- this applies dmarc_moderation_action to
344               posts From: a domain with DMARC p=none if
345               dmarc_moderation_action is Munge From or Wrap Message and
346               dmarc_quarantine_moderation_action is Yes.
347               <p>The intent of this setting is to eliminate failure reports
348               to the owner of a domain that publishes DMARC p=none by applying
349               the message transformations that would be applied if the
350               domain's DMARC policy were stronger.""")),
351
352            ('dmarc_moderation_notice', mm_cfg.Text, (10, WIDTH), 1,
353             _("""Text to include in any
354             <a href="?VARHELP=privacy/sender/dmarc_moderation_action"
355             >rejection notice</a> to
356             be sent to anyone who posts to this list from a domain
357             with a DMARC Reject%(quarantine)s Policy.""")),
358
359            ('dmarc_moderation_addresses', mm_cfg.EmailListEx, (10, WIDTH), 1,
360             _("""List of addresses (or regexps) whose posts should always apply
361             <a href="?VARHELP=privacy/sender/dmarc_moderation_action"
362             >dmarc_moderation_action</a>
363             regardless of any domain specific DMARC Policy."""),
364
365             _("""Postings from any of these addresses will automatically
366             apply any DMARC action mitigation.  This can be utilized to
367             automatically wrap or munge postings from known addresses or
368             domains that might have policies rejecting external mail From:
369             themselves.
370
371             <p>Add member addresses one per line; start the line with a ^
372             character to designate a regular expression match.""")),
373
374            ('dmarc_wrapped_message_text', mm_cfg.Text, (10, WIDTH), 1,
375             _("""If dmarc_moderation_action applies and is Wrap Message,
376             and this text is provided, the text will be placed in a
377             separate text/plain MIME part preceding the original message
378             part in the wrapped message."""),
379
380             _("""A wrapped message will either be a multipart/mixed message
381             with up to four sub-parts; a text/plain part containing
382             msg_header, a text/plain part containing
383             dmarc_wrapped_message_text, a message/rfc822 part containing the
384             original message and a text/plain part containing msg_footer, or
385             a message/rfc822 message containing only the original message if
386             none of the other parts are applicable.""")),
387
388            ('equivalent_domains', mm_cfg.Text, (10, WIDTH), 1,
389             _("""A 'two dimensional' list of email address domains which are
390               considered equivalent when checking if a post is from a list
391               member."""),
392
393             _("""If two poster addresses with the same local part but
394               different domains are to be considered equivalents for list
395               membership tests, the domains are put here.  The format is
396               one or more groups of equivalent domains.  Within a group,
397               the domains are separated by commas and multiple groups are
398               separated by semicolons. White space is ignored.
399               <p>For example:<pre>
400               example.com,mail.example.com;mac.com,me.com,icloud.com
401               </pre>
402               <p>In this example, if user@example.com is a list member,
403               a post from user@mail.example.com will be treated as if it is
404               from user@example.com for list membership/moderation purposes,
405               and likewise, if user@me.com is a list member, posts from
406               user@mac.com or user@icloud.com will be treated as if from
407               user@me.com.
408               <p>Note that the poster's address is first tested for list
409               membership, and the equivalent domain addresses are only tested
410               if the poster's address is not that of a member.
411               <p>Also note that moderation of the equivalent domain address
412               will apply to the post, but other options such as 'ack' or
413               'not&nbsp;metoo' will not.""")),
414
415            _('Non-member filters'),
416
417            ('accept_these_nonmembers', mm_cfg.EmailListEx, (10, WIDTH), 1,
418             _("""List of non-member addresses whose postings should be
419             automatically accepted."""),
420
421             # XXX Needs to be reviewed for list@domain names.  Also, the
422             # implementation allows the @listname to work in all
423             # *_these_nonmembers. It doesn't make much sense in the others,
424             # but it could be useful. Should we mention it?
425             _("""Postings from any of these non-members will be automatically
426             accepted with no further moderation applied.  Add member
427             addresses one per line; start the line with a ^ character to
428             designate a regular expression match.  A line consisting of
429             the @ character followed by a list name specifies another
430             Mailman list in this installation, all of whose member
431             addresses will be accepted for this list.""")),
432
433            ('hold_these_nonmembers', mm_cfg.EmailListEx, (10, WIDTH), 1,
434             _("""List of non-member addresses whose postings will be
435             immediately held for moderation."""),
436
437             _("""Postings from any of these non-members will be immediately
438             and automatically held for moderation by the list moderators.
439             The sender will receive a notification message which will allow
440             them to cancel their held message.  Add member addresses one per
441             line; start the line with a ^ character to designate a regular
442             expression match.""")),
443
444            ('reject_these_nonmembers', mm_cfg.EmailListEx, (10, WIDTH), 1,
445             _("""List of non-member addresses whose postings will be
446             automatically rejected."""),
447
448             _("""Postings from any of these non-members will be automatically
449             rejected.  In other words, their messages will be bounced back to
450             the sender with a notification of automatic rejection.  This
451             option is not appropriate for known spam senders; their messages
452             should be
453             <a href="?VARHELP=privacy/sender/discard_these_nonmembers"
454             >automatically discarded</a>.
455
456             <p>Add member addresses one per line; start the line with a ^
457             character to designate a regular expression match.""")),
458
459            ('discard_these_nonmembers', mm_cfg.EmailListEx, (10, WIDTH), 1,
460             _("""List of non-member addresses whose postings will be
461             automatically discarded."""),
462
463             _("""Postings from any of these non-members will be automatically
464             discarded.  That is, the message will be thrown away with no
465             further processing or notification.  The sender will not receive
466             a notification or a bounce, however the list moderators can
467             optionally <a href="?VARHELP=privacy/sender/forward_auto_discards"
468             >receive copies of auto-discarded messages.</a>.
469
470             <p>Add member addresses one per line; start the line with a ^
471             character to designate a regular expression match.""")),
472
473            ('generic_nonmember_action', mm_cfg.Radio,
474             (_('Accept'), _('Hold'), _('Reject'), _('Discard')), 0,
475             _("""Action to take for postings from non-members for which no
476             explicit action is defined."""),
477
478             _("""When a post from a non-member is received, the message's
479             sender is matched against the list of explicitly
480             <a href="?VARHELP=privacy/sender/accept_these_nonmembers"
481             >accepted</a>,
482             <a href="?VARHELP=privacy/sender/hold_these_nonmembers">held</a>,
483             <a href="?VARHELP=privacy/sender/reject_these_nonmembers"
484             >rejected</a> (bounced), and
485             <a href="?VARHELP=privacy/sender/discard_these_nonmembers"
486             >discarded</a> addresses.  If no match is found, then this action
487             is taken.""")),
488
489            ('forward_auto_discards', mm_cfg.Radio, (_('No'), _('Yes')), 0,
490             _("""Should messages from non-members, which are automatically
491             discarded, be forwarded to the list moderator?""")),
492
493            ('nonmember_rejection_notice', mm_cfg.Text, (10, WIDTH), 1,
494             _("""Text to include in any rejection notice to be sent to
495             non-members who post to this list. This notice can include
496             the list's owner address by %%(listowner)s and replaces the
497             internally crafted default message.""")),
498
499            ]
500
501        recip_rtn = [
502            _("""This section allows you to configure various filters based on
503            the recipient of the message."""),
504
505            _('Recipient filters'),
506
507            ('require_explicit_destination', mm_cfg.Radio,
508             (_('No'), _('Yes')), 0,
509             _("""Must posts have list named in destination (to, cc) field
510             (or be among the acceptable alias names, specified below)?"""),
511
512             _("""Many (in fact, most) spams do not explicitly name their
513             myriad destinations in the explicit destination addresses - in
514             fact often the To: field has a totally bogus address for
515             obfuscation.  The constraint applies only to the stuff in the
516             address before the '@' sign, but still catches all such spams.
517
518             <p>The cost is that the list will not accept unhindered any
519             postings relayed from other addresses, unless
520
521             <ol>
522                 <li>The relaying address has the same name, or
523
524                 <li>The relaying address name is included on the options that
525                 specifies acceptable aliases for the list.
526
527             </ol>""")),
528
529            ('acceptable_aliases', mm_cfg.Text, (4, WIDTH), 0,
530             _("""Alias names (regexps) which qualify as explicit to or cc
531             destination names for this list."""),
532
533             _("""Alternate addresses that are acceptable when
534             `require_explicit_destination' is enabled.  This option takes a
535             list of regular expressions, one per line, which is matched
536             against every recipient address in the message.  The matching is
537             performed with Python's re.match() function, meaning they are
538             anchored to the start of the string.
539
540             <p>For backwards compatibility with Mailman 1.1, if the regexp
541             does not contain an `@', then the pattern is matched against just
542             the local part of the recipient address.  If that match fails, or
543             if the pattern does contain an `@', then the pattern is matched
544             against the entire recipient address.
545
546             <p>Matching against the local part is deprecated; in a future
547             release, the pattern will always be matched against the entire
548             recipient address.""")),
549
550            ('max_num_recipients', mm_cfg.Number, 5, 0,
551             _('Ceiling on acceptable number of recipients for a posting.'),
552
553             _("""If a posting has this number, or more, of recipients, it is
554             held for admin approval.  Use 0 for no ceiling.""")),
555            ]
556
557        spam_rtn = [
558            _("""This section allows you to configure various anti-spam
559            filters posting filters, which can help reduce the amount of spam
560            your list members end up receiving.
561            """),
562
563            _('Header filters'),
564
565            ('header_filter_rules', mm_cfg.HeaderFilter, 0, 0,
566             _('Filter rules to match against the headers of a message.'),
567
568             _("""Each header filter rule has two parts, a list of regular
569             expressions, one per line, and an action to take.  Mailman
570             matches the message's headers against every regular expression in
571             the rule and if any match, the message is rejected, held, or
572             discarded based on the action you specify.  Use <em>Defer</em> to
573             temporarily disable a rule.
574
575             You can have more than one filter rule for your list.  In that
576             case, each rule is matched in turn, with processing stopped after
577             the first match.
578
579             Note that headers are collected from all the attachments
580             (except for the mailman administrivia message) and
581             matched against the regular expressions. With this feature,
582             you can effectively sort out messages with dangerous file
583             types or file name extensions.""")),
584
585            _('Legacy anti-spam filters'),
586
587            ('bounce_matching_headers', mm_cfg.Text, (6, WIDTH), 0,
588             _('Hold posts with header value matching a specified regexp.'),
589             _("""Use this option to prohibit posts according to specific
590             header values.  The target value is a regular-expression for
591             matching against the specified header.  The match is done
592             disregarding letter case.  Lines beginning with '#' are ignored
593             as comments.
594
595             <p>For example:<pre>to: .*@public.com </pre> says to hold all
596             postings with a <em>To:</em> mail header containing '@public.com'
597             anywhere among the addresses.
598
599             <p>Note that leading whitespace is trimmed from the regexp.  This
600             can be circumvented in a number of ways, e.g. by escaping or
601             bracketing it.""")),
602          ]
603
604        if subcat == 'sender':
605            return sender_rtn
606        elif subcat == 'recipient':
607            return recip_rtn
608        elif subcat == 'spam':
609            return spam_rtn
610        else:
611            return subscribing_rtn
612
613    def _setValue(self, mlist, property, val, doc):
614        # Ignore any hdrfilter_* form variables
615        if property.startswith('hdrfilter_'):
616            return
617        # For subscribe_policy when ALLOW_OPEN_SUBSCRIBE is true, we need to
618        # add one to the value because the page didn't present an open list as
619        # an option.
620        if property == 'subscribe_policy' and not mm_cfg.ALLOW_OPEN_SUBSCRIBE:
621            val += 1
622        if (property == 'dmarc_moderation_action' and
623                val < mm_cfg.DEFAULT_DMARC_MODERATION_ACTION):
624            doc.addError(_("""dmarc_moderation_action must be >= the configured
625                           default value."""))
626            val = mm_cfg.DEFAULT_DMARC_MODERATION_ACTION
627        setattr(mlist, property, val)
628
629    # We need to handle the header_filter_rules widgets specially, but
630    # everything else can be done by the base class's handleForm() method.
631    # However, to do this we need an awful hack.  _setValue() and
632    # _getValidValue() will essentially ignore any hdrfilter_* form variables.
633    # TK: we should call this function only in subcat == 'spam'
634    def _handleForm(self, mlist, category, subcat, cgidata, doc):
635        # TK: If there is no hdrfilter_* in cgidata, we should not touch
636        # the header filter rules.
637        if not cgidata.has_key('hdrfilter_rebox_01'):
638            return
639        # First deal with
640        rules = []
641        # We start i at 1 and keep going until we no longer find items keyed
642        # with the marked tags.
643        i = 1
644        downi = None
645        while True:
646            deltag    = 'hdrfilter_delete_%02d' % i
647            reboxtag  = 'hdrfilter_rebox_%02d' % i
648            actiontag = 'hdrfilter_action_%02d' % i
649            wheretag  = 'hdrfilter_where_%02d' % i
650            addtag    = 'hdrfilter_add_%02d' % i
651            newtag    = 'hdrfilter_new_%02d' % i
652            uptag     = 'hdrfilter_up_%02d' % i
653            downtag   = 'hdrfilter_down_%02d' % i
654            i += 1
655            # Was this a delete?  If so, we can just ignore this entry
656            if cgidata.has_key(deltag):
657                continue
658            # Get the data for the current box
659            pattern = cgidata.getfirst(reboxtag)
660            try:
661                action  = int(cgidata.getfirst(actiontag))
662                # We'll get a TypeError when the actiontag is missing and the
663                # .getvalue() call returns None.
664            except (ValueError, TypeError):
665                action = mm_cfg.DEFER
666            if pattern is None:
667                # We came to the end of the boxes
668                break
669            if cgidata.has_key(newtag) and not pattern:
670                # This new entry is incomplete.
671                if i == 2:
672                    # OK it is the first.
673                    continue
674                doc.addError(_("""Header filter rules require a pattern.
675                Incomplete filter rules will be ignored."""))
676                continue
677            # Make sure the pattern was a legal regular expression.
678            # Convert it to unicode if necessary.
679            mo = re.match('.*charset=([-_a-z0-9]+)',
680                          os.environ.get('CONTENT_TYPE', ''),
681                          re.IGNORECASE
682                         )
683            if mo:
684                cset = mo.group(1)
685            else:
686                cset = Utils.GetCharSet(mlist.preferred_language)
687            try:
688                upattern = Utils.xml_to_unicode(pattern, cset)
689                re.compile(upattern)
690                pattern = upattern
691            except (re.error, TypeError):
692                safepattern = Utils.websafe(pattern)
693                doc.addError(_("""The header filter rule pattern
694                '%(safepattern)s' is not a legal regular expression.  This
695                rule will be ignored."""))
696                continue
697            # Was this an add item?
698            if cgidata.has_key(addtag):
699                # Where should the new one be added?
700                where = cgidata.getfirst(wheretag)
701                if where == 'before':
702                    # Add a new empty rule box before the current one
703                    rules.append(('', mm_cfg.DEFER, True))
704                    rules.append((pattern, action, False))
705                    # Default is to add it after...
706                else:
707                    rules.append((pattern, action, False))
708                    rules.append(('', mm_cfg.DEFER, True))
709            # Was this an up movement?
710            elif cgidata.has_key(uptag):
711                # As long as this one isn't the first rule, move it up
712                if rules:
713                    rules.insert(-1, (pattern, action, False))
714                else:
715                    rules.append((pattern, action, False))
716            # Was this the down movement?
717            elif cgidata.has_key(downtag):
718                downi = i - 2
719                rules.append((pattern, action, False))
720            # Otherwise, just retain this one in the list
721            else:
722                rules.append((pattern, action, False))
723        # Move any down button filter rule
724        if downi is not None:
725            rule = rules[downi]
726            del rules[downi]
727            rules.insert(downi+1, rule)
728        mlist.header_filter_rules = rules
729
730    def handleForm(self, mlist, category, subcat, cgidata, doc):
731        if subcat == 'spam':
732            self._handleForm(mlist, category, subcat, cgidata, doc)
733        # Everything else is dealt with by the base handler
734        GUIBase.handleForm(self, mlist, category, subcat, cgidata, doc)
735