summaryrefslogtreecommitdiff
path: root/Mailman/Bouncer.py
diff options
context:
space:
mode:
authorbwarsaw2001-12-27 06:49:12 +0000
committerbwarsaw2001-12-27 06:49:12 +0000
commitc8a029828285a686dd1999f110d0bc528618ab74 (patch)
tree2316635ce723a12fee0dd255b7612f6409eab150 /Mailman/Bouncer.py
parent825134c6d898e53f6715f31440d8384f11241e20 (diff)
downloadmailman-c8a029828285a686dd1999f110d0bc528618ab74.tar.gz
mailman-c8a029828285a686dd1999f110d0bc528618ab74.tar.zst
mailman-c8a029828285a686dd1999f110d0bc528618ab74.zip
Diffstat (limited to 'Mailman/Bouncer.py')
-rw-r--r--Mailman/Bouncer.py366
1 files changed, 143 insertions, 223 deletions
diff --git a/Mailman/Bouncer.py b/Mailman/Bouncer.py
index b9fa7a37d..64dabb5e6 100644
--- a/Mailman/Bouncer.py
+++ b/Mailman/Bouncer.py
@@ -14,13 +14,8 @@
# 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 bounce messages, doing filtering when list is set for it."
-
-
-# It's possible to get the mail-list senders address (list-admin) in the
-# bounce list. You probably don't want to have list mail sent to that
-# address anyway.
+"""Handle delivery bounces.
+"""
import sys
import time
@@ -29,245 +24,170 @@ from email.MIMEText import MIMEText
from email.MIMEMessage import MIMEMessage
from Mailman import mm_cfg
-from Mailman import Errors
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...
#
- # self.bounce_info registers observed bounce incidents. It's a
- # dict mapping members addrs to a list:
- # [
- # time.time() of last bounce,
- # post_id of first offending bounce in current sequence,
- # post_id of last offending bounce in current sequence
- # ]
+ # 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 = {}
- # Configurable...
- self.bounce_processing = mm_cfg.DEFAULT_BOUNCE_PROCESSING
- self.minimum_removal_date = mm_cfg.DEFAULT_MINIMUM_REMOVAL_DATE
- self.minimum_post_count_before_bounce_action = \
- mm_cfg.DEFAULT_MINIMUM_POST_COUNT_BEFORE_BOUNCE_ACTION
- self.automatic_bounce_action = mm_cfg.DEFAULT_AUTOMATIC_BOUNCE_ACTION
- self.max_posts_between_bounces = \
- mm_cfg.DEFAULT_MAX_POSTS_BETWEEN_BOUNCES
# New style delivery status
self.delivery_status = {}
- def ClearBounceInfo(self, member):
- member = member.lower()
- if self.bounce_info.has_key(member):
- del self.bounce_info[member]
-
- def RegisterBounce(self, member, msg):
- """Detect and handle repeat-offender bounce addresses.
-
- We use very sketchy bounce history profiles in self.bounce_info
- (see comment above its initialization), together with list-
- specific thresholds self.minimum_post_count_before_bounce_action
- and self.max_posts_between_bounces.
- """
- report = "%s: %s - " % (self.real_name, member)
- now = time.time()
- days = mm_cfg.days
- # Take the opportunity to cull expired entries.
- pid = self.post_id
- maxposts = self.max_posts_between_bounces
- # BAW: This can't be right. minimum_removal_date should only be
- # multiplied by days(1) :(
- stalesecs = self.minimum_removal_date * days(5)
- for k, v in self.bounce_info.items():
- if now - v[0] > stalesecs:
- # It's been long enough to drop their bounce record:
- del self.bounce_info[k]
-
- # Is this the first bounce we're seeing from this address?
- this_dude = Utils.FindMatchingAddresses(member, self.bounce_info)
- if not this_dude:
- # No (or expired) priors - new record.
- self.bounce_info[member.lower()] = [now, self.post_id,
- self.post_id]
- syslog('bounce', '%sfirst', report)
+ def registerBounce(self, member, msg, weight=1.0):
+ if not self.isMember(member):
return
-
- # No, there are some priors.
- addr = this_dude[0].lower()
- hist = self.bounce_info[addr]
- difference = now - hist[0]
- # FIXME: Use MemberAdaptor interface
- if len(Utils.FindMatchingAddresses(addr, self.members)):
- if self.post_id - hist[2] > self.max_posts_between_bounces:
- # There's been enough posts since last bounce that we're
- # restarting. (Might should keep track of who goes stale
- # how often.)
- syslog('bounce', '%sfirst fresh', report)
- self.bounce_info[addr] = [now, self.post_id, self.post_id]
- return
- self.bounce_info[addr][2] = self.post_id
- if ((self.post_id - hist[1] >
- self.minimum_post_count_before_bounce_action)
- and
- (difference > self.minimum_removal_date * days(1))):
- syslog('bounce', '%sexceeded limits', report)
- self.HandleBouncingAddress(addr, msg)
- return
- else:
- post_count = (self.minimum_post_count_before_bounce_action
- - (self.post_id - hist[1]))
- if post_count < 0:
- post_count = 0
- remain = self.minimum_removal_date * days(1) - difference
- syslog('bounce', '%s%d more allowed over %d secs',
- report, post_count, remain)
- return
-
- elif len(Utils.FindMatchingAddresses(addr, self.digest_members)):
- if self.volume > hist[1]:
- syslog('bounce', '%s: first fresh (D)', self._internal_name)
- self.bounce_info[addr] = [now, self.volume, self.volume]
- return
- if difference > self.minimum_removal_date * days(1):
- syslog('bounce', '%sexceeded limits (D)', report)
- self.HandleBouncingAddress(addr, msg)
- return
- syslog('bounce', '%sdigester lucked out', report)
+ 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:
- syslog('bounce', '%s: address %s not a member.',
- self.internal_name(), addr)
-
- def HandleBouncingAddress(self, addr, msg):
- """Disable or remove addr according to bounce_action setting."""
- disabled = 0
- if self.automatic_bounce_action == 0:
- return
- elif self.automatic_bounce_action == 1:
- # Only send if call works ok.
- (succeeded, send) = self.DisableBouncingAddress(addr)
- did = _('disabled')
- disabled = 1
- elif self.automatic_bounce_action == 2:
- (succeeded, send) = self.DisableBouncingAddress(addr)
- did = _('disabled')
- disabled = 1
- # Never send.
- send = 0
- elif self.automatic_bounce_action == 3:
- succeeded, send = self.RemoveBouncingAddress(addr)
- did = _('removed')
- # Always send.
- send = 1
- if send:
- if succeeded <> 1:
- negative = _('not ')
+ # 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 = _BounceInfo(member, weight, today, 0)
+ self.setBounceInfo(member, info)
+ syslog('bounce', '%s: %s has stale bounce info, resetting',
+ self.internal_name(), member)
else:
- negative = ''
- recipient = self.GetAdminEmail()
- if addr in self.owner + [recipient]:
- # Whoops! This is a bounce of a bounce notice - do not
- # perpetuate the bounce loop! Log it prominently and be
- # satisfied with that.
- syslog('error', '''\
-%s: Bounce recipient loop encountered!
-(I.e., bounce notification address itself bounces.)
-Bad admin recipient: %s''', self.internal_name(), addr)
- return
- # report about success
- but = ''
- if succeeded <> 1:
- but = _('BUT: %(succeeded)s')
- # disabled?
- if disabled and succeeded == 1:
- reenable = Utils.maketext(
- 'reenable.txt',
- {'admin_url': self.GetScriptURL('admin', absolute=1),},
- mlist=self)
- else:
- reenable = ''
- # the mail message text
- text = Utils.maketext(
- 'bounce.txt',
- {'listname' : self.real_name,
- 'addr' : addr,
- 'negative' : negative,
- 'did' : did,
- 'but' : but,
- 'reenable' : reenable,
- 'owneraddr': Utils.get_site_email(self.host_name, 'admin'),
- }, mlist=self)
- rname = self.real_name
- msg0 = Message.UserNotification(
- recipient, Utils.get_site_email(self.host_name, 'admin'),
- _('%(rname)s member %(addr)s bouncing - %(negative)s%(did)s'))
- msg0['MIME-Version'] = '1.0'
- msg0['Content-Type'] = 'multipart/mixed'
- msg1 = MIMEText(text,
- _charset=Utils.GetCharSet(self.preferred_language))
- msg2 = MIMEMessage(msg)
- msg0.add_payload(msg1)
- msg0.add_payload(msg2)
- # add this here so it doesn't get wrapped/filled
- if negative:
- negative = negative.upper()
- # send the bounce message
- msg0.send(self)
-
- def DisableBouncingAddress(self, addr):
- """Disable delivery for bouncing user address.
+ # Nope, the information isn't stale, so add to the bounce
+ # score and take any necessary action.
+ info.score += weight
+ info.date = today
+ 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)
- Returning success and notification status.
- """
- if not self.isMember(addr):
- reason = _('User not found.')
- syslog('bounce', '%s: NOT disabled %s: %s',
- self.real_name, addr, reason)
- return reason, 1
- try:
- if self.getDeliveryStatus(addr) <> MemberAdaptor.ENABLED:
- # No need to send out notification if they're already disabled.
- syslog('bounce', '%s: already disabled %s',
- self.real_name, addr)
- return 1, 0
- else:
- self.setDeliveryStatus(addr, MemberAdaptor.BYBOUNCE)
- syslog('bounce', '%s: disabled %s', self.real_name, addr)
- self.Save()
- return 1, 1
- except Errors.MMNoSuchUserError:
- syslog('bounce', '%s: NOT disabled %s: %s',
- self.real_name, addr, Errors.MMNoSuchUserError)
- self.ClearBounceInfo(addr)
- self.Save()
- return Errors.MMNoSuchUserError, 1
-
- def RemoveBouncingAddress(self, addr):
- """Unsubscribe user with bouncing address.
+ 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)
- Returning success and notification status."""
- if not self.isMember(addr):
- reason = _('User not found.')
- syslog('bounce', '%s: NOT removed %s: %s',
- self.real_name, addr, reason)
- return reason, 1
- try:
- self.ApprovedDeleteMember(addr, "bouncing addr")
- syslog('bounce', '%s: removed %s', self.real_name, addr)
- self.Save()
- return 1, 1
- except Errors.MMNoSuchUserError:
- syslog('bounce', '%s: NOT removed %s: %s',
- self.real_name, addr, Errors.MMNoSuchUserError)
- self.ClearBounceInfo(addr)
- self.Save()
- return Errors.MMNoSuchUserError, 1
+ 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