1
# Copyright (C) 2001,2002 by the Free Software Foundation, Inc.
3
# This program is free software; you can redistribute it and/or
4
# modify it under the terms of the GNU General Public License
5
# as published by the Free Software Foundation; either version 2
6
# of the License, or (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17
"""Bounce queue runner."""
20
from email.MIMEText import MIMEText
21
from email.MIMEMessage import MIMEMessage
22
from email.Utils import parseaddr
24
from Mailman import mm_cfg
25
from Mailman import Utils
26
from Mailman import LockFile
27
from Mailman.Message import UserNotification
28
from Mailman.Bouncers import BouncerAPI
29
from Mailman.Queue.Runner import Runner
30
from Mailman.Queue.sbcache import get_switchboard
31
from Mailman.Logging.Syslog import syslog
32
from Mailman.i18n import _
38
class BounceRunner(Runner):
39
QDIR = mm_cfg.BOUNCEQUEUE_DIR
40
# We only do bounce processing once per minute.
41
SLEEPTIME = mm_cfg.minutes(1)
43
def _dispose(self, mlist, msg, msgdata):
44
# Make sure we have the most up-to-date state
46
outq = get_switchboard(mm_cfg.OUTQUEUE_DIR)
47
# There are a few possibilities here:
49
# - the message could have been VERP'd in which case, we know exactly
50
# who the message was destined for. That make our job easy.
51
# - the message could have been originally destined for a list owner,
52
# but a list owner address itself bounced. That's bad, and for now
53
# we'll simply log the problem and attempt to deliver the message to
56
# All messages to list-owner@vdom.ain have their envelope sender set
57
# to site-owner@dom.ain (no virtual domain). Is this a bounce for a
58
# message to a list owner, coming to the site owner?
59
if msg.get('to', '') == Utils.get_site_email(extra='-owner'):
60
# Send it on to the site owners, but craft the envelope sender to
61
# be the -loop detection address, so if /they/ bounce, we won't
62
# get stuck in a bounce loop.
63
outq.enqueue(msg, msgdata,
64
recips=[Utils.get_site_email()],
65
envsender=Utils.get_site_email(extra='loop'),
67
# List isn't doing bounce processing?
68
if not mlist.bounce_processing:
70
# Try VERP detection first, since it's quick and easy
71
addrs = verp_bounce(mlist, msg)
73
# That didn't give us anything useful, so try the old fashion
74
# bounce matching modules
75
addrs = BouncerAPI.ScanMessages(mlist, msg)
76
# If that still didn't return us any useful addresses, then send it on
79
syslog('bounce', 'bounce message w/no discernable addresses: %s',
80
msg.get('message-id'))
81
maybe_forward(mlist, msg)
83
# BAW: It's possible that there are None's in the list of addresses,
84
# although I'm unsure how that could happen. Possibly ScanMessages()
85
# can let None's sneak through. In any event, this will kill them.
86
addrs = filter(None, addrs)
87
# Okay, we have some recognized addresses. We now need to register
88
# the bounces for each of these. If the bounce came to the site list,
89
# then we'll register the address on every list in the system, but
90
# note: this could be VERY resource intensive!
92
listname = mlist.internal_name()
93
if listname == mm_cfg.MAILMAN_SITE_LIST:
95
for listname in Utils.list_names():
96
xlist = self._open_list(listname)
99
if xlist.isMember(addr):
101
if not xlist.Locked():
103
xlist.Lock(timeout=mm_cfg.LIST_LOCK_TIMEOUT)
104
except LockFile.TimeOutError:
105
# Oh well, forget aboutf this list
109
xlist.registerBounce(addr, msg)
117
mlist.Lock(timeout=mm_cfg.LIST_LOCK_TIMEOUT)
118
except LockFile.TimeOutError:
120
syslog('bounce', "%s: couldn't get list lock", listname)
125
if mlist.isMember(addr):
126
mlist.registerBounce(addr, msg)
132
# It means an address was recognized but it wasn't an address
133
# that's on any mailing list at this site. BAW: don't forward
134
# these, but do log it.
135
syslog('bounce', 'bounce message with non-members of %s: %s',
136
listname, COMMASPACE.join(addrs))
137
#maybe_forward(mlist, msg)
141
def verp_bounce(mlist, msg):
142
bmailbox, bdomain = Utils.ParseEmail(mlist.GetBouncesEmail())
143
# Sadly not every MTA bounces VERP messages correctly, or consistently.
144
# Fall back to Delivered-To: (Postfix), Envelope-To: (Exim) and
145
# Apparently-To:, and then short-circuit if we still don't have anything
146
# to work with. Note that there can be multiple Delivered-To: headers so
147
# we need to search them all (and we don't worry about false positives for
148
# forwarded email, because only one should match VERP_REGEXP).
150
for header in ('to', 'delivered-to', 'envelope-to', 'apparently-to'):
151
vals.extend(msg.get_all(header, []))
153
to = parseaddr(field)[1]
155
continue # empty header
156
mo = re.search(mm_cfg.VERP_REGEXP, to)
158
continue # no match of regexp
160
if bmailbox <> mo.group('bounces'):
161
continue # not a bounce to our list
163
addr = '%s@%s' % mo.group('mailbox', 'host')
166
"VERP_REGEXP doesn't yield the right match groups: %s",
173
def maybe_forward(mlist, msg):
174
# Does the list owner want to get non-matching bounce messages?
175
# If not, simply discard it.
176
if mlist.bounce_unrecognized_goes_to_list_owner:
177
adminurl = mlist.GetScriptURL('admin', absolute=1) + '/bounce'
178
mlist.ForwardMessage(msg,
180
The attached message was received as a bounce, but either the bounce format
181
was not recognized, or no member addresses could be extracted from it. This
182
mailing list has been configured to send all unrecognized bounce messages to
183
the list administrator(s).
185
For more information see:
189
subject=_('Uncaught bounce notification'),
191
syslog('bounce', 'forwarding unrecognized, message-id: %s',
192
msg.get('message-id', 'n/a'))
194
syslog('bounce', 'discarding unrecognized, message-id: %s',
195
msg.get('message-id', 'n/a'))