diff options
Diffstat (limited to '')
-rwxr-xr-x | Mailman/MailList.py | 179 |
1 files changed, 155 insertions, 24 deletions
diff --git a/Mailman/MailList.py b/Mailman/MailList.py index 2d653acb..d1dc17a4 100755 --- a/Mailman/MailList.py +++ b/Mailman/MailList.py @@ -1,4 +1,4 @@ -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# Copyright (C) 1998-2016 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 @@ -74,6 +74,7 @@ from Mailman.Logging.Syslog import syslog _ = i18n._ EMPTYSTRING = '' +OR = '|' try: True, False @@ -347,6 +348,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, self.bounce_matching_headers = \ mm_cfg.DEFAULT_BOUNCE_MATCHING_HEADERS self.header_filter_rules = [] + self.from_is_list = mm_cfg.DEFAULT_FROM_IS_LIST self.anonymous_list = mm_cfg.DEFAULT_ANONYMOUS_LIST internalname = self.internal_name() self.real_name = internalname[0].upper() + internalname[1:] @@ -355,6 +357,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, self.welcome_msg = '' self.goodbye_msg = '' self.subscribe_policy = mm_cfg.DEFAULT_SUBSCRIBE_POLICY + self.subscribe_auto_approval = mm_cfg.DEFAULT_SUBSCRIBE_AUTO_APPROVAL self.unsubscribe_policy = mm_cfg.DEFAULT_UNSUBSCRIBE_POLICY self.private_roster = mm_cfg.DEFAULT_PRIVATE_ROSTER self.obscure_addresses = mm_cfg.DEFAULT_OBSCURE_ADDRESSES @@ -383,11 +386,25 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, mm_cfg.DEFAULT_DEFAULT_MEMBER_MODERATION # Emergency moderation bit self.emergency = 0 + self.member_verbosity_threshold = ( + mm_cfg.DEFAULT_MEMBER_VERBOSITY_THRESHOLD) + self.member_verbosity_interval = ( + mm_cfg.DEFAULT_MEMBER_VERBOSITY_INTERVAL) # This really ought to default to mm_cfg.HOLD, but that doesn't work # with the current GUI description model. So, 0==Hold, 1==Reject, # 2==Discard self.member_moderation_action = 0 self.member_moderation_notice = '' + self.dmarc_moderation_action = mm_cfg.DEFAULT_DMARC_MODERATION_ACTION + self.dmarc_quarantine_moderation_action = ( + mm_cfg.DEFAULT_DMARC_QUARANTINE_MODERATION_ACTION) + self.dmarc_none_moderation_action = ( + mm_cfg.DEFAULT_DMARC_NONE_MODERATION_ACTION) + self.dmarc_moderation_notice = '' + self.dmarc_wrapped_message_text = ( + mm_cfg.DEFAULT_DMARC_WRAPPED_MESSAGE_TEXT) + self.equivalent_domains = ( + mm_cfg.DEFAULT_EQUIVALENT_DOMAINS) self.accept_these_nonmembers = [] self.hold_these_nonmembers = [] self.reject_these_nonmembers = [] @@ -617,6 +634,7 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, if e.errno <> errno.ENOENT: raise # The file doesn't exist yet return None, e + now = int(time.time()) try: try: dict = loadfunc(fp) @@ -628,8 +646,9 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, finally: fp.close() # Update the timestamp. We use current time here rather than mtime - # so the test above might succeed the next time. - self.__timestamp = int(time.time()) + # so the test above might succeed the next time. And we get the time + # before unpickling in case it takes more than a second. (LP: #266464) + self.__timestamp = now return dict, None def Load(self, check_version=True): @@ -768,10 +787,11 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, goodtopics = [] for name, pattern, desc, emptyflag in self.topics: try: - re.compile(pattern) + orpattern = OR.join(pattern.splitlines()) + re.compile(orpattern) except (re.error, TypeError): syslog('error', 'Bad topic pattern "%s" for list: %s', - pattern, self.internal_name()) + orpattern, self.internal_name()) else: goodtopics.append((name, pattern, desc, emptyflag)) self.topics = goodtopics @@ -791,6 +811,8 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, # check for banned address pattern = self.GetBannedPattern(invitee) if pattern: + syslog('vette', '%s banned invitation: %s (matched: %s)', + self.real_name, invitee, pattern) raise Errors.MembershipIsBanned, pattern # Hack alert! Squirrel away a flag that only invitations have, so # that we can do something slightly different when an invitation @@ -871,8 +893,12 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, # Is the subscribing address banned from this list? pattern = self.GetBannedPattern(email) if pattern: - syslog('vette', '%s banned subscription: %s (matched: %s)', - realname, email, pattern) + if remote: + whence = ' from %s' % remote + else: + whence = '' + syslog('vette', '%s banned subscription: %s%s (matched: %s)', + realname, email, whence, pattern) raise Errors.MembershipIsBanned, pattern # Sanity check the digest flag if digest and not self.digestable: @@ -936,6 +962,9 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, syslog('subscribe', '%s: pending %s %s', self.internal_name(), who, by) raise Errors.MMSubscribeNeedsConfirmation + elif self.HasAutoApprovedSender(email): + # no approval necessary: + self.ApprovedAddMember(userdesc) else: # Subscription approval is required. Add this entry to the admin # requests database. BAW: this should probably take a userdesc @@ -986,6 +1015,12 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, # and confirmations. pattern = self.GetBannedPattern(email) if pattern: + if whence: + source = ' from %s' % whence + else: + source = '' + syslog('vette', '%s banned subscription: %s%s (matched: %s)', + self.real_name, email, source, pattern) raise Errors.MembershipIsBanned, pattern # Do the actual addition self.addNewMember(email, realname=name, digest=digest, @@ -1046,7 +1081,8 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, # And send an acknowledgement to the user... if userack: self.SendUnsubscribeAck(emailaddr, userlang) - # ...and to the administrator + # ...and to the administrator in the correct language. (LP: #1308655) + i18n.set_language(self.preferred_language) if admin_notif: realname = self.real_name subject = _('%(realname)s unsubscribe notification') @@ -1149,6 +1185,9 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, # exception. pattern = self.GetBannedPattern(newaddr) if pattern: + syslog('vette', + '%s banned address change: %s -> %s (matched: %s)', + self.real_name, oldaddr, newaddr, pattern) raise Errors.MembershipIsBanned, pattern # It's possible they were a member of this list, but choose to change # their membership globally. In that case, we simply remove the old @@ -1158,12 +1197,14 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, # CP address of a member, then if the old address yields a different # CP address, we can simply remove the old address, otherwise we can # do nothing. + cpoldaddr = self.getMemberCPAddress(oldaddr) if self.isMember(newaddr) and (self.getMemberCPAddress(newaddr) == newaddr): - if self.getMemberCPAddress(oldaddr) <> newaddr: + if cpoldaddr <> newaddr: self.removeMember(oldaddr) else: self.changeMemberAddress(oldaddr, newaddr) + self.log_and_notify_admin(cpoldaddr, newaddr) # If globally is true, then we also include every list for which # oldaddr is a member. if not globally: @@ -1183,16 +1224,46 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, mlist.Lock() try: # Same logic as above, re newaddr is already a member + cpoldaddr = mlist.getMemberCPAddress(oldaddr) if mlist.isMember(newaddr) and ( mlist.getMemberCPAddress(newaddr) == newaddr): - if mlist.getMemberCPAddress(oldaddr) <> newaddr: + if cpoldaddr <> newaddr: mlist.removeMember(oldaddr) else: mlist.changeMemberAddress(oldaddr, newaddr) + mlist.log_and_notify_admin(cpoldaddr, newaddr) mlist.Save() finally: mlist.Unlock() + def log_and_notify_admin(self, oldaddr, newaddr): + """Log member address change and notify admin if requested.""" + syslog('subscribe', '%s: changed member address from %s to %s', + self.internal_name(), oldaddr, newaddr) + if self.admin_notify_mchanges: + lang = self.preferred_language + otrans = i18n.get_translation() + i18n.set_language(lang) + try: + realname = self.real_name + subject = _('%(realname)s address change notification') + finally: + i18n.set_translation(otrans) + name = self.getMemberName(newaddr) + if name is None: + name = '' + if isinstance(name, UnicodeType): + name = name.encode(Utils.GetCharSet(lang), 'replace') + text = Utils.maketext( + 'adminaddrchgack.txt', + {'name' : name, + 'oldaddr' : oldaddr, + 'newaddr' : newaddr, + 'listname': self.real_name, + }, mlist=self) + msg = Message.OwnerNotification(self, subject, text) + msg.send(self) + # # Confirmation processing @@ -1236,7 +1307,8 @@ class MailList(HTMLFormatter, Deliverer, ListAdmin, # list administrators. self.SendHostileSubscriptionNotice(invitation, addr) raise Errors.HostileSubscriptionError - elif self.subscribe_policy in (2, 3): + elif self.subscribe_policy in (2, 3) and \ + not self.HasAutoApprovedSender(addr): self.HoldSubscription(addr, fullname, password, digest, lang) name = self.real_name raise Errors.MMNeedApproval, _( @@ -1515,26 +1587,85 @@ bad regexp in bounce_matching_header line: %s """Returns matched entry in ban_list if email matches. Otherwise returns None. """ - ban = False - for pattern in self.ban_list: + return (self.GetPattern(email, self.ban_list) or + self.GetPattern(email, mm_cfg.GLOBAL_BAN_LIST) + ) + + def HasAutoApprovedSender(self, sender): + """Returns True and logs if sender matches address or pattern + or is a member of a referenced list in subscribe_auto_approval. + Otherwise returns False. + """ + auto_approve = False + if self.GetPattern(sender, + self.subscribe_auto_approval, + at_list='subscribe_auto_approval' + ): + auto_approve = True + syslog('vette', '%s: auto approved subscribe from %s', + self.internal_name(), sender) + return auto_approve + + def GetPattern(self, email, pattern_list, at_list=None): + """Returns matched entry in pattern_list if email matches. + Otherwise returns None. The at_list argument, if "true", + says process the @listname syntax and provides the name of + the list attribute for log messages. + """ + matched = None + # First strip out all the regular expressions and listnames because + # documentation says we do non-regexp first (Why?). + plainaddrs = [x.strip() for x in pattern_list if x.strip() and not + (x.startswith('^') or x.startswith('@'))] + addrdict = Utils.List2Dict(plainaddrs, foldcase=1) + if addrdict.has_key(email.lower()): + return email + for pattern in pattern_list: if pattern.startswith('^'): # This is a regular expression match try: if re.search(pattern, email, re.IGNORECASE): - ban = True + matched = pattern break - except re.error: + except re.error, e: # BAW: we should probably remove this pattern - pass - else: - # Do the comparison case insensitively - if pattern.lower() == email.lower(): - ban = True + # The GUI won't add a bad regexp, but at least log it. + # The following kludge works because the ban_list stuff + # is the only caller with no at_list. + attr_name = at_list or 'ban_list' + syslog('error', + '%s in %s has bad regexp "%s": %s', + attr_name, + self.internal_name(), + pattern, + str(e) + ) + elif at_list and pattern.startswith('@'): + # XXX Needs to be reviewed for list@domain names. + # this refers to the members of another list in this + # installation. + mname = pattern[1:].lower().strip() + if mname == self.internal_name(): + # don't reference your own list + syslog('error', + '%s in %s references own list', + at_list, + self.internal_name()) + continue + try: + mother = MailList(mname, lock = False) + except Errors.MMUnknownListError: + syslog('error', + '%s in %s references non-existent list %s', + at_list, + self.internal_name(), + mname + ) + continue + if mother.isMember(email.lower()): + matched = pattern break - if ban: - return pattern - else: - return None + return matched |