diff options
Diffstat (limited to '')
-rw-r--r-- | Mailman/Bouncers/.cvsignore | 1 | ||||
-rw-r--r-- | Mailman/Bouncers/BouncerAPI.py | 71 | ||||
-rw-r--r-- | Mailman/Bouncers/Caiwireless.py | 45 | ||||
-rw-r--r-- | Mailman/Bouncers/Compuserve.py | 45 | ||||
-rw-r--r-- | Mailman/Bouncers/DSN.py | 79 | ||||
-rw-r--r-- | Mailman/Bouncers/Exchange.py | 47 | ||||
-rw-r--r-- | Mailman/Bouncers/Exim.py | 30 | ||||
-rw-r--r-- | Mailman/Bouncers/GroupWise.py | 70 | ||||
-rw-r--r-- | Mailman/Bouncers/LLNL.py | 31 | ||||
-rw-r--r-- | Mailman/Bouncers/Makefile.in | 74 | ||||
-rw-r--r-- | Mailman/Bouncers/Microsoft.py | 48 | ||||
-rw-r--r-- | Mailman/Bouncers/Netscape.py | 88 | ||||
-rw-r--r-- | Mailman/Bouncers/Postfix.py | 86 | ||||
-rw-r--r-- | Mailman/Bouncers/Qmail.py | 61 | ||||
-rw-r--r-- | Mailman/Bouncers/SMTP32.py | 57 | ||||
-rw-r--r-- | Mailman/Bouncers/SimpleMatch.py | 100 | ||||
-rw-r--r-- | Mailman/Bouncers/SimpleWarning.py | 44 | ||||
-rw-r--r-- | Mailman/Bouncers/Sina.py | 47 | ||||
-rw-r--r-- | Mailman/Bouncers/Yahoo.py | 53 | ||||
-rw-r--r-- | Mailman/Bouncers/Yale.py | 79 | ||||
-rw-r--r-- | Mailman/Bouncers/__init__.py | 15 |
21 files changed, 1171 insertions, 0 deletions
diff --git a/Mailman/Bouncers/.cvsignore b/Mailman/Bouncers/.cvsignore new file mode 100644 index 00000000..f3c7a7c5 --- /dev/null +++ b/Mailman/Bouncers/.cvsignore @@ -0,0 +1 @@ +Makefile diff --git a/Mailman/Bouncers/BouncerAPI.py b/Mailman/Bouncers/BouncerAPI.py new file mode 100644 index 00000000..e8994145 --- /dev/null +++ b/Mailman/Bouncers/BouncerAPI.py @@ -0,0 +1,71 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Contains all the common functionality for msg bounce scanning API. + +This module can also be used as the basis for a bounce detection testing +framework. When run as a script, it expects two arguments, the listname and +the filename containing the bounce message. + +""" + +import sys + +from Mailman.Logging.Syslog import syslog + +# If a bounce detector returns Stop, that means to just discard the message. +# An example is warning messages for temporary delivery problems. These +# shouldn't trigger a bounce notification, but we also don't want to send them +# on to the list administrator. +class _Stop: + pass +Stop = _Stop() + + +BOUNCE_PIPELINE = [ + 'DSN', + 'Qmail', + 'Postfix', + 'Yahoo', + 'Caiwireless', + 'Exchange', + 'Exim', + 'Netscape', + 'Compuserve', + 'Microsoft', + 'GroupWise', + 'SMTP32', + 'SimpleMatch', + 'SimpleWarning', + 'Yale', + 'LLNL', + ] + + + +# msg must be a mimetools.Message +def ScanMessages(mlist, msg): + for module in BOUNCE_PIPELINE: + modname = 'Mailman.Bouncers.' + module + __import__(modname) + addrs = sys.modules[modname].process(msg) + if addrs is Stop: + # One of the detectors recognized the bounce, but there were no + # addresses to extract. Return the empty list. + return [] + elif addrs: + return addrs + return [] diff --git a/Mailman/Bouncers/Caiwireless.py b/Mailman/Bouncers/Caiwireless.py new file mode 100644 index 00000000..0e3e71fc --- /dev/null +++ b/Mailman/Bouncers/Caiwireless.py @@ -0,0 +1,45 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Parse mystery style generated by MTA at caiwireless.net.""" + +import re +import email +from cStringIO import StringIO + +tcre = re.compile(r'the following recipients did not receive this message:', + re.IGNORECASE) +acre = re.compile(r'<(?P<addr>[^>]*)>') + + + +def process(msg): + if msg.get_type() <> 'multipart/mixed': + return None + # simple state machine + # 0 == nothing seen + # 1 == tag line seen + state = 0 + # This format thinks it's a MIME, but it really isn't + for line in email.Iterators.body_line_iterator(msg): + line = line.strip() + if state == 0 and tcre.match(line): + state = 1 + elif state == 1 and line: + mo = acre.match(line) + if not mo: + return None + return [mo.group('addr')] diff --git a/Mailman/Bouncers/Compuserve.py b/Mailman/Bouncers/Compuserve.py new file mode 100644 index 00000000..516c2237 --- /dev/null +++ b/Mailman/Bouncers/Compuserve.py @@ -0,0 +1,45 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Compuserve has its own weird format for bounces.""" + +import re +import email + +dcre = re.compile(r'your message could not be delivered', re.IGNORECASE) +acre = re.compile(r'Invalid receiver address: (?P<addr>.*)') + + + +def process(msg): + # simple state machine + # 0 = nothing seen yet + # 1 = intro line seen + state = 0 + addrs = [] + for line in email.Iterators.body_line_iterator(msg): + if state == 0: + mo = dcre.search(line) + if mo: + state = 1 + elif state == 1: + mo = dcre.search(line) + if mo: + break + mo = acre.search(line) + if mo: + addrs.append(mo.group('addr')) + return addrs diff --git a/Mailman/Bouncers/DSN.py b/Mailman/Bouncers/DSN.py new file mode 100644 index 00000000..3e040bef --- /dev/null +++ b/Mailman/Bouncers/DSN.py @@ -0,0 +1,79 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Parse RFC 1894 (i.e. DSN) bounce formats.""" + +from email.Iterators import typed_subpart_iterator +from email.Utils import parseaddr +from cStringIO import StringIO + + + +def check(msg): + # Iterate over each message/delivery-status subpart + addrs = [] + for part in typed_subpart_iterator(msg, 'message', 'delivery-status'): + if not part.is_multipart(): + # Huh? + continue + # Each message/delivery-status contains a list of Message objects + # which are the header blocks. Iterate over those too. + for msgblock in part.get_payload(): + # We try to dig out the Original-Recipient (which is optional) and + # Final-Recipient (which is mandatory, but may not exactly match + # an address on our list). Some MTA's also use X-Actual-Recipient + # as a synonym for Original-Recipient, but some apparently use + # that for other purposes :( + # + # Also grok out Action so we can do something with that too. + action = msgblock.get('action', '') + # BAW: Should we treat delayed bounces the same? Yes, because if + # the transient problem clears up, they should get unbounced. The + # other problem is what to do about a DSN that has both delayed + # and failed actions in multiple header blocks? We're not + # architected to handle that. ;/ + if action.lower() not in ('failed', 'failure', 'delayed'): + # Some non-permanent failure, so ignore this block + continue + params = [] + foundp = 0 + for header in ('original-recipient', 'final-recipient'): + for k, v in msgblock.get_params([], header): + if k.lower() == 'rfc822': + foundp = 1 + else: + params.append(k) + if foundp: + # Note that params should already be unquoted. + addrs.extend(params) + break + # Uniquify + rtnaddrs = {} + for a in addrs: + if a is not None: + realname, a = parseaddr(a) + rtnaddrs[a] = 1 + return rtnaddrs.keys() + + + +def process(msg): + # The report-type parameter should be "delivery-status", but it seems that + # some DSN generating MTAs don't include this on the Content-Type: header, + # so let's relax the test a bit. + if not msg.is_multipart() or msg.get_subtype() <> 'report': + return None + return check(msg) diff --git a/Mailman/Bouncers/Exchange.py b/Mailman/Bouncers/Exchange.py new file mode 100644 index 00000000..1f73aeb1 --- /dev/null +++ b/Mailman/Bouncers/Exchange.py @@ -0,0 +1,47 @@ +# Copyright (C) 2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Recognizes (some) Microsoft Exchange formats.""" + +import re +import email.Iterators + +scre = re.compile('did not reach the following recipient') +ecre = re.compile('MSEXCH:') +a1cre = re.compile('SMTP=(?P<addr>[^;]+); on ') +a2cre = re.compile('(?P<addr>[^ ]+) on ') + + + +def process(msg): + addrs = {} + it = email.Iterators.body_line_iterator(msg) + # Find the start line + for line in it: + if scre.search(line): + break + else: + return [] + # Search each line until we hit the end line + for line in it: + if ecre.search(line): + break + mo = a1cre.search(line) + if not mo: + mo = a2cre.search(line) + if mo: + addrs[mo.group('addr')] = 1 + return addrs.keys() diff --git a/Mailman/Bouncers/Exim.py b/Mailman/Bouncers/Exim.py new file mode 100644 index 00000000..1f03df2d --- /dev/null +++ b/Mailman/Bouncers/Exim.py @@ -0,0 +1,30 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Parse bounce messages generated by Exim. + +Exim adds an X-Failed-Recipients: header to bounce messages containing +an `addresslist' of failed addresses. + +""" + +from email.Utils import getaddresses + + + +def process(msg): + all = msg.get_all('x-failed-recipients', []) + return [a for n, a in getaddresses(all)] diff --git a/Mailman/Bouncers/GroupWise.py b/Mailman/Bouncers/GroupWise.py new file mode 100644 index 00000000..8bde4405 --- /dev/null +++ b/Mailman/Bouncers/GroupWise.py @@ -0,0 +1,70 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""This appears to be the format for Novell GroupWise and NTMail + +X-Mailer: Novell GroupWise Internet Agent 5.5.3.1 +X-Mailer: NTMail v4.30.0012 +X-Mailer: Internet Mail Service (5.5.2653.19) +""" + +import re +from email.Message import Message +from cStringIO import StringIO + +acre = re.compile(r'<(?P<addr>[^>]*)>') + + + +def find_textplain(msg): + if msg.get_type(msg.get_default_type()) == 'text/plain': + return msg + if msg.is_multipart: + for part in msg.get_payload(): + if not isinstance(part, Message): + continue + ret = find_textplain(part) + if ret: + return ret + return None + + + +def process(msg): + if msg.get_type() <> 'multipart/mixed' or not msg['x-mailer']: + return None + addrs = {} + # find the first text/plain part in the message + textplain = find_textplain(msg) + if not textplain: + return None + body = StringIO(textplain.get_payload()) + while 1: + line = body.readline() + if not line: + break + mo = acre.search(line) + if mo: + addrs[mo.group('addr')] = 1 + elif '@' in line: + i = line.find(' ') + if i == 0: + continue + if i < 0: + addrs[line] = 1 + else: + addrs[line[:i]] = 1 + return addrs.keys() diff --git a/Mailman/Bouncers/LLNL.py b/Mailman/Bouncers/LLNL.py new file mode 100644 index 00000000..faadb0b9 --- /dev/null +++ b/Mailman/Bouncers/LLNL.py @@ -0,0 +1,31 @@ +# Copyright (C) 2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""LLNL's custom Sendmail bounce message.""" + +import re +import email + +acre = re.compile(r',\s*(?P<addr>\S+@[^,]+),', re.IGNORECASE) + + + +def process(msg): + for line in email.Iterators.body_line_iterator(msg): + mo = acre.search(line) + if mo: + return [mo.group('addr')] + return [] diff --git a/Mailman/Bouncers/Makefile.in b/Mailman/Bouncers/Makefile.in new file mode 100644 index 00000000..d4c9dfca --- /dev/null +++ b/Mailman/Bouncers/Makefile.in @@ -0,0 +1,74 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +# NOTE: Makefile.in is converted into Makefile by the configure script +# in the parent directory. Once configure has run, you can recreate +# the Makefile by running just config.status. + +# Variables set by configure + +VPATH= @srcdir@ +srcdir= @srcdir@ +bindir= @bindir@ +prefix= @prefix@ +exec_prefix= @exec_prefix@ + +CC= @CC@ +CHMOD= @CHMOD@ +INSTALL= @INSTALL@ + +DEFS= @DEFS@ + +# Customizable but not set by configure + +OPT= @OPT@ +CFLAGS= $(OPT) $(DEFS) +PACKAGEDIR= $(prefix)/Mailman/Bouncers +SHELL= /bin/sh + +MODULES= *.py + +# Modes for directories and executables created by the install +# process. Default to group-writable directories but +# user-only-writable for executables. +DIRMODE= 775 +EXEMODE= 755 +FILEMODE= 644 +INSTALL_PROGRAM=$(INSTALL) -m $(EXEMODE) + + +# Rules + +all: + +install: + for f in $(MODULES); \ + do \ + $(INSTALL) -m $(FILEMODE) $(srcdir)/$$f $(PACKAGEDIR); \ + done + +finish: + +clean: + +distclean: + -rm *.pyc + -rm Makefile + + +# Local Variables: +# indent-tabs-mode: t +# End: diff --git a/Mailman/Bouncers/Microsoft.py b/Mailman/Bouncers/Microsoft.py new file mode 100644 index 00000000..65d49cc1 --- /dev/null +++ b/Mailman/Bouncers/Microsoft.py @@ -0,0 +1,48 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Microsoft's `SMTPSVC' nears I kin tell.""" + +import re +from cStringIO import StringIO + +scre = re.compile(r'transcript of session follows', re.IGNORECASE) + + + +def process(msg): + if msg.get_type() <> 'multipart/mixed': + return None + # Find the first subpart, which has no MIME type + try: + subpart = msg.get_payload(0) + except IndexError: + # The message *looked* like a multipart but wasn't + return None + body = StringIO(subpart.get_payload()) + state = 0 + addrs = [] + while 1: + line = body.readline() + if not line: + break + if state == 0: + if scre.search(line): + state = 1 + if state == 1: + if '@' in line: + addrs.append(line) + return addrs diff --git a/Mailman/Bouncers/Netscape.py b/Mailman/Bouncers/Netscape.py new file mode 100644 index 00000000..21aea7c5 --- /dev/null +++ b/Mailman/Bouncers/Netscape.py @@ -0,0 +1,88 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Netscape Messaging Server bounce formats. + +I've seen at least one NMS server version 3.6 (envy.gmp.usyd.edu.au) bounce +messages of this format. Bounces come in DSN MIME format, but don't include +any -Recipient: headers. Gotta just parse the text :( + +NMS 4.1 (dfw-smtpin1.email.verio.net) seems even worse, but we'll try to +decipher the format here too. + +""" + +import re +from cStringIO import StringIO + +pcre = re.compile( + r'This Message was undeliverable due to the following reason:', + re.IGNORECASE) + +acre = re.compile( + r'(?P<reply>please reply to)?.*<(?P<addr>[^>]*)>', + re.IGNORECASE) + + + +def flatten(msg, leaves): + # give us all the leaf (non-multipart) subparts + if msg.is_multipart(): + for part in msg.get_payload(): + flatten(part, leaves) + else: + leaves.append(msg) + + + +def process(msg): + # Sigh. Some show NMS 3.6's show + # multipart/report; report-type=delivery-status + # and some show + # multipart/mixed; + if not msg.is_multipart(): + return None + # We're looking for a text/plain subpart occuring before a + # message/delivery-status subpart. + plainmsg = None + leaves = [] + flatten(msg, leaves) + for i, subpart in zip(range(len(leaves)-1), leaves): + if subpart.get_type() == 'text/plain': + plainmsg = subpart + break + if not plainmsg: + return None + # Total guesswork, based on captured examples... + body = StringIO(plainmsg.get_payload()) + addrs = [] + while 1: + line = body.readline() + if not line: + break + mo = pcre.search(line) + if mo: + # We found a bounce section, but I have no idea what the official + # format inside here is. :( We'll just search for <addr> + # strings. + while 1: + line = body.readline() + if not line: + break + mo = acre.search(line) + if mo and not mo.group('reply'): + addrs.append(mo.group('addr')) + return addrs diff --git a/Mailman/Bouncers/Postfix.py b/Mailman/Bouncers/Postfix.py new file mode 100644 index 00000000..fb1a1233 --- /dev/null +++ b/Mailman/Bouncers/Postfix.py @@ -0,0 +1,86 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Parse bounce messages generated by Postfix. + +This also matches something called `Keftamail' which looks just like Postfix +bounces with the word Postfix scratched out and the word `Keftamail' written +in in crayon. + +It also matches something claiming to be `The BNS Postfix program'. +/Everybody's/ gotta be different, huh? + +""" + + +import re +from cStringIO import StringIO + + + +def flatten(msg, leaves): + # give us all the leaf (non-multipart) subparts + if msg.is_multipart(): + for part in msg.get_payload(): + flatten(part, leaves) + else: + leaves.append(msg) + + + +# are these heuristics correct or guaranteed? +pcre = re.compile(r'[ \t]*the\s*(bns)?\s*(postfix|keftamail)', re.IGNORECASE) +rcre = re.compile(r'failure reason:$', re.IGNORECASE) +acre = re.compile(r'<(?P<addr>[^>]*)>:') + +def findaddr(msg): + addrs = [] + body = StringIO(msg.get_payload()) + # simple state machine + # 0 == nothing found + # 1 == salutation found + state = 0 + while 1: + line = body.readline() + if not line: + break + # preserve leading whitespace + line = line.rstrip() + # yes use match to match at beginning of string + if state == 0 and (pcre.match(line) or rcre.match(line)): + state = 1 + elif state == 1 and line: + mo = acre.search(line) + if mo: + addrs.append(mo.group('addr')) + # probably a continuation line + return addrs + + + +def process(msg): + if msg.get_type() <> 'multipart/mixed': + return None + # We're looking for the plain/text subpart with a Content-Description: of + # `notification'. + leaves = [] + flatten(msg, leaves) + for subpart in leaves: + if subpart.get_type() == 'text/plain' and \ + subpart.get('content-description', '').lower() == 'notification': + # then... + return findaddr(subpart) + return None diff --git a/Mailman/Bouncers/Qmail.py b/Mailman/Bouncers/Qmail.py new file mode 100644 index 00000000..d6a3e3c3 --- /dev/null +++ b/Mailman/Bouncers/Qmail.py @@ -0,0 +1,61 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Parse bounce messages generated by qmail. + +Qmail actually has a standard, called QSBMF (qmail-send bounce message +format), as described in + + http://cr.yp.to/proto/qsbmf.txt + +This module should be conformant. + +""" + +import re +import email.Iterators + +introtag = 'Hi. This is the' +acre = re.compile(r'<(?P<addr>[^>]*)>:') + + + +def process(msg): + addrs = [] + # simple state machine + # 0 = nothing seen yet + # 1 = intro paragraph seen + # 2 = recip paragraphs seen + state = 0 + for line in email.Iterators.body_line_iterator(msg): + line = line.strip() + if state == 0 and line.startswith(introtag): + state = 1 + elif state == 1 and not line: + # Looking for the end of the intro paragraph + state = 2 + elif state == 2: + if line.startswith('-'): + # We're looking at the break paragraph, so we're done + break + # At this point we know we must be looking at a recipient + # paragraph + mo = acre.match(line) + if mo: + addrs.append(mo.group('addr')) + # Otherwise, it must be a continuation line, so just ignore it + # Not looking at anything in particular + return addrs diff --git a/Mailman/Bouncers/SMTP32.py b/Mailman/Bouncers/SMTP32.py new file mode 100644 index 00000000..62982461 --- /dev/null +++ b/Mailman/Bouncers/SMTP32.py @@ -0,0 +1,57 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Something which claims +X-Mailer: <SMTP32 vXXXXXX> + +What the heck is this thing? Here's a recent host: + +% telnet 207.51.255.218 smtp +Trying 207.51.255.218... +Connected to 207.51.255.218. +Escape character is '^]'. +220 X1 NT-ESMTP Server 208.24.118.205 (IMail 6.00 45595-15) + +""" + +import re +import email + +ecre = re.compile('original message follows', re.IGNORECASE) +acre = re.compile(r''' + ( # several different prefixes + user\ mailbox[^:]*: # have been spotted in the + |delivery\ failed[^:]*: # wild... + |undeliverable\ to + ) + \s* # space separator + (?P<addr>.*) # and finally, the address + ''', re.IGNORECASE | re.VERBOSE) + + + +def process(msg): + mailer = msg.get('x-mailer', '') + if not mailer.startswith('<SMTP32 v'): + return + addrs = {} + for line in email.Iterators.body_line_iterator(msg): + if ecre.search(line): + break + mo = acre.search(line) + if mo: + addrs[mo.group('addr')] = 1 + return addrs.keys() diff --git a/Mailman/Bouncers/SimpleMatch.py b/Mailman/Bouncers/SimpleMatch.py new file mode 100644 index 00000000..ccc8d6ed --- /dev/null +++ b/Mailman/Bouncers/SimpleMatch.py @@ -0,0 +1,100 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Recognizes simple heuristically delimited bounces.""" + +import re +import email.Iterators + + + +def _c(pattern): + return re.compile(pattern, re.IGNORECASE) + +# This is a list of tuples of the form +# +# (start cre, end cre, address cre) +# +# where `cre' means compiled regular expression, start is the line just before +# the bouncing address block, end is the line just after the bouncing address +# block, and address cre is the regexp that will recognize the addresses. It +# must have a group called `addr' which will contain exactly and only the +# address that bounced. +PATTERNS = [ + # sdm.de + (_c('here is your list of failed recipients'), + _c('here is your returned mail'), + _c(r'<(?P<addr>[^>]*)>')), + # sz-sb.de, corridor.com, nfg.nl + (_c('the following addresses had'), + _c('transcript of session follows'), + _c(r'<(?P<fulladdr>[^>]*)>|\(expanded from: <?(?P<addr>[^>)]*)>?\)')), + # robanal.demon.co.uk + (_c('this message was created automatically by mail delivery software'), + _c('original message follows'), + _c('rcpt to:\s*<(?P<addr>[^>]*)>')), + # s1.com (InterScan E-Mail VirusWall NT ???) + (_c('message from interscan e-mail viruswall nt'), + _c('end of message'), + _c('rcpt to:\s*<(?P<addr>[^>]*)>')), + # Smail + (_c('failed addresses follow:'), + _c('message text follows:'), + _c(r'\s*(?P<addr>\S+@\S+)')), + # newmail.ru + (_c('This is the machine generated message from mail service.'), + _c('--- Below the next line is a copy of the message.'), + _c('<(?P<addr>[^>]*)>')), + # turbosport.com runs something called `MDaemon 3.5.2' ??? + (_c('The following addresses did NOT receive a copy of your message:'), + _c('--- Session Transcript ---'), + _c('[>]\s*(?P<addr>.*)$')), + # usa.net + (_c('Intended recipient:\s*(?P<addr>.*)$'), + _c('--------RETURNED MAIL FOLLOWS--------'), + _c('Intended recipient:\s*(?P<addr>.*)$')), + # hotpop.com + (_c('Undeliverable Address:\s*(?P<addr>.*)$'), + _c('Original message attached'), + _c('Undeliverable Address:\s*(?P<addr>.*)$')), + # Next one goes here... + ] + + + +def process(msg, patterns=None): + if patterns is None: + patterns = PATTERNS + # simple state machine + # 0 = nothing seen yet + # 1 = intro seen + addrs = {} + state = 0 + for line in email.Iterators.body_line_iterator(msg): + if state == 0: + for scre, ecre, acre in patterns: + if scre.search(line): + state = 1 + break + if state == 1: + mo = acre.search(line) + if mo: + addr = mo.group('addr') + if addr: + addrs[mo.group('addr')] = 1 + elif ecre.search(line): + break + return addrs.keys() diff --git a/Mailman/Bouncers/SimpleWarning.py b/Mailman/Bouncers/SimpleWarning.py new file mode 100644 index 00000000..bc515515 --- /dev/null +++ b/Mailman/Bouncers/SimpleWarning.py @@ -0,0 +1,44 @@ +# Copyright (C) 2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Recognizes simple heuristically delimited warnings.""" + +from Mailman.Bouncers.SimpleMatch import _c +from Mailman.Bouncers.SimpleMatch import process as _process + + + +# This is a list of tuples of the form +# +# (start cre, end cre, address cre) +# +# where `cre' means compiled regular expression, start is the line just before +# the bouncing address block, end is the line just after the bouncing address +# block, and address cre is the regexp that will recognize the addresses. It +# must have a group called `addr' which will contain exactly and only the +# address that bounced. +patterns = [ + # pop3.pta.lia.net + (_c('The address to which the message has not yet been delivered is'), + _c('No action is required on your part'), + _c(r'\s*(?P<addr>\S+@\S+)\s*')), + # Next one goes here... + ] + + + +def process(msg): + return _process(msg, patterns) diff --git a/Mailman/Bouncers/Sina.py b/Mailman/Bouncers/Sina.py new file mode 100644 index 00000000..2cc2e69b --- /dev/null +++ b/Mailman/Bouncers/Sina.py @@ -0,0 +1,47 @@ +# Copyright (C) 2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""sina.com bounces""" + +import re +from email import Iterators + +acre = re.compile(r'<(?P<addr>[^>]*)>') + + + +def process(msg): + if msg.get('from', '').lower() <> 'mailer-daemon@sina.com': + print 'out 1' + return [] + if not msg.is_multipart(): + print 'out 2' + return [] + # The interesting bits are in the first text/plain multipart + part = None + try: + part = msg.get_payload(0) + except IndexError: + pass + if not part: + print 'out 3' + return [] + addrs = {} + for line in Iterators.body_line_iterator(part): + mo = acre.match(line) + if mo: + addrs[mo.group('addr')] = 1 + return addrs.keys() diff --git a/Mailman/Bouncers/Yahoo.py b/Mailman/Bouncers/Yahoo.py new file mode 100644 index 00000000..fd952915 --- /dev/null +++ b/Mailman/Bouncers/Yahoo.py @@ -0,0 +1,53 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Yahoo! has its own weird format for bounces.""" + +import re +import email +from email.Utils import parseaddr + +tcre = re.compile(r'message\s+from\s+yahoo\.\S+', re.IGNORECASE) +acre = re.compile(r'<(?P<addr>[^>]*)>:') +ecre = re.compile(r'--- Original message follows') + + + +def process(msg): + # Yahoo! bounces seem to have a known subject value and something called + # an x-uidl: header, the value of which seems unimportant. + sender = parseaddr(msg.get('from', '').lower())[1] or '' + if not sender.startswith('mailer-daemon@yahoo'): + return None + addrs = [] + # simple state machine + # 0 == nothing seen + # 1 == tag line seen + state = 0 + for line in email.Iterators.body_line_iterator(msg): + line = line.strip() + if state == 0 and tcre.match(line): + state = 1 + elif state == 1: + mo = acre.match(line) + if mo: + addrs.append(mo.group('addr')) + continue + mo = ecre.match(line) + if mo: + # we're at the end of the error response + break + return addrs diff --git a/Mailman/Bouncers/Yale.py b/Mailman/Bouncers/Yale.py new file mode 100644 index 00000000..6afc4d97 --- /dev/null +++ b/Mailman/Bouncers/Yale.py @@ -0,0 +1,79 @@ +# Copyright (C) 2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +"""Yale's mail server is pretty dumb. + +Its reports include the end user's name, but not the full domain. I think we +can usually guess it right anyway. This is completely based on examination of +the corpse, and is subject to failure whenever Yale even slightly changes +their MTA. :( + +""" + +import re +from cStringIO import StringIO +from email.Utils import getaddresses + +scre = re.compile(r'Message not delivered to the following', re.IGNORECASE) +ecre = re.compile(r'Error Detail', re.IGNORECASE) +acre = re.compile(r'\s+(?P<addr>\S+)\s+') + + + +def process(msg): + if msg.is_multipart(): + return None + try: + whofrom = getaddresses([msg.get('from', '')])[0][1] + if not whofrom: + return None + username, domain = whofrom.split('@', 1) + except (IndexError, ValueError): + return None + if username.lower() <> 'mailer-daemon': + return None + parts = domain.split('.') + parts.reverse() + for part1, part2 in zip(parts, ('edu', 'yale')): + if part1 <> part2: + return None + # Okay, we've established that the bounce came from the mailer-daemon at + # yale.edu. Let's look for a name, and then guess the relevant domains. + names = {} + body = StringIO(msg.get_payload()) + state = 0 + # simple state machine + # 0 == init + # 1 == intro found + while 1: + line = body.readline() + if not line: + break + if state == 0 and scre.search(line): + state = 1 + elif state == 1 and ecre.search(line): + break + elif state == 1: + mo = acre.search(line) + if mo: + names[mo.group('addr')] = 1 + # Now we have a bunch of names, these are either @yale.edu or + # @cs.yale.edu. Add them both. + addrs = [] + for name in names.keys(): + addrs.append(name + '@yale.edu') + addrs.append(name + '@cs.yale.edu') + return addrs diff --git a/Mailman/Bouncers/__init__.py b/Mailman/Bouncers/__init__.py new file mode 100644 index 00000000..2cbbabb1 --- /dev/null +++ b/Mailman/Bouncers/__init__.py @@ -0,0 +1,15 @@ +# Copyright (C) 1998,1999,2000,2001,2002 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 +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |