@@ -351,73 +351,122 @@ def _validate_message(self, msg, msgdata):
351351 return msg , False
352352
353353 def _dispose (self , mlist , msg , msgdata ):
354- # Validate message type first
355- msg , success = self ._validate_message (msg , msgdata )
356- if not success :
357- mailman_log ('error' , 'Message validation failed for command message' )
358- return False
359-
360- # Get the list name from the mlist object
354+ """Process a command message.
355+
356+ Args:
357+ mlist: The MailList instance this message is destined for
358+ msg: The Message object representing the message
359+ msgdata: Dictionary of message metadata
360+
361+ Returns:
362+ bool: True if message should be requeued, False if processing is complete
363+ """
364+ msgid = msg .get ('message-id' , 'n/a' )
365+ filebase = msgdata .get ('_filebase' , 'unknown' )
366+
367+ # Ensure we have a MailList object
368+ if isinstance (mlist , str ):
369+ try :
370+ mlist = get_maillist ()(mlist , lock = 0 )
371+ should_unlock = True
372+ except Errors .MMUnknownListError :
373+ syslog ('error' , 'CommandRunner: Unknown list %s' , mlist )
374+ self ._shunt .enqueue (msg , msgdata )
375+ return False
376+ else :
377+ should_unlock = False
378+
361379 try :
362- listname = mlist .internal_name () if hasattr (mlist , 'internal_name' ) else str (mlist )
363- MailList = get_maillist ()
364- mlist_obj = MailList (listname , lock = False )
365- except Errors .MMListError as e :
366- mailman_log ('error' , 'Failed to get MailList object for %s: %s' , listname , str (e ))
367- return False
368-
369- # The policy here is similar to the Replybot policy. If a message has
370- # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard
371- # it to prevent replybot response storms.
372- precedence = msg .get ('precedence' , '' ).lower ()
373- ack = msg .get ('x-ack' , '' ).lower ()
374- if ack != 'yes' and precedence in ('bulk' , 'junk' , 'list' ):
375- syslog ('vette' , 'Precedence: %s message discarded by: %s' ,
376- precedence , mlist_obj .GetRequestEmail ())
377- return False
380+ syslog ('debug' , 'CommandRunner._dispose: Starting to process command message %s (file: %s) for list %s' ,
381+ msgid , filebase , mlist .internal_name ())
382+
383+ # Check retry delay and duplicate processing
384+ if not self ._check_retry_delay (msgid , filebase ):
385+ syslog ('debug' , 'CommandRunner._dispose: Message %s failed retry delay check, skipping' , msgid )
386+ return True
378387
379- # Lock the list before any operations
380- try :
381- mlist_obj .Lock (timeout = mm_cfg .LIST_LOCK_TIMEOUT )
382- except LockFile .TimeOutError :
383- # Oh well, try again later
384- return True
388+ # Validate message type first
389+ msg , success = self ._validate_message (msg , msgdata )
390+ if not success :
391+ syslog ('error' , 'CommandRunner._dispose: Message validation failed for message %s' , msgid )
392+ msgdata ['_validation_failure' ] = 'Missing required headers'
393+ self ._shunt .enqueue (msg , msgdata )
394+ return False
385395
386- try :
387- # Do replybot for commands
388- mlist_obj . Load ()
389- Replybot = get_replybot ()
390- Replybot . process ( mlist_obj , msg , msgdata )
391- if mlist_obj . autorespond_requests == 1 :
392- syslog ('vette' , 'replied and discard' )
393- # w/discard
396+ # The policy here is similar to the Replybot policy. If a message has
397+ # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard
398+ # it to prevent replybot response storms.
399+ precedence = msg . get ( 'precedence' , '' ). lower ()
400+ ack = msg . get ( 'x-ack' , '' ). lower ( )
401+ if ack != 'yes' and precedence in ( 'bulk' , 'junk' , 'list' ) :
402+ syslog ('vette' , 'Precedence: %s message discarded by: %s' ,
403+ precedence , mlist . GetRequestEmail ())
394404 return False
395405
396- # Now craft the response
397- res = Results (mlist_obj , msg , msgdata )
398- # This message will have been delivered to one of mylist-request,
399- # mylist-join, or mylist-leave, and the message metadata will contain
400- # a key to which one was used.
401- ret = BADCMD
402- if msgdata .get ('torequest' , False ):
403- ret = res .process ()
404- elif msgdata .get ('tojoin' , False ):
405- ret = res .do_command ('join' )
406- elif msgdata .get ('toleave' , False ):
407- ret = res .do_command ('leave' )
408- elif msgdata .get ('toconfirm' , False ):
409- mo = re .match (mm_cfg .VERP_CONFIRM_REGEXP , msg .get ('to' , '' ), re .IGNORECASE )
410- if mo :
411- ret = res .do_command ('confirm' , (mo .group ('cookie' ),))
412- if ret == BADCMD and mm_cfg .DISCARD_MESSAGE_WITH_NO_COMMAND :
413- syslog ('vette' ,
414- 'No command, message discarded, msgid: %s' ,
415- msg .get ('message-id' , 'n/a' ))
416- else :
417- res .send_response ()
418- mlist_obj .Save ()
406+ # Lock the list before any operations
407+ try :
408+ mlist .Lock (timeout = mm_cfg .LIST_LOCK_TIMEOUT )
409+ except LockFile .TimeOutError :
410+ # Oh well, try again later
411+ return True
412+
413+ try :
414+ # Check if list is temporarily unavailable
415+ try :
416+ mlist .Load ()
417+ except Errors .MMCorruptListDatabaseError as e :
418+ syslog ('error' , 'CommandRunner._dispose: List %s is temporarily unavailable: %s' ,
419+ mlist .internal_name (), str (e ))
420+ return True
421+ except Exception as e :
422+ syslog ('error' , 'CommandRunner._dispose: Error loading list %s: %s' ,
423+ mlist .internal_name (), str (e ))
424+ return True
425+
426+ # Do replybot for commands
427+ Replybot = get_replybot ()
428+ Replybot .process (mlist , msg , msgdata )
429+ if mlist .autorespond_requests == 1 :
430+ syslog ('vette' , 'replied and discard' )
431+ # w/discard
432+ return False
433+
434+ # Now craft the response
435+ res = Results (mlist , msg , msgdata )
436+ # This message will have been delivered to one of mylist-request,
437+ # mylist-join, or mylist-leave, and the message metadata will contain
438+ # a key to which one was used.
439+ ret = BADCMD
440+ if msgdata .get ('torequest' , False ):
441+ ret = res .process ()
442+ elif msgdata .get ('tojoin' , False ):
443+ ret = res .do_command ('join' )
444+ elif msgdata .get ('toleave' , False ):
445+ ret = res .do_command ('leave' )
446+ elif msgdata .get ('toconfirm' , False ):
447+ mo = re .match (mm_cfg .VERP_CONFIRM_REGEXP , msg .get ('to' , '' ), re .IGNORECASE )
448+ if mo :
449+ ret = res .do_command ('confirm' , (mo .group ('cookie' ),))
450+ if ret == BADCMD and mm_cfg .DISCARD_MESSAGE_WITH_NO_COMMAND :
451+ syslog ('vette' ,
452+ 'No command, message discarded, msgid: %s' ,
453+ msg .get ('message-id' , 'n/a' ))
454+ return False
455+ else :
456+ res .send_response ()
457+ mlist .Save ()
458+ return False
459+ finally :
460+ mlist .Unlock ()
461+
462+ except Exception as e :
463+ syslog ('error' , 'CommandRunner._dispose: Error processing command message %s: %s\n Traceback:\n %s' ,
464+ msgid , str (e ), traceback .format_exc ())
465+ self ._shunt .enqueue (msg , msgdata )
466+ return False
419467 finally :
420- mlist_obj .Unlock ()
468+ if should_unlock :
469+ mlist .Unlock ()
421470
422471 def _oneloop (self ):
423472 """Process one batch of messages from the command queue."""
@@ -462,19 +511,19 @@ def _oneloop(self):
462511 self ._shunt .enqueue (msg , msgdata )
463512 continue
464513
465- # Validate message
466- msg , success = self ._validate_message (msg , msgdata )
467- if not success :
468- syslog ('error' , 'CommandRunner._oneloop: Message validation failed for %s' , filebase )
469- continue
470-
471514 # Process message
472515 try :
473- self ._dispose (mlist , msg , msgdata )
516+ success = self ._dispose (mlist , msg , msgdata )
517+ if not success :
518+ # If _dispose returns False, the message was shunted or discarded
519+ # Remove it from the queue
520+ self ._switchboard .finish (filebase )
474521 except Exception as e :
475522 syslog ('error' , 'CommandRunner._oneloop: Error processing message %s: %s' ,
476523 msg .get ('message-id' , 'n/a' ), str (e ))
477524 self ._shunt .enqueue (msg , msgdata )
525+ # Remove the message from the queue after shunting
526+ self ._switchboard .finish (filebase )
478527 except Exception as e :
479528 syslog ('error' , 'CommandRunner._oneloop: Error processing file %s: %s' , filebase , str (e ))
480529 except Exception as e :
0 commit comments