aboutsummaryrefslogtreecommitdiffstats
path: root/Mailman/Queue/BounceRunner.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--Mailman/Queue/BounceRunner.py195
1 files changed, 195 insertions, 0 deletions
diff --git a/Mailman/Queue/BounceRunner.py b/Mailman/Queue/BounceRunner.py
new file mode 100644
index 00000000..e59ac47e
--- /dev/null
+++ b/Mailman/Queue/BounceRunner.py
@@ -0,0 +1,195 @@
+# 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.
+
+"""Bounce queue runner."""
+
+import re
+from email.MIMEText import MIMEText
+from email.MIMEMessage import MIMEMessage
+from email.Utils import parseaddr
+
+from Mailman import mm_cfg
+from Mailman import Utils
+from Mailman import LockFile
+from Mailman.Message import UserNotification
+from Mailman.Bouncers import BouncerAPI
+from Mailman.Queue.Runner import Runner
+from Mailman.Queue.sbcache import get_switchboard
+from Mailman.Logging.Syslog import syslog
+from Mailman.i18n import _
+
+COMMASPACE = ', '
+
+
+
+class BounceRunner(Runner):
+ QDIR = mm_cfg.BOUNCEQUEUE_DIR
+ # We only do bounce processing once per minute.
+ SLEEPTIME = mm_cfg.minutes(1)
+
+ def _dispose(self, mlist, msg, msgdata):
+ # Make sure we have the most up-to-date state
+ mlist.Load()
+ outq = get_switchboard(mm_cfg.OUTQUEUE_DIR)
+ # There are a few possibilities here:
+ #
+ # - the message could have been VERP'd in which case, we know exactly
+ # who the message was destined for. That make our job easy.
+ # - the message could have been originally destined for a list owner,
+ # but a list owner address itself bounced. That's bad, and for now
+ # we'll simply log the problem and attempt to deliver the message to
+ # the site owner.
+ #
+ # All messages to list-owner@vdom.ain have their envelope sender set
+ # to site-owner@dom.ain (no virtual domain). Is this a bounce for a
+ # message to a list owner, coming to the site owner?
+ if msg.get('to', '') == Utils.get_site_email(extra='-owner'):
+ # Send it on to the site owners, but craft the envelope sender to
+ # be the -loop detection address, so if /they/ bounce, we won't
+ # get stuck in a bounce loop.
+ outq.enqueue(msg, msgdata,
+ recips=[Utils.get_site_email()],
+ envsender=Utils.get_site_email(extra='loop'),
+ )
+ # List isn't doing bounce processing?
+ if not mlist.bounce_processing:
+ return
+ # Try VERP detection first, since it's quick and easy
+ addrs = verp_bounce(mlist, msg)
+ if not addrs:
+ # That didn't give us anything useful, so try the old fashion
+ # bounce matching modules
+ addrs = BouncerAPI.ScanMessages(mlist, msg)
+ # If that still didn't return us any useful addresses, then send it on
+ # or discard it.
+ if not addrs:
+ syslog('bounce', 'bounce message w/no discernable addresses: %s',
+ msg.get('message-id'))
+ maybe_forward(mlist, msg)
+ return
+ # BAW: It's possible that there are None's in the list of addresses,
+ # although I'm unsure how that could happen. Possibly ScanMessages()
+ # can let None's sneak through. In any event, this will kill them.
+ addrs = filter(None, addrs)
+ # Okay, we have some recognized addresses. We now need to register
+ # the bounces for each of these. If the bounce came to the site list,
+ # then we'll register the address on every list in the system, but
+ # note: this could be VERY resource intensive!
+ foundp = 0
+ listname = mlist.internal_name()
+ if listname == mm_cfg.MAILMAN_SITE_LIST:
+ foundp = 1
+ for listname in Utils.list_names():
+ xlist = self._open_list(listname)
+ xlist.Load()
+ for addr in addrs:
+ if xlist.isMember(addr):
+ unlockp = 0
+ if not xlist.Locked():
+ try:
+ xlist.Lock(timeout=mm_cfg.LIST_LOCK_TIMEOUT)
+ except LockFile.TimeOutError:
+ # Oh well, forget aboutf this list
+ continue
+ unlockp = 1
+ try:
+ xlist.registerBounce(addr, msg)
+ foundp = 1
+ xlist.Save()
+ finally:
+ if unlockp:
+ xlist.Unlock()
+ else:
+ try:
+ mlist.Lock(timeout=mm_cfg.LIST_LOCK_TIMEOUT)
+ except LockFile.TimeOutError:
+ # Try again later
+ syslog('bounce', "%s: couldn't get list lock", listname)
+ return 1
+ else:
+ try:
+ for addr in addrs:
+ if mlist.isMember(addr):
+ mlist.registerBounce(addr, msg)
+ foundp = 1
+ mlist.Save()
+ finally:
+ mlist.Unlock()
+ if not foundp:
+ # It means an address was recognized but it wasn't an address
+ # that's on any mailing list at this site. BAW: don't forward
+ # these, but do log it.
+ syslog('bounce', 'bounce message with non-members of %s: %s',
+ listname, COMMASPACE.join(addrs))
+ #maybe_forward(mlist, msg)
+
+
+
+def verp_bounce(mlist, msg):
+ bmailbox, bdomain = Utils.ParseEmail(mlist.GetBouncesEmail())
+ # Sadly not every MTA bounces VERP messages correctly, or consistently.
+ # Fall back to Delivered-To: (Postfix), Envelope-To: (Exim) and
+ # Apparently-To:, and then short-circuit if we still don't have anything
+ # to work with. Note that there can be multiple Delivered-To: headers so
+ # we need to search them all (and we don't worry about false positives for
+ # forwarded email, because only one should match VERP_REGEXP).
+ vals = []
+ for header in ('to', 'delivered-to', 'envelope-to', 'apparently-to'):
+ vals.extend(msg.get_all(header, []))
+ for field in vals:
+ to = parseaddr(field)[1]
+ if not to:
+ continue # empty header
+ mo = re.search(mm_cfg.VERP_REGEXP, to)
+ if not mo:
+ continue # no match of regexp
+ try:
+ if bmailbox <> mo.group('bounces'):
+ continue # not a bounce to our list
+ # All is good
+ addr = '%s@%s' % mo.group('mailbox', 'host')
+ except IndexError:
+ syslog('error',
+ "VERP_REGEXP doesn't yield the right match groups: %s",
+ mm_cfg.VERP_REGEXP)
+ return []
+ return [addr]
+
+
+
+def maybe_forward(mlist, msg):
+ # Does the list owner want to get non-matching bounce messages?
+ # If not, simply discard it.
+ if mlist.bounce_unrecognized_goes_to_list_owner:
+ adminurl = mlist.GetScriptURL('admin', absolute=1) + '/bounce'
+ mlist.ForwardMessage(msg,
+ text=_("""\
+The attached message was received as a bounce, but either the bounce format
+was not recognized, or no member addresses could be extracted from it. This
+mailing list has been configured to send all unrecognized bounce messages to
+the list administrator(s).
+
+For more information see:
+%(adminurl)s
+
+"""),
+ subject=_('Uncaught bounce notification'),
+ tomoderators=0)
+ syslog('bounce', 'forwarding unrecognized, message-id: %s',
+ msg.get('message-id', 'n/a'))
+ else:
+ syslog('bounce', 'discarding unrecognized, message-id: %s',
+ msg.get('message-id', 'n/a'))