def do_reject(mlist): listowner = mlist.GetOwnerEmail() if mlist.nonmember_rejection_notice: raise Errors.RejectMessage(Utils.wrap(_(mlist.nonmember_rejection_notice))) else: raise Errors.RejectMessage(Utils.wrap(_("""\ Your message has been rejected, probably because you are not subscribed to the mailing list and the list's policy is to prohibit non-members from posting to it. If you think that your messages are being rejected in error, contact the mailing list owner at %(listowner)s.""")))
def process(mlist, msg, msgdata): # Short circuit if we've already calculated the recipients list, # regardless of whether the list is empty or not. if 'recips' in msgdata: return # Should the original sender should be included in the recipients list? include_sender = 1 sender = msg.get_sender() try: if mlist.getMemberOption(sender, mm_cfg.DontReceiveOwnPosts): include_sender = 0 except Errors.NotAMemberError: pass # Support for urgent messages, which bypasses digests and disabled # delivery and forces an immediate delivery to all members Right Now. We # are specifically /not/ allowing the site admins password to work here # because we want to discourage the practice of sending the site admin # password through email in the clear. (see also Approve.py) missing = [] password = msg.get('urgent', missing) if password is not missing: if mlist.Authenticate((mm_cfg.AuthListPoster, mm_cfg.AuthListModerator, mm_cfg.AuthListAdmin), password): recips = mlist.getMemberCPAddresses(mlist.getRegularMemberKeys() + mlist.getDigestMemberKeys()) msgdata['recips'] = recips return else: # Bad Urgent: password, so reject it instead of passing it on. I # think it's better that the sender know they screwed up than to # deliver it normally. realname = mlist.real_name text = _("""\ Your urgent message to the %(realname)s mailing list was not authorized for delivery. The original message as received by Mailman is attached. """) raise Errors.RejectMessage(Utils.wrap(text)) # Calculate the regular recipients of the message recips = [ mlist.getMemberCPAddress(m) for m in mlist.getRegularMemberKeys() if mlist.getDeliveryStatus(m) == ENABLED ] # Remove the sender if they don't want to receive their own posts if not include_sender: try: recips.remove(mlist.getMemberCPAddress(sender)) except (Errors.NotAMemberError, ValueError): # Sender does not want to get copies of their own messages (not # metoo), but delivery to their address is disabled (nomail). Or # the sender is not a member of the mailing list. pass # Handle topic classifications do_topic_filters(mlist, msg, msgdata, recips) # Regular delivery exclude/include (if in/not_in To: or Cc:) lists recips = do_exclude(mlist, msg, msgdata, recips) recips = do_include(mlist, msg, msgdata, recips) # Bookkeeping msgdata['recips'] = recips
def process(mlist, msg, msgdata): if msgdata.get('approved'): return # First do site hard coded header spam checks for header, regex in mm_cfg.KNOWN_SPAMMERS: cre = re.compile(regex, re.IGNORECASE) for value in msg.get_all(header, []): mo = cre.search(value) if mo: # we've detected spam, so throw the message away raise SpamDetected # Now do header_filter_rules # TK: Collect headers in sub-parts because attachment filename # extension may be a clue to possible virus/spam. headers = '' # Get the character set of the lists preferred language for headers lcset = GetCharSet(mlist.preferred_language) for p in msg.walk(): headers += getDecodedHeaders(p, lcset) for patterns, action, empty in mlist.header_filter_rules: if action == mm_cfg.DEFER: continue for pattern in patterns.splitlines(): if pattern.startswith('#'): continue # ignore 'empty' patterns if not pattern.strip(): continue if re.search(pattern, headers, re.IGNORECASE | re.MULTILINE): if action == mm_cfg.DISCARD: raise Errors.DiscardMessage if action == mm_cfg.REJECT: if msgdata.get('toowner'): # Don't send rejection notice if addressed to '-owner' # because it may trigger a loop of notices if the # sender address is forged. We just discard it here. raise Errors.DiscardMessage raise Errors.RejectMessage( _('Message rejected by filter rule match')) if action == mm_cfg.HOLD: if msgdata.get('toowner'): # Don't hold '-owner' addressed message. We just # pass it here but list-owner can set this to be # discarded on the GUI if he wants. return hold_for_approval(mlist, msg, msgdata, HeaderMatchHold) if action == mm_cfg.ACCEPT: return
def dispose(mlist, msg, msgdata, why): # filter_action == 0 just discards, see below if mlist.filter_action == 1: # Bounce the message to the original author raise Errors.RejectMessage(why) if mlist.filter_action == 2: # Forward it on to the list owner listname = mlist.internal_name() mlist.ForwardMessage( msg, text=_("""\ The attached message matched the %(listname)s mailing list's content filtering rules and was prevented from being forwarded on to the list membership. You are receiving the only remaining copy of the discarded message. """), subject=_('Content filtered message notification')) if mlist.filter_action == 3 and \ mm_cfg.OWNERS_CAN_PRESERVE_FILTERED_MESSAGES: badq = get_switchboard(mm_cfg.BADQUEUE_DIR) badq.enqueue(msg, msgdata) # Most cases also discard the message raise Errors.DiscardMessage
def process(mlist, msg, msgdata): if msgdata.get('approved'): return # Is the poster a member or not? for sender in msg.get_senders(): if mlist.isMember(sender): break for sender in Utils.check_eq_domains(sender, mlist.equivalent_domains): if mlist.isMember(sender): break if mlist.isMember(sender): break else: sender = None if sender: # If the member's moderation flag is on, then perform the moderation # action. if mlist.getMemberOption(sender, mm_cfg.Moderate): # Note that for member_moderation_action, 0==Hold, 1=Reject, # 2==Discard if mlist.member_moderation_action == 0: # Hold. BAW: WIBNI we could add the member_moderation_notice # to the notice sent back to the sender? msgdata['sender'] = sender Hold.hold_for_approval(mlist, msg, msgdata, ModeratedMemberPost) elif mlist.member_moderation_action == 1: # Reject text = mlist.member_moderation_notice if text: text = Utils.wrap(text) else: # Use the default RejectMessage notice string text = None raise Errors.RejectMessage(text) elif mlist.member_moderation_action == 2: # Discard. BAW: Again, it would be nice if we could send a # discard notice to the sender raise Errors.DiscardMessage else: assert 0, 'bad member_moderation_action' # Should we do anything explict to mark this message as getting past # this point? No, because further pipeline handlers will need to do # their own thing. return else: sender = msg.get_sender() # From here on out, we're dealing with non-members. listname = mlist.internal_name() if mlist.GetPattern(sender, mlist.accept_these_nonmembers, at_list='accept_these_nonmembers' ): return if mlist.GetPattern(sender, mlist.hold_these_nonmembers, at_list='hold_these_nonmembers' ): Hold.hold_for_approval(mlist, msg, msgdata, Hold.NonMemberPost) # No return if mlist.GetPattern(sender, mlist.reject_these_nonmembers, at_list='reject_these_nonmembers' ): do_reject(mlist) # No return if mlist.GetPattern(sender, mlist.discard_these_nonmembers, at_list='discard_these_nonmembers' ): do_discard(mlist, msg) # No return # Okay, so the sender wasn't specified explicitly by any of the non-member # moderation configuration variables. Handle by way of generic non-member # action. assert 0 <= mlist.generic_nonmember_action <= 4 if mlist.generic_nonmember_action == 0 or msgdata.get('fromusenet'): # Accept return elif mlist.generic_nonmember_action == 1: Hold.hold_for_approval(mlist, msg, msgdata, Hold.NonMemberPost) elif mlist.generic_nonmember_action == 2: do_reject(mlist) elif mlist.generic_nonmember_action == 3: do_discard(mlist, msg)
def process(mlist, msg, msgdata): # Short circuits # Do not short circuit. The problem is SpamDetect comes before Approve. # Suppose a message with an Approved: header is held by SpamDetect (or # any other handler that might come before Approve) and then approved # by a moderator. When the approved message reaches Approve in the # pipeline, we still need to remove the Approved: (pseudo-)header, so # we can't short circuit. #if msgdata.get('approved'): # Digests, Usenet postings, and some other messages come pre-approved. # TBD: we may want to further filter Usenet messages, so the test # above may not be entirely correct. #return # See if the message has an Approved or Approve header with a valid # list-moderator, list-admin. Also look at the first non-whitespace line # in the file to see if it looks like an Approved header. We are # specifically /not/ allowing the site admins password to work here # because we want to discourage the practice of sending the site admin # password through email in the clear. missing = [] for hdr in ('approved', 'approve', 'x-approved', 'x-approve'): passwd = msg.get(hdr, missing) if passwd is not missing: break if passwd is missing: # Find the first text/plain part in the message part = None stripped = False for part in typed_subpart_iterator(msg, 'text', 'plain'): break # XXX I'm not entirely sure why, but it is possible for the payload of # the part to be None, and you can't splitlines() on None. if part is not None and part.get_payload() is not None: lines = part.get_payload(decode=True).splitlines() line = '' for lineno, line in zip(list(range(len(lines))), lines): if line.strip(): break i = line.find(':') if i >= 0: name = line[:i] value = line[i + 1:] if name.lower() in ( 'approve', 'approved', 'x-approve', 'x-approved', ): passwd = value.lstrip() # Now strip the first line from the payload so the # password doesn't leak. del lines[lineno] reset_payload(part, NL.join(lines)) stripped = True if stripped: # MAS: Bug 1181161 - Now try all the text parts in case it's # multipart/alternative with the approved line in HTML or other # text part. We make a pattern from the Approved line and delete # it from all text/* parts in which we find it. It would be # better to just iterate forward, but email compatability for pre # Python 2.2 returns a list, not a true iterator. Also, there # are pathological MUAs that put the HTML part first. # # This will process all the multipart/alternative parts in the # message as well as all other text parts. We shouldn't find the # pattern outside the mp/a parts, but if we do, it is probably # best to delete it anyway as it does contain the password. # # Make a pattern to delete. We can't just delete a line because # line of HTML or other fancy text may include additional message # text. This pattern works with HTML. It may not work with rtf # or whatever else is possible. # # If we don't find the pattern in the decoded part, but we do # find it after stripping HTML tags, we don't know how to remove # it, so we just reject the post. pattern = name + ':(\xA0|\s| )*' + re.escape(passwd) for part in typed_subpart_iterator(msg, 'text'): if part is not None and part.get_payload() is not None: lines = part.get_payload(decode=True) if re.search(pattern, lines): reset_payload(part, re.sub(pattern, '', lines)) elif re.search(pattern, re.sub('(?s)<.*?>', '', lines)): raise Errors.RejectMessage(REJECT) if passwd is not missing and mlist.Authenticate( (mm_cfg.AuthListPoster, mm_cfg.AuthListModerator, mm_cfg.AuthListAdmin), passwd): # BAW: should we definitely deny if the password exists but does not # match? For now we'll let it percolate up for further determination. msgdata['approved'] = 1 # Used by the Emergency module msgdata['adminapproved'] = 1 # has this message already been posted to this list? beentheres = [s.strip().lower() for s in msg.get_all('x-beenthere', [])] if mlist.GetListEmail().lower() in beentheres: raise Errors.LoopError
def process(mlist, msg, msgdata): # Before anything else, check DMARC if necessary. We do this as early # as possible so reject/discard actions trump other holds/approvals and # wrap/munge actions get flagged even for approved messages. # But not for owner mail which should not be subject to DMARC reject or # discard actions. if not msgdata.get('toowner'): msgdata['from_is_list'] = 0 dn, addr = parseaddr(msg.get('from')) if addr and mlist.dmarc_moderation_action > 0: if Utils.IsDMARCProhibited(mlist, addr): # Note that for dmarc_moderation_action, 0 = Accept, # 1 = Munge, 2 = Wrap, 3 = Reject, 4 = Discard if mlist.dmarc_moderation_action == 1: msgdata['from_is_list'] = 1 elif mlist.dmarc_moderation_action == 2: msgdata['from_is_list'] = 2 elif mlist.dmarc_moderation_action == 3: # Reject text = mlist.dmarc_moderation_notice if text: text = Utils.wrap(text) else: listowner = mlist.GetOwnerEmail() text = Utils.wrap( _("""You are not allowed to post to this mailing list From: a domain which publishes a DMARC policy of reject or quarantine, and your message has been automatically rejected. If you think that your messages are being rejected in error, contact the mailing list owner at %(listowner)s.""")) raise Errors.RejectMessage, text elif mlist.dmarc_moderation_action == 4: raise Errors.DiscardMessage # Get member address if any. for sender in msg.get_senders(): if mlist.isMember(sender): break else: sender = msg.get_sender() if (mlist.member_verbosity_threshold > 0 and Utils.IsVerboseMember(mlist, sender)): mlist.setMemberOption(sender, mm_cfg.Moderate, 1) syslog('vette', '%s: Automatically Moderated %s for verbose postings.', mlist.real_name, sender) if msgdata.get('approved'): return # First do site hard coded header spam checks for header, regex in mm_cfg.KNOWN_SPAMMERS: cre = re.compile(regex, re.IGNORECASE) for value in msg.get_all(header, []): mo = cre.search(value) if mo: # we've detected spam, so throw the message away raise SpamDetected # Now do header_filter_rules # TK: Collect headers in sub-parts because attachment filename # extension may be a clue to possible virus/spam. headers = u'' # Get the character set of the lists preferred language for headers lcset = Utils.GetCharSet(mlist.preferred_language) for p in msg.walk(): headers += getDecodedHeaders(p, lcset) for patterns, action, empty in mlist.header_filter_rules: if action == mm_cfg.DEFER: continue for pattern in patterns.splitlines(): if pattern.startswith('#'): continue # ignore 'empty' patterns if not pattern.strip(): continue pattern = Utils.xml_to_unicode(pattern, lcset) pattern = normalize(mm_cfg.NORMALIZE_FORM, pattern) try: mo = re.search(pattern, headers, re.IGNORECASE | re.MULTILINE | re.UNICODE) except (re.error, TypeError): syslog('error', 'ignoring header_filter_rules invalid pattern: %s', pattern) if mo: if action == mm_cfg.DISCARD: raise Errors.DiscardMessage if action == mm_cfg.REJECT: if msgdata.get('toowner'): # Don't send rejection notice if addressed to '-owner' # because it may trigger a loop of notices if the # sender address is forged. We just discard it here. raise Errors.DiscardMessage raise Errors.RejectMessage( _('Message rejected by filter rule match')) if action == mm_cfg.HOLD: if msgdata.get('toowner'): # Don't hold '-owner' addressed message. We just # pass it here but list-owner can set this to be # discarded on the GUI if he wants. return hold_for_approval(mlist, msg, msgdata, HeaderMatchHold(pattern)) if action == mm_cfg.ACCEPT: return