Pipeline runner crashes with AttributeError when member.preferences is None during roster iteration

Summary

The pipeline runner crashes with AttributeError: 'NoneType' object has no attribute 'delivery_mode' when iterating the member roster to build the recipient list. This causes the entire mailing to be shunted, preventing delivery to all recipients.

Version

GNU Mailman 3.3.9

Steps to Reproduce

This is a race condition that occurs under the following circumstances:

  1. A message is approved for delivery to a large mailing list (70k+ members)
  2. The pipeline runner begins iterating the member roster via member_recipients handler
  3. Concurrently, the bounce runner processes a bounce for a member and removes them (or deletes their preferences record)
  4. The pipeline runner encounters the now-orphaned member record with preferences = None
  5. The Member._lookup() method calls getattr(self.preferences, preference) which raises AttributeError

Traceback

Uncaught runner exception: 'NoneType' object has no attribute 'delivery_mode'
Traceback (most recent call last):
  File "/usr/lib/python3.11/site-packages/mailman/core/runner.py", line 179, in _one_iteration
    self._process_one_file(msg, msgdata)
  File "/usr/lib/python3.11/site-packages/mailman/core/runner.py", line 272, in _process_one_file
    keepqueued = self._dispose(mlist, msg, msgdata)
  File "/usr/lib/python3.11/site-packages/mailman/runners/pipeline.py", line 37, in _dispose
    process(mlist, msg, msgdata, pipeline)
  File "/usr/lib/python3.11/site-packages/mailman/core/pipelines.py", line 53, in process
    handler.process(mlist, msg, msgdata)
  File "/usr/lib/python3.11/site-packages/mailman/handlers/member_recipients.py", line 84, in process
    recipients = set(member.address.email
  File "/usr/lib/python3.11/site-packages/mailman/handlers/member_recipients.py", line 84, in <genexpr>
    recipients = set(member.address.email
  File "/usr/lib/python3.11/site-packages/mailman/model/roster.py", line 247, in members
    yield from self._get_members(DeliveryMode.regular)
  File "/usr/lib/python3.11/site-packages/mailman/model/roster.py", line 234, in _get_members
    if member.delivery_mode in delivery_modes:
  File "/usr/lib/python3.11/site-packages/mailman/model/member.py", line 217, in delivery_mode
    return self._lookup('delivery_mode')
  File "/usr/lib/python3.11/site-packages/mailman/model/member.py", line 176, in _lookup
    pref = getattr(self.preferences, preference)
AttributeError: 'NoneType' object has no attribute 'delivery_mode'

Impact

  • The entire mailing is shunted and no recipients receive the message
  • On high-volume lists (70k+ members), this can happen frequently due to the long iteration time increasing the window for concurrent modifications
  • The affected member is typically already removed by the time an administrator investigates, making the issue appear transient

Evidence of Race Condition

  • The member that triggered the crash was no longer a member when checked after the incident
  • The crash occurred at the same time as bounce processing activity
  • The issue is not reproducible on demand but occurs periodically during large list deliveries

Possible Fix

In src/mailman/model/member.py, the _lookup method should handle self.preferences being None gracefully:

def _lookup(self, preference, default=None):
    if self.preferences is None:
        log.warning(
            'Member %s on list %s has no preferences record, '
            'using default for %s',
            self.address.email if self.address else 'unknown',
            self.list_id, preference)
        return default
    pref = getattr(self.preferences, preference)
    if pref is not None:
        return pref
    # ... rest of method unchanged

This allows the roster iteration to skip the affected member rather than crashing the entire pipeline. The member will be excluded from the recipient list for that send (which is acceptable since they are in the process of being removed anyway).