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 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