aboutsummaryrefslogtreecommitdiffstats
path: root/Mailman/Gui/Privacy.py
blob: 5fe4a2d92b489c1e5638e065c9b50bdacc856081 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
# Copyright (C) 2001-2005 by the Free Software Foundation, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software 
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.

"""MailList mixin class managing the privacy options."""

import re

from Mailman import mm_cfg
from Mailman import Utils
from Mailman.i18n import _
from Mailman.Gui.GUIBase import GUIBase

try:
    True, False
except NameError:
    True = 1
    False = 0



class Privacy(GUIBase):
    def GetConfigCategory(self):
        return 'privacy', _('Privacy options...')

    def GetConfigSubCategories(self, category):
        if category == 'privacy':
            return [('subscribing', _('Subscription rules')),
                    ('sender',      _('Sender filters')),
                    ('recipient',   _('Recipient filters')),
                    ('spam',        _('Spam filters')),
                    ]
        return None

    def GetConfigInfo(self, mlist, category, subcat=None):
        if category <> 'privacy':
            return None
        # Pre-calculate some stuff.  Technically, we shouldn't do the
        # sub_cfentry calculation here, but it's too ugly to indent it any
        # further, and besides, that'll mess up i18n catalogs.
        WIDTH = mm_cfg.TEXTFIELDWIDTH
        if mm_cfg.ALLOW_OPEN_SUBSCRIBE:
            sub_cfentry = ('subscribe_policy', mm_cfg.Radio,
                           # choices
                           (_('None'),
                            _('Confirm'),
                            _('Require approval'),
                            _('Confirm and approve')),
                           0, 
                           _('What steps are required for subscription?<br>'),
                           _('''None - no verification steps (<em>Not
                           Recommended </em>)<br>
                           Confirm (*) - email confirmation step required <br>
                           Require approval - require list administrator
                           Approval for subscriptions <br>
                           Confirm and approve - both confirm and approve
                           
                           <p>(*) when someone requests a subscription,
                           Mailman sends them a notice with a unique
                           subscription request number that they must reply to
                           in order to subscribe.<br>

                           This prevents mischievous (or malicious) people
                           from creating subscriptions for others without
                           their consent.'''))
        else:
            sub_cfentry = ('subscribe_policy', mm_cfg.Radio,
                           # choices
                           (_('Confirm'),
                            _('Require approval'),
                            _('Confirm and approve')),
                           1,
                           _('What steps are required for subscription?<br>'),
                           _('''Confirm (*) - email confirmation required <br>
                           Require approval - require list administrator
                           approval for subscriptions <br>
                           Confirm and approve - both confirm and approve
                           
                           <p>(*) when someone requests a subscription,
                           Mailman sends them a notice with a unique
                           subscription request number that they must reply to
                           in order to subscribe.<br> This prevents
                           mischievous (or malicious) people from creating
                           subscriptions for others without their consent.'''))

        # some helpful values
        admin = mlist.GetScriptURL('admin')

        subscribing_rtn = [
            _("""This section allows you to configure subscription and
            membership exposure policy.  You can also control whether this
            list is public or not.  See also the
            <a href="%(admin)s/archive">Archival Options</a> section for
            separate archive-related privacy settings."""),

            _('Subscribing'),
            ('advertised', mm_cfg.Radio, (_('No'), _('Yes')), 0,
             _('''Advertise this list when people ask what lists are on this
             machine?''')),

            sub_cfentry,
            
            ('unsubscribe_policy', mm_cfg.Radio, (_('No'), _('Yes')), 0,
             _("""Is the list moderator's approval required for unsubscription
             requests?  (<em>No</em> is recommended)"""),

             _("""When members want to leave a list, they will make an
             unsubscription request, either via the web or via email.
             Normally it is best for you to allow open unsubscriptions so that
             users can easily remove themselves from mailing lists (they get
             really upset if they can't get off lists!).

             <p>For some lists though, you may want to impose moderator
             approval before an unsubscription request is processed.  Examples
             of such lists include a corporate mailing list that all employees
             are required to be members of.""")),

            _('Ban list'),
            ('ban_list', mm_cfg.EmailListEx, (10, WIDTH), 1,
             _("""List of addresses which are banned from membership in this
             mailing list."""),

             _("""Addresses in this list are banned outright from subscribing
             to this mailing list, with no further moderation required.  Add
             addresses one per line; start the line with a ^ character to
             designate a regular expression match.""")),

            _("Membership exposure"),
            ('private_roster', mm_cfg.Radio,
             (_('Anyone'), _('List members'), _('List admin only')), 0,
             _('Who can view subscription list?'),

             _('''When set, the list of subscribers is protected by member or
             admin password authentication.''')),

            ('obscure_addresses', mm_cfg.Radio, (_('No'), _('Yes')), 0,
             _("""Show member addresses so they're not directly recognizable
             as email addresses?"""),
             _("""Setting this option causes member email addresses to be
             transformed when they are presented on list web pages (both in
             text and as links), so they're not trivially recognizable as
             email addresses.  The intention is to prevent the addresses
             from being snarfed up by automated web scanners for use by
             spammers.""")),
            ]

        adminurl = mlist.GetScriptURL('admin', absolute=1)
        sender_rtn = [
            _("""When a message is posted to the list, a series of
            moderation steps are take to decide whether the a moderator must
            first approve the message or not.  This section contains the
            controls for moderation of both member and non-member postings.

            <p>Member postings are held for moderation if their
            <b>moderation flag</b> is turned on.  You can control whether
            member postings are moderated by default or not.

            <p>Non-member postings can be automatically
            <a href="?VARHELP=privacy/sender/accept_these_nonmembers"
            >accepted</a>,
            <a href="?VARHELP=privacy/sender/hold_these_nonmembers">held for
            moderation</a>,
            <a href="?VARHELP=privacy/sender/reject_these_nonmembers"
            >rejected</a> (bounced), or
            <a href="?VARHELP=privacy/sender/discard_these_nonmembers"
            >discarded</a>,
            either individually or as a group.  Any
            posting from a non-member who is not explicitly accepted,
            rejected, or discarded, will have their posting filtered by the
            <a href="?VARHELP=privacy/sender/generic_nonmember_action">general
            non-member rules</a>.

            <p>In the text boxes below, add one address per line; start the
            line with a ^ character to designate a <a href=
            "http://www.python.org/doc/current/lib/module-re.html"
            >Python regular expression</a>.  When entering backslashes, do so
            as if you were using Python raw strings (i.e. you generally just
            use a single backslash).

            <p>Note that non-regexp matches are always done first."""),

            _('Member filters'),

            ('default_member_moderation', mm_cfg.Radio, (_('No'), _('Yes')),
             0, _('By default, should new list member postings be moderated?'),

             _("""Each list member has a <em>moderation flag</em> which says
             whether messages from the list member can be posted directly to
             the list, or must first be approved by the list moderator.  When
             the moderation flag is turned on, list member postings must be
             approved first.  You, the list administrator can decide whether a
             specific individual's postings will be moderated or not.

             <p>When a new member is subscribed, their initial moderation flag
             takes its value from this option.  Turn this option off to accept
             member postings by default.  Turn this option on to, by default,
             moderate member postings first.  You can always manually set an
             individual member's moderation bit by using the
             <a href="%(adminurl)s/members">membership management
             screens</a>.""")),

            ('member_moderation_action', mm_cfg.Radio,
             (_('Hold'), _('Reject'), _('Discard')), 0,
             _("""Action to take when a moderated member posts to the
             list."""),
             _("""<ul><li><b>Hold</b> -- this holds the message for approval
             by the list moderators.

             <p><li><b>Reject</b> -- this automatically rejects the message by
             sending a bounce notice to the post's author.  The text of the
             bounce notice can be <a
             href="?VARHELP=privacy/sender/member_moderation_notice"
             >configured by you</a>.

             <p><li><b>Discard</b> -- this simply discards the message, with
             no notice sent to the post's author.
             </ul>""")),

            ('member_moderation_notice', mm_cfg.Text, (10, WIDTH), 1,
             _("""Text to include in any
             <a href="?VARHELP/privacy/sender/member_moderation_action"
             >rejection notice</a> to
             be sent to moderated members who post to this list.""")),

            _('Non-member filters'),

            ('accept_these_nonmembers', mm_cfg.EmailListEx, (10, WIDTH), 1,
             _("""List of non-member addresses whose postings should be
             automatically accepted."""),

             _("""Postings from any of these non-members will be automatically
             accepted with no further moderation applied.  Add member
             addresses one per line; start the line with a ^ character to
             designate a regular expression match.""")),

            ('hold_these_nonmembers', mm_cfg.EmailListEx, (10, WIDTH), 1,
             _("""List of non-member addresses whose postings will be
             immediately held for moderation."""),

             _("""Postings from any of these non-members will be immediately
             and automatically held for moderation by the list moderators.
             The sender will receive a notification message which will allow
             them to cancel their held message.  Add member addresses one per
             line; start the line with a ^ character to designate a regular
             expression match.""")),

            ('reject_these_nonmembers', mm_cfg.EmailListEx, (10, WIDTH), 1,
             _("""List of non-member addresses whose postings will be
             automatically rejected."""),

             _("""Postings from any of these non-members will be automatically
             rejected.  In other words, their messages will be bounced back to
             the sender with a notification of automatic rejection.  This
             option is not appropriate for known spam senders; their messages
             should be
             <a href="?VARHELP=privacy/sender/discard_these_nonmembers"
             >automatically discarded</a>.

             <p>Add member addresses one per line; start the line with a ^
             character to designate a regular expression match.""")),

            ('discard_these_nonmembers', mm_cfg.EmailListEx, (10, WIDTH), 1,
             _("""List of non-member addresses whose postings will be
             automatically discarded."""),

             _("""Postings from any of these non-members will be automatically
             discarded.  That is, the message will be thrown away with no
             further processing or notification.  The sender will not receive
             a notification or a bounce, however the list moderators can
             optionally <a href="?VARHELP=privacy/sender/forward_auto_discards"
             >receive copies of auto-discarded messages.</a>.

             <p>Add member addresses one per line; start the line with a ^
             character to designate a regular expression match.""")),

            ('generic_nonmember_action', mm_cfg.Radio,
             (_('Accept'), _('Hold'), _('Reject'), _('Discard')), 0,
             _("""Action to take for postings from non-members for which no
             explicit action is defined."""),

             _("""When a post from a non-member is received, the message's
             sender is matched against the list of explicitly
             <a href="?VARHELP=privacy/sender/accept_these_nonmembers"
             >accepted</a>,
             <a href="?VARHELP=privacy/sender/hold_these_nonmembers">held</a>,
             <a href="?VARHELP=privacy/sender/reject_these_nonmembers"
             >rejected</a> (bounced), and
             <a href="?VARHELP=privacy/sender/discard_these_nonmembers"
             >discarded</a> addresses.  If no match is found, then this action
             is taken.""")),

            ('forward_auto_discards', mm_cfg.Radio, (_('No'), _('Yes')), 0,
             _("""Should messages from non-members, which are automatically
             discarded, be forwarded to the list moderator?""")),

            ('nonmember_rejection_notice', mm_cfg.Text, (10, WIDTH), 1,
             _("""Text to include in any rejection notice to be sent to
             non-members who post to this list. This notice can include
             the list's owner address by %%(listowner)s and replaces the
             internally crafted default message.""")),

            ]

        recip_rtn = [
            _("""This section allows you to configure various filters based on
            the recipient of the message."""),

            _('Recipient filters'),

            ('require_explicit_destination', mm_cfg.Radio,
             (_('No'), _('Yes')), 0,
             _("""Must posts have list named in destination (to, cc) field
             (or be among the acceptable alias names, specified below)?"""),

             _("""Many (in fact, most) spams do not explicitly name their
             myriad destinations in the explicit destination addresses - in
             fact often the To: field has a totally bogus address for
             obfuscation.  The constraint applies only to the stuff in the
             address before the '@' sign, but still catches all such spams.

             <p>The cost is that the list will not accept unhindered any
             postings relayed from other addresses, unless

             <ol>
                 <li>The relaying address has the same name, or

                 <li>The relaying address name is included on the options that
                 specifies acceptable aliases for the list.

             </ol>""")),

            ('acceptable_aliases', mm_cfg.Text, (4, WIDTH), 0,
             _("""Alias names (regexps) which qualify as explicit to or cc
             destination names for this list."""),

             _("""Alternate addresses that are acceptable when
             `require_explicit_destination' is enabled.  This option takes a
             list of regular expressions, one per line, which is matched
             against every recipient address in the message.  The matching is
             performed with Python's re.match() function, meaning they are
             anchored to the start of the string.
             
             <p>For backwards compatibility with Mailman 1.1, if the regexp
             does not contain an `@', then the pattern is matched against just
             the local part of the recipient address.  If that match fails, or
             if the pattern does contain an `@', then the pattern is matched
             against the entire recipient address.
             
             <p>Matching against the local part is deprecated; in a future
             release, the pattern will always be matched against the entire
             recipient address.""")),

            ('max_num_recipients', mm_cfg.Number, 5, 0, 
             _('Ceiling on acceptable number of recipients for a posting.'),

             _('''If a posting has this number, or more, of recipients, it is
             held for admin approval.  Use 0 for no ceiling.''')),
            ]

        spam_rtn = [
            _("""This section allows you to configure various anti-spam
            filters posting filters, which can help reduce the amount of spam
            your list members end up receiving.
            """),

            _('Header filters'),

            ('header_filter_rules', mm_cfg.HeaderFilter, 0, 0,
             _('Filter rules to match against the headers of a message.'),

             _("""Each header filter rule has two parts, a list of regular
             expressions, one per line, and an action to take.  Mailman
             matches the message's headers against every regular expression in
             the rule and if any match, the message is rejected, held, or
             discarded based on the action you specify.  Use <em>Defer</em> to
             temporarily disable a rule.

             You can have more than one filter rule for your list.  In that
             case, each rule is matched in turn, with processing stopped after
             the first match.

             Note that headers are collected from all the attachments 
             (except for the mailman administrivia message) and
             matched against the regular expressions. With this feature,
             you can effectively sort out messages with dangerous file
             types or file name extensions.""")),

            _('Legacy anti-spam filters'),

            ('bounce_matching_headers', mm_cfg.Text, (6, WIDTH), 0,
             _('Hold posts with header value matching a specified regexp.'),
             _("""Use this option to prohibit posts according to specific
             header values.  The target value is a regular-expression for
             matching against the specified header.  The match is done
             disregarding letter case.  Lines beginning with '#' are ignored
             as comments.

             <p>For example:<pre>to: .*@public.com </pre> says to hold all
             postings with a <em>To:</em> mail header containing '@public.com'
             anywhere among the addresses.

             <p>Note that leading whitespace is trimmed from the regexp.  This
             can be circumvented in a number of ways, e.g. by escaping or
             bracketing it.""")),
          ]

        if subcat == 'sender':
            return sender_rtn
        elif subcat == 'recipient':
            return recip_rtn
        elif subcat == 'spam':
            return spam_rtn
        else:
            return subscribing_rtn

    def _setValue(self, mlist, property, val, doc):
        # Ignore any hdrfilter_* form variables
        if property.startswith('hdrfilter_'):
            return
        # For subscribe_policy when ALLOW_OPEN_SUBSCRIBE is true, we need to
        # add one to the value because the page didn't present an open list as
        # an option.
        if property == 'subscribe_policy' and not mm_cfg.ALLOW_OPEN_SUBSCRIBE:
            val += 1
        setattr(mlist, property, val)

    # We need to handle the header_filter_rules widgets specially, but
    # everything else can be done by the base class's handleForm() method.
    # However, to do this we need an awful hack.  _setValue() and
    # _getValidValue() will essentially ignore any hdrfilter_* form variables.
    # TK: we should call this function only in subcat == 'spam'
    def _handleForm(self, mlist, category, subcat, cgidata, doc):
        # TK: If there is no hdrfilter_* in cgidata, we should not touch
        # the header filter rules.
        if not cgidata.has_key('hdrfilter_rebox_01'):
            return
        # First deal with
        rules = []
        # We start i at 1 and keep going until we no longer find items keyed
        # with the marked tags.
        i = 1
        downi = None
        while True:
            deltag    = 'hdrfilter_delete_%02d' % i
            reboxtag  = 'hdrfilter_rebox_%02d' % i
            actiontag = 'hdrfilter_action_%02d' % i
            wheretag  = 'hdrfilter_where_%02d' % i
            addtag    = 'hdrfilter_add_%02d' % i
            newtag    = 'hdrfilter_new_%02d' % i
            uptag     = 'hdrfilter_up_%02d' % i
            downtag   = 'hdrfilter_down_%02d' % i
            i += 1
            # Was this a delete?  If so, we can just ignore this entry
            if cgidata.has_key(deltag):
                continue
            # Get the data for the current box
            pattern = cgidata.getvalue(reboxtag)
            try:
                action  = int(cgidata.getvalue(actiontag))
                # We'll get a TypeError when the actiontag is missing and the
                # .getvalue() call returns None.
            except (ValueError, TypeError):
                action = mm_cfg.DEFER
            if pattern is None:
                # We came to the end of the boxes
                break
            if cgidata.has_key(newtag) and not pattern:
                # This new entry is incomplete.
                if i == 2:
                    # OK it is the first.
                    continue
                doc.addError(_("""Header filter rules require a pattern.
                Incomplete filter rules will be ignored."""))
                continue
            # Make sure the pattern was a legal regular expression
            try:
                re.compile(pattern)
            except (re.error, TypeError):
                safepattern = Utils.websafe(pattern)
                doc.addError(_("""The header filter rule pattern
                '%(safepattern)s' is not a legal regular expression.  This
                rule will be ignored."""))
                continue
            # Was this an add item?
            if cgidata.has_key(addtag):
                # Where should the new one be added?
                where = cgidata.getvalue(wheretag)
                if where == 'before':
                    # Add a new empty rule box before the current one
                    rules.append(('', mm_cfg.DEFER, True))
                    rules.append((pattern, action, False))
                    # Default is to add it after...
                else:
                    rules.append((pattern, action, False))
                    rules.append(('', mm_cfg.DEFER, True))
            # Was this an up movement?
            elif cgidata.has_key(uptag):
                # As long as this one isn't the first rule, move it up
                if rules:
                    rules.insert(-1, (pattern, action, False))
                else:
                    rules.append((pattern, action, False))
            # Was this the down movement?
            elif cgidata.has_key(downtag):
                downi = i - 2
                rules.append((pattern, action, False))
            # Otherwise, just retain this one in the list
            else:
                rules.append((pattern, action, False))
        # Move any down button filter rule
        if downi is not None:
            rule = rules[downi]
            del rules[downi]
            rules.insert(downi+1, rule)
        mlist.header_filter_rules = rules

    def handleForm(self, mlist, category, subcat, cgidata, doc):
        if subcat == 'spam':
            self._handleForm(mlist, category, subcat, cgidata, doc)
        # Everything else is dealt with by the base handler
        GUIBase.handleForm(self, mlist, category, subcat, cgidata, doc)