2323import time
2424import pickle
2525
26+ from email .iterators import typed_subpart_iterator
2627from email .mime .text import MIMEText
2728from email .mime .message import MIMEMessage
2829from email .utils import parseaddr
2930
3031from Mailman import mm_cfg
3132from Mailman import Utils
33+ from Mailman .MailList import MailList
3234from Mailman import LockFile
3335from Mailman .Errors import NotAMemberError
3436from Mailman .Message import UserNotification
@@ -74,8 +76,9 @@ def __init__(self):
7476 # site list's bounce address. The bounce runner would then dutifully
7577 # register a bounce for all 4 lists that aperson@example.com was a
7678 # member of, and eventually that person would get disabled on all
77- # their lists. So now we ignore site list bounces. Ce La Vie for
78- # password reminder bounces.
79+ # their lists. So now we ignore most site list bounces. Membership
80+ # reminder bounces can optionally be processed; see
81+ # BOUNCE_PROCESS_REMINDER_BOUNCES.
7982 self ._bounce_events_file = os .path .join (
8083 mm_cfg .DATA_DIR , 'bounce-events-%05d.pck' % os .getpid ())
8184 self ._bounce_events_fp = None
@@ -97,6 +100,22 @@ def _queue_bounces(self, listname, addrs, msg):
97100 os .fsync (self ._bounce_events_fp .fileno ())
98101 self ._bouncecnt += len (addrs )
99102
103+ def _queue_reminder_bounces (self , addr , msg ):
104+ """Queue a reminder bounce for each subscribed list that sends reminders."""
105+ for listname in Utils .list_names ():
106+ if listname .lower () == mm_cfg .MAILMAN_SITE_LIST .lower ():
107+ continue
108+ mlist = MailList (listname , lock = 0 )
109+ if not mlist .bounce_processing :
110+ continue
111+ if not mlist .send_reminders :
112+ continue
113+ if not mlist .isMember (addr ):
114+ continue
115+ if mlist .getMemberOption (addr , mm_cfg .SuppressPasswordReminder ):
116+ continue
117+ self ._queue_bounces (listname , [addr ], msg )
118+
100119 def _register_bounces (self ):
101120 syslog ('bounce' , '%s processing %s queued bounces' ,
102121 self , self ._bouncecnt )
@@ -206,18 +225,24 @@ def _dispose(self, mlist, msg, msgdata):
206225 # message sent directly to the -bounces address. We have to do these
207226 # cases separately, because sending to site-owner will reset the
208227 # envelope sender.
228+ is_site_list = (mlist .internal_name ().lower () ==
229+ mm_cfg .MAILMAN_SITE_LIST .lower ())
230+ process_reminder_bounce = False
209231 # Is this a site list bounce?
210- if (mlist .internal_name ().lower () ==
211- mm_cfg .MAILMAN_SITE_LIST .lower ()):
212- # Send it on to the site owners, but craft the envelope sender to
213- # be the -loop detection address, so if /they/ bounce, we won't
214- # get stuck in a bounce loop.
215- outq .enqueue (msg , msgdata ,
216- recips = mlist .owner ,
217- envsender = Utils .get_site_email (extra = 'loop' ),
218- nodecorate = 1 ,
219- )
220- return
232+ if is_site_list :
233+ if (mm_cfg .BOUNCE_PROCESS_REMINDER_BOUNCES and
234+ is_membership_reminder_bounce (msg )):
235+ process_reminder_bounce = True
236+ else :
237+ # Send it on to the site owners, but craft the envelope sender
238+ # to be the -loop detection address, so if /they/ bounce, we
239+ # won't get stuck in a bounce loop.
240+ outq .enqueue (msg , msgdata ,
241+ recips = mlist .owner ,
242+ envsender = Utils .get_site_email (extra = 'loop' ),
243+ nodecorate = 1 ,
244+ )
245+ return
221246 # Is this a possible looping message sent directly to a list-bounces
222247 # address other than the site list?
223248 # Check From: because unix_from might be VERP'd.
@@ -233,7 +258,7 @@ def _dispose(self, mlist, msg, msgdata):
233258 )
234259 return
235260 # List isn't doing bounce processing?
236- if not mlist .bounce_processing :
261+ if not process_reminder_bounce and not mlist .bounce_processing :
237262 return
238263 # Try VERP detection first, since it's quick and easy
239264 addrs = verp_bounce (mlist , msg )
@@ -268,7 +293,11 @@ def _dispose(self, mlist, msg, msgdata):
268293 # can let None's sneak through. In any event, this will kill them.
269294 # addrs = filter(None, addrs)
270295 # MAS above filter moved up so we don't try to queue an empty list.
271- self ._queue_bounces (mlist .internal_name (), addrs , msg )
296+ if process_reminder_bounce :
297+ for addr in addrs :
298+ self ._queue_reminder_bounces (addr , msg )
299+ else :
300+ self ._queue_bounces (mlist .internal_name (), addrs , msg )
272301
273302 _doperiodic = BounceMixin ._doperiodic
274303
@@ -277,6 +306,40 @@ def _cleanup(self):
277306 Runner ._cleanup (self )
278307
279308
309+
310+ def is_membership_reminder_bounce (msg ):
311+ """Return true if this bounce is for a monthly membership reminder."""
312+ for part in typed_subpart_iterator (msg , 'message' , 'rfc822' ):
313+ orig = part .get_payload (0 )
314+ if orig is None :
315+ continue
316+ if orig .get ('X-Mailman-Membership-Reminder' , '' ).lower () == 'yes' :
317+ return True
318+ if orig .get ('x-list-administrivia' , '' ).lower () != 'yes' :
319+ continue
320+ sender = parseaddr (orig .get ('from' , '' ))[1 ].lower ()
321+ if not sender :
322+ continue
323+ # Owner notifications are sent from the site -bounces address.
324+ if _is_site_bounces_address (sender ):
325+ continue
326+ subj = Utils .oneline (orig .get ('subject' , '' ), 'us-ascii' ).lower ()
327+ if 'reminder' in subj :
328+ return True
329+ return False
330+
331+
332+
333+ def _is_site_bounces_address (addr ):
334+ addr = addr .lower ()
335+ mailbox , domain = Utils .ParseEmail (addr )
336+ if mailbox == '%s-bounces' % mm_cfg .MAILMAN_SITE_LIST :
337+ return True
338+ if mailbox .startswith ('%s-bounces+' % mm_cfg .MAILMAN_SITE_LIST ):
339+ return True
340+ return False
341+
342+
280343
281344def verp_bounce (mlist , msg ):
282345 bmailbox , bdomain = Utils .ParseEmail (mlist .GetBouncesEmail ())
0 commit comments