aboutsummaryrefslogtreecommitdiffstats
path: root/Mailman/Bouncers
diff options
context:
space:
mode:
author <>2003-01-02 05:25:50 +0000
committer <>2003-01-02 05:25:50 +0000
commitb132a73f15e432eaf43310fce9196ca0c0651465 (patch)
treec15f816ba7c4de99fef510e3bd75af0890d47441 /Mailman/Bouncers
downloadmailman2-b132a73f15e432eaf43310fce9196ca0c0651465.tar.gz
mailman2-b132a73f15e432eaf43310fce9196ca0c0651465.tar.xz
mailman2-b132a73f15e432eaf43310fce9196ca0c0651465.zip
This commit was manufactured by cvs2svn to create branch
'Release_2_1-maint'.
Diffstat (limited to 'Mailman/Bouncers')
-rw-r--r--Mailman/Bouncers/.cvsignore1
-rw-r--r--Mailman/Bouncers/BouncerAPI.py71
-rw-r--r--Mailman/Bouncers/Caiwireless.py45
-rw-r--r--Mailman/Bouncers/Compuserve.py45
-rw-r--r--Mailman/Bouncers/DSN.py79
-rw-r--r--Mailman/Bouncers/Exchange.py47
-rw-r--r--Mailman/Bouncers/Exim.py30
-rw-r--r--Mailman/Bouncers/GroupWise.py70
-rw-r--r--Mailman/Bouncers/LLNL.py31
-rw-r--r--Mailman/Bouncers/Makefile.in74
-rw-r--r--Mailman/Bouncers/Microsoft.py48
-rw-r--r--Mailman/Bouncers/Netscape.py88
-rw-r--r--Mailman/Bouncers/Postfix.py86
-rw-r--r--Mailman/Bouncers/Qmail.py61
-rw-r--r--Mailman/Bouncers/SMTP32.py57
-rw-r--r--Mailman/Bouncers/SimpleMatch.py100
-rw-r--r--Mailman/Bouncers/SimpleWarning.py44
-rw-r--r--Mailman/Bouncers/Sina.py47
-rw-r--r--Mailman/Bouncers/Yahoo.py53
-rw-r--r--Mailman/Bouncers/Yale.py79
-rw-r--r--Mailman/Bouncers/__init__.py15
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.