def check(self, mlist, msg, msgdata): """See `IRule`.""" if not as_boolean(config.mailman.hold_digest): return False # Convert the header value to a str because it may be an # email.header.Header instance. subject = str(msg.get('subject', '')).strip() if DIGRE.search(subject): msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Message has a digest subject')) return True # Get the masthead, but without emails. mastheadtxt = getUtility(ITemplateLoader).get( 'list:member:digest:masthead', mlist) mastheadtxt = wrap( expand( mastheadtxt, mlist, dict( display_name=mlist.display_name, listname='', list_id=mlist.list_id, request_email='', owner_email='', ))) msgtext = '' for part in msg.walk(): if part.get_content_maintype() == 'text': cset = part.get_content_charset('utf-8') msgtext += part.get_payload(decode=True).decode( cset, errors='replace') matches = 0 lines = mastheadtxt.splitlines() for line in lines: line = line.strip() if not line: continue if msgtext.find(line) >= 0: matches += 1 if matches >= int(config.mailman.masthead_threshold): msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Message quotes digest boilerplate')) return True return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" # The MemberModeration rule misses unconditionally if any of the # senders are banned. ban_manager = IBanManager(mlist) for sender in msg.senders: if ban_manager.is_banned(sender): return False member = _find_sender_member(mlist, msg) if member is None: return False action = (mlist.default_member_action if member.moderation_action is None else member.moderation_action) if action is Action.defer: # The regular moderation rules apply. return False elif action is not None: # We must stringify the moderation action so that it can be # stored in the pending request table. msgdata['member_moderation_action'] = action.name msgdata['moderation_sender'] = sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('The message comes from a moderated member')) return True # The sender is not a member so this rule does not match. return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" # Collect all the headers in all subparts. headers = [] for p in msg.walk(): headers.extend(p.get_all(self.header, [])) for value in headers: if isinstance(value, Header): value = value.encode() # RFC2047 decode, but don't change value as it affects the msg. new_value = str(make_header(decode_header(value))) try: mo = re.search(self.pattern, new_value, re.IGNORECASE) except re.error as error: log.error( "Invalid regexp '{}' in header_matches for {}: {}".format( self.pattern, mlist.list_id, error.msg)) return False else: if mo: msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( (_('Header "{}" matched a header rule'), str(value))) return True return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" # Implicit destination checking must be enabled in the mailing list. if not mlist.require_explicit_destination: return False # Messages gated from NNTP will always have an implicit destination so # are never checked. if msgdata.get('fromusenet'): return False # Calculate the list of acceptable aliases. If the alias starts with # a caret (i.e. ^), then it's a regular expression to match against. aliases = set() alias_patterns = set() # Adapt the mailing list to the appropriate interface. alias_set = IAcceptableAliasSet(mlist) for alias in alias_set.aliases: if alias.startswith('^'): alias_patterns.add(alias) else: aliases.add(alias) # Add the list's posting address, i.e. the explicit address, to the # set of acceptable aliases. aliases.add(mlist.posting_address) # Look at all the recipients. If the recipient is any acceptable # alias (or the explicit posting address), then this rule does not # match. If not, then add it to the set of recipients we'll check # against the alias patterns later. recipients = set() for header in ('to', 'cc', 'resent-to', 'resent-cc'): for fullname, address in getaddresses(msg.get_all(header, [])): if isinstance(address, bytes): address = address.decode('ascii') address = address.lower() if address in aliases: return False recipients.add(address) # Now for all alias patterns, see if any of the recipients matches a # pattern. If so, then this rule does not match. for pattern in alias_patterns: escaped = re.escape(pattern) for recipient in recipients: try: if re.match(pattern, recipient, re.IGNORECASE): return False except re.error: # The pattern is a malformed regular expression. Try # matching again with the pattern escaped. with suppress(re.error): if re.match(escaped, recipient, re.IGNORECASE): return False # Nothing matched. msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Message has implicit destination')) return True
def check(self, mlist, msg, msgdata): """See `IRule`.""" if mlist.emergency and not msgdata.get('moderator_approved'): msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Emergency moderation is in effect for this list')) return True return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" if mlist.newsgroup_moderation is NewsgroupModeration.moderated: msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Post to a moderated newsgroup gateway')) return True return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" if msg.sender: return False else: msgdata['moderation_sender'] = 'N/A' with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('The message has no valid senders')) return True
def check(self, mlist, msg, msgdata): """See `IRule`.""" ban_manager = IBanManager(mlist) for sender in msg.senders: if ban_manager.is_banned(sender): msgdata['moderation_sender'] = sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( (_('Message sender {} is banned from this list'), sender)) return True return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" # Has this message already been posted to this list? list_posts = set(value.strip().lower() for value in msg.get_all('list-post', [])) if mlist.posting_address in list_posts: msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Message has already been posted to this list')) return True return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" # Convert the header value to a str because it may be an # email.header.Header instance. subject = str(msg.get('subject', '')).strip() if subject == '': msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append(_('Message has no subject')) return True return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" if mlist.dmarc_mitigate_action is DMARCMitigateAction.no_mitigation: # Don't bother to check if we're not going to do anything. return False display_name, address = parseaddr(str(msg.get('from', ''))) if maybe_mitigate(mlist, address): # If dmarc_mitigate_action is discard or reject, this rule fires # and jumps to the 'moderation' chain to do the actual discard. # Otherwise, the rule misses but sets a flag for the dmarc handler # to do the appropriate action. msgdata['dmarc'] = True if mlist.dmarc_mitigate_action is DMARCMitigateAction.discard: msgdata['dmarc_action'] = 'discard' with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append(_('DMARC moderation')) elif mlist.dmarc_mitigate_action is DMARCMitigateAction.reject: listowner = mlist.owner_address # noqa F841 with _.defer_translation(): # This will be translated at the point of use. reason = (mlist.dmarc_moderation_notice or _( '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}.')) msgdata.setdefault('moderation_reasons', []).append(wrap(reason)) msgdata['dmarc_action'] = 'reject' else: return False msgdata['moderation_sender'] = address return True return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" if mlist.max_message_size == 0: return False assert hasattr(msg, 'original_size'), ( 'Message was not sized on initial parsing.') # The maximum size is specified in 1024 bytes. if msg.original_size / 1024.0 > mlist.max_message_size: msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( (_('The message is larger than the {} KB maximum size'), mlist.max_message_size)) return True return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" # Zero means any number of recipients are allowed. if mlist.max_num_recipients == 0: return False # Figure out how many recipients there are recipients = getaddresses( msg.get_all('to', []) + msg.get_all('cc', [])) if len(recipients) >= mlist.max_num_recipients: msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( (_('Message has more than {} recipients'), mlist.max_num_recipients)) return True return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" # The list must have the administrivia check enabled. if not mlist.administrivia: return False # First check the Subject text. lines_to_check = [] subject = str(msg.get('subject', '')) if subject != '': lines_to_check.append(subject) # Search only the first text/plain subpart of the message. There's # really no good way to find email commands in any other content type. for part in typed_subpart_iterator(msg, 'text', 'plain'): payload = part.get_payload() lines = payload.splitlines() # Count lines without using enumerate() because blank lines in the # payload don't count against the maximum examined. lineno = 0 for line in lines: line = line.strip() if len(line) == 0: continue lineno += 1 if lineno > int(config.mailman.email_commands_max_lines): break lines_to_check.append(line) # Only look at the first text/plain part. break # For each line we're checking, split the line into words. Then see # if it looks like a command with the min-to-max number of arguments. for line in lines_to_check: words = [word.lower() for word in line.split()] if words[0] not in EMAIL_COMMANDS: # This is not an administrivia command. continue minargs, maxargs = EMAIL_COMMANDS[words[0]] if minargs <= len(words) - 1 <= maxargs: msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append( _('Message contains administrivia')) return True return False
def has_matching_bounce_header(mlist, msg, msgdata): """Does the message have a matching bounce header? :param mlist: The mailing list the message is destined for. :param msg: The email message object. :return: True if a header field matches a regexp in the bounce_matching_header mailing list variable. """ for header, cre, line in _parse_matching_header_opt(mlist): for value in msg.get_all(header, []): # Convert the header value to a str because it may be an # email.header.Header instance. if cre.search(str(value)): msgdata['moderation_sender'] = msg.sender with _.defer_translation(): # This will be translated at the point of use. msgdata.setdefault('moderation_reasons', []).append((_( 'Header "{}" matched a bounce_matching_header line'), str(value))) return True return False
def check(self, mlist, msg, msgdata): """See `IRule`.""" ban_manager = IBanManager(mlist) user_manager = getUtility(IUserManager) # The NonmemberModeration rule misses unconditionally if any of the # senders are banned. for sender in msg.senders: if ban_manager.is_banned(sender): return False if len(msg.senders) == 0: with _.defer_translation(): # This will be translated at the point of use. reason = _('No sender was found in the message.') _record_action(msgdata, mlist.default_nonmember_action, 'No sender', reason) return True # Every sender email must be a member or nonmember directly. If it is # neither, make the email a nonmembers. for sender in msg.senders: if (mlist.members.get_member(sender) is None and mlist.nonmembers.get_member(sender) is None): # noqa # The email must already be registered, since this happens in # the incoming runner itself. address = user_manager.get_address(sender) assert address is not None, ( 'Posting address is not registered: {}'.format(sender)) mlist.subscribe(address, MemberRole.nonmember) # Check to see if any of the sender emails is already a member. If # so, then this rule misses. member = _find_sender_member(mlist, msg) if member is not None: return False # Do nonmember moderation check. for sender in msg.senders: nonmember = mlist.nonmembers.get_member(sender) assert nonmember is not None, ( "sender didn't get subscribed as a nonmember".format(sender)) # Check the '*_these_nonmembers' properties first. XXX These are # legacy attributes from MM2.1; their database type is 'pickle' and # they should eventually get replaced. for action_name in ('accept', 'hold', 'reject', 'discard'): legacy_attribute_name = '{}_these_nonmembers'.format( action_name) checklist = getattr(mlist, legacy_attribute_name) for addr in checklist: if ((addr.startswith('^') and re.match(addr, sender)) or addr == sender): # noqa: W503 with _.defer_translation(): # This will be translated at the point of use. reason = ( _('The sender is in the nonmember {} list'), action_name) _record_action(msgdata, action_name, sender, reason) return True action = (mlist.default_nonmember_action if nonmember.moderation_action is None else nonmember.moderation_action) if action is Action.defer: # The regular moderation rules apply. return False elif action is not None: # We must stringify the moderation action so that it can be # stored in the pending request table. with _.defer_translation(): # This will be translated at the point of use. reason = _('The message is not from a list member') _record_action(msgdata, action.name, sender, reason) return True # The sender must be a member, so this rule does not match. return False