/tags/mailman-1_0b3/mailman/Mailman/Bouncer.py
Python | 331 lines | 310 code | 10 blank | 11 comment | 21 complexity | 501ec1a078b8fe9e53de71ed37eb92a0 MD5 | raw file
Possible License(s): GPL-2.0
- "Handle delivery bounce messages, doing filtering when list is set for it."
- __version__ = "$Revision: 467 $"
- # It's possible to get the mail-list senders address (list-admin) in the
- # bounce list. You probably don't want to have list mail sent to that
- # address anyway.
- import sys
- import time
- import regsub, string, regex
- import mm_utils, mm_cfg, mm_err
- class Bouncer:
- def InitVars(self):
- # Not configurable...
- self.bounce_info = {}
- # Configurable...
- self.bounce_processing = mm_cfg.DEFAULT_BOUNCE_PROCESSING
- self.minimum_removal_date = mm_cfg.DEFAULT_MINIMUM_REMOVAL_DATE
- self.minimum_post_count_before_bounce_action = \
- mm_cfg.DEFAULT_MINIMUM_POST_COUNT_BEFORE_BOUNCE_ACTION
- self.automatic_bounce_action = mm_cfg.DEFAULT_AUTOMATIC_BOUNCE_ACTION
- self.max_posts_between_bounces = \
- mm_cfg.DEFAULT_MAX_POSTS_BETWEEN_BOUNCES
- def GetConfigInfo(self):
- return [
- "Policies regarding systematic processing of bounce messages,"
- " to help automate recognition and handling of defunct"
- " addresses.",
- ('bounce_processing', mm_cfg.Toggle, ('No', 'Yes'), 0,
- 'Try to figure out error messages automatically? '),
- ('minimum_removal_date', mm_cfg.Number, 3, 0,
- 'Minimum number of days an address has been non-fatally '
- 'bad before we take action'),
- ('minimum_post_count_before_bounce_action', mm_cfg.Number, 3, 0,
- 'Minimum number of posts to the list since members first '
- 'bounce before we consider removing them from the list'),
- ('max_posts_between_bounces', mm_cfg.Number, 3, 0,
- "Maximum number of messages your list gets in an hour. "
- "(Yes, bounce detection finds this info useful)"),
- ('automatic_bounce_action', mm_cfg.Radio,
- ("Do nothing",
- "Disable and notify me",
- "Disable and DON'T notify me",
- "Remove and notify me"),
- 0, "Action when critical or excessive bounces are detected.")
- ]
- def ClearBounceInfo(self, email):
- email = string.lower(email)
- if self.bounce_info.has_key(email):
- del self.bounce_info[email]
- def RegisterBounce(self, email):
- report = "%s: %s - " % (self.real_name, email)
- bouncees = self.bounce_info.keys()
- this_dude = mm_utils.FindMatchingAddresses(email, bouncees)
- now = time.time()
- if not len(this_dude):
- # Time address went bad, post where address went bad,
- # What the last post ID was that we saw a bounce.
- self.bounce_info[string.lower(email)] = [now, self.post_id,
- self.post_id]
- self.LogMsg("bounce", report + "first")
- self.Save()
- return
- addr = string.lower(this_dude[0])
- inf = self.bounce_info[addr]
- difference = now - inf[0]
- if len(mm_utils.FindMatchingAddresses(addr, self.members)):
- if self.post_id - inf[2] > self.max_posts_between_bounces:
- # Stale entry that's now being restarted...
- # Should maybe keep track in see if people become stale entries
- # often...
- self.LogMsg("bounce",
- report + "first fresh")
- self.bounce_info[addr] = [now, self.post_id, self.post_id]
- return
- self.bounce_info[addr][2] = self.post_id
- if ((self.post_id - inf[1] >
- self.minimum_post_count_before_bounce_action)
- and difference > self.minimum_removal_date * 24 * 60 * 60):
- self.LogMsg("bounce", report + "exceeded limits")
- self.HandleBouncingAddress(addr)
- return
- else:
- post_count = (self.minimum_post_count_before_bounce_action -
- self.post_id - inf[1])
- if post_count < 0:
- post_count = 0
- remain = self.minimum_removal_date * 24 * 60 * 60 - difference
- self.LogMsg("bounce",
- report + ("%d more posts, %d more secs"
- % (post_count, remain)))
- self.Save()
- return
- elif len(mm_utils.FindMatchingAddresses(addr, self.digest_members)):
- if self.volume > inf[1]:
- self.LogMsg("bounce",
- "%s: first fresh (D)",
- self._internal_name)
- self.bounce_info[addr] = [now, self.volume, self.volume]
- return
- if difference > self.minimum_removal_date * 24 * 60 * 60:
- self.LogMsg("bounce", "exceeded limits (D)")
- self.HandleBouncingAddress(addr)
- return
- self.LogMsg("bounce",
- "digester lucked out")
- else:
- self.LogMsg("bounce",
- "%s: address %s not a member.",
- self._internal_name,
- addr)
- def HandleBouncingAddress(self, addr):
- """Disable or remove addr according to bounce_action setting."""
- if self.automatic_bounce_action == 0:
- return
- elif self.automatic_bounce_action == 1:
- succeeded = self.DisableBouncingAddress(addr)
- did = "disabled"
- send = 1
- elif self.automatic_bounce_action == 2:
- succeeded = self.DisableBouncingAddress(addr)
- did = "disabled"
- send = 0
- elif self.automatic_bounce_action == 3:
- succeeded = self.RemoveBouncingAddress(addr)
- did = "removed"
- send = 1
- if send:
- if succeeded != 1:
- negative="not "
- recipient = mm_cfg.MAILMAN_OWNER
- else:
- negative=""
- recipient = self.GetAdminEmail()
- text = ("This is a mailman maillist administrator notice.\n"
- "\n\tMaillist:\t%s\n"
- "\tMember:\t\t%s\n"
- "\tAction:\t\tSubscription %s%s.\n"
- "\tReason:\t\tExcessive or fatal bounces.\n"
- % (self.real_name, addr, negative, did))
- if succeeded != 1:
- text = text + "\tBUT:\t\t%s\n\n" % succeeded
- else:
- text = text + "\n"
- if did == "disabled" and succeeded == 1:
- text = text + (
- "You can reenable their subscription by visiting "
- "their options page\n"
- "(via %s) and using your\n"
- "list admin password to authorize the option change.\n\n"
- % self.GetScriptURL('listinfo'))
- text = text + ("Questions? Contact the mailman site admin,\n%s"
- % mm_cfg.MAILMAN_OWNER)
- if negative:
- negative = string.upper(negative)
- self.SendTextToUser(subject = ("%s member %s %s%s due to bounces"
- % (self.real_name, addr,
- negative, did)),
- recipient = recipient,
- sender = mm_cfg.MAILMAN_OWNER,
- add_headers = ["Errors-To: %s"
- % mm_cfg.MAILMAN_OWNER],
- text = text)
- def DisableBouncingAddress(self, addr):
- if not self.IsMember(addr):
- reason = "User not found."
- self.LogMsg("bounce", "%s: NOT disabled %s: %s",
- self.real_name, addr, reason)
- return reason
- try:
- self.SetUserOption(addr, mm_cfg.DisableDelivery, 1)
- self.LogMsg("bounce", "%s: disabled %s", self.real_name, addr)
- self.Save()
- return 1
- except mm_err.MMNoSuchUserError:
- self.LogMsg("bounce", "%s: NOT disabled %s: %s",
- self.real_name, addr, mm_err.MMNoSuchUserError)
- self.ClearBounceInfo(addr)
- self.Save()
- return mm_err.MMNoSuchUserError
-
- def RemoveBouncingAddress(self, addr):
- if not self.IsMember(addr):
- reason = "User not found."
- self.LogMsg("bounce", "%s: NOT removed %s: %s",
- self.real_name, addr, reason)
- return reason
- try:
- self.DeleteMember(addr, "bouncing addr")
- self.LogMsg("bounce", "%s: removed %s", self.real_name, addr)
- self.Save()
- return 1
- except mm_err.MMNoSuchUserError:
- self.LogMsg("bounce", "%s: NOT removed %s: %s",
- self.real_name, addr, mm_err.MMNoSuchUserError)
- self.ClearBounceInfo(addr)
- self.Save()
- return mm_err.MMNoSuchUserError
- # Return 0 if we couldn't make any sense of it, 1 if we handled it.
- def ScanMessage(self, msg):
- ## realname, who_from = msg.getaddr('from')
- ## who_info = string.lower(who_from)
- candidates = []
- who_info = string.lower(msg.GetSender())
- at_index = string.find(who_info, '@')
- if at_index != -1:
- who_from = who_info[:at_index]
- remote_host = who_info[at_index+1:]
- else:
- who_from = who_info
- remote_host = self.host_name
- if not who_from in ['mailer-daemon', 'postmaster', 'orphanage',
- 'postoffice', 'ucx_smtp', 'a2']:
- return 0
- mime_info = msg.getheader('content-type')
- boundry = None
- if mime_info:
- mime_info_parts = regsub.splitx(
- mime_info, '[Bb][Oo][Uu][Nn][Dd][Aa][Rr][Yy]="[^"]+"')
- if len(mime_info_parts) > 1:
- boundry = regsub.splitx(mime_info_parts[1],
- '"[^"]+"')[1][1:-1]
- if boundry:
- relevant_text = string.split(msg.body, '--%s' % boundry)[1]
- else:
- # This looks strange, but at least 2 are going to be no-ops.
- relevant_text = regsub.split(msg.body,
- '^.*Message header follows.*$')[0]
- relevant_text = regsub.split(relevant_text,
- '^The text you sent follows:.*$')[0]
- relevant_text = regsub.split(
- relevant_text, '^Additional Message Information:.*$')[0]
- relevant_text = regsub.split(relevant_text,
- '^-+Your original message-+.*$')[0]
-
- BOUNCE = 1
- REMOVE = 2
- # Bounce patterns where it's simple to figure out the email addr.
- email_regexp = '<?[^ \t@<>]+@[^ \t@<>]+\.[^ \t<>.]+>?'
- simple_bounce_pats = (
- (regex.compile('.*451 %s.*' % email_regexp), BOUNCE),
- (regex.compile('.*554 %s.*' % email_regexp), BOUNCE),
- (regex.compile('.*552 %s.*' % email_regexp), BOUNCE),
- (regex.compile('.*501 %s.*' % email_regexp), BOUNCE),
- (regex.compile('.*553 %s.*' % email_regexp), BOUNCE),
- (regex.compile('.*550 %s.*' % email_regexp), REMOVE),
- (regex.compile('%s .bounced.*' % email_regexp), BOUNCE),
- (regex.compile('.*%s\.\.\. Deferred.*' % email_regexp), BOUNCE),
- (regex.compile('.*User %s not known.*' % email_regexp), REMOVE),
- (regex.compile('.*%s: User unknown.*' % email_regexp), REMOVE))
- # patterns we can't directly extract the email (special case these)
- messy_pattern_1 = regex.compile('^Recipient .*$')
- messy_pattern_2 = regex.compile('^Addressee: .*$')
- messy_pattern_3 = regex.compile('^User .* not listed.*$')
- messy_pattern_4 = regex.compile('^550 [^ ]+\.\.\. User unknown.*$')
- messy_pattern_5 = regex.compile('^User [^ ]+ is not defined.*$')
- messy_pattern_6 = regex.compile('^[ \t]*[^ ]+: User unknown.*$')
- messy_pattern_7 = regex.compile('^[^ ]+ - User currently disabled.*$')
- message_groked = 0
- for line in string.split(relevant_text, '\n'):
- for pattern, action in simple_bounce_pats:
- if pattern.match(line) <> -1:
- email = self.ExtractBouncingAddr(line)
- if action == REMOVE:
- candidates = candidates + string.split(email,',')
- message_groked = 1
- continue
- elif action == BOUNCE:
- emails = string.split(email,',')
- for email_addr in emails:
- self.RegisterBounce(email_addr)
- message_groked = 1
- continue
- else:
- message_groked = 1
- continue
- # Now for the special case messages that are harder to parse...
- if (messy_pattern_1.match(line) <> -1
- or messy_pattern_2.match(line) <> -1):
- username = string.split(line)[1]
- self.RegisterBounce('%s@%s' % (username, remote_host))
- message_groked = 1
- continue
- if (messy_pattern_3.match(line) <> -1
- or messy_pattern_4.match(line) <> -1
- or messy_pattern_5.match(line) <> -1):
- username = string.split(line)[1]
- candidates.append('%s@%s' % (username, remote_host))
- message_groked = 1
- continue
- if messy_pattern_6.match(line) <> -1:
- username = string.split(string.strip(line))[0][:-1]
- candidates.append('%s@%s' % (username, remote_host))
- message_groked = 1
- continue
- if messy_pattern_7.match(line) <> -1:
- username = string.split(string.strip(line))[0]
- candidates.append('%s@%s' % (username, remote_host))
- message_groked = 1
- continue
- did = []
- for i in candidates:
- el = string.find(i, "...")
- if el != -1: i = i[:el]
- if i not in did:
- self.HandleBouncingAddress(i)
- did.append(i)
- return message_groked
- def ExtractBouncingAddr(self, line):
- email = regsub.splitx(line, '[^ \t@<>]+@[^ \t@<>]+\.[^ \t<>.]+')[1]
- if email[0] == '<':
- # Remove what's within the angles.
- return regsub.splitx(email[1:], ">")[0]
- else:
- return email