1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
|
# Copyright (C) 1998,1999,2000,2001 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.
"""Handle delivery bounces.
"""
import sys
import time
from email.MIMEText import MIMEText
from email.MIMEMessage import MIMEMessage
from Mailman import mm_cfg
from Mailman import Utils
from Mailman import Message
from Mailman import MemberAdaptor
from Mailman import Pending
from Mailman.Logging.Syslog import syslog
from Mailman.i18n import _
EMPTYSTRING = ''
class _BounceInfo:
def __init__(self, member, score, date, noticesleft, cookie):
self.member = member
self.score = score
self.date = date
self.noticesleft = noticesleft
self.lastnotice = None
self.cookie = cookie
def __repr__(self):
# For debugging
return """\
<bounce info for member %(member)s
current score: %(score)s
last bounce date: %(date)s
email notices left: %(noticesleft)s
last notice date: %(lastnotice)s
confirmation cookie: %(cookie)s
>""" % self.__dict__
class Bouncer:
def InitVars(self):
# Configurable...
self.bounce_processing = mm_cfg.DEFAULT_BOUNCE_PROCESSING
self.bounce_score_threshold = mm_cfg.DEFAULT_BOUNCE_SCORE_THRESHOLD
self.bounce_info_stale_after = mm_cfg.DEFAULT_BOUNCE_INFO_STALE_AFTER
self.bounce_you_are_disabled_warnings = \
mm_cfg.DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS
self.bounce_you_are_disabled_warnings_interval = \
mm_cfg.DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS_INTERVAL
# Not configurable...
#
# This holds legacy member related information. It's keyed by the
# member address, and the value is an object containing the bounce
# score, the date of the last received bounce, and a count of the
# notifications left to send.
self.bounce_info = {}
# New style delivery status
self.delivery_status = {}
def registerBounce(self, member, msg, weight=1.0):
if not self.isMember(member):
return
info = self.getBounceInfo(member)
today = time.localtime()[:3]
if info is None:
# This is the first bounce we've seen from this member
cookie = Pending.new(Pending.RE_ENABLE, self.internal_name(),
member)
info = _BounceInfo(member, weight, today,
self.bounce_you_are_disabled_warnings,
cookie)
self.setBounceInfo(member, info)
syslog('bounce', '%s: %s bounce score: %s', self.internal_name(),
member, info.score)
elif self.getDeliveryStatus(member) <> MemberAdaptor.ENABLED:
# The user is already disabled, so we can just ignore subsequent
# bounces. These are likely due to residual messages that were
# sent before disabling the member, but took a while to bounce.
syslog('bounce', '%s: %s residual bounce received',
self.internal_name(), member)
elif info.date == today:
# We've already scored any bounces for today, so ignore this one.
syslog('bounce', '%s: %s already scored a bounce for today',
self.internal_name(), member)
else:
# See if this member's bounce information is stale.
now = time.mktime(today + (0,) * 6)
lastbounce = time.mktime(info.date + (0,) * 6)
if lastbounce + self.bounce_info_stale_after < now:
# Information is stale, so simply reset it
info.score = weight
info.date = today
info.noticesleft = 0
info.lastnotice = None
# BAW: The following isn't strictly necessary
self.setBounceInfo(member, info)
syslog('bounce', '%s: %s has stale bounce info, resetting',
self.internal_name(), member)
else:
# Nope, the information isn't stale, so add to the bounce
# score and take any necessary action.
info.score += weight
info.date = today
# BAW: The following isn't strictly necessary
self.setBounceInfo(member, info)
syslog('bounce', '%s: %s current bounce score: %s',
member, self.internal_name(), info.score)
if info.score >= self.bounce_score_threshold:
# Disable them
syslog('bounce',
'%s: %s disabling due to bounce score %s >= %s',
self.internal_name(), member,
info.score, self.bounce_score_threshold)
self.setDeliveryStatus(member, MemberAdaptor.BYBOUNCE)
self.sendNextNotification(member)
self.__sendAdminBounceNotice(member, msg)
def __sendAdminBounceNotice(self, member, msg):
# BAW: This is a bit kludgey, but we're not providing as much
# information in the new admin bounce notices as we used to (some of
# it was of dubious value). However, we'll provide empty, strange, or
# meaningless strings for the unused %()s fields so that the language
# translators don't have to provide new templates.
siteowner = Utils.get_site_email(self.host_name)
text = Utils.maketext(
'bounce.txt',
{'listname' : self.real_name,
'addr' : member,
'negative' : '',
'did' : _('disabled'),
'but' : '',
'reenable' : '',
'owneraddr': siteowner,
}, mlist=self)
subject = _('Bounce action notification')
umsg = Message.UserNotification(self.GetOwnerEmail(),
siteowner, subject)
umsg.attach(MIMEText(text))
umsg.attach(MIMEMessage(msg))
umsg['Content-Type'] = 'multipart/mixed'
umsg['MIME-Version'] = '1.0'
umsg.send(self)
def sendNextNotification(self, member):
info = self.getBounceInfo(member)
if info is None:
return
if info.noticesleft <= 0:
# BAW: Remove them now, with a notification message
self.ApprovedDeleteMember(member, 'bouncing address',
admin_notif=1, userack=1)
self.setBounceInfo(member, None)
# Expunge the pending cookie for the user. We throw away the
# returned data.
Pending.confirm(info.cookie)
syslog('bounce', '%s: %s deleted after exhausting notices',
self.internal_name(), member)
return
# Send the next notification
confirmurl = '%s/%s' % (self.GetScriptURL('confirm', absolute=1),
info.cookie)
optionsurl = self.GetOptionsURL(member, absolute=1)
subject = 'confirm ' + info.cookie
requestaddr = self.GetRequestEmail()
text = Utils.maketext(
'disabled.txt',
{'listname' : self.real_name,
'noticesleft': info.noticesleft,
'confirmurl' : confirmurl,
'optionsurl' : optionsurl,
'password' : self.getMemberPassword(member),
'owneraddr' : self.GetOwnerEmail(),
}, lang=self.getMemberLanguage(member), mlist=self)
msg = Message.UserNotification(member, requestaddr, subject, text)
msg.send(self)
info.noticesleft -= 1
info.lastnotice = time.localtime()[:3]
def BounceMessage(self, msg, msgdata, e=None):
# Bounce a message back to the sender, with an error message if
# provided in the exception argument.
sender = msg.get_sender()
subject = msg.get('subject', _('(no subject)'))
if e is None:
notice = _('[No bounce details are available]')
else:
notice = _(e.notice())
# Currently we always craft bounces as MIME messages.
bmsg = Message.UserNotification(msg.get_sender(),
self.GetOwnerEmail(),
subject)
bmsg['Content-Type'] = 'multipart/mixed'
bmsg['MIME-Version'] = '1.0'
txt = MIMEText(notice,
_charset=Utils.GetCharSet(self.preferred_language))
bmsg.add_payload(txt)
bmsg.add_payload(MIMEMessage(msg))
bmsg.send(self)
|