Skip to content

Commit 9b7711e

Browse files
committed
update
1 parent 937c9b5 commit 9b7711e

5 files changed

Lines changed: 381 additions & 495 deletions

File tree

Mailman/Queue/BounceRunner.py

Lines changed: 95 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
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

2023
from builtins import object, str
2124
import os
@@ -25,6 +28,9 @@
2528
import email
2629
from email.utils import getaddresses
2730
from email.iterators import body_line_iterator
31+
import traceback
32+
from io import StringIO
33+
import sys
2834

2935
from email.mime.text import MIMEText
3036
from email.mime.message import MIMEMessage
@@ -33,58 +39,67 @@
3339
from Mailman import mm_cfg
3440
from Mailman import Utils
3541
from Mailman import LockFile
42+
from Mailman import Errors
43+
from Mailman import i18n
3644
from Mailman.Errors import NotAMemberError
3745
from Mailman.Message import Message, UserNotification
3846
from Mailman.Bouncer import _BounceInfo
3947
from Mailman.Bouncers import BouncerAPI
4048
from Mailman.Queue.Runner import Runner
4149
from Mailman.Queue.sbcache import get_switchboard
42-
from Mailman.Logging.Syslog import mailman_log
50+
from Mailman.Logging.Syslog import syslog
4351
from Mailman.i18n import _
4452

4553
COMMASPACE = ', '
4654

4755
class 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\nTraceback:\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\nTraceback:\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\nTraceback:\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\nTraceback:\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\nTraceback:\n%s',
196-
str(e), traceback.format_exc())
161+
syslog('error', 'BounceRunner: Initialization failed: %s\nTraceback:\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\nTraceback:\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\nTraceback:\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\nTraceback:\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\nTraceback:\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\nTraceback:\n%s',
225+
syslog('error', 'BounceRunner._extract_bounce_info: Error extracting bounce information: %s\nTraceback:\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\nTraceback:\n%s',
236+
syslog('error', 'BounceRunner: Cleanup failed: %s\nTraceback:\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

Comments
 (0)