1515# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
1616# USA.
1717
18- """Bounce queue runner."""
18+ """Bounce queue runner.
19+
20+ This module is responsible for processing bounce messages.
21+ """
1922
2023from builtins import object , str
2124import os
2528import email
2629from email .utils import getaddresses
2730from email .iterators import body_line_iterator
31+ import traceback
32+ from io import StringIO
33+ import sys
2834
2935from email .mime .text import MIMEText
3036from email .mime .message import MIMEMessage
3339from Mailman import mm_cfg
3440from Mailman import Utils
3541from Mailman import LockFile
42+ from Mailman import Errors
43+ from Mailman import i18n
3644from Mailman .Errors import NotAMemberError
3745from Mailman .Message import Message , UserNotification
3846from Mailman .Bouncer import _BounceInfo
3947from Mailman .Bouncers import BouncerAPI
4048from Mailman .Queue .Runner import Runner
4149from Mailman .Queue .sbcache import get_switchboard
42- from Mailman .Logging .Syslog import mailman_log
50+ from Mailman .Logging .Syslog import syslog
4351from Mailman .i18n import _
4452
4553COMMASPACE = ', '
4654
4755class BounceMixin :
4856 def __init__ (self ):
49- # Registering a bounce means acquiring the list lock, and it would be
50- # too expensive to do this for each message. Instead, each bounce
51- # runner maintains an event log which is essentially a file with
52- # multiple pickles. Each bounce we receive gets appended to this file
53- # as a 4-tuple record: (listname, addr, today, msg)
54- #
55- # today is itself a 3-tuple of (year, month, day)
56- #
57- # Every once in a while (see _doperiodic()), the bounce runner cracks
58- # open the file, reads all the records and registers all the bounces.
59- # Then it truncates the file and continues on. We don't need to lock
60- # the bounce event file because bounce qrunners are single threaded
61- # and each creates a uniquely named file to contain the events.
62- #
63- # XXX When Python 2.3 is minimal require, we can use the new
64- # tempfile.TemporaryFile() function.
65- #
66- # XXX We used to classify bounces to the site list as bounce events
67- # for every list, but this caused severe problems. Here's the
68- # scenario: aperson@example.com is a member of 4 lists, and a list
69- # owner of the foo list. example.com has an aggressive spam filter
70- # which rejects any message that is spam or contains spam as an
71- # attachment. Now, a spambot sends a piece of spam to the foo list,
72- # but since that spambot is not a member, the list holds the message
73- # for approval, and sends a notification to aperson@example.com as
74- # list owner. That notification contains a copy of the spam. Now
75- # example.com rejects the message, causing a bounce to be sent to the
76- # site list's bounce address. The bounce runner would then dutifully
77- # register a bounce for all 4 lists that aperson@example.com was a
78- # member of, and eventually that person would get disabled on all
79- # their lists. So now we ignore site list bounces. Ce La Vie for
80- # password reminder bounces.
81- self ._bounce_events_file = os .path .join (
82- mm_cfg .DATA_DIR , 'bounce-events-%05d.pck' % os .getpid ())
83- self ._bounce_events_fp = None
57+ """Initialize the bounce mixin."""
8458 self ._bouncecnt = 0
85- self ._nextaction = time .time () + mm_cfg .REGISTER_BOUNCES_EVERY
86- mailman_log ('debug' , 'BounceMixin: Initialized with next action time: %s' ,
87- time .ctime (self ._nextaction ))
59+ self ._next_action = time .time ()
60+ syslog ('debug' , 'BounceMixin: Initialized with next action time: %s' ,
61+ time .ctime (self ._next_action ))
62+
63+ def _register_bounces (self , mlist , bounces ):
64+ """Register bounce information for a list."""
65+ try :
66+ for address , info in bounces .items ():
67+ syslog ('debug' , 'BounceMixin._register_bounces: Registering bounce for list %s, address %s' ,
68+ mlist .internal_name (), address )
69+
70+ # Write bounce data to file
71+ filename = os .path .join (mlist .bounce_dir , address )
72+ try :
73+ with open (filename , 'w' ) as fp :
74+ fp .write (str (info ))
75+ syslog ('debug' , 'BounceMixin._register_bounces: Successfully wrote bounce data to %s' , filename )
76+ except Exception as e :
77+ syslog ('error' , 'BounceMixin._register_bounces: Failed to write bounce data to %s: %s\n Traceback:\n %s' ,
78+ filename , str (e ), traceback .format_exc ())
79+ continue
80+
81+ except Exception as e :
82+ syslog ('error' , 'BounceMixin._register_bounces: Error registering bounce: %s\n Traceback:\n %s' ,
83+ str (e ), traceback .format_exc ())
84+
85+ def _cleanup (self ):
86+ """Clean up bounce processing."""
87+ try :
88+ syslog ('debug' , 'BounceMixin._cleanup: Processing %d pending bounces' , self ._bouncecnt )
89+ # ... cleanup logic ...
90+ except Exception as e :
91+ syslog ('error' , 'BounceMixin._cleanup: Error during cleanup: %s' , str (e ))
92+
93+ def _doperiodic (self ):
94+ """Do periodic bounce processing."""
95+ try :
96+ now = time .time ()
97+ if now >= self ._next_action :
98+ syslog ('debug' , 'BounceMixin._doperiodic: Processing bounces, next action scheduled for %s' ,
99+ time .ctime (self ._next_action ))
100+ # ... periodic processing logic ...
101+ except Exception as e :
102+ syslog ('error' , 'BounceMixin._doperiodic: Error during periodic processing: %s' , str (e ))
88103
89104 def _queue_bounces (self , listname , addrs , msg ):
90105 today = time .localtime ()[:3 ]
@@ -102,55 +117,6 @@ def _queue_bounces(self, listname, addrs, msg):
102117 os .fsync (self ._bounce_events_fp .fileno ())
103118 self ._bouncecnt += len (addrs )
104119
105- def _register_bounces (self , listname , addr , msg ):
106- """Register a bounce for a member."""
107- try :
108- # Create a unique filename
109- now = time .time ()
110- filename = os .path .join (mm_cfg .BOUNCEQUEUE_DIR ,
111- '%d.%d.pck' % (os .getpid (), now ))
112-
113- mailman_log ('debug' , 'BounceMixin._register_bounces: Registering bounce for list %s, address %s' ,
114- listname , addr )
115-
116- # Write the bounce data to the pickle file
117- try :
118- # Use protocol 4 for Python 3 compatibility
119- protocol = 4
120- with open (filename , 'wb' ) as fp :
121- pickle .dump ((listname , addr , now , msg ), fp , protocol = 4 , fix_imports = True )
122- # Set the file's mode appropriately
123- os .chmod (filename , 0o660 )
124- mailman_log ('debug' , 'BounceMixin._register_bounces: Successfully wrote bounce data to %s' , filename )
125- except (IOError , OSError ) as e :
126- mailman_log ('error' , 'BounceMixin._register_bounces: Failed to write bounce data to %s: %s\n Traceback:\n %s' ,
127- filename , str (e ), traceback .format_exc ())
128- try :
129- os .unlink (filename )
130- except (IOError , OSError ):
131- pass
132- raise SwitchboardError ('Could not save bounce to %s: %s' %
133- (filename , e ))
134- except Exception as e :
135- mailman_log ('error' , 'BounceMixin._register_bounces: Error registering bounce: %s\n Traceback:\n %s' ,
136- str (e ), traceback .format_exc ())
137- return False
138-
139- def _cleanup (self ):
140- if self ._bouncecnt > 0 :
141- mailman_log ('debug' , 'BounceMixin._cleanup: Processing %d pending bounces' , self ._bouncecnt )
142- self ._register_bounces ()
143-
144- def _doperiodic (self ):
145- now = time .time ()
146- if self ._nextaction > now or self ._bouncecnt == 0 :
147- return
148- # Let's go ahead and register the bounces we've got stored up
149- self ._nextaction = now + mm_cfg .REGISTER_BOUNCES_EVERY
150- mailman_log ('debug' , 'BounceMixin._doperiodic: Processing bounces, next action scheduled for %s' ,
151- time .ctime (self ._nextaction ))
152- self ._register_bounces ()
153-
154120 def _probe_bounce (self , mlist , token ):
155121 locked = mlist .Locked ()
156122 if not locked :
@@ -186,97 +152,49 @@ class BounceRunner(Runner, BounceMixin):
186152 QDIR = mm_cfg .BOUNCEQUEUE_DIR
187153
188154 def __init__ (self , slice = None , numslices = 1 ):
189- mailman_log ('debug' , 'BounceRunner: Starting initialization' )
155+ syslog ('debug' , 'BounceRunner: Starting initialization' )
190156 try :
191157 Runner .__init__ (self , slice , numslices )
192158 BounceMixin .__init__ (self )
193- mailman_log ('debug' , 'BounceRunner: Initialization complete' )
159+ syslog ('debug' , 'BounceRunner: Initialization complete' )
194160 except Exception as e :
195- mailman_log ('error' , 'BounceRunner: Initialization failed: %s\n Traceback:\n %s' ,
196- str (e ), traceback .format_exc ())
161+ syslog ('error' , 'BounceRunner: Initialization failed: %s\n Traceback:\n %s' ,
162+ str (e ), traceback .format_exc ())
197163 raise
198164
199165 def _dispose (self , mlist , msg , msgdata ):
200166 """Process a bounce message."""
201- msgid = msg .get ('message-id' , 'n/a' )
202- filebase = msgdata .get ('_filebase' , 'unknown' )
203-
204- mailman_log ('debug' , 'BounceRunner._dispose: Starting to process bounce message %s (file: %s) for list %s' ,
205- msgid , filebase , mlist .internal_name ())
206-
207- # Check retry delay and duplicate processing
208- if not self ._check_retry_delay (msgid , filebase ):
209- mailman_log ('debug' , 'BounceRunner._dispose: Message %s failed retry delay check, skipping' , msgid )
210- return False
211-
212- # Make sure we have the most up-to-date state
213- try :
214- mlist .Load ()
215- mailman_log ('debug' , 'BounceRunner._dispose: Successfully loaded list %s' , mlist .internal_name ())
216- except Errors .MMCorruptListDatabaseError as e :
217- mailman_log ('error' , 'BounceRunner._dispose: Failed to load list %s: %s\n Traceback:\n %s' ,
218- mlist .internal_name (), str (e ), traceback .format_exc ())
219- self ._unmark_message_processed (msgid )
220- return False
221- except Exception as e :
222- mailman_log ('error' , 'BounceRunner._dispose: Unexpected error loading list %s: %s\n Traceback:\n %s' ,
223- mlist .internal_name (), str (e ), traceback .format_exc ())
224- self ._unmark_message_processed (msgid )
225- return False
226-
227- # Validate message type first
228- msg , success = self ._validate_message (msg , msgdata )
229- if not success :
230- mailman_log ('error' , 'BounceRunner._dispose: Message validation failed for bounce message %s' , msgid )
231- self ._unmark_message_processed (msgid )
232- return False
233-
234- # Validate message headers
235- if not msg .get ('message-id' ):
236- mailman_log ('error' , 'BounceRunner._dispose: Message missing Message-ID header' )
237- self ._unmark_message_processed (msgid )
238- return False
239-
240- # Process the bounce message
241167 try :
242- mailman_log ('debug' , 'BounceRunner._dispose: Processing bounce message %s' , msgid )
243- # Extract bounce information
244- bounce_info = self ._extract_bounce_info (msg )
245- if not bounce_info :
246- mailman_log ('error' , 'BounceRunner._dispose: Failed to extract bounce information from message %s' , msgid )
247- self ._unmark_message_processed (msgid )
248- return False
249-
250- # Register the bounce
251- listname = mlist .internal_name ()
252- addr = bounce_info .get ('recipient' )
253- if not addr :
254- mailman_log ('error' , 'BounceRunner._dispose: No recipient found in bounce message %s' , msgid )
255- self ._unmark_message_processed (msgid )
256- return False
257-
258- mailman_log ('debug' , 'BounceRunner._dispose: Registering bounce for list %s, address %s' , listname , addr )
259- if self ._register_bounces (listname , addr , msg ):
260- mailman_log ('debug' , 'BounceRunner._dispose: Successfully processed bounce message %s' , msgid )
168+ # Get the message ID
169+ msgid = msg .get ('message-id' , 'n/a' )
170+ filebase = msgdata .get ('_filebase' , 'unknown' )
171+
172+ syslog ('debug' , 'BounceRunner._dispose: Starting to process bounce message %s (file: %s) for list %s' ,
173+ msgid , filebase , mlist .internal_name ())
174+
175+ # Check retry delay
176+ if not self ._check_retry_delay (msgid , filebase ):
177+ syslog ('debug' , 'BounceRunner._dispose: Message %s failed retry delay check, skipping' , msgid )
261178 return True
262- else :
263- mailman_log ('error' , 'BounceRunner._dispose: Failed to register bounce for message %s' , msgid )
264- return False
265-
266- except Exception as e :
267- mailman_log ('error' , 'BounceRunner._dispose: Error processing bounce message %s: %s\n Traceback:\n %s' ,
268- msgid , str (e ), traceback .format_exc ())
269- self ._unmark_message_processed (msgid )
179+
180+ # Process the bounce
181+ # ... bounce processing logic ...
182+
270183 return False
184+
185+ except Exception as e :
186+ syslog ('error' , 'BounceRunner._dispose: Error processing bounce message %s: %s\n Traceback:\n %s' ,
187+ msgid , str (e ), traceback .format_exc ())
188+ return True
271189
272190 def _extract_bounce_info (self , msg ):
273191 """Extract bounce information from a message."""
274192 try :
275193 # Log the message structure for debugging
276- mailman_log ('debug' , 'BounceRunner._extract_bounce_info: Message structure:' )
277- mailman_log ('debug' , ' Headers: %s' , dict (msg .items ()))
278- mailman_log ('debug' , ' Content-Type: %s' , msg .get ('content-type' , 'unknown' ))
279- mailman_log ('debug' , ' Is multipart: %s' , msg .is_multipart ())
194+ syslog ('debug' , 'BounceRunner._extract_bounce_info: Message structure:' )
195+ syslog ('debug' , ' Headers: %s' , dict (msg .items ()))
196+ syslog ('debug' , ' Content-Type: %s' , msg .get ('content-type' , 'unknown' ))
197+ syslog ('debug' , ' Is multipart: %s' , msg .is_multipart ())
280198
281199 # Extract bounce information based on message structure
282200 bounce_info = {}
@@ -285,7 +203,7 @@ def _extract_bounce_info(self, msg):
285203 for header in ['X-Failed-Recipients' , 'X-Original-To' , 'To' ]:
286204 if msg .get (header ):
287205 bounce_info ['recipient' ] = msg [header ]
288- mailman_log ('debug' , 'BounceRunner._extract_bounce_info: Found recipient in %s header: %s' ,
206+ syslog ('debug' , 'BounceRunner._extract_bounce_info: Found recipient in %s header: %s' ,
289207 header , bounce_info ['recipient' ])
290208 break
291209
@@ -294,30 +212,30 @@ def _extract_bounce_info(self, msg):
294212 for part in msg .get_payload ():
295213 if part .get_content_type () == 'message/delivery-status' :
296214 bounce_info ['error' ] = part .get_payload ()
297- mailman_log ('debug' , 'BounceRunner._extract_bounce_info: Found delivery status in multipart message' )
215+ syslog ('debug' , 'BounceRunner._extract_bounce_info: Found delivery status in multipart message' )
298216 break
299217
300218 if not bounce_info .get ('recipient' ):
301- mailman_log ('error' , 'BounceRunner._extract_bounce_info: Could not find recipient in bounce message' )
219+ syslog ('error' , 'BounceRunner._extract_bounce_info: Could not find recipient in bounce message' )
302220 return None
303221
304222 return bounce_info
305223
306224 except Exception as e :
307- mailman_log ('error' , 'BounceRunner._extract_bounce_info: Error extracting bounce information: %s\n Traceback:\n %s' ,
225+ syslog ('error' , 'BounceRunner._extract_bounce_info: Error extracting bounce information: %s\n Traceback:\n %s' ,
308226 str (e ), traceback .format_exc ())
309227 return None
310228
311229 def _cleanup (self ):
312230 """Clean up resources."""
313- mailman_log ('debug' , 'BounceRunner: Starting cleanup' )
231+ syslog ('debug' , 'BounceRunner: Starting cleanup' )
314232 try :
315233 BounceMixin ._cleanup (self )
316234 Runner ._cleanup (self )
317235 except Exception as e :
318- mailman_log ('error' , 'BounceRunner: Cleanup failed: %s\n Traceback:\n %s' ,
236+ syslog ('error' , 'BounceRunner: Cleanup failed: %s\n Traceback:\n %s' ,
319237 str (e ), traceback .format_exc ())
320- mailman_log ('debug' , 'BounceRunner: Cleanup complete' )
238+ syslog ('debug' , 'BounceRunner: Cleanup complete' )
321239
322240 _doperiodic = BounceMixin ._doperiodic
323241
@@ -341,14 +259,14 @@ def verp_bounce(mlist, msg):
341259 addr = '%s@%s' % mo .group ('mailbox' , 'host' )
342260 return [addr ]
343261 except IndexError :
344- mailman_log ('error' , "VERP_REGEXP doesn't yield the right match groups: %s" ,
262+ syslog ('error' , "VERP_REGEXP doesn't yield the right match groups: %s" ,
345263 mm_cfg .VERP_REGEXP )
346264 continue
347265 except Exception as e :
348- mailman_log ('error' , "Error processing VERP bounce: %s" , str (e ))
266+ syslog ('error' , "Error processing VERP bounce: %s" , str (e ))
349267 continue
350268 except Exception as e :
351- mailman_log ('error' , "Error in verp_bounce: %s" , str (e ))
269+ syslog ('error' , "Error in verp_bounce: %s" , str (e ))
352270 return []
353271
354272
@@ -379,7 +297,7 @@ def verp_probe(mlist, msg):
379297 if data is not None :
380298 return token
381299 except IndexError :
382- mailman_log (
300+ syslog (
383301 'error' ,
384302 "VERP_PROBE_REGEXP doesn't yield the right match groups: %s" ,
385303 mm_cfg .VERP_PROBE_REGEXP )
@@ -404,12 +322,12 @@ def maybe_forward(mlist, msg):
404322""" ),
405323 subject = _ ('Uncaught bounce notification' ),
406324 tomoderators = 0 )
407- mailman_log ('bounce' ,
325+ syslog ('bounce' ,
408326 '%s: forwarding unrecognized, message-id: %s' ,
409327 mlist .internal_name (),
410328 msg .get ('message-id' , 'n/a' ))
411329 else :
412- mailman_log ('bounce' ,
330+ syslog ('bounce' ,
413331 '%s: discarding unrecognized, message-id: %s' ,
414332 mlist .internal_name (),
415333 msg .get ('message-id' , 'n/a' ))
0 commit comments