2525import email
2626from email .utils import getaddresses
2727from email .iterators import body_line_iterator
28+
2829from email .mime .text import MIMEText
2930from email .mime .message import MIMEMessage
3031from email .utils import parseaddr
31- import threading
32- import traceback
3332
3433from Mailman import mm_cfg
3534from Mailman import Utils
@@ -60,102 +59,49 @@ def __init__(self):
6059 # Then it truncates the file and continues on. We don't need to lock
6160 # the bounce event file because bounce qrunners are single threaded
6261 # 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.
6381 self ._bounce_events_file = os .path .join (
6482 mm_cfg .DATA_DIR , 'bounce-events-%05d.pck' % os .getpid ())
6583 self ._bounce_events_fp = None
6684 self ._bouncecnt = 0
6785 self ._nextaction = time .time () + mm_cfg .REGISTER_BOUNCES_EVERY
68- self ._max_bounce_size = 1024 * 1024 # 1MB max bounce size
69- self ._max_bounce_age = 7 * 24 * 3600 # 7 days max bounce age
70- self ._bounce_lock = threading .Lock ()
71- self ._last_cleanup = time .time ()
72- self ._cleanup_interval = 3600 # Clean up every hour
73-
74- def _cleanup_old_bounces (self ):
75- """Clean up old bounce files."""
76- try :
77- current_time = time .time ()
78- if current_time - self ._last_cleanup < self ._cleanup_interval :
79- return
80-
81- with self ._bounce_lock :
82- # Clean up old bounce event files
83- for filename in os .listdir (mm_cfg .DATA_DIR ):
84- if filename .startswith ('bounce-events-' ) and filename .endswith ('.pck' ):
85- filepath = os .path .join (mm_cfg .DATA_DIR , filename )
86- try :
87- if current_time - os .path .getmtime (filepath ) > self ._max_bounce_age :
88- os .unlink (filepath )
89- except OSError :
90- pass
91-
92- # Clean up old bounce queue files
93- for filename in os .listdir (mm_cfg .BOUNCEQUEUE_DIR ):
94- if filename .endswith ('.pck' ):
95- filepath = os .path .join (mm_cfg .BOUNCEQUEUE_DIR , filename )
96- try :
97- if current_time - os .path .getmtime (filepath ) > self ._max_bounce_age :
98- os .unlink (filepath )
99- except OSError :
100- pass
101-
102- self ._last_cleanup = current_time
103- except Exception as e :
104- mailman_log ('error' , 'Error cleaning up old bounces: %s' , str (e ))
105-
106- def _validate_bounce (self , msg ):
107- """Validate bounce message format."""
108- try :
109- # Check required headers
110- if not msg .get ('from' ):
111- return False
112- if not msg .get ('to' ):
113- return False
114- if not msg .get ('message-id' ):
115- return False
116-
117- # Check message size
118- if len (str (msg )) > self ._max_bounce_size :
119- return False
120-
121- # Check for valid bounce format
122- if not msg .get ('content-type' , '' ).startswith ('message/delivery-status' ):
123- return False
124-
125- return True
126- except Exception :
127- return False
12886
12987 def _queue_bounces (self , listname , addrs , msg ):
130- """Queue bounce messages with proper validation and error handling."""
131- if not self ._validate_bounce (msg ):
132- mailman_log ('error' , 'Invalid bounce message format' )
133- return False
134-
13588 today = time .localtime ()[:3 ]
136- try :
137- with self ._bounce_lock :
138- if self ._bounce_events_fp is None :
139- omask = os .umask (0o006 )
140- try :
141- self ._bounce_events_fp = open (self ._bounce_events_file , 'ab' )
142- finally :
143- os .umask (omask )
144-
145- for addr in addrs :
146- # Use protocol 4 for Python 3 compatibility
147- pickle .dump ((listname , addr , today , msg ),
148- self ._bounce_events_fp , protocol = 4 , fix_imports = True )
149- self ._bounce_events_fp .flush ()
150- os .fsync (self ._bounce_events_fp .fileno ())
151- self ._bouncecnt += len (addrs )
152- return True
153- except Exception as e :
154- mailman_log ('error' , 'Error queueing bounces: %s' , str (e ))
155- return False
89+ if self ._bounce_events_fp is None :
90+ omask = os .umask (0o006 )
91+ try :
92+ self ._bounce_events_fp = open (self ._bounce_events_file , 'ab' )
93+ finally :
94+ os .umask (omask )
95+ for addr in addrs :
96+ # Use protocol 4 for Python 3 compatibility and fix_imports for Python 2/3 compatibility
97+ pickle .dump ((listname , addr , today , msg ),
98+ self ._bounce_events_fp , protocol = 4 , fix_imports = True )
99+ self ._bounce_events_fp .flush ()
100+ os .fsync (self ._bounce_events_fp .fileno ())
101+ self ._bouncecnt += len (addrs )
156102
157103 def _register_bounces (self , listname , addr , msg ):
158- """Register a bounce for a member with proper error handling ."""
104+ """Register a bounce for a member."""
159105 try :
160106 # Create a unique filename
161107 now = time .time ()
@@ -164,60 +110,53 @@ def _register_bounces(self, listname, addr, msg):
164110
165111 # Write the bounce data to the pickle file
166112 try :
113+ # Use protocol 4 for Python 3 compatibility
114+ protocol = 4
167115 with open (filename , 'wb' ) as fp :
168116 pickle .dump ((listname , addr , now , msg ), fp , protocol = 4 , fix_imports = True )
117+ # Set the file's mode appropriately
169118 os .chmod (filename , 0o660 )
170- return True
171119 except (IOError , OSError ) as e :
172120 try :
173121 os .unlink (filename )
174122 except (IOError , OSError ):
175123 pass
176- mailman_log ( 'error' , ' Could not save bounce to %s: %s', filename , e )
177- return False
124+ raise SwitchboardError ( ' Could not save bounce to %s: %s' %
125+ ( filename , e ))
178126 except Exception as e :
179127 mailman_log ('error' , 'Error registering bounce: %s' , e )
180128 return False
181129
182130 def _cleanup (self ):
183- """Clean up resources."""
184- try :
185- if self ._bounce_events_fp is not None :
186- self ._bounce_events_fp .close ()
187- self ._bounce_events_fp = None
188- if self ._bouncecnt > 0 :
189- self ._register_bounces ()
190- self ._cleanup_old_bounces ()
191- except Exception as e :
192- mailman_log ('error' , 'Error in bounce cleanup: %s' , str (e ))
131+ if self ._bouncecnt > 0 :
132+ self ._register_bounces ()
193133
194134 def _doperiodic (self ):
195- """Periodic cleanup and processing."""
196135 now = time .time ()
197136 if self ._nextaction > now or self ._bouncecnt == 0 :
198137 return
199138 # Let's go ahead and register the bounces we've got stored up
200139 self ._nextaction = now + mm_cfg .REGISTER_BOUNCES_EVERY
201140 self ._register_bounces ()
202- self ._cleanup_old_bounces ()
203141
204142 def _probe_bounce (self , mlist , token ):
205- """Process a probe bounce with proper error handling."""
206143 locked = mlist .Locked ()
207144 if not locked :
208145 mlist .Lock ()
209146 try :
210147 op , addr , bmsg = mlist .pend_confirm (token )
148+ # For Python 2.4 compatibility we need an inner try because
149+ # try: ... except: ... finally: requires Python 2.5+
211150 try :
212151 info = mlist .getBounceInfo (addr )
213152 if not info :
214153 # info was deleted before probe bounce was received.
215154 # Just create a new info.
216155 info = _BounceInfo (addr ,
217- 0.0 ,
218- time .localtime ()[:3 ],
219- mlist .bounce_you_are_disabled_warnings
220- )
156+ 0.0 ,
157+ time .localtime ()[:3 ],
158+ mlist .bounce_you_are_disabled_warnings
159+ )
221160 mlist .disableBouncingMember (addr , info , bmsg )
222161 # Only save the list if we're unlocking it
223162 if not locked :
@@ -226,8 +165,6 @@ def _probe_bounce(self, mlist, token):
226165 # Member was removed before probe bounce returned.
227166 # Just ignore it.
228167 pass
229- except Exception as e :
230- mailman_log ('error' , 'Error processing probe bounce: %s' , str (e ))
231168 finally :
232169 if not locked :
233170 mlist .Unlock ()
@@ -241,13 +178,12 @@ def __init__(self, slice=None, numslices=1):
241178 BounceMixin .__init__ (self )
242179
243180 def _dispose (self , mlist , msg , msgdata ):
244- """Process a bounce message with proper validation and error handling ."""
181+ """Process a bounce message."""
245182 msgid = msg .get ('message-id' , 'n/a' )
246183 filebase = msgdata .get ('_filebase' , 'unknown' )
247184
248- # Validate bounce message
249- if not self ._validate_bounce (msg ):
250- mailman_log ('error' , 'Invalid bounce message format for %s' , msgid )
185+ # Check retry delay and duplicate processing
186+ if not self ._check_retry_delay (msgid , filebase ):
251187 return False
252188
253189 # Make sure we have the most up-to-date state
@@ -256,10 +192,25 @@ def _dispose(self, mlist, msg, msgdata):
256192 except Errors .MMCorruptListDatabaseError as e :
257193 mailman_log ('error' , 'Failed to load list %s: %s' ,
258194 mlist .internal_name (), e )
195+ self ._unmark_message_processed (msgid )
259196 return False
260197 except Exception as e :
261198 mailman_log ('error' , 'Unexpected error loading list %s: %s' ,
262199 mlist .internal_name (), e )
200+ self ._unmark_message_processed (msgid )
201+ return False
202+
203+ # Validate message type first
204+ msg , success = self ._validate_message (msg , msgdata )
205+ if not success :
206+ mailman_log ('error' , 'Message validation failed for bounce message' )
207+ self ._unmark_message_processed (msgid )
208+ return False
209+
210+ # Validate message headers
211+ if not msg .get ('message-id' ):
212+ mailman_log ('error' , 'Message missing Message-ID header' )
213+ self ._unmark_message_processed (msgid )
263214 return False
264215
265216 try :
@@ -269,13 +220,15 @@ def _dispose(self, mlist, msg, msgdata):
269220
270221 # Process the bounce
271222 if not self ._register_bounces (mlist .internal_name (), msg , msgdata ):
223+ self ._unmark_message_processed (msgid )
272224 return False
273225
274226 # Queue the bounce for further processing
275227 try :
276228 self ._queue_bounces (mlist .internal_name (), msg , msgdata )
277229 except Exception as e :
278230 mailman_log ('error' , 'Error queueing bounces: %s' , e )
231+ self ._unmark_message_processed (msgid )
279232 return False
280233
281234 # Log successful completion
@@ -294,6 +247,9 @@ def _dispose(self, mlist, msg, msgdata):
294247 mailman_log ('error' , ' Message type: %s' , type (msg ).__name__ )
295248 mailman_log ('error' , ' Message data: %s' , str (msgdata ))
296249 mailman_log ('error' , 'Traceback:\n %s' , traceback .format_exc ())
250+
251+ # Remove from processed messages on error
252+ self ._unmark_message_processed (msgid )
297253 return False
298254
299255 _doperiodic = BounceMixin ._doperiodic
0 commit comments