Skip to content

Commit 824be77

Browse files
committed
update
1 parent 38cd632 commit 824be77

3 files changed

Lines changed: 249 additions & 381 deletions

File tree

Mailman/Queue/BounceRunner.py

Lines changed: 70 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,10 @@
2525
import email
2626
from email.utils import getaddresses
2727
from email.iterators import body_line_iterator
28+
2829
from email.mime.text import MIMEText
2930
from email.mime.message import MIMEMessage
3031
from email.utils import parseaddr
31-
import threading
32-
import traceback
3332

3433
from Mailman import mm_cfg
3534
from 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

Comments
 (0)