summaryrefslogtreecommitdiff
path: root/src/mailman/queue
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/queue')
-rw-r--r--src/mailman/queue/__init__.py483
-rw-r--r--src/mailman/queue/archive.py89
-rw-r--r--src/mailman/queue/bounce.py80
-rw-r--r--src/mailman/queue/command.py219
-rw-r--r--src/mailman/queue/digest.py368
-rw-r--r--src/mailman/queue/docs/OVERVIEW.txt80
-rw-r--r--src/mailman/queue/docs/archiver.txt35
-rw-r--r--src/mailman/queue/docs/command.txt289
-rw-r--r--src/mailman/queue/docs/digester.txt604
-rw-r--r--src/mailman/queue/docs/incoming.txt263
-rw-r--r--src/mailman/queue/docs/lmtp.txt313
-rw-r--r--src/mailman/queue/docs/news.txt161
-rw-r--r--src/mailman/queue/docs/outgoing.txt413
-rw-r--r--src/mailman/queue/docs/rest.txt25
-rw-r--r--src/mailman/queue/docs/runner.txt73
-rw-r--r--src/mailman/queue/docs/switchboard.txt187
-rw-r--r--src/mailman/queue/incoming.py66
-rw-r--r--src/mailman/queue/lmtp.py236
-rw-r--r--src/mailman/queue/maildir.py193
-rw-r--r--src/mailman/queue/news.py167
-rw-r--r--src/mailman/queue/outgoing.py160
-rw-r--r--src/mailman/queue/pipeline.py35
-rw-r--r--src/mailman/queue/rest.py58
-rw-r--r--src/mailman/queue/retry.py45
-rw-r--r--src/mailman/queue/tests/__init__.py0
-rw-r--r--src/mailman/queue/tests/test_bounce.py236
-rw-r--r--src/mailman/queue/tests/test_outgoing.py549
-rw-r--r--src/mailman/queue/virgin.py39
28 files changed, 0 insertions, 5466 deletions
diff --git a/src/mailman/queue/__init__.py b/src/mailman/queue/__init__.py
deleted file mode 100644
index 8abc5e9a6..000000000
--- a/src/mailman/queue/__init__.py
+++ /dev/null
@@ -1,483 +0,0 @@
-# Copyright (C) 2001-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Queing and dequeuing message/metadata pickle files.
-
-Messages are represented as email.message.Message objects (or an instance ofa
-subclass). Metadata is represented as a Python dictionary. For every
-message/metadata pair in a queue, a single file containing two pickles is
-written. First, the message is written to the pickle, then the metadata
-dictionary is written.
-"""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Runner',
- 'Switchboard',
- ]
-
-
-import os
-import time
-import email
-import pickle
-import cPickle
-import hashlib
-import logging
-import traceback
-
-from cStringIO import StringIO
-from lazr.config import as_boolean, as_timedelta
-from zope.component import getUtility
-from zope.interface import implements
-
-from mailman.config import config
-from mailman.core.i18n import _
-from mailman.email.message import Message
-from mailman.interfaces.languages import ILanguageManager
-from mailman.interfaces.listmanager import IListManager
-from mailman.interfaces.runner import IRunner
-from mailman.interfaces.switchboard import ISwitchboard
-from mailman.utilities.filesystem import makedirs
-from mailman.utilities.string import expand
-
-
-# 20 bytes of all bits set, maximum hashlib.sha.digest() value
-shamax = 0xffffffffffffffffffffffffffffffffffffffffL
-
-# Small increment to add to time in case two entries have the same time. This
-# prevents skipping one of two entries with the same time until the next pass.
-DELTA = .0001
-DOT = '.'
-# We count the number of times a file has been moved to .bak and recovered.
-# In order to prevent loops and a message flood, when the count reaches this
-# value, we move the file to the bad queue as a .psv.
-MAX_BAK_COUNT = 3
-
-elog = logging.getLogger('mailman.error')
-dlog = logging.getLogger('mailman.debug')
-
-
-
-class Switchboard:
- implements(ISwitchboard)
-
- @staticmethod
- def initialize():
- """Initialize the global switchboards for input/output."""
- for conf in config.qrunner_configs:
- name = conf.name.split('.')[-1]
- assert name not in config.switchboards, (
- 'Duplicate qrunner name: {0}'.format(name))
- substitutions = config.paths
- substitutions['name'] = name
- path = expand(conf.path, substitutions)
- config.switchboards[name] = Switchboard(name, path)
-
- def __init__(self, name, queue_directory,
- slice=None, numslices=1, recover=False):
- """Create a switchboard object.
-
- :param name: The queue name.
- :type name: str
- :param queue_directory: The queue directory.
- :type queue_directory: str
- :param slice: The slice number for this switchboard, or None. If not
- None, it must be [0..`numslices`).
- :type slice: int or None
- :param numslices: The total number of slices to split this queue
- directory into. It must be a power of 2.
- :type numslices: int
- :param recover: True if backup files should be recovered.
- :type recover: bool
- """
- assert (numslices & (numslices - 1)) == 0, (
- 'Not a power of 2: {0}'.format(numslices))
- self.name = name
- self.queue_directory = queue_directory
- # If configured to, create the directory if it doesn't yet exist.
- if config.create_paths:
- makedirs(self.queue_directory, 0770)
- # Fast track for no slices
- self._lower = None
- self._upper = None
- # BAW: test performance and end-cases of this algorithm
- if numslices <> 1:
- self._lower = ((shamax + 1) * slice) / numslices
- self._upper = (((shamax + 1) * (slice + 1)) / numslices) - 1
- if recover:
- self.recover_backup_files()
-
- def enqueue(self, _msg, _metadata=None, **_kws):
- """See `ISwitchboard`."""
- if _metadata is None:
- _metadata = {}
- # Calculate the SHA hexdigest of the message to get a unique base
- # filename. We're also going to use the digest as a hash into the set
- # of parallel qrunner processes.
- data = _metadata.copy()
- data.update(_kws)
- listname = data.get('listname', '--nolist--')
- # Get some data for the input to the sha hash.
- now = time.time()
- if data.get('_plaintext'):
- protocol = 0
- msgsave = cPickle.dumps(str(_msg), protocol)
- else:
- protocol = pickle.HIGHEST_PROTOCOL
- msgsave = cPickle.dumps(_msg, protocol)
- # listname is unicode but the input to the hash function must be an
- # 8-bit string (eventually, a bytes object).
- hashfood = msgsave + listname.encode('utf-8') + repr(now)
- # Encode the current time into the file name for FIFO sorting. The
- # file name consists of two parts separated by a '+': the received
- # time for this message (i.e. when it first showed up on this system)
- # and the sha hex digest.
- rcvtime = data.setdefault('received_time', now)
- filebase = repr(rcvtime) + '+' + hashlib.sha1(hashfood).hexdigest()
- filename = os.path.join(self.queue_directory, filebase + '.pck')
- tmpfile = filename + '.tmp'
- # Always add the metadata schema version number
- data['version'] = config.QFILE_SCHEMA_VERSION
- # Filter out volatile entries. Use .keys() so that we can mutate the
- # dictionary during the iteration.
- for k in data.keys():
- if k.startswith('_'):
- del data[k]
- # We have to tell the dequeue() method whether to parse the message
- # object or not.
- data['_parsemsg'] = (protocol == 0)
- # Write to the pickle file the message object and metadata.
- with open(tmpfile, 'w') as fp:
- fp.write(msgsave)
- cPickle.dump(data, fp, protocol)
- fp.flush()
- os.fsync(fp.fileno())
- os.rename(tmpfile, filename)
- return filebase
-
- def dequeue(self, filebase):
- """See `ISwitchboard`."""
- # Calculate the filename from the given filebase.
- filename = os.path.join(self.queue_directory, filebase + '.pck')
- backfile = os.path.join(self.queue_directory, filebase + '.bak')
- # Read the message object and metadata.
- with open(filename) as fp:
- # Move the file to the backup file name for processing. If this
- # process crashes uncleanly the .bak file will be used to
- # re-instate the .pck file in order to try again.
- os.rename(filename, backfile)
- msg = cPickle.load(fp)
- data = cPickle.load(fp)
- if data.get('_parsemsg'):
- # Calculate the original size of the text now so that we won't
- # have to generate the message later when we do size restriction
- # checking.
- original_size = len(msg)
- msg = email.message_from_string(msg, Message)
- msg.original_size = original_size
- data['original_size'] = original_size
- return msg, data
-
- def finish(self, filebase, preserve=False):
- """See `ISwitchboard`."""
- bakfile = os.path.join(self.queue_directory, filebase + '.bak')
- try:
- if preserve:
- bad_dir = config.switchboards['bad'].queue_directory
- psvfile = os.path.join(bad_dir, filebase + '.psv')
- os.rename(bakfile, psvfile)
- else:
- os.unlink(bakfile)
- except EnvironmentError:
- elog.exception(
- 'Failed to unlink/preserve backup file: %s', bakfile)
-
- @property
- def files(self):
- """See `ISwitchboard`."""
- return self.get_files()
-
- def get_files(self, extension='.pck'):
- """See `ISwitchboard`."""
- times = {}
- lower = self._lower
- upper = self._upper
- for f in os.listdir(self.queue_directory):
- # By ignoring anything that doesn't end in .pck, we ignore
- # tempfiles and avoid a race condition.
- filebase, ext = os.path.splitext(f)
- if ext <> extension:
- continue
- when, digest = filebase.split('+', 1)
- # Throw out any files which don't match our bitrange. BAW: test
- # performance and end-cases of this algorithm. MAS: both
- # comparisons need to be <= to get complete range.
- if lower is None or (lower <= long(digest, 16) <= upper):
- key = float(when)
- while key in times:
- key += DELTA
- times[key] = filebase
- # FIFO sort
- return [times[key] for key in sorted(times)]
-
- def recover_backup_files(self):
- """See `ISwitchboard`."""
- # Move all .bak files in our slice to .pck. It's impossible for both
- # to exist at the same time, so the move is enough to ensure that our
- # normal dequeuing process will handle them. We keep count in
- # _bak_count in the metadata of the number of times we recover this
- # file. When the count reaches MAX_BAK_COUNT, we move the .bak file
- # to a .psv file in the bad queue.
- for filebase in self.get_files('.bak'):
- src = os.path.join(self.queue_directory, filebase + '.bak')
- dst = os.path.join(self.queue_directory, filebase + '.pck')
- fp = open(src, 'rb+')
- try:
- try:
- msg = cPickle.load(fp)
- data_pos = fp.tell()
- data = cPickle.load(fp)
- except Exception as error:
- # If unpickling throws any exception, just log and
- # preserve this entry
- elog.error('Unpickling .bak exception: %s\n'
- 'Preserving file: %s', error, filebase)
- self.finish(filebase, preserve=True)
- else:
- data['_bak_count'] = data.get('_bak_count', 0) + 1
- fp.seek(data_pos)
- if data.get('_parsemsg'):
- protocol = 0
- else:
- protocol = 1
- cPickle.dump(data, fp, protocol)
- fp.truncate()
- fp.flush()
- os.fsync(fp.fileno())
- if data['_bak_count'] >= MAX_BAK_COUNT:
- elog.error('.bak file max count, preserving file: %s',
- filebase)
- self.finish(filebase, preserve=True)
- else:
- os.rename(src, dst)
- finally:
- fp.close()
-
-
-
-class Runner:
- implements(IRunner)
-
- intercept_signals = True
-
- def __init__(self, name, slice=None):
- """Create a queue runner.
-
- :param slice: The slice number for this queue runner. This is passed
- directly to the underlying `ISwitchboard` object.
- :type slice: int or None
- """
- # Grab the configuration section.
- self.name = name
- section = getattr(config, 'qrunner.' + name)
- substitutions = config.paths
- substitutions['name'] = name
- self.queue_directory = expand(section.path, substitutions)
- numslices = int(section.instances)
- self.switchboard = Switchboard(
- name, self.queue_directory, slice, numslices, True)
- self.sleep_time = as_timedelta(section.sleep_time)
- # sleep_time is a timedelta; turn it into a float for time.sleep().
- self.sleep_float = (86400 * self.sleep_time.days +
- self.sleep_time.seconds +
- self.sleep_time.microseconds / 1.0e6)
- self.max_restarts = int(section.max_restarts)
- self.start = as_boolean(section.start)
- self._stop = False
-
- def __repr__(self):
- return '<{0} at {1:#x}>'.format(self.__class__.__name__, id(self))
-
- def stop(self):
- """See `IRunner`."""
- self._stop = True
-
- def run(self):
- """See `IRunner`."""
- # Start the main loop for this queue runner.
- try:
- while True:
- # Once through the loop that processes all the files in the
- # queue directory.
- filecnt = self._one_iteration()
- # Do the periodic work for the subclass.
- self._do_periodic()
- # If the stop flag is set, we're done.
- if self._stop:
- break
- # Give the runner an opportunity to snooze for a while, but
- # pass it the file count so it can decide whether to do more
- # work now or not.
- self._snooze(filecnt)
- except KeyboardInterrupt:
- pass
- finally:
- self._clean_up()
-
- def _one_iteration(self):
- """See `IRunner`."""
- me = self.__class__.__name__
- dlog.debug('[%s] starting oneloop', me)
- # List all the files in our queue directory. The switchboard is
- # guaranteed to hand us the files in FIFO order.
- files = self.switchboard.files
- for filebase in files:
- dlog.debug('[%s] processing filebase: %s', me, filebase)
- try:
- # Ask the switchboard for the message and metadata objects
- # associated with this queue file.
- msg, msgdata = self.switchboard.dequeue(filebase)
- except Exception as error:
- # This used to just catch email.Errors.MessageParseError, but
- # other problems can occur in message parsing, e.g.
- # ValueError, and exceptions can occur in unpickling too. We
- # don't want the runner to die, so we just log and skip this
- # entry, but preserve it for analysis.
- self._log(error)
- elog.error('Skipping and preserving unparseable message: %s',
- filebase)
- self.switchboard.finish(filebase, preserve=True)
- config.db.abort()
- continue
- try:
- dlog.debug('[%s] processing onefile', me)
- self._process_one_file(msg, msgdata)
- dlog.debug('[%s] finishing filebase: %s', me, filebase)
- self.switchboard.finish(filebase)
- except Exception as error:
- # All runners that implement _dispose() must guarantee that
- # exceptions are caught and dealt with properly. Still, there
- # may be a bug in the infrastructure, and we do not want those
- # to cause messages to be lost. Any uncaught exceptions will
- # cause the message to be stored in the shunt queue for human
- # intervention.
- self._log(error)
- # Put a marker in the metadata for unshunting.
- msgdata['whichq'] = self.switchboard.name
- # It is possible that shunting can throw an exception, e.g. a
- # permissions problem or a MemoryError due to a really large
- # message. Try to be graceful.
- try:
- shunt = config.switchboards['shunt']
- new_filebase = shunt.enqueue(msg, msgdata)
- elog.error('SHUNTING: %s', new_filebase)
- self.switchboard.finish(filebase)
- except Exception as error:
- # The message wasn't successfully shunted. Log the
- # exception and try to preserve the original queue entry
- # for possible analysis.
- self._log(error)
- elog.error(
- 'SHUNTING FAILED, preserving original entry: %s',
- filebase)
- self.switchboard.finish(filebase, preserve=True)
- config.db.abort()
- # Other work we want to do each time through the loop.
- dlog.debug('[%s] doing periodic', me)
- self._do_periodic()
- dlog.debug('[%s] checking short circuit', me)
- if self._short_circuit():
- dlog.debug('[%s] short circuiting', me)
- break
- dlog.debug('[%s] commiting', me)
- config.db.commit()
- dlog.debug('[%s] ending oneloop: %s', me, len(files))
- return len(files)
-
- def _process_one_file(self, msg, msgdata):
- """See `IRunner`."""
- # Do some common sanity checking on the message metadata. It's got to
- # be destined for a particular mailing list. This switchboard is used
- # to shunt off badly formatted messages. We don't want to just trash
- # them because they may be fixable with human intervention. Just get
- # them out of our sight.
- #
- # Find out which mailing list this message is destined for.
- missing = object()
- listname = msgdata.get('listname', missing)
- mlist = (None
- if listname is missing
- else getUtility(IListManager).get(unicode(listname)))
- if mlist is None:
- elog.error(
- '%s runner "%s" shunting message for missing list: %s',
- msg['message-id'], self.name,
- ('n/a' if listname is missing else listname))
- config.switchboards['shunt'].enqueue(msg, msgdata)
- return
- # Now process this message. We also want to set up the language
- # context for this message. The context will be the preferred
- # language for the user if the sender is a member of the list, or it
- # will be the list's preferred language. However, we must take
- # special care to reset the defaults, otherwise subsequent messages
- # may be translated incorrectly.
- if mlist is None:
- language_manager = getUtility(ILanguageManager)
- language = language_manager[config.mailman.default_language]
- elif msg.sender:
- member = mlist.members.get_member(msg.sender)
- language = (member.preferred_language
- if member is not None
- else mlist.preferred_language)
- else:
- language = mlist.preferred_language
- with _.using(language.code):
- msgdata['lang'] = language.code
- keepqueued = self._dispose(mlist, msg, msgdata)
- if keepqueued:
- self.switchboard.enqueue(msg, msgdata)
-
- def _log(self, exc):
- elog.error('Uncaught runner exception: %s', exc)
- s = StringIO()
- traceback.print_exc(file=s)
- elog.error('%s', s.getvalue())
-
- def _clean_up(self):
- """See `IRunner`."""
- pass
-
- def _dispose(self, mlist, msg, msgdata):
- """See `IRunner`."""
- raise NotImplementedError
-
- def _do_periodic(self):
- """See `IRunner`."""
- pass
-
- def _snooze(self, filecnt):
- """See `IRunner`."""
- if filecnt or self.sleep_float <= 0:
- return
- time.sleep(self.sleep_float)
-
- def _short_circuit(self):
- """See `IRunner`."""
- return self._stop
diff --git a/src/mailman/queue/archive.py b/src/mailman/queue/archive.py
deleted file mode 100644
index 99682f310..000000000
--- a/src/mailman/queue/archive.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# Copyright (C) 2000-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Archive queue runner."""
-
-__metaclass__ = type
-__all__ = [
- 'ArchiveRunner',
- ]
-
-
-import os
-import logging
-
-from datetime import datetime
-from email.utils import parsedate_tz, mktime_tz, formatdate
-from flufl.lock import Lock
-from lazr.config import as_timedelta
-
-from mailman.config import config
-from mailman.queue import Runner
-
-log = logging.getLogger('mailman.error')
-
-
-
-class ArchiveRunner(Runner):
- """The archive runner."""
-
- def _dispose(self, mlist, msg, msgdata):
- # Support clobber_date, i.e. setting the date in the archive to the
- # received date, not the (potentially bogus) Date: header of the
- # original message.
- clobber = False
- original_date = msg.get('date')
- received_time = formatdate(msgdata['received_time'])
- if not original_date:
- clobber = True
- elif int(config.archiver.pipermail.clobber_date_policy) == 1:
- clobber = True
- elif int(config.archiver.pipermail.clobber_date_policy) == 2:
- # What's the timestamp on the original message?
- timetup = parsedate_tz(original_date)
- now = datetime.now()
- try:
- if not timetup:
- clobber = True
- else:
- utc_timestamp = datetime.fromtimestamp(mktime_tz(timetup))
- date_skew = as_timedelta(
- config.archiver.pipermail.allowable_sane_date_skew)
- clobber = (abs(now - utc_timestamp) > date_skew)
- except (ValueError, OverflowError):
- # The likely cause of this is that the year in the Date: field
- # is horribly incorrect, e.g. (from SF bug # 571634):
- # Date: Tue, 18 Jun 0102 05:12:09 +0500
- # Obviously clobber such dates.
- clobber = True
- if clobber:
- del msg['date']
- del msg['x-original-date']
- msg['Date'] = received_time
- if original_date:
- msg['X-Original-Date'] = original_date
- # Always put an indication of when we received the message.
- msg['X-List-Received-Date'] = received_time
- # While a list archiving lock is acquired, archive the message.
- with Lock(os.path.join(mlist.data_path, 'archive.lck')):
- for archiver in config.archivers:
- # A problem in one archiver should not prevent other archivers
- # from running.
- try:
- archiver.archive_message(mlist, msg)
- except Exception:
- log.exception('Broken archiver: %s' % archiver.name)
diff --git a/src/mailman/queue/bounce.py b/src/mailman/queue/bounce.py
deleted file mode 100644
index a714f2669..000000000
--- a/src/mailman/queue/bounce.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# Copyright (C) 2001-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Bounce queue runner."""
-
-import logging
-
-from zope.component import getUtility
-
-from mailman.app.bounces import (
- ProbeVERP, StandardVERP, maybe_forward, scan_message)
-from mailman.interfaces.bounce import BounceContext, IBounceProcessor, Stop
-from mailman.queue import Runner
-
-
-COMMASPACE = ', '
-
-log = logging.getLogger('mailman.bounce')
-elog = logging.getLogger('mailman.error')
-
-
-
-class BounceRunner(Runner):
- """The bounce runner."""
-
- def __init__(self, name, slice=None):
- super(BounceRunner, self).__init__(name, slice)
- self._processor = getUtility(IBounceProcessor)
-
- def _dispose(self, mlist, msg, msgdata):
- # List isn't doing bounce processing?
- if not mlist.bounce_processing:
- return False
- # Try VERP detection first, since it's quick and easy
- context = BounceContext.normal
- addresses = StandardVERP().get_verp(mlist, msg)
- if addresses:
- # We have an address, but check if the message is non-fatal. It
- # will be non-fatal if the bounce scanner returns Stop. It will
- # return a set of addresses when the bounce is fatal, but we don't
- # care about those addresses, since we got it out of the VERP.
- if scan_message(mlist, msg) is Stop:
- return False
- else:
- # See if this was a probe message.
- addresses = ProbeVERP().get_verp(mlist, msg)
- if addresses:
- context = BounceContext.probe
- else:
- # That didn't give us anything useful, so try the old fashion
- # bounce matching modules.
- addresses = scan_message(mlist, msg)
- if addresses is Stop:
- # This is a recognized, non-fatal notice. Ignore it.
- return False
- # If that still didn't return us any useful addresses, then send it on
- # or discard it.
- if len(addresses) > 0:
- for address in addresses:
- self._processor.register(mlist, address, msg, context)
- else:
- log.info('Bounce message w/no discernable addresses: %s',
- msg.get('message-id', 'n/a'))
- maybe_forward(mlist, msg)
- # Dequeue this message.
- return False
diff --git a/src/mailman/queue/command.py b/src/mailman/queue/command.py
deleted file mode 100644
index ec3ec0089..000000000
--- a/src/mailman/queue/command.py
+++ /dev/null
@@ -1,219 +0,0 @@
-# Copyright (C) 1998-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""-request robot command queue runner."""
-
-__metaclass__ = type
-__all__ = [
- 'CommandRunner',
- 'Results',
- ]
-
-# See the delivery diagram in IncomingRunner.py. This module handles all
-# email destined for mylist-request, -join, and -leave. It no longer handles
-# bounce messages (i.e. -admin or -bounces), nor does it handle mail to
-# -owner.
-
-import re
-import logging
-
-from StringIO import StringIO
-from email.errors import HeaderParseError
-from email.header import decode_header, make_header
-from email.iterators import typed_subpart_iterator
-from zope.component import getUtility
-from zope.interface import implements
-
-from mailman.config import config
-from mailman.core.i18n import _
-from mailman.email.message import UserNotification
-from mailman.interfaces.command import ContinueProcessing, IEmailResults
-from mailman.interfaces.languages import ILanguageManager
-from mailman.queue import Runner
-
-
-NL = '\n'
-log = logging.getLogger('mailman.vette')
-
-
-
-class CommandFinder:
- """Generate commands from the content of a message."""
-
- def __init__(self, msg, msgdata, results):
- self.command_lines = []
- self.ignored_lines = []
- self.processed_lines = []
- # Depending on where the message was destined to, add some implicit
- # commands. For example, if this was sent to the -join or -leave
- # addresses, it's the same as if 'join' or 'leave' commands were sent
- # to the -request address.
- subaddress = msgdata.get('subaddress')
- if subaddress == 'join':
- self.command_lines.append('join')
- elif subaddress == 'leave':
- self.command_lines.append('leave')
- elif subaddress == 'confirm':
- mo = re.match(config.mta.verp_confirm_regexp, msg.get('to', ''))
- if mo:
- self.command_lines.append('confirm ' + mo.group('cookie'))
- # Extract the subject header and do RFC 2047 decoding.
- raw_subject = msg.get('subject', '')
- try:
- subject = unicode(make_header(decode_header(raw_subject)))
- # Mail commands must be ASCII.
- self.command_lines.append(subject.encode('us-ascii'))
- except (HeaderParseError, UnicodeError, LookupError):
- # The Subject header was unparseable or not ASCII, so just ignore
- # it.
- pass
- # Find the first text/plain part of the message.
- part = None
- for part in typed_subpart_iterator(msg, 'text', 'plain'):
- break
- if part is None or part is not msg:
- # Either there was no text/plain part or we ignored some
- # non-text/plain parts.
- print >> results, _('Ignoring non-text/plain MIME parts')
- if part is None:
- # There was no text/plain part to be found.
- return
- body = part.get_payload(decode=True)
- # text/plain parts better have string payloads.
- assert isinstance(body, basestring), 'Non-string decoded payload'
- lines = body.splitlines()
- # Use no more lines than specified
- max_lines = int(config.mailman.email_commands_max_lines)
- self.command_lines.extend(lines[:max_lines])
- self.ignored_lines.extend(lines[max_lines:])
-
- def __iter__(self):
- """Return each command line, split into commands and arguments.
-
- :return: 2-tuples where the first element is the command and the
- second element is a tuple of the arguments.
- """
- while self.command_lines:
- line = self.command_lines.pop(0)
- self.processed_lines.append(line)
- parts = line.strip().split()
- if len(parts) == 0:
- continue
- command = parts.pop(0)
- yield command, tuple(parts)
-
-
-
-class Results:
- """The email command results."""
-
- implements(IEmailResults)
-
- def __init__(self):
- self._output = StringIO()
- print >> self._output, _("""\
-The results of your email command are provided below.
-""")
-
- def write(self, text):
- self._output.write(text)
-
- def __unicode__(self):
- value = self._output.getvalue()
- assert isinstance(value, unicode), 'Not a unicode: %r' % value
- return value
-
-
-
-class CommandRunner(Runner):
- """The email command runner."""
-
- def _dispose(self, mlist, msg, msgdata):
- message_id = msg.get('message-id', 'n/a')
- # The policy here is similar to the Replybot policy. If a message has
- # "Precedence: bulk|junk|list" and no "X-Ack: yes" header, we discard
- # the command message.
- precedence = msg.get('precedence', '').lower()
- ack = msg.get('x-ack', '').lower()
- if ack <> 'yes' and precedence in ('bulk', 'junk', 'list'):
- log.info('%s Precedence: %s message discarded by: %s',
- message_id, precedence, mlist.request_address)
- return False
- # Do replybot for commands.
- replybot = config.handlers['replybot']
- replybot.process(mlist, msg, msgdata)
- if mlist.autorespond_requests == 1:
- # Respond and discard.
- log.info('%s -request message replied and discard', message_id)
- return False
- # Now craft the response and process the command lines.
- results = Results()
- # Include just a few key pieces of information from the original: the
- # sender, date, and message id.
- print >> results, _('- Original message details:')
- subject = msg.get('subject', 'n/a')
- date = msg.get('date', 'n/a')
- from_ = msg.get('from', 'n/a')
- print >> results, _(' From: $from_')
- print >> results, _(' Subject: $subject')
- print >> results, _(' Date: $date')
- print >> results, _(' Message-ID: $message_id')
- print >> results, _('\n- Results:')
- finder = CommandFinder(msg, msgdata, results)
- for command_name, arguments in finder:
- command = config.commands.get(command_name)
- if command is None:
- print >> results, _('No such command: $command_name')
- else:
- status = command.process(
- mlist, msg, msgdata, arguments, results)
- assert status in ContinueProcessing, (
- 'Invalid status: %s' % status)
- if status == ContinueProcessing.no:
- break
- # All done. Strip blank lines and send the response.
- lines = filter(None, (line.strip() for line in finder.command_lines))
- if len(lines) > 0:
- print >> results, _('\n- Unprocessed:')
- for line in lines:
- print >> results, line
- lines = filter(None, (line.strip() for line in finder.ignored_lines))
- if len(lines) > 0:
- print >> results, _('\n- Ignored:')
- for line in lines:
- print >> results, line
- print >> results, _('\n- Done.')
- # Send a reply, but do not attach the original message. This is a
- # compromise because the original message is often helpful in tracking
- # down problems, but it's also a vector for backscatter spam.
- language = getUtility(ILanguageManager)[msgdata['lang']]
- reply = UserNotification(msg.sender, mlist.bounces_address,
- _('The results of your email commands'),
- lang=language)
- # Find a charset for the response body. Try ascii first, then
- # latin-1 and finally falling back to utf-8.
- reply_body = unicode(results)
- for charset in ('us-ascii', 'latin-1'):
- try:
- reply_body.encode(charset)
- break
- except UnicodeError:
- pass
- else:
- charset = 'utf-8'
- reply.set_payload(reply_body, charset=charset)
- reply.send(mlist)
diff --git a/src/mailman/queue/digest.py b/src/mailman/queue/digest.py
deleted file mode 100644
index 075335158..000000000
--- a/src/mailman/queue/digest.py
+++ /dev/null
@@ -1,368 +0,0 @@
-# Copyright (C) 2009-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Digest queue runner."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'DigestRunner',
- ]
-
-
-import re
-
-# cStringIO doesn't support unicode.
-from StringIO import StringIO
-from contextlib import nested
-from copy import deepcopy
-from email.header import Header
-from email.message import Message
-from email.mime.message import MIMEMessage
-from email.mime.multipart import MIMEMultipart
-from email.mime.text import MIMEText
-from email.utils import formatdate, getaddresses, make_msgid
-
-from mailman.config import config
-from mailman.core.errors import DiscardMessage
-from mailman.core.i18n import _
-from mailman.interfaces.member import DeliveryMode, DeliveryStatus
-from mailman.pipeline.decorate import decorate
-from mailman.pipeline.scrubber import process as scrubber
-from mailman.queue import Runner
-from mailman.utilities.i18n import make
-from mailman.utilities.mailbox import Mailbox
-from mailman.utilities.string import oneline, wrap
-
-
-
-class Digester:
- """Base digester class."""
-
- def __init__(self, mlist, volume, digest_number):
- self._mlist = mlist
- self._charset = mlist.preferred_language.charset
- # This will be used in the Subject, so use $-strings.
- realname = mlist.real_name
- issue = digest_number
- self._digest_id = _('$realname Digest, Vol $volume, Issue $issue')
- self._subject = Header(self._digest_id,
- self._charset,
- header_name='Subject')
- self._message = self._make_message()
- self._message['From'] = mlist.request_address
- self._message['Subject'] = self._subject
- self._message['To'] = mlist.posting_address
- self._message['Reply-To'] = mlist.posting_address
- self._message['Date'] = formatdate(localtime=True)
- self._message['Message-ID'] = make_msgid()
- # In the rfc1153 digest, the masthead contains the digest boilerplate
- # plus any digest header. In the MIME digests, the masthead and
- # digest header are separate MIME subobjects. In either case, it's
- # the first thing in the digest, and we can calculate it now, so go
- # ahead and add it now.
- self._masthead = make('masthead.txt',
- mailing_list=mlist,
- real_name=mlist.real_name,
- got_list_email=mlist.posting_address,
- got_listinfo_url=mlist.script_url('listinfo'),
- got_request_email=mlist.request_address,
- got_owner_email=mlist.owner_address,
- )
- # Set things up for the table of contents.
- self._header = decorate(mlist, mlist.digest_header)
- self._toc = StringIO()
- print >> self._toc, _("Today's Topics:\n")
-
- def add_to_toc(self, msg, count):
- """Add a message to the table of contents."""
- subject = msg.get('subject', _('(no subject)'))
- subject = oneline(subject, in_unicode=True)
- # Don't include the redundant subject prefix in the toc
- mo = re.match('(re:? *)?({0})'.format(
- re.escape(self._mlist.subject_prefix)),
- subject, re.IGNORECASE)
- if mo:
- subject = subject[:mo.start(2)] + subject[mo.end(2):]
- # Take only the first author we find.
- username = ''
- addresses = getaddresses(
- [oneline(msg.get('from', ''), in_unicode=True)])
- if addresses:
- username = addresses[0][0]
- if not username:
- username = addresses[0][1]
- if username:
- username = ' ({0})'.format(username)
- lines = wrap('{0:2}. {1}'. format(count, subject), 65).split('\n')
- # See if the user's name can fit on the last line
- if len(lines[-1]) + len(username) > 70:
- lines.append(username)
- else:
- lines[-1] += username
- # Add this subject to the accumulating topics
- first = True
- for line in lines:
- if first:
- print >> self._toc, ' ', line
- first = False
- else:
- print >> self._toc, ' ', line.lstrip()
-
- def add_message(self, msg, count):
- """Add the message to the digest."""
- # We do not want all the headers of the original message to leak
- # through in the digest messages.
- keepers = {}
- for header in self._keepers:
- keepers[header] = msg.get_all(header, [])
- # Remove all the unkempt <wink> headers. Use .keys() to allow for
- # destructive iteration...
- for header in msg.keys():
- del msg[header]
- # ... and add them in the designated order.
- for header in self._keepers:
- for value in keepers[header]:
- msg[header] = value
- # Add some useful extra stuff.
- msg['Message'] = unicode(count)
-
-
-
-class MIMEDigester(Digester):
- """A MIME digester."""
-
- def __init__(self, mlist, volume, digest_number):
- super(MIMEDigester, self).__init__(mlist, volume, digest_number)
- masthead = MIMEText(self._masthead.encode(self._charset),
- _charset=self._charset)
- masthead['Content-Description'] = self._subject
- self._message.attach(masthead)
- # Add the optional digest header.
- if mlist.digest_header:
- header = MIMEText(self._header.encode(self._charset),
- _charset=self._charset)
- header['Content-Description'] = _('Digest Header')
- self._message.attach(header)
- # Calculate the set of headers we're to keep in the MIME digest.
- self._keepers = set(config.digests.mime_digest_keep_headers.split())
-
- def _make_message(self):
- return MIMEMultipart('mixed')
-
- def add_toc(self, count):
- """Add the table of contents."""
- toc_text = self._toc.getvalue()
- try:
- toc_part = MIMEText(toc_text.encode(self._charset),
- _charset=self._charset)
- except UnicodeError:
- toc_part = MIMEText(toc_text.encode('utf-8'), _charset='utf-8')
- toc_part['Content-Description']= _("Today's Topics ($count messages)")
- self._message.attach(toc_part)
-
- def add_message(self, msg, count):
- """Add the message to the digest."""
- # Make a copy of the message object, since the RFC 1153 processing
- # scrubs out attachments.
- self._message.attach(MIMEMessage(deepcopy(msg)))
-
- def finish(self):
- """Finish up the digest, producing the email-ready copy."""
- if self._mlist.digest_footer:
- footer_text = decorate(self._mlist, self._mlist.digest_footer)
- footer = MIMEText(footer_text.encode(self._charset),
- _charset=self._charset)
- footer['Content-Description'] = _('Digest Footer')
- self._message.attach(footer)
- # This stuff is outside the normal MIME goo, and it's what the old
- # MIME digester did. No one seemed to complain, probably because you
- # won't see it in an MUA that can't display the raw message. We've
- # never got complaints before, but if we do, just wax this. It's
- # primarily included for (marginally useful) backwards compatibility.
- self._message.postamble = _('End of ') + self._digest_id
- return self._message
-
-
-
-class RFC1153Digester(Digester):
- """A digester of the format specified by RFC 1153."""
-
- def __init__(self, mlist, volume, digest_number):
- super(RFC1153Digester, self).__init__(mlist, volume, digest_number)
- self._separator70 = '-' * 70
- self._separator30 = '-' * 30
- self._text = StringIO()
- print >> self._text, self._masthead
- print >> self._text
- # Add the optional digest header.
- if mlist.digest_header:
- print >> self._text, self._header
- print >> self._text
- # Calculate the set of headers we're to keep in the RFC1153 digest.
- self._keepers = set(config.digests.plain_digest_keep_headers.split())
-
- def _make_message(self):
- return Message()
-
- def add_toc(self, count):
- """Add the table of contents."""
- print >> self._text, self._toc.getvalue()
- print >> self._text
- print >> self._text, self._separator70
- print >> self._text
-
- def add_message(self, msg, count):
- """Add the message to the digest."""
- if count > 1:
- print >> self._text, self._separator30
- print >> self._text
- # Scrub attachements.
- try:
- msg = scrubber(self._mlist, msg)
- except DiscardMessage:
- print >> self._text, _('[Message discarded by content filter]')
- return
- # Each message section contains a few headers.
- for header in config.digests.plain_digest_keep_headers.split():
- if header in msg:
- value = oneline(msg[header], in_unicode=True)
- value = wrap('{0}: {1}'.format(header, value))
- value = '\n\t'.join(value.split('\n'))
- print >> self._text, value
- print >> self._text
- # Add the payload. If the decoded payload is empty, this may be a
- # multipart message. In that case, just stringify it.
- payload = msg.get_payload(decode=True)
- payload = (payload if payload else msg.as_string().split('\n\n', 1)[1])
- try:
- charset = msg.get_content_charset('us-ascii')
- payload = unicode(payload, charset, 'replace')
- except (LookupError, TypeError):
- # Unknown or empty charset.
- payload = unicode(payload, 'us-ascii', 'replace')
- print >> self._text, payload
- if not payload.endswith('\n'):
- print >> self._text
-
- def finish(self):
- """Finish up the digest, producing the email-ready copy."""
- if self._mlist.digest_footer:
- footer_text = decorate(self._mlist, self._mlist.digest_footer)
- # This is not strictly conformant RFC 1153. The trailer is only
- # supposed to contain two lines, i.e. the "End of ... Digest" line
- # and the row of asterisks. If this screws up MUAs, the solution
- # is to add the footer as the last message in the RFC 1153 digest.
- # I just hate the way that VM does that and I think it's confusing
- # to users, so don't do it unless there's a clamor.
- print >> self._text, self._separator30
- print >> self._text
- print >> self._text, footer_text
- print >> self._text
- # Add the sign-off.
- sign_off = _('End of ') + self._digest_id
- print >> self._text, sign_off
- print >> self._text, '*' * len(sign_off)
- # If the digest message can't be encoded by the list character set,
- # fall back to utf-8.
- text = self._text.getvalue()
- try:
- self._message.set_payload(text.encode(self._charset),
- charset=self._charset)
- except UnicodeError:
- self._message.set_payload(text.encode('utf-8'), charset='utf-8')
- return self._message
-
-
-
-class DigestRunner(Runner):
- """The digest queue runner."""
-
- def _dispose(self, mlist, msg, msgdata):
- """See `IRunner`."""
- volume = msgdata['volume']
- digest_number = msgdata['digest_number']
- with nested(Mailbox(msgdata['digest_path']),
- _.using(mlist.preferred_language.code)) as (mailbox,
- language_code):
- # Create the digesters.
- mime_digest = MIMEDigester(mlist, volume, digest_number)
- rfc1153_digest = RFC1153Digester(mlist, volume, digest_number)
- # Cruise through all the messages in the mailbox, first building
- # the table of contents and accumulating Subject: headers and
- # authors. The question really is whether it's better from a
- # performance and memory footprint to go through the mailbox once
- # and cache the messages in a list, or to cruise through the
- # mailbox twice. We'll do the latter, but it's a complete guess.
- count = None
- for count, (key, message) in enumerate(mailbox.iteritems(), 1):
- mime_digest.add_to_toc(message, count)
- rfc1153_digest.add_to_toc(message, count)
- assert count is not None, 'No digest messages?'
- # Add the table of contents.
- mime_digest.add_toc(count)
- rfc1153_digest.add_toc(count)
- # Cruise through the set of messages a second time, adding them to
- # the actual digest.
- for count, (key, message) in enumerate(mailbox.iteritems(), 1):
- mime_digest.add_message(message, count)
- rfc1153_digest.add_message(message, count)
- # Finish up the digests.
- mime = mime_digest.finish()
- rfc1153 = rfc1153_digest.finish()
- # Calculate the recipients lists
- mime_recipients = set()
- rfc1153_recipients = set()
- # When someone turns off digest delivery, they will get one last
- # digest to ensure that there will be no gaps in the messages they
- # receive.
- digest_members = set(mlist.digest_members.members)
- for member in digest_members:
- if member.delivery_status <> DeliveryStatus.enabled:
- continue
- # Send the digest to the case-preserved address of the digest
- # members.
- email_address = member.address.original_email
- if member.delivery_mode == DeliveryMode.plaintext_digests:
- rfc1153_recipients.add(email_address)
- elif member.delivery_mode == DeliveryMode.mime_digests:
- mime_recipients.add(email_address)
- else:
- raise AssertionError(
- 'Digest member "{0}" unexpected delivery mode: {1}'.format(
- email_address, member.delivery_mode))
- # Add also the folks who are receiving one last digest.
- for address, delivery_mode in mlist.last_digest_recipients:
- if delivery_mode == DeliveryMode.plaintext_digests:
- rfc1153_recipients.add(address.original_email)
- elif delivery_mode == DeliveryMode.mime_digests:
- mime_recipients.add(address.original_email)
- else:
- raise AssertionError(
- 'OLD recipient "{0}" unexpected delivery mode: {1}'.format(
- address, delivery_mode))
- # Send the digests to the virgin queue for final delivery.
- queue = config.switchboards['virgin']
- queue.enqueue(mime,
- recipients=mime_recipients,
- listname=mlist.fqdn_listname,
- isdigest=True)
- queue.enqueue(rfc1153,
- recipients=rfc1153_recipients,
- listname=mlist.fqdn_listname,
- isdigest=True)
diff --git a/src/mailman/queue/docs/OVERVIEW.txt b/src/mailman/queue/docs/OVERVIEW.txt
deleted file mode 100644
index 41ccc18c9..000000000
--- a/src/mailman/queue/docs/OVERVIEW.txt
+++ /dev/null
@@ -1,80 +0,0 @@
-==============
-Alias Overview
-==============
-
-A typical Mailman list exposes nine aliases which point to seven different
-wrapped scripts. E.g. for a list named ``mylist``, you'd have::
-
- mylist-bounces -> bounces
- mylist-confirm -> confirm
- mylist-join -> join (-subscribe is an alias)
- mylist-leave -> leave (-unsubscribe is an alias)
- mylist-owner -> owner
- mylist -> post
- mylist-request -> request
-
-``-request``, ``-join``, and ``-leave`` are a robot addresses; their sole
-purpose is to process emailed commands, although the latter two are hardcoded
-to subscription and unsubscription requests. ``-bounces`` is the automated
-bounce processor, and all messages to list members have their return address
-set to ``-bounces``. If the bounce processor fails to extract a bouncing
-member address, it can optionally forward the message on to the list owners.
-
-``-owner`` is for reaching a human operator with minimal list interaction
-(i.e. no bounce processing). ``-confirm`` is another robot address which
-processes replies to VERP-like confirmation notices.
-
-So delivery flow of messages look like this::
-
- joerandom ---> mylist ---> list members
- | |
- | |[bounces]
- | mylist-bounces <---+ <-------------------------------+
- | | |
- | +--->[internal bounce processing] |
- | ^ | |
- | | | [bounce found] |
- | [bounces *] +--->[register and discard] |
- | | | | |
- | | | |[*] |
- | [list owners] |[no bounce found] | |
- | ^ | | |
- | | | | |
- +-------> mylist-owner <--------+ | |
- | | |
- | data/owner-bounces.mbox <--[site list] <---+ |
- | |
- +-------> mylist-join--+ |
- | | |
- +------> mylist-leave--+ |
- | | |
- | v |
- +-------> mylist-request |
- | | |
- | +---> [command processor] |
- | | |
- +-----> mylist-confirm ----> +---> joerandom |
- | |
- |[bounces] |
- +----------------------+
-
-A person can send an email to the list address (for posting), the ``-owner``
-address (to reach the human operator), or the ``-confirm``, ``-join``,
-``-leave``, and ``-request`` mailbots. Message to the list address are then
-forwarded on to the list membership, with bounces directed to the -bounces
-address.
-
-[*] Messages sent to the ``-owner`` address are forwarded on to the list
-owner/moderators. All ``-owner`` destined messages have their bounces
-directed to the site list ``-bounces`` address, regardless of whether a human
-sent the message or the message was crafted internally. The intention here is
-that the site owners want to be notified when one of their list owners'
-addresses starts bouncing (yes, the will be automated in a future release).
-
-Any messages to site owners has their bounces directed to a special *loop
-killer* address, which just dumps the message into
-``data/owners-bounces.mbox``.
-
-Finally, message to any of the mailbots causes the requested action to be
-performed. Results notifications are sent to the author of the message, which
-all bounces pointing back to the -bounces address.
diff --git a/src/mailman/queue/docs/archiver.txt b/src/mailman/queue/docs/archiver.txt
deleted file mode 100644
index cdee449e1..000000000
--- a/src/mailman/queue/docs/archiver.txt
+++ /dev/null
@@ -1,35 +0,0 @@
-=========
-Archiving
-=========
-
-Mailman can archive to any number of archivers that adhere to the
-``IArchiver`` interface. By default, there's a Pipermail archiver.
-::
-
- >>> mlist = create_list('test@example.com')
- >>> transaction.commit()
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: test@example.com
- ... Subject: My first post
- ... Message-ID: <first>
- ...
- ... First post!
- ... """)
-
- >>> archiver_queue = config.switchboards['archive']
- >>> ignore = archiver_queue.enqueue(msg, {}, listname=mlist.fqdn_listname)
-
- >>> from mailman.queue.archive import ArchiveRunner
- >>> from mailman.testing.helpers import make_testable_runner
- >>> runner = make_testable_runner(ArchiveRunner)
- >>> runner.run()
-
- # The best we can do is verify some landmark exists. Let's use the
- # Pipermail pickle file exists.
- >>> listname = mlist.fqdn_listname
- >>> import os
- >>> os.path.exists(os.path.join(
- ... config.PUBLIC_ARCHIVE_FILE_DIR, listname, 'pipermail.pck'))
- True
diff --git a/src/mailman/queue/docs/command.txt b/src/mailman/queue/docs/command.txt
deleted file mode 100644
index c767e6f5f..000000000
--- a/src/mailman/queue/docs/command.txt
+++ /dev/null
@@ -1,289 +0,0 @@
-========================
-The command queue runner
-========================
-
-This queue runner's purpose is to process and respond to email commands.
-Commands are extensible using the Mailman plug-in system, but Mailman comes
-with a number of email commands out of the box. These are processed when a
-message is sent to the list's ``-request`` address.
-
- >>> mlist = create_list('test@example.com')
-
-
-A command in the Subject
-========================
-
-For example, the ``echo`` command simply echoes the original command back to
-the sender. The command can be in the ``Subject`` header.
-::
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: test-request@example.com
- ... Subject: echo hello
- ... Message-ID: <aardvark>
- ...
- ... """)
-
- >>> from mailman.inject import inject_message
- >>> inject_message(mlist, msg, switchboard='command')
- >>> from mailman.queue.command import CommandRunner
- >>> from mailman.testing.helpers import make_testable_runner
- >>> command = make_testable_runner(CommandRunner)
- >>> command.run()
-
-And now the response is in the ``virgin`` queue.
-
- >>> from mailman.queue import Switchboard
- >>> virgin_queue = config.switchboards['virgin']
- >>> len(virgin_queue.files)
- 1
- >>> from mailman.testing.helpers import get_queue_messages
- >>> item = get_queue_messages('virgin')[0]
- >>> print item.msg.as_string()
- Subject: The results of your email commands
- From: test-bounces@example.com
- To: aperson@example.com
- ...
- <BLANKLINE>
- The results of your email command are provided below.
- <BLANKLINE>
- - Original message details:
- From: aperson@example.com
- Subject: echo hello
- Date: ...
- Message-ID: <aardvark>
- <BLANKLINE>
- - Results:
- echo hello
- <BLANKLINE>
- - Done.
- <BLANKLINE>
- >>> dump_msgdata(item.msgdata)
- _parsemsg : False
- listname : test@example.com
- nodecorate : True
- recipients : set([u'aperson@example.com'])
- reduced_list_headers: True
- version : ...
-
-
-A command in the body
-=====================
-
-The command can also be found in the body of the message, as long as the
-message is plain text.
-::
-
- >>> msg = message_from_string("""\
- ... From: bperson@example.com
- ... To: test-request@example.com
- ... Message-ID: <bobcat>
- ...
- ... echo foo bar
- ... """)
-
- >>> inject_message(mlist, msg, switchboard='command')
- >>> command.run()
- >>> len(virgin_queue.files)
- 1
- >>> item = get_queue_messages('virgin')[0]
- >>> print item.msg.as_string()
- Subject: The results of your email commands
- From: test-bounces@example.com
- To: bperson@example.com
- ...
- Precedence: bulk
- <BLANKLINE>
- The results of your email command are provided below.
- <BLANKLINE>
- - Original message details:
- From: bperson@example.com
- Subject: n/a
- Date: ...
- Message-ID: <bobcat>
- <BLANKLINE>
- - Results:
- echo foo bar
- <BLANKLINE>
- - Done.
- <BLANKLINE>
-
-
-Implicit commands
-=================
-
-For some commands, specifically for joining and leaving a mailing list, there
-are email aliases that act like commands, even when there's nothing else in
-the ``Subject`` or body. For example, to join a mailing list, a user need
-only email the ``-join`` address or ``-subscribe`` address (the latter is
-deprecated).
-
-Because Dirk has never registered with Mailman before, he gets two responses.
-The first is a confirmation message so that Dirk can validate his email
-address, and the other is the results of his email command.
-::
-
- >>> msg = message_from_string("""\
- ... From: Dirk Person <dperson@example.com>
- ... To: test-join@example.com
- ...
- ... """)
-
- >>> inject_message(mlist, msg, switchboard='command', subaddress='join')
- >>> command.run()
- >>> len(virgin_queue.files)
- 2
-
- >>> def sortkey(item):
- ... return str(item.msg['subject'])
- >>> messages = sorted(get_queue_messages('virgin'), key=sortkey)
-
- >>> from mailman.interfaces.registrar import IRegistrar
- >>> from zope.component import getUtility
- >>> registrar = getUtility(IRegistrar)
- >>> for item in messages:
- ... subject = item.msg['subject']
- ... print 'Subject:', subject
- ... if 'confirm' in str(subject):
- ... token = str(subject).split()[1].strip()
- ... status = registrar.confirm(token)
- ... assert status, 'Confirmation failed'
- Subject: The results of your email commands
- Subject: confirm ...
-
-Similarly, to leave a mailing list, the user need only email the ``-leave`` or
-``-unsubscribe`` address (the latter is deprecated).
-::
-
- >>> msg = message_from_string("""\
- ... From: dperson@example.com
- ... To: test-leave@example.com
- ...
- ... """)
-
- >>> inject_message(mlist, msg, switchboard='command', subaddress='leave')
- >>> command.run()
- >>> len(virgin_queue.files)
- 1
- >>> item = get_queue_messages('virgin')[0]
- >>> print item.msg.as_string()
- Subject: The results of your email commands
- From: test-bounces@example.com
- To: dperson@example.com
- ...
- <BLANKLINE>
- The results of your email command are provided below.
- <BLANKLINE>
- - Original message details:
- From: dperson@example.com
- Subject: n/a
- Date: ...
- Message-ID: ...
- <BLANKLINE>
- - Results:
- Dirk Person <dperson@example.com> left test@example.com
- <BLANKLINE>
- - Done.
- <BLANKLINE>
-
-The ``-confirm`` address is also available as an implicit command.
-::
-
- >>> msg = message_from_string("""\
- ... From: dperson@example.com
- ... To: test-confirm+123@example.com
- ...
- ... """)
-
- >>> inject_message(mlist, msg, switchboard='command', subaddress='confirm')
- >>> command.run()
- >>> len(virgin_queue.files)
- 1
- >>> item = get_queue_messages('virgin')[0]
- >>> print item.msg.as_string()
- Subject: The results of your email commands
- From: test-bounces@example.com
- To: dperson@example.com
- ...
- <BLANKLINE>
- The results of your email command are provided below.
- <BLANKLINE>
- - Original message details:
- From: dperson@example.com
- Subject: n/a
- Date: ...
- Message-ID: ...
- <BLANKLINE>
- - Results:
- Confirmation token did not match
- <BLANKLINE>
- - Done.
- <BLANKLINE>
-
-
-Stopping command processing
-===========================
-
-The ``end`` command stops email processing, so that nothing following is
-looked at by the command queue.
-::
-
- >>> msg = message_from_string("""\
- ... From: cperson@example.com
- ... To: test-request@example.com
- ... Message-ID: <caribou>
- ...
- ... echo foo bar
- ... end ignored
- ... echo baz qux
- ... """)
-
- >>> inject_message(mlist, msg, switchboard='command')
- >>> command.run()
- >>> len(virgin_queue.files)
- 1
- >>> item = get_queue_messages('virgin')[0]
- >>> print item.msg.as_string()
- Subject: The results of your email commands
- ...
- <BLANKLINE>
- - Results:
- echo foo bar
- <BLANKLINE>
- - Unprocessed:
- echo baz qux
- <BLANKLINE>
- - Done.
- <BLANKLINE>
-
-The ``stop`` command is an alias for ``end``.
-::
-
- >>> msg = message_from_string("""\
- ... From: cperson@example.com
- ... To: test-request@example.com
- ... Message-ID: <caribou>
- ...
- ... echo foo bar
- ... stop ignored
- ... echo baz qux
- ... """)
-
- >>> inject_message(mlist, msg, switchboard='command')
- >>> command.run()
- >>> len(virgin_queue.files)
- 1
- >>> item = get_queue_messages('virgin')[0]
- >>> print item.msg.as_string()
- Subject: The results of your email commands
- ...
- <BLANKLINE>
- - Results:
- echo foo bar
- <BLANKLINE>
- - Unprocessed:
- echo baz qux
- <BLANKLINE>
- - Done.
- <BLANKLINE>
diff --git a/src/mailman/queue/docs/digester.txt b/src/mailman/queue/docs/digester.txt
deleted file mode 100644
index 285b2072a..000000000
--- a/src/mailman/queue/docs/digester.txt
+++ /dev/null
@@ -1,604 +0,0 @@
-=========
-Digesting
-=========
-
-Mailman crafts and sends digests by a separate digest queue runner process.
-This starts by a number of messages being posted to the mailing list.
-::
-
- >>> mlist = create_list('test@example.com')
- >>> mlist.digest_size_threshold = 0.6
- >>> mlist.volume = 1
- >>> mlist.next_digest_number = 1
-
- >>> from string import Template
- >>> process = config.handlers['to-digest'].process
-
- >>> def fill_digest():
- ... size = 0
- ... for i in range(1, 5):
- ... text = Template("""\
- ... From: aperson@example.com
- ... To: xtest@example.com
- ... Subject: Test message $i
- ... List-Post: <test@example.com>
- ...
- ... Here is message $i
- ... """).substitute(i=i)
- ... msg = message_from_string(text)
- ... process(mlist, msg, {})
- ... size += len(text)
- ... if size >= mlist.digest_size_threshold * 1024:
- ... break
-
- >>> fill_digest()
-
-The queue runner gets kicked off when a marker message gets dropped into the
-digest queue. The message metadata points to the mailbox file containing the
-messages to put in the digest.
-::
-
- >>> digestq = config.switchboards['digest']
- >>> len(digestq.files)
- 1
-
- >>> from mailman.testing.helpers import get_queue_messages
- >>> entry = get_queue_messages('digest')[0]
-
-The marker message is empty.
-
- >>> print entry.msg.as_string()
-
-But the message metadata has a reference to the digest file.
-::
-
- >>> dump_msgdata(entry.msgdata)
- _parsemsg : False
- digest_number: 1
- digest_path : .../lists/test@example.com/digest.1.1.mmdf
- listname : test@example.com
- version : 3
- volume : 1
-
- # Put the messages back in the queue for the runner to handle.
- >>> filebase = digestq.enqueue(entry.msg, entry.msgdata)
-
-There are 4 messages in the digest.
-
- >>> from mailman.utilities.mailbox import Mailbox
- >>> sum(1 for item in Mailbox(entry.msgdata['digest_path']))
- 4
-
-When the queue runner runs, it processes the digest mailbox, crafting both the
-plain text (RFC 1153) digest and the MIME digest.
-
- >>> from mailman.queue.digest import DigestRunner
- >>> from mailman.testing.helpers import make_testable_runner
- >>> runner = make_testable_runner(DigestRunner)
- >>> runner.run()
-
-The digest runner places both digests into the virgin queue for final
-delivery.
-
- >>> messages = get_queue_messages('virgin')
- >>> len(messages)
- 2
-
-The MIME digest is a multipart, and the RFC 1153 digest is the other one.
-::
-
- >>> def mime_rfc1153(messages):
- ... if messages[0].msg.is_multipart():
- ... return messages[0], messages[1]
- ... return messages[1], messages[0]
-
- >>> mime, rfc1153 = mime_rfc1153(messages)
-
-The MIME digest has lots of good stuff, all contained in the multipart.
-
- >>> print mime.msg.as_string()
- Content-Type: multipart/mixed; boundary="===============...=="
- MIME-Version: 1.0
- From: test-request@example.com
- Subject: Test Digest, Vol 1, Issue 1
- To: test@example.com
- Reply-To: test@example.com
- Date: ...
- Message-ID: ...
- <BLANKLINE>
- --===============...==
- Content-Type: text/plain; charset="us-ascii"
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
- Content-Description: Test Digest, Vol 1, Issue 1
- <BLANKLINE>
- Send Test mailing list submissions to
- test@example.com
- <BLANKLINE>
- To subscribe or unsubscribe via the World Wide Web, visit
- http://lists.example.com/listinfo/test@example.com
- or, via email, send a message with subject or body 'help' to
- test-request@example.com
- <BLANKLINE>
- You can reach the person managing the list at
- test-owner@example.com
- <BLANKLINE>
- When replying, please edit your Subject line so it is more specific
- than "Re: Contents of Test digest..."
- <BLANKLINE>
- --===============...==
- Content-Type: text/plain; charset="us-ascii"
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
- Content-Description: Today's Topics (4 messages)
- <BLANKLINE>
- Today's Topics:
- <BLANKLINE>
- 1. Test message 1 (aperson@example.com)
- 2. Test message 2 (aperson@example.com)
- 3. Test message 3 (aperson@example.com)
- 4. Test message 4 (aperson@example.com)
- <BLANKLINE>
- --===============...==
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
- From: aperson@example.com
- To: xtest@example.com
- Subject: Test message 1
- List-Post: <test@example.com>
- <BLANKLINE>
- Here is message 1
- <BLANKLINE>
- --===============...==
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
- From: aperson@example.com
- To: xtest@example.com
- Subject: Test message 2
- List-Post: <test@example.com>
- <BLANKLINE>
- Here is message 2
- <BLANKLINE>
- --===============...==
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
- From: aperson@example.com
- To: xtest@example.com
- Subject: Test message 3
- List-Post: <test@example.com>
- <BLANKLINE>
- Here is message 3
- <BLANKLINE>
- --===============...==
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
- From: aperson@example.com
- To: xtest@example.com
- Subject: Test message 4
- List-Post: <test@example.com>
- <BLANKLINE>
- Here is message 4
- <BLANKLINE>
- --===============...==
- Content-Type: text/plain; charset="us-ascii"
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
- Content-Description: Digest Footer
- <BLANKLINE>
- _______________________________________________
- Test mailing list
- test@example.com
- http://lists.example.com/listinfo/test@example.com
- <BLANKLINE>
- --===============...==--
-
-The RFC 1153 contains the digest in a single plain text message.
-
- >>> print rfc1153.msg.as_string()
- From: test-request@example.com
- Subject: Test Digest, Vol 1, Issue 1
- To: test@example.com
- Reply-To: test@example.com
- Date: ...
- Message-ID: ...
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- <BLANKLINE>
- Send Test mailing list submissions to
- test@example.com
- <BLANKLINE>
- To subscribe or unsubscribe via the World Wide Web, visit
- http://lists.example.com/listinfo/test@example.com
- or, via email, send a message with subject or body 'help' to
- test-request@example.com
- <BLANKLINE>
- You can reach the person managing the list at
- test-owner@example.com
- <BLANKLINE>
- When replying, please edit your Subject line so it is more specific
- than "Re: Contents of Test digest..."
- <BLANKLINE>
- <BLANKLINE>
- Today's Topics:
- <BLANKLINE>
- 1. Test message 1 (aperson@example.com)
- 2. Test message 2 (aperson@example.com)
- 3. Test message 3 (aperson@example.com)
- 4. Test message 4 (aperson@example.com)
- <BLANKLINE>
- <BLANKLINE>
- ----------------------------------------------------------------------
- <BLANKLINE>
- From: aperson@example.com
- Subject: Test message 1
- To: xtest@example.com
- Message-ID: ...
- <BLANKLINE>
- Here is message 1
- <BLANKLINE>
- ------------------------------
- <BLANKLINE>
- From: aperson@example.com
- Subject: Test message 2
- To: xtest@example.com
- Message-ID: ...
- <BLANKLINE>
- Here is message 2
- <BLANKLINE>
- ------------------------------
- <BLANKLINE>
- From: aperson@example.com
- Subject: Test message 3
- To: xtest@example.com
- Message-ID: ...
- <BLANKLINE>
- Here is message 3
- <BLANKLINE>
- ------------------------------
- <BLANKLINE>
- From: aperson@example.com
- Subject: Test message 4
- To: xtest@example.com
- Message-ID: ...
- <BLANKLINE>
- Here is message 4
- <BLANKLINE>
- ------------------------------
- <BLANKLINE>
- _______________________________________________
- Test mailing list
- test@example.com
- http://lists.example.com/listinfo/test@example.com
- <BLANKLINE>
- <BLANKLINE>
- End of Test Digest, Vol 1, Issue 1
- **********************************
- <BLANKLINE>
-
-
-Internationalized digests
-=========================
-
-When messages come in with a content-type character set different than that of
-the list's preferred language, recipients will get an internationalized
-digest. French is not enabled by default site-wide, so enable that now.
-::
-
- # Simulate the site administrator setting the default server language to
- # French in the configuration file. Without this, the English template
- # will be found and the masthead won't be translated.
- >>> config.push('french', """
- ... [mailman]
- ... default_language: fr
- ... """)
-
- >>> mlist.preferred_language = 'fr'
- >>> msg = message_from_string("""\
- ... From: aperson@example.org
- ... To: test@example.com
- ... Subject: =?iso-2022-jp?b?GyRCMGxIVhsoQg==?=
- ... MIME-Version: 1.0
- ... Content-Type: text/plain; charset=iso-2022-jp
- ... Content-Transfer-Encoding: 7bit
- ...
- ... \x1b$B0lHV\x1b(B
- ... """)
-
-Set the digest threshold to zero so that the digests will be sent immediately.
-
- >>> mlist.digest_size_threshold = 0
- >>> process(mlist, msg, {})
-
-The marker message is sitting in the digest queue.
-
- >>> len(digestq.files)
- 1
- >>> entry = get_queue_messages('digest')[0]
- >>> dump_msgdata(entry.msgdata)
- _parsemsg : False
- digest_number: 2
- digest_path : .../lists/test@example.com/digest.1.2.mmdf
- listname : test@example.com
- version : 3
- volume : 1
-
-The digest queue runner runs a loop, placing the two digests into the virgin
-queue.
-::
-
- # Put the messages back in the queue for the runner to handle.
- >>> filebase = digestq.enqueue(entry.msg, entry.msgdata)
- >>> runner.run()
- >>> messages = get_queue_messages('virgin')
- >>> len(messages)
- 2
-
-One of which is the MIME digest and the other of which is the RFC 1153 digest.
-
- >>> mime, rfc1153 = mime_rfc1153(messages)
-
-You can see that the digests contain a mix of French and Japanese.
-
- >>> print mime.msg.as_string()
- Content-Type: multipart/mixed; boundary="===============...=="
- MIME-Version: 1.0
- From: test-request@example.com
- Subject: Groupe Test, Vol. 1, Parution 2
- To: test@example.com
- Reply-To: test@example.com
- Date: ...
- Message-ID: ...
- <BLANKLINE>
- --===============...==
- Content-Type: text/plain; charset="iso-8859-1"
- MIME-Version: 1.0
- Content-Transfer-Encoding: quoted-printable
- Content-Description: Groupe Test, Vol. 1, Parution 2
- <BLANKLINE>
- Envoyez vos messages pour la liste Test =E0
- test@example.com
- <BLANKLINE>
- Pour vous (d=E9s)abonner par le web, consultez
- http://lists.example.com/listinfo/test@example.com
- <BLANKLINE>
- ou, par courriel, envoyez un message avec =AB=A0help=A0=BB dans le corps ou
- dans le sujet =E0
- test-request@example.com
- <BLANKLINE>
- Vous pouvez contacter l'administrateur de la liste =E0 l'adresse
- test-owner@example.com
- <BLANKLINE>
- Si vous r=E9pondez, n'oubliez pas de changer l'objet du message afin
- qu'il soit plus sp=E9cifique que =AB=A0Re: Contenu du groupe de Test...=A0=
- =BB
- --===============...==
- Content-Type: text/plain; charset="utf-8"
- MIME-Version: 1.0
- Content-Transfer-Encoding: base64
- Content-Description: Today's Topics (1 messages)
- <BLANKLINE>
- VGjDqG1lcyBkdSBqb3VyIDoKCiAgIDEuIOS4gOeVqiAoYXBlcnNvbkBleGFtcGxlLm9yZykK
- <BLANKLINE>
- --===============...==
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
- From: aperson@example.org
- To: test@example.com
- Subject: =?iso-2022-jp?b?GyRCMGxIVhsoQg==?=
- MIME-Version: 1.0
- Content-Type: text/plain; charset=iso-2022-jp
- Content-Transfer-Encoding: 7bit
- <BLANKLINE>
- $B0lHV(B
- <BLANKLINE>
- --===============...==
- Content-Type: text/plain; charset="iso-8859-1"
- MIME-Version: 1.0
- Content-Transfer-Encoding: quoted-printable
- Content-Description: =?utf-8?q?Pied_de_page_des_remises_group=C3=A9es?=
- <BLANKLINE>
- _______________________________________________
- Test mailing list
- test@example.com
- http://lists.example.com/listinfo/test@example.com
- <BLANKLINE>
- --===============...==--
-
-The RFC 1153 digest will be encoded in UTF-8 since it contains a mixture of
-French and Japanese characters.
-
- >>> print rfc1153.msg.as_string()
- From: test-request@example.com
- Subject: Groupe Test, Vol. 1, Parution 2
- To: test@example.com
- Reply-To: test@example.com
- Date: ...
- Message-ID: ...
- MIME-Version: 1.0
- Content-Type: text/plain; charset="utf-8"
- Content-Transfer-Encoding: base64
- <BLANKLINE>
- RW52b...
- <BLANKLINE>
-
-The content can be decoded to see the actual digest text.
-::
-
- # We must display the repr of the decoded value because doctests cannot
- # handle the non-ascii characters.
- >>> [repr(line)
- ... for line in rfc1153.msg.get_payload(decode=True).splitlines()]
- ["'Envoyez vos messages pour la liste Test \\xc3\\xa0'",
- "'\\ttest@example.com'",
- "''",
- "'Pour vous (d\\xc3\\xa9s)abonner par le web, consultez'",
- "'\\thttp://lists.example.com/listinfo/test@example.com'",
- "''",
- "'ou, par courriel, envoyez un message avec \\xc2\\xab\\xc2\\xa0...
- "'dans le sujet \\xc3\\xa0'",
- "'\\ttest-request@example.com'",
- "''",
- '"Vous pouvez contacter l\'administrateur de la liste \\xc3\\xa0 ...
- "'\\ttest-owner@example.com'",
- "''",
- '"Si vous r\\xc3\\xa9pondez, n\'oubliez pas de changer l\'objet du ...
- '"qu\'il soit plus sp\\xc3\\xa9cifique que \\xc2\\xab\\xc2\\xa0Re: ...
- "''",
- "'Th\\xc3\\xa8mes du jour :'",
- "''",
- "' 1. \\xe4\\xb8\\x80\\xe7\\x95\\xaa (aperson@example.org)'",
- "''",
- "''",
- "'---------------------------------------------------------------------...
- "''",
- "'From: aperson@example.org'",
- "'Subject: \\xe4\\xb8\\x80\\xe7\\x95\\xaa'",
- "'To: test@example.com'",
- "'Message-ID: ...
- "'Content-Type: text/plain; charset=iso-2022-jp'",
- "''",
- "'\\xe4\\xb8\\x80\\xe7\\x95\\xaa'",
- "''",
- "'------------------------------'",
- "''",
- "'_______________________________________________'",
- "'Test mailing list'",
- "'test@example.com'",
- "'http://lists.example.com/listinfo/test@example.com'",
- "''",
- "''",
- "'Fin de Groupe Test, Vol. 1, Parution 2'",
- "'**************************************'"]
-
- >>> config.pop('french')
-
-
-Digest delivery
-===============
-
-A mailing list's members can choose to receive normal delivery, plain text
-digests, or MIME digests.
-::
-
- >>> len(get_queue_messages('virgin'))
- 0
-
- >>> from mailman.interfaces.usermanager import IUserManager
- >>> from zope.component import getUtility
- >>> user_manager = getUtility(IUserManager)
-
- >>> from mailman.interfaces.member import DeliveryMode, MemberRole
- >>> def subscribe(email, mode):
- ... address = user_manager.create_address(email)
- ... member = mlist.subscribe(address, MemberRole.member)
- ... member.preferences.delivery_mode = mode
- ... return member
-
-Two regular delivery members subscribe to the mailing list.
-
- >>> member_1 = subscribe('uperson@example.com', DeliveryMode.regular)
- >>> member_2 = subscribe('vperson@example.com', DeliveryMode.regular)
-
-Two MIME digest members subscribe to the mailing list.
-
- >>> member_3 = subscribe('wperson@example.com', DeliveryMode.mime_digests)
- >>> member_4 = subscribe('xperson@example.com', DeliveryMode.mime_digests)
-
-One RFC 1153 digest member subscribes to the mailing list.
-
- >>> member_5 = subscribe(
- ... 'yperson@example.com', DeliveryMode.plaintext_digests)
- >>> member_6 = subscribe(
- ... 'zperson@example.com', DeliveryMode.plaintext_digests)
-
-When a digest gets sent, the appropriate recipient list is chosen.
-
- >>> mlist.preferred_language = 'en'
- >>> mlist.digest_size_threshold = 0.5
- >>> fill_digest()
- >>> runner.run()
-
-The digests are sitting in the virgin queue. One of them is the MIME digest
-and the other is the RFC 1153 digest.
-::
-
- >>> messages = get_queue_messages('virgin')
- >>> len(messages)
- 2
-
- >>> mime, rfc1153 = mime_rfc1153(messages)
-
-Only wperson and xperson get the MIME digests.
-
- >>> sorted(mime.msgdata['recipients'])
- [u'wperson@example.com', u'xperson@example.com']
-
-Only yperson and zperson get the RFC 1153 digests.
-
- >>> sorted(rfc1153.msgdata['recipients'])
- [u'yperson@example.com', u'zperson@example.com']
-
-Now uperson decides that they would like to start receiving digests too.
-::
-
- >>> member_1.preferences.delivery_mode = DeliveryMode.mime_digests
- >>> fill_digest()
- >>> runner.run()
-
- >>> messages = get_queue_messages('virgin')
- >>> len(messages)
- 2
-
- >>> mime, rfc1153 = mime_rfc1153(messages)
- >>> sorted(mime.msgdata['recipients'])
- [u'uperson@example.com', u'wperson@example.com', u'xperson@example.com']
-
- >>> sorted(rfc1153.msgdata['recipients'])
- [u'yperson@example.com', u'zperson@example.com']
-
-At this point, both uperson and wperson decide that they'd rather receive
-regular deliveries instead of digests. uperson would like to get any last
-digest that may be sent so that she doesn't miss anything. wperson does care
-as much and does not want to receive one last digest.
-::
-
- >>> mlist.send_one_last_digest_to(
- ... member_1.address, member_1.preferences.delivery_mode)
-
- >>> member_1.preferences.delivery_mode = DeliveryMode.regular
- >>> member_3.preferences.delivery_mode = DeliveryMode.regular
-
- >>> fill_digest()
- >>> runner.run()
-
- >>> messages = get_queue_messages('virgin')
- >>> mime, rfc1153 = mime_rfc1153(messages)
- >>> sorted(mime.msgdata['recipients'])
- [u'uperson@example.com', u'xperson@example.com']
-
- >>> sorted(rfc1153.msgdata['recipients'])
- [u'yperson@example.com', u'zperson@example.com']
-
-Since uperson has received their last digest, they will not get any more of
-them.
-::
-
- >>> fill_digest()
- >>> runner.run()
-
- >>> messages = get_queue_messages('virgin')
- >>> len(messages)
- 2
-
- >>> mime, rfc1153 = mime_rfc1153(messages)
- >>> sorted(mime.msgdata['recipients'])
- [u'xperson@example.com']
-
- >>> sorted(rfc1153.msgdata['recipients'])
- [u'yperson@example.com', u'zperson@example.com']
diff --git a/src/mailman/queue/docs/incoming.txt b/src/mailman/queue/docs/incoming.txt
deleted file mode 100644
index 6455db20b..000000000
--- a/src/mailman/queue/docs/incoming.txt
+++ /dev/null
@@ -1,263 +0,0 @@
-=========================
-The incoming queue runner
-=========================
-
-This runner's sole purpose in life is to decide the disposition of the
-message. It can either be accepted for delivery, rejected (i.e. bounced),
-held for moderator approval, or discarded.
-
-The runner operates by processing chains on a message/metadata pair in the
-context of a mailing list. Each mailing list may have a 'start chain' where
-processing begins, with a global default. This chain is processed with the
-message eventually ending up in one of the four disposition states described
-above.
-
- >>> mlist = create_list('test@example.com')
- >>> print mlist.start_chain
- built-in
-
-
-Sender addresses
-================
-
-The incoming queue runner ensures that the sender addresses on the message are
-registered with the system. This is used for determining nonmember posting
-privileges. The addresses will not be linked to a user and will be
-unverified, so if the real user comes along later and claims the address, it
-will be linked to their user account (and must be verified).
-
-While configurable, the *sender addresses* by default are those named in the
-`From:`, `Sender:`, and `Reply-To:` headers, as well as the envelope sender
-(though we won't worry about the latter).
-::
-
- >>> msg = message_from_string("""\
- ... From: zperson@example.com
- ... Reply-To: yperson@example.com
- ... Sender: xperson@example.com
- ... To: test@example.com
- ... Subject: This is spiced ham
- ... Message-ID: <bogus>
- ...
- ... """)
-
- >>> from zope.component import getUtility
- >>> from mailman.interfaces.usermanager import IUserManager
- >>> user_manager = getUtility(IUserManager)
- >>> print user_manager.get_address('xperson@example.com')
- None
- >>> print user_manager.get_address('yperson@example.com')
- None
- >>> print user_manager.get_address('zperson@example.com')
- None
-
-Inject the message into the incoming queue, similar to the way the upstream
-mail server normally would.
-
- >>> from mailman.inject import inject_message
- >>> inject_message(mlist, msg)
-
-The incoming queue runner runs until it is empty.
-
- >>> from mailman.queue.incoming import IncomingRunner
- >>> from mailman.testing.helpers import make_testable_runner
- >>> incoming = make_testable_runner(IncomingRunner, 'in')
- >>> incoming.run()
-
-And now the addresses are known to the system. As mentioned above, they are
-not linked to a user and are unverified.
-
- >>> for localpart in ('xperson', 'yperson', 'zperson'):
- ... email = '{0}@example.com'.format(localpart)
- ... address = user_manager.get_address(email)
- ... print '{0}; verified? {1}; user? {2}'.format(
- ... address.email,
- ... ('No' if address.verified_on is None else 'Yes'),
- ... user_manager.get_user(email))
- xperson@example.com; verified? No; user? None
- yperson@example.com; verified? No; user? None
- zperson@example.com; verified? No; user? None
-
-..
- Clear the pipeline queue of artifacts that affect the following tests.
- >>> from mailman.testing.helpers import get_queue_messages
- >>> ignore = get_queue_messages('pipeline')
-
-
-Accepted messages
-=================
-
-We have a message that is going to be sent to the mailing list. Once Anne is
-a member of the mailing list, this message is so perfectly fine for posting
-that it will be accepted and forward to the pipeline queue.
-
- >>> from mailman.testing.helpers import subscribe
- >>> subscribe(mlist, 'Anne')
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: test@example.com
- ... Subject: My first post
- ... Message-ID: <first>
- ...
- ... First post!
- ... """)
-
-Inject the message into the incoming queue and run until the queue is empty.
-
- >>> inject_message(mlist, msg)
- >>> incoming.run()
-
-Now the message is in the pipeline queue.
-
- >>> pipeline_queue = config.switchboards['pipeline']
- >>> len(pipeline_queue.files)
- 1
- >>> incoming_queue = config.switchboards['in']
- >>> len(incoming_queue.files)
- 0
- >>> item = get_queue_messages('pipeline')[0]
- >>> print item.msg.as_string()
- From: aperson@example.com
- To: test@example.com
- Subject: My first post
- Message-ID: <first>
- Date: ...
- X-Mailman-Rule-Misses: approved; emergency; loop; member-moderation;
- administrivia; implicit-dest; max-recipients; max-size;
- news-moderation; no-subject; suspicious-header; nonmember-moderation
- <BLANKLINE>
- First post!
- <BLANKLINE>
- >>> dump_msgdata(item.msgdata)
- _parsemsg : False
- envsender : noreply@example.com
- ...
-
-
-Held messages
-=============
-
-The list moderator sets the emergency flag on the mailing list. The built-in
-chain will now hold all posted messages, so nothing will show up in the
-pipeline queue.
-::
-
- >>> from mailman.chains.base import ChainNotification
- >>> def on_chain(event):
- ... if isinstance(event, ChainNotification):
- ... print event
- ... print event.chain
- ... print 'From: {0}\nTo: {1}\nMessage-ID: {2}'.format(
- ... event.msg['from'], event.msg['to'],
- ... event.msg['message-id'])
-
- >>> mlist.emergency = True
-
- >>> from mailman.testing.helpers import event_subscribers
- >>> with event_subscribers(on_chain):
- ... inject_message(mlist, msg)
- ... incoming.run()
- <mailman.chains.hold.HoldNotification ...>
- <mailman.chains.hold.HoldChain ...>
- From: aperson@example.com
- To: test@example.com
- Message-ID: <first>
-
- >>> mlist.emergency = False
-
-
-Discarded messages
-==================
-
-Another possibility is that the message would get immediately discarded. The
-built-in chain does not have such a disposition by default, so let's craft a
-new chain and set it as the mailing list's start chain.
-::
-
- >>> from mailman.chains.base import Chain, Link
- >>> from mailman.interfaces.chain import LinkAction
- >>> def make_chain(name, target_chain):
- ... truth_rule = config.rules['truth']
- ... target_chain = config.chains[target_chain]
- ... test_chain = Chain(name, 'Testing {0}'.format(target_chain))
- ... config.chains[test_chain.name] = test_chain
- ... link = Link(truth_rule, LinkAction.jump, target_chain)
- ... test_chain.append_link(link)
- ... return test_chain
-
- >>> test_chain = make_chain('always-discard', 'discard')
- >>> mlist.start_chain = test_chain.name
-
- >>> msg.replace_header('message-id', '<second>')
- >>> with event_subscribers(on_chain):
- ... inject_message(mlist, msg)
- ... incoming.run()
- <mailman.chains.discard.DiscardNotification ...>
- <mailman.chains.discard.DiscardChain ...>
- From: aperson@example.com
- To: test@example.com
- Message-ID: <second>
-
- >>> del config.chains[test_chain.name]
-
-..
- The virgin queue needs to be cleared out due to artifacts from the
- previous tests above.
-
- >>> virgin_queue = config.switchboards['virgin']
- >>> ignore = get_queue_messages('virgin')
-
-
-Rejected messages
-=================
-
-Similar to discarded messages, a message can be rejected, or bounced back to
-the original sender. Again, the built-in chain doesn't support this so we'll
-just create a new chain that does.
-
- >>> test_chain = make_chain('always-reject', 'reject')
- >>> mlist.start_chain = test_chain.name
-
- >>> msg.replace_header('message-id', '<third>')
- >>> with event_subscribers(on_chain):
- ... inject_message(mlist, msg)
- ... incoming.run()
- <mailman.chains.reject.RejectNotification ...>
- <mailman.chains.reject.RejectChain ...>
- From: aperson@example.com
- To: test@example.com
- Message-ID: <third>
-
-The rejection message is sitting in the virgin queue waiting to be delivered
-to the original sender.
-
- >>> len(virgin_queue.files)
- 1
- >>> item = get_queue_messages('virgin')[0]
- >>> print item.msg.as_string()
- Subject: My first post
- From: test-owner@example.com
- To: aperson@example.com
- ...
- <BLANKLINE>
- --===============...
- Content-Type: text/plain; charset="us-ascii"
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
- <BLANKLINE>
- [No bounce details are available]
- --===============...
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
- From: aperson@example.com
- To: test@example.com
- Subject: My first post
- Message-ID: <third>
- Date: ...
- <BLANKLINE>
- First post!
- <BLANKLINE>
- --===============...
-
- >>> del config.chains['always-reject']
diff --git a/src/mailman/queue/docs/lmtp.txt b/src/mailman/queue/docs/lmtp.txt
deleted file mode 100644
index c95c6aa2b..000000000
--- a/src/mailman/queue/docs/lmtp.txt
+++ /dev/null
@@ -1,313 +0,0 @@
-===========
-LTMP server
-===========
-
-Mailman can accept messages via LMTP (RFC 2033). Most modern mail servers
-support LMTP local delivery, so this is a very portable way to connect Mailman
-with your mail server.
-
-Our LMTP server is fairly simple though; all it does is make sure that the
-message is destined for a valid endpoint, e.g. ``mylist-join@example.com``.
-
-Let's start a testable LMTP queue runner.
-
- >>> from mailman.testing import helpers
- >>> master = helpers.TestableMaster()
- >>> master.start('lmtp')
-
-It also helps to have a nice LMTP client.
-
- >>> lmtp = helpers.get_lmtp_client()
- (220, '... Python LMTP queue runner 1.0')
- >>> lmtp.lhlo('remote.example.org')
- (250, ...)
-
-
-Posting address
-===============
-
-If the mail server tries to send a message to a nonexistent mailing list, it
-will get a 550 error.
-
- >>> lmtp.sendmail(
- ... 'anne.person@example.com',
- ... ['mylist@example.com'], """\
- ... From: anne.person@example.com
- ... To: mylist@example.com
- ... Subject: An interesting message
- ... Message-ID: <aardvark>
- ...
- ... This is an interesting message.
- ... """)
- Traceback (most recent call last):
- ...
- SMTPDataError: (550, 'Requested action not taken: mailbox unavailable')
-
-Once the mailing list is created, the posting address is valid.
-::
-
- >>> create_list('mylist@example.com')
- <mailing list "mylist@example.com" at ...>
-
- >>> transaction.commit()
- >>> lmtp.sendmail(
- ... 'anne.person@example.com',
- ... ['mylist@example.com'], """\
- ... From: anne.person@example.com
- ... To: mylist@example.com
- ... Subject: An interesting message
- ... Message-ID: <badger>
- ...
- ... This is an interesting message.
- ... """)
- {}
-
- >>> from mailman.testing.helpers import get_queue_messages
- >>> messages = get_queue_messages('in')
- >>> len(messages)
- 1
- >>> print messages[0].msg.as_string()
- From: anne.person@example.com
- To: mylist@example.com
- Subject: An interesting message
- Message-ID: <badger>
- X-MailFrom: anne.person@example.com
- <BLANKLINE>
- This is an interesting message.
- >>> dump_msgdata(messages[0].msgdata)
- _parsemsg : False
- listname : mylist@example.com
- original_size: ...
- to_list : True
- version : ...
-
-
-Sub-addresses
-=============
-
-The LMTP server understands each of the list's sub-addreses, such as -join,
--leave, -request and so on. If the message is posted to an invalid
-sub-address though, it is rejected.
-
- >>> lmtp.sendmail(
- ... 'anne.person@example.com',
- ... ['mylist-bogus@example.com'], """\
- ... From: anne.person@example.com
- ... To: mylist-bogus@example.com
- ... Subject: Help
- ... Message-ID: <cow>
- ...
- ... Please help me.
- ... """)
- Traceback (most recent call last):
- ...
- SMTPDataError: (550, 'Requested action not taken: mailbox unavailable')
-
-But the message is accepted if posted to a valid sub-address.
-
- >>> lmtp.sendmail(
- ... 'anne.person@example.com',
- ... ['mylist-request@example.com'], """\
- ... From: anne.person@example.com
- ... To: mylist-request@example.com
- ... Subject: Help
- ... Message-ID: <dog>
- ...
- ... Please help me.
- ... """)
- {}
-
-
-Request subaddress
-------------------
-
-Depending on the subaddress, there is a message in the appropriate queue for
-later processing. For example, all -request messages are put into the command
-queue for processing.
-
- >>> messages = get_queue_messages('command')
- >>> len(messages)
- 1
- >>> print messages[0].msg.as_string()
- From: anne.person@example.com
- To: mylist-request@example.com
- Subject: Help
- Message-ID: <dog>
- X-MailFrom: anne.person@example.com
- <BLANKLINE>
- Please help me.
- >>> dump_msgdata(messages[0].msgdata)
- _parsemsg : False
- listname : mylist@example.com
- original_size: ...
- subaddress : request
- version : ...
-
-
-Bounce processor
-----------------
-
-A message to the -bounces address goes to the bounce processor.
-
- >>> lmtp.sendmail(
- ... 'mail-daemon@example.com',
- ... ['mylist-bounces@example.com'], """\
- ... From: mail-daemon@example.com
- ... To: mylist-bounces@example.com
- ... Subject: A bounce
- ... Message-ID: <elephant>
- ...
- ... Bouncy bouncy.
- ... """)
- {}
- >>> messages = get_queue_messages('bounces')
- >>> len(messages)
- 1
- >>> dump_msgdata(messages[0].msgdata)
- _parsemsg : False
- listname : mylist@example.com
- original_size: ...
- subaddress : bounces
- version : ...
-
-
-Command processor
------------------
-
-Confirmation messages go to the command processor...
-
- >>> lmtp.sendmail(
- ... 'anne.person@example.com',
- ... ['mylist-confirm@example.com'], """\
- ... From: anne.person@example.com
- ... To: mylist-confirm@example.com
- ... Subject: A bounce
- ... Message-ID: <falcon>
- ...
- ... confirm 123
- ... """)
- {}
- >>> messages = get_queue_messages('command')
- >>> len(messages)
- 1
- >>> dump_msgdata(messages[0].msgdata)
- _parsemsg : False
- listname : mylist@example.com
- original_size: ...
- subaddress : confirm
- version : ...
-
-...as do join messages...
-::
-
- >>> lmtp.sendmail(
- ... 'anne.person@example.com',
- ... ['mylist-join@example.com'], """\
- ... From: anne.person@example.com
- ... To: mylist-join@example.com
- ... Message-ID: <giraffe>
- ...
- ... """)
- {}
- >>> messages = get_queue_messages('command')
- >>> len(messages)
- 1
- >>> dump_msgdata(messages[0].msgdata)
- _parsemsg : False
- listname : mylist@example.com
- original_size: ...
- subaddress : join
- version : ...
-
- >>> lmtp.sendmail(
- ... 'anne.person@example.com',
- ... ['mylist-subscribe@example.com'], """\
- ... From: anne.person@example.com
- ... To: mylist-subscribe@example.com
- ... Message-ID: <hippopotamus>
- ...
- ... """)
- {}
- >>> messages = get_queue_messages('command')
- >>> len(messages)
- 1
- >>> dump_msgdata(messages[0].msgdata)
- _parsemsg : False
- listname : mylist@example.com
- original_size: ...
- subaddress : join
- version : ...
-
-...and leave messages.
-::
-
- >>> lmtp.sendmail(
- ... 'anne.person@example.com',
- ... ['mylist-leave@example.com'], """\
- ... From: anne.person@example.com
- ... To: mylist-leave@example.com
- ... Message-ID: <iguana>
- ...
- ... """)
- {}
- >>> messages = get_queue_messages('command')
- >>> len(messages)
- 1
- >>> dump_msgdata(messages[0].msgdata)
- _parsemsg : False
- listname : mylist@example.com
- original_size: ...
- subaddress : leave
- version : ...
-
- >>> lmtp.sendmail(
- ... 'anne.person@example.com',
- ... ['mylist-unsubscribe@example.com'], """\
- ... From: anne.person@example.com
- ... To: mylist-unsubscribe@example.com
- ... Message-ID: <jackal>
- ...
- ... """)
- {}
- >>> messages = get_queue_messages('command')
- >>> len(messages)
- 1
- >>> dump_msgdata(messages[0].msgdata)
- _parsemsg : False
- listname : mylist@example.com
- original_size: ...
- subaddress : leave
- version : ...
-
-
-Incoming processor
-------------------
-
-Messages to the -owner address go to the incoming processor.
-
- >>> lmtp.sendmail(
- ... 'anne.person@example.com',
- ... ['mylist-owner@example.com'], """\
- ... From: anne.person@example.com
- ... To: mylist-owner@example.com
- ... Message-ID: <kangaroo>
- ...
- ... """)
- {}
- >>> messages = get_queue_messages('in')
- >>> len(messages)
- 1
- >>> dump_msgdata(messages[0].msgdata)
- _parsemsg : False
- envsender : changeme@example.com
- listname : mylist@example.com
- original_size: ...
- subaddress : owner
- to_owner : True
- version : ...
-
-
-Clean up
-========
-
- >>> master.stop()
diff --git a/src/mailman/queue/docs/news.txt b/src/mailman/queue/docs/news.txt
deleted file mode 100644
index 7261aa333..000000000
--- a/src/mailman/queue/docs/news.txt
+++ /dev/null
@@ -1,161 +0,0 @@
-===============
-The news runner
-===============
-
-The news runner is the queue runner that gateways mailing list messages to an
-NNTP newsgroup. One of the most important things this runner does is prepare
-the message for Usenet (yes, I know that NNTP is not Usenet, but this runner
-was originally written to gate to Usenet, which has its own rules).
-
- >>> mlist = create_list('_xtest@example.com')
- >>> mlist.linked_newsgroup = 'comp.lang.python'
-
-Some NNTP servers such as INN reject messages containing a set of prohibited
-headers, so one of the things that the news runner does is remove these
-prohibited headers.
-::
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: _xtest@example.com
- ... NNTP-Posting-Host: news.example.com
- ... NNTP-Posting-Date: today
- ... X-Trace: blah blah
- ... X-Complaints-To: abuse@dom.ain
- ... Xref: blah blah
- ... Xref: blah blah
- ... Date-Received: yesterday
- ... Posted: tomorrow
- ... Posting-Version: 99.99
- ... Relay-Version: 88.88
- ... Received: blah blah
- ...
- ... A message
- ... """)
- >>> msgdata = {}
-
- >>> from mailman.queue.news import prepare_message
- >>> prepare_message(mlist, msg, msgdata)
- >>> msgdata['prepped']
- True
- >>> print msg.as_string()
- From: aperson@example.com
- To: _xtest@example.com
- Newsgroups: comp.lang.python
- Message-ID: ...
- Lines: 1
- <BLANKLINE>
- A message
- <BLANKLINE>
-
-Some NNTP servers will reject messages where certain headers are duplicated,
-so the news runner must collapse or move these duplicate headers to an
-``X-Original-*`` header that the news server doesn't care about.
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: _xtest@example.com
- ... To: two@example.com
- ... Cc: three@example.com
- ... Cc: four@example.com
- ... Cc: five@example.com
- ... Content-Transfer-Encoding: yes
- ... Content-Transfer-Encoding: no
- ... Content-Transfer-Encoding: maybe
- ...
- ... A message
- ... """)
- >>> msgdata = {}
- >>> prepare_message(mlist, msg, msgdata)
- >>> msgdata['prepped']
- True
- >>> print msg.as_string()
- From: aperson@example.com
- Newsgroups: comp.lang.python
- Message-ID: ...
- Lines: 1
- To: _xtest@example.com
- X-Original-To: two@example.com
- CC: three@example.com
- X-Original-CC: four@example.com
- X-Original-CC: five@example.com
- Content-Transfer-Encoding: yes
- X-Original-Content-Transfer-Encoding: no
- X-Original-Content-Transfer-Encoding: maybe
- <BLANKLINE>
- A message
- <BLANKLINE>
-
-But if no headers are duplicated, then the news runner doesn't need to modify
-the message.
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: _xtest@example.com
- ... Cc: someother@example.com
- ... Content-Transfer-Encoding: yes
- ...
- ... A message
- ... """)
- >>> msgdata = {}
- >>> prepare_message(mlist, msg, msgdata)
- >>> msgdata['prepped']
- True
- >>> print msg.as_string()
- From: aperson@example.com
- To: _xtest@example.com
- Cc: someother@example.com
- Content-Transfer-Encoding: yes
- Newsgroups: comp.lang.python
- Message-ID: ...
- Lines: 1
- <BLANKLINE>
- A message
- <BLANKLINE>
-
-
-Newsgroup moderation
-====================
-
-When the newsgroup is moderated, an ``Approved:`` header with the list's
-posting address is added for the benefit of the Usenet system.
-::
-
- >>> from mailman.interfaces.nntp import NewsModeration
- >>> mlist.news_moderation = NewsModeration.open_moderated
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: _xtest@example.com
- ... Approved: this gets deleted
- ...
- ... """)
- >>> prepare_message(mlist, msg, {})
- >>> print msg['approved']
- _xtest@example.com
-
- >>> mlist.news_moderation = NewsModeration.moderated
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: _xtest@example.com
- ... Approved: this gets deleted
- ...
- ... """)
- >>> prepare_message(mlist, msg, {})
- >>> print msg['approved']
- _xtest@example.com
-
-But if the newsgroup is not moderated, the ``Approved:`` header is not changed.
-
- >>> mlist.news_moderation = NewsModeration.none
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: _xtest@example.com
- ... Approved: this doesn't get deleted
- ...
- ... """)
- >>> prepare_message(mlist, msg, {})
- >>> msg['approved']
- u"this doesn't get deleted"
-
-
-XXX More of the NewsRunner should be tested.
diff --git a/src/mailman/queue/docs/outgoing.txt b/src/mailman/queue/docs/outgoing.txt
deleted file mode 100644
index 0af22b808..000000000
--- a/src/mailman/queue/docs/outgoing.txt
+++ /dev/null
@@ -1,413 +0,0 @@
-=====================
-Outgoing queue runner
-=====================
-
-The outgoing queue runner is the process that delivers messages to the
-directly upstream SMTP server. It is this upstream SMTP server that performs
-final delivery to the intended recipients.
-
-Messages that appear in the outgoing queue are processed individually through
-a *delivery module*, essentially a pluggable interface for determining how the
-recipient set will be batched, whether messages will be personalized and
-VERP'd, etc. The outgoing runner doesn't itself support retrying but it can
-move messages to the 'retry queue' for handling delivery failures.
-::
-
- >>> mlist = create_list('test@example.com')
-
- >>> from mailman.app.membership import add_member
- >>> from mailman.interfaces.member import DeliveryMode
- >>> add_member(mlist, 'aperson@example.com', 'Anne Person',
- ... 'password', DeliveryMode.regular, 'en')
- <Member: Anne Person <aperson@example.com>
- on test@example.com as MemberRole.member>
- >>> add_member(mlist, 'bperson@example.com', 'Bart Person',
- ... 'password', DeliveryMode.regular, 'en')
- <Member: Bart Person <bperson@example.com>
- on test@example.com as MemberRole.member>
- >>> add_member(mlist, 'cperson@example.com', 'Cris Person',
- ... 'password', DeliveryMode.regular, 'en')
- <Member: Cris Person <cperson@example.com>
- on test@example.com as MemberRole.member>
-
-Normally, messages would show up in the outgoing queue after the message has
-been processed by the rule set and pipeline. But we can simulate that here by
-injecting a message directly into the outgoing queue. First though, we must
-call the ``calculate-recipients`` handler so that the message metadata will be
-populated with the list of addresses to deliver the message to.
-::
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: test@example.com
- ... Subject: My first post
- ... Message-ID: <first>
- ...
- ... First post!
- ... """)
-
- >>> msgdata = {}
- >>> handler = config.handlers['calculate-recipients']
- >>> handler.process(mlist, msg, msgdata)
- >>> outgoing_queue = config.switchboards['out']
-
-The ``to-outgoing`` handler populates the message metadata with the
-destination mailing list name. Simulate that here too.
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... tolist=True,
- ... listname=mlist.fqdn_listname)
-
-Running the outgoing queue runner processes the message, delivering it to the
-upstream SMTP.
-
- >>> from mailman.queue.outgoing import OutgoingRunner
- >>> from mailman.testing.helpers import make_testable_runner
- >>> outgoing = make_testable_runner(OutgoingRunner, 'out')
- >>> outgoing.run()
-
-Every recipient got the same copy of the message.
-::
-
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 1
-
- >>> print messages[0].as_string()
- From: aperson@example.com
- To: test@example.com
- Subject: My first post
- Message-ID: <first>
- X-Peer: ...
- X-MailFrom: test-bounces@example.com
- X-RcptTo: cperson@example.com, bperson@example.com, aperson@example.com
- <BLANKLINE>
- First post!
-
-
-Personalization
-===============
-
-Mailman supports sending individual messages to each recipient by
-personalizing delivery. This increases the bandwidth between Mailman and the
-upstream mail server, and between the upstream mail server and the remote
-recipient mail servers. The benefit is that personalization provides for a
-much better user experience, because the messages can be tailored for each
-recipient.
-
- >>> from mailman.interfaces.mailinglist import Personalization
- >>> mlist.personalize = Personalization.individual
- >>> transaction.commit()
-
-Now when we send the message, our mail server will get three copies instead of
-just one.
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... listname=mlist.fqdn_listname)
- >>> outgoing.run()
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 3
-
-Since we've done no other configuration, the only difference in the messages
-is the recipient address. Specifically, the Sender header is the same for all
-recipients.
-::
-
- >>> from operator import itemgetter
- >>> def show_headers(messages):
- ... for message in sorted(messages, key=itemgetter('x-rcptto')):
- ... print message['X-RcptTo'], message['X-MailFrom']
-
- >>> show_headers(messages)
- aperson@example.com test-bounces@example.com
- bperson@example.com test-bounces@example.com
- cperson@example.com test-bounces@example.com
-
-
-VERP
-====
-
-An even more interesting personalization opportunity arises if VERP_ is
-enabled. Here, Mailman takes advantage of the fact that it's sending
-individualized messages anyway, so it also encodes the recipients address in
-the Sender header.
-
-.. _VERP: ../../mta/docs/verp.html
-
-
-Forcing VERP
-------------
-
-A handler can force VERP by setting the ``verp`` key in the message metadata.
-::
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... verp=True,
- ... listname=mlist.fqdn_listname)
- >>> outgoing.run()
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 3
-
- >>> show_headers(messages)
- aperson@example.com test-bounces+aperson=example.com@example.com
- bperson@example.com test-bounces+bperson=example.com@example.com
- cperson@example.com test-bounces+cperson=example.com@example.com
-
-
-VERP personalized deliveries
-----------------------------
-
-The site administrator can enable VERP whenever messages are personalized.
-
- >>> config.push('verp', """
- ... [mta]
- ... verp_personalized_deliveries: yes
- ... """)
-
-Again, we get three individual messages, with VERP'd ``Sender`` headers.
-::
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... listname=mlist.fqdn_listname)
- >>> outgoing.run()
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 3
-
- >>> show_headers(messages)
- aperson@example.com test-bounces+aperson=example.com@example.com
- bperson@example.com test-bounces+bperson=example.com@example.com
- cperson@example.com test-bounces+cperson=example.com@example.com
-
- >>> config.pop('verp')
- >>> mlist.personalize = Personalization.none
- >>> transaction.commit()
-
-
-VERP every once in a while
---------------------------
-
-Perhaps personalization is too much of an overhead, but the list owners would
-still like to occasionally get the benefits of VERP. The site administrator
-can enable occasional VERPing of messages every so often, by setting a
-delivery interval. Every N non-personalized deliveries turns on VERP for just
-the next one.
-::
-
- >>> config.push('verp occasionally', """
- ... [mta]
- ... verp_delivery_interval: 3
- ... """)
-
- # Reset the list's post_id, which is used to calculate the intervals.
- >>> mlist.post_id = 1
- >>> transaction.commit()
-
-The first message is sent to the list, and it is neither personalized nor
-VERP'd.
-::
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... listname=mlist.fqdn_listname)
- >>> outgoing.run()
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 1
-
- >>> show_headers(messages)
- cperson@example.com, bperson@example.com, aperson@example.com
- test-bounces@example.com
-
- # Perform post-delivery bookkeeping.
- >>> after = config.handlers['after-delivery']
- >>> after.process(mlist, msg, msgdata)
- >>> transaction.commit()
-
-The second message sent to the list is also not VERP'd.
-::
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... listname=mlist.fqdn_listname)
- >>> outgoing.run()
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 1
-
- >>> show_headers(messages)
- cperson@example.com, bperson@example.com, aperson@example.com
- test-bounces@example.com
-
- # Perform post-delivery bookkeeping.
- >>> after.process(mlist, msg, msgdata)
- >>> transaction.commit()
-
-The third message though is VERP'd.
-::
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... listname=mlist.fqdn_listname)
- >>> outgoing.run()
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 3
-
- >>> show_headers(messages)
- aperson@example.com test-bounces+aperson=example.com@example.com
- bperson@example.com test-bounces+bperson=example.com@example.com
- cperson@example.com test-bounces+cperson=example.com@example.com
-
- # Perform post-delivery bookkeeping.
- >>> after.process(mlist, msg, msgdata)
- >>> transaction.commit()
-
-The next one is back to bulk delivery.
-::
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... listname=mlist.fqdn_listname)
- >>> outgoing.run()
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 1
-
- >>> show_headers(messages)
- cperson@example.com, bperson@example.com, aperson@example.com
- test-bounces@example.com
-
- >>> config.pop('verp occasionally')
-
-
-VERP every time
----------------
-
-If the site administrator wants to enable VERP for every delivery, even if no
-personalization is going on, they can set the interval to 1.
-::
-
- >>> config.push('always verp', """
- ... [mta]
- ... verp_delivery_interval: 1
- ... """)
-
- # Reset the list's post_id, which is used to calculate the intervals.
- >>> mlist.post_id = 1
- >>> transaction.commit()
-
-The first message is VERP'd.
-::
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... listname=mlist.fqdn_listname)
- >>> outgoing.run()
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 3
-
- >>> show_headers(messages)
- aperson@example.com test-bounces+aperson=example.com@example.com
- bperson@example.com test-bounces+bperson=example.com@example.com
- cperson@example.com test-bounces+cperson=example.com@example.com
-
- # Perform post-delivery bookkeeping.
- >>> after.process(mlist, msg, msgdata)
- >>> transaction.commit()
-
-As is the second message.
-::
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... listname=mlist.fqdn_listname)
- >>> outgoing.run()
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 3
-
- >>> show_headers(messages)
- aperson@example.com test-bounces+aperson=example.com@example.com
- bperson@example.com test-bounces+bperson=example.com@example.com
- cperson@example.com test-bounces+cperson=example.com@example.com
-
- # Perform post-delivery bookkeeping.
- >>> after.process(mlist, msg, msgdata)
- >>> transaction.commit()
-
-And the third message.
-::
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... listname=mlist.fqdn_listname)
- >>> outgoing.run()
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 3
-
- >>> show_headers(messages)
- aperson@example.com test-bounces+aperson=example.com@example.com
- bperson@example.com test-bounces+bperson=example.com@example.com
- cperson@example.com test-bounces+cperson=example.com@example.com
-
- # Perform post-delivery bookkeeping.
- >>> after.process(mlist, msg, msgdata)
- >>> transaction.commit()
-
- >>> config.pop('always verp')
-
-
-Never VERP
-----------
-
-Similarly, the site administrator can disable occasional VERP'ing of
-non-personalized messages by setting the interval to zero.
-::
-
- >>> config.push('never verp', """
- ... [mta]
- ... verp_delivery_interval: 0
- ... """)
-
- # Reset the list's post_id, which is used to calculate the intervals.
- >>> mlist.post_id = 1
- >>> transaction.commit()
-
-Neither the first message...
-::
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... listname=mlist.fqdn_listname)
- >>> outgoing.run()
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 1
-
- >>> show_headers(messages)
- cperson@example.com, bperson@example.com, aperson@example.com
- test-bounces@example.com
-
-...nor the second message is VERP'd.
-::
-
- >>> ignore = outgoing_queue.enqueue(
- ... msg, msgdata,
- ... listname=mlist.fqdn_listname)
- >>> outgoing.run()
- >>> messages = list(smtpd.messages)
- >>> len(messages)
- 1
-
- >>> show_headers(messages)
- cperson@example.com, bperson@example.com, aperson@example.com
- test-bounces@example.com
diff --git a/src/mailman/queue/docs/rest.txt b/src/mailman/queue/docs/rest.txt
deleted file mode 100644
index 9e8851eca..000000000
--- a/src/mailman/queue/docs/rest.txt
+++ /dev/null
@@ -1,25 +0,0 @@
-===========
-REST server
-===========
-
-Mailman is controllable through an administrative `REST`_ HTTP server.
-
- >>> from mailman.testing import helpers
- >>> master = helpers.TestableMaster(helpers.wait_for_webservice)
- >>> master.start('rest')
-
-The RESTful server can be used to access basic version information.
-
- >>> dump_json('http://localhost:9001/3.0/system')
- http_etag: "..."
- mailman_version: GNU Mailman 3.0... (...)
- python_version: ...
- self_link: http://localhost:9001/3.0/system
-
-
-Clean up
-========
-
- >>> master.stop()
-
-.. _REST: http://en.wikipedia.org/wiki/REST
diff --git a/src/mailman/queue/docs/runner.txt b/src/mailman/queue/docs/runner.txt
deleted file mode 100644
index 39e8fede2..000000000
--- a/src/mailman/queue/docs/runner.txt
+++ /dev/null
@@ -1,73 +0,0 @@
-=============
-Queue runners
-=============
-
-The queue runners (*qrunner*) are the processes that move messages around the
-Mailman system. Each qrunner is responsible for a slice of the hash space in
-a queue directory. It processes all the files in its slice, sleeps a little
-while, then wakes up and runs through its queue files again.
-
-
-Basic architecture
-==================
-
-The basic architecture of qrunner is implemented in the base class that all
-runners inherit from. This base class implements a ``.run()`` method that
-runs continuously in a loop until the ``.stop()`` method is called.
-
- >>> mlist = create_list('_xtest@example.com')
-
-Here is a very simple derived qrunner class. Queue runners use a
-configuration section in the configuration files to determine run
-characteristics, such as the queue directory to use. Here we push a
-configuration section for the test runner.
-::
-
- >>> config.push('test-runner', """
- ... [qrunner.test]
- ... max_restarts: 1
- ... """)
-
- >>> from mailman.queue import Runner
- >>> class TestableRunner(Runner):
- ... def _dispose(self, mlist, msg, msgdata):
- ... self.msg = msg
- ... self.msgdata = msgdata
- ... return False
- ...
- ... def _do_periodic(self):
- ... self.stop()
- ...
- ... def _snooze(self, filecnt):
- ... return
-
- >>> runner = TestableRunner('test')
-
-This qrunner doesn't do much except run once, storing the message and metadata
-on instance variables.
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: _xtest@example.com
- ...
- ... A test message.
- ... """)
- >>> switchboard = config.switchboards['test']
- >>> filebase = switchboard.enqueue(msg, listname=mlist.fqdn_listname,
- ... foo='yes', bar='no')
- >>> runner.run()
- >>> print runner.msg.as_string()
- From: aperson@example.com
- To: _xtest@example.com
- <BLANKLINE>
- A test message.
- <BLANKLINE>
- >>> dump_msgdata(runner.msgdata)
- _parsemsg: False
- bar : no
- foo : yes
- lang : en
- listname : _xtest@example.com
- version : 3
-
-XXX More of the Runner API should be tested.
diff --git a/src/mailman/queue/docs/switchboard.txt b/src/mailman/queue/docs/switchboard.txt
deleted file mode 100644
index d89aa3693..000000000
--- a/src/mailman/queue/docs/switchboard.txt
+++ /dev/null
@@ -1,187 +0,0 @@
-The switchboard
-===============
-
-The switchboard is subsystem that moves messages between queues. Each
-instance of a switchboard is responsible for one queue directory.
-
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: _xtest@example.com
- ...
- ... A test message.
- ... """)
-
-Create a switchboard by giving its queue name and directory.
-
- >>> import os
- >>> queue_directory = os.path.join(config.QUEUE_DIR, 'test')
- >>> from mailman.queue import Switchboard
- >>> switchboard = Switchboard('test', queue_directory)
- >>> print switchboard.name
- test
- >>> switchboard.queue_directory == queue_directory
- True
-
-Here's a helper function for ensuring things work correctly.
-
- >>> def check_qfiles(directory=None):
- ... if directory is None:
- ... directory = queue_directory
- ... files = {}
- ... for qfile in os.listdir(directory):
- ... root, ext = os.path.splitext(qfile)
- ... files[ext] = files.get(ext, 0) + 1
- ... if len(files) == 0:
- ... print 'empty'
- ... for ext in sorted(files):
- ... print '{0}: {1}'.format(ext, files[ext])
-
-
-Enqueing and dequeing
----------------------
-
-The message can be enqueued with metadata specified in the passed in
-dictionary.
-
- >>> filebase = switchboard.enqueue(msg)
- >>> check_qfiles()
- .pck: 1
-
-To read the contents of a queue file, dequeue it.
-
- >>> msg, msgdata = switchboard.dequeue(filebase)
- >>> print msg.as_string()
- From: aperson@example.com
- To: _xtest@example.com
- <BLANKLINE>
- A test message.
- <BLANKLINE>
- >>> dump_msgdata(msgdata)
- _parsemsg: False
- version : 3
- >>> check_qfiles()
- .bak: 1
-
-To complete the dequeing process, removing all traces of the message file,
-finish it (without preservation).
-
- >>> switchboard.finish(filebase)
- >>> check_qfiles()
- empty
-
-When enqueing a file, you can provide additional metadata keys by using
-keyword arguments.
-
- >>> filebase = switchboard.enqueue(msg, {'foo': 1}, bar=2)
- >>> msg, msgdata = switchboard.dequeue(filebase)
- >>> switchboard.finish(filebase)
- >>> dump_msgdata(msgdata)
- _parsemsg: False
- bar : 2
- foo : 1
- version : 3
-
-Keyword arguments override keys from the metadata dictionary.
-
- >>> filebase = switchboard.enqueue(msg, {'foo': 1}, foo=2)
- >>> msg, msgdata = switchboard.dequeue(filebase)
- >>> switchboard.finish(filebase)
- >>> dump_msgdata(msgdata)
- _parsemsg: False
- foo : 2
- version : 3
-
-
-Iterating over files
---------------------
-
-There are two ways to iterate over all the files in a switchboard's queue.
-Normally, queue files end in .pck (for 'pickle') and the easiest way to
-iterate over just these files is to use the .files attribute.
-
- >>> filebase_1 = switchboard.enqueue(msg, foo=1)
- >>> filebase_2 = switchboard.enqueue(msg, foo=2)
- >>> filebase_3 = switchboard.enqueue(msg, foo=3)
- >>> filebases = sorted((filebase_1, filebase_2, filebase_3))
- >>> sorted(switchboard.files) == filebases
- True
- >>> check_qfiles()
- .pck: 3
-
-You can also use the .get_files() method if you want to iterate over all the
-file bases for some other extension.
-
- >>> for filebase in switchboard.get_files():
- ... msg, msgdata = switchboard.dequeue(filebase)
- >>> bakfiles = sorted(switchboard.get_files('.bak'))
- >>> bakfiles == filebases
- True
- >>> check_qfiles()
- .bak: 3
- >>> for filebase in switchboard.get_files('.bak'):
- ... switchboard.finish(filebase)
- >>> check_qfiles()
- empty
-
-
-Recovering files
-----------------
-
-Calling .dequeue() without calling .finish() leaves .bak backup files in
-place. These can be recovered when the switchboard is instantiated.
-
- >>> filebase_1 = switchboard.enqueue(msg, foo=1)
- >>> filebase_2 = switchboard.enqueue(msg, foo=2)
- >>> filebase_3 = switchboard.enqueue(msg, foo=3)
- >>> for filebase in switchboard.files:
- ... msg, msgdata = switchboard.dequeue(filebase)
- ... # Don't call .finish()
- >>> check_qfiles()
- .bak: 3
- >>> switchboard_2 = Switchboard('test', queue_directory, recover=True)
- >>> check_qfiles()
- .pck: 3
-
-The files can be recovered explicitly.
-
- >>> for filebase in switchboard.files:
- ... msg, msgdata = switchboard.dequeue(filebase)
- ... # Don't call .finish()
- >>> check_qfiles()
- .bak: 3
- >>> switchboard.recover_backup_files()
- >>> check_qfiles()
- .pck: 3
-
-But the files will only be recovered at most three times before they are
-considered defective. In order to prevent mail bombs and loops, once this
-maximum is reached, the files will be preserved in the 'bad' queue.
-::
-
- >>> for filebase in switchboard.files:
- ... msg, msgdata = switchboard.dequeue(filebase)
- ... # Don't call .finish()
- >>> check_qfiles()
- .bak: 3
- >>> switchboard.recover_backup_files()
- >>> check_qfiles()
- empty
-
- >>> bad = config.switchboards['bad']
- >>> check_qfiles(bad.queue_directory)
- .psv: 3
-
-
-Clean up
---------
-
- >>> for file in os.listdir(bad.queue_directory):
- ... os.remove(os.path.join(bad.queue_directory, file))
- >>> check_qfiles(bad.queue_directory)
- empty
-
-
-Queue slices
-------------
-
-XXX Add tests for queue slices.
diff --git a/src/mailman/queue/incoming.py b/src/mailman/queue/incoming.py
deleted file mode 100644
index f8d671177..000000000
--- a/src/mailman/queue/incoming.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# Copyright (C) 1998-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Incoming queue runner.
-
-This runner's sole purpose in life is to decide the disposition of the
-message. It can either be accepted for delivery, rejected (i.e. bounced),
-held for moderator approval, or discarded.
-
-When accepted, the message is forwarded on to the `prep queue` where it is
-prepared for delivery. Rejections, discards, and holds are processed
-immediately.
-"""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'IncomingRunner',
- ]
-
-
-from zope.component import getUtility
-
-from mailman.config import config
-from mailman.core.chains import process
-from mailman.interfaces.address import ExistingAddressError
-from mailman.interfaces.usermanager import IUserManager
-from mailman.queue import Runner
-
-
-
-class IncomingRunner(Runner):
- """The incoming queue runner."""
-
- def _dispose(self, mlist, msg, msgdata):
- """See `IRunner`."""
- if msgdata.get('envsender') is None:
- msgdata['envsender'] = mlist.no_reply_address
- # Ensure that the email addresses of the message's senders are known
- # to Mailman. This will be used in nonmember posting dispositions.
- user_manager = getUtility(IUserManager)
- for sender in msg.senders:
- try:
- user_manager.create_address(sender)
- except ExistingAddressError:
- pass
- config.db.commit()
- # Process the message through the mailing list's start chain.
- process(mlist, msg, msgdata, mlist.start_chain)
- # Do not keep this message queued.
- return False
diff --git a/src/mailman/queue/lmtp.py b/src/mailman/queue/lmtp.py
deleted file mode 100644
index 9163a88e6..000000000
--- a/src/mailman/queue/lmtp.py
+++ /dev/null
@@ -1,236 +0,0 @@
-# Copyright (C) 2006-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Mailman LMTP runner (server).
-
-Most mail servers can be configured to deliver local messages via 'LMTP'[1].
-This module is actually an LMTP server rather than a standard queue runner.
-
-The LMTP runner opens a local TCP port and waits for the mail server to
-connect to it. The messages it receives over LMTP are very minimally parsed
-for sanity and if they look okay, they are accepted and injected into
-Mailman's incoming queue for normal processing. If they don't look good, or
-are destined for a bogus sub-address, they are rejected right away, hopefully
-so that the peer mail server can provide better diagnostics.
-
-[1] RFC 2033 Local Mail Transport Protocol
- http://www.faqs.org/rfcs/rfc2033.html
-"""
-
-import email
-import smtpd
-import logging
-import asyncore
-
-from email.utils import parseaddr
-from zope.component import getUtility
-
-from mailman.config import config
-from mailman.database.transaction import txn
-from mailman.email.message import Message
-from mailman.interfaces.listmanager import IListManager
-from mailman.queue import Runner
-
-elog = logging.getLogger('mailman.error')
-qlog = logging.getLogger('mailman.qrunner')
-
-
-# We only care about the listname and the sub-addresses as in listname@ or
-# listname-request@. This maps user visible subaddress names (which may
-# include aliases) to the internal canonical subaddress name.
-SUBADDRESS_NAMES = dict(
- admin='bounces',
- bounces='bounces',
- confirm='confirm',
- join='join',
- leave='leave',
- owner='owner',
- request='request',
- subscribe='join',
- unsubscribe='leave',
- )
-
-# This maps subaddress canonical name to the destination queue that handles
-# messages sent to that subaddress.
-SUBADDRESS_QUEUES = dict(
- bounces='bounces',
- confirm='command',
- join='command',
- leave='command',
- owner='in',
- request='command',
- )
-
-DASH = '-'
-CRLF = '\r\n'
-ERR_451 = '451 Requested action aborted: error in processing'
-ERR_501 = '501 Message has defects'
-ERR_502 = '502 Error: command HELO not implemented'
-ERR_550 = '550 Requested action not taken: mailbox unavailable'
-
-# XXX Blech
-smtpd.__version__ = 'Python LMTP queue runner 1.0'
-
-
-
-def split_recipient(address):
- """Split an address into listname, subaddress and domain parts.
-
- For example:
-
- >>> split_recipient('mylist@example.com')
- ('mylist', None, 'example.com')
-
- >>> split_recipient('mylist-request@example.com')
- ('mylist', 'request', 'example.com')
-
- :param address: The destination address.
- :return: A 3-tuple of the form (list-shortname, subaddress, domain).
- subaddress may be None if this is the list's posting address.
- """
- localpart, domain = address.split('@', 1)
- localpart = localpart.split(config.mta.verp_delimiter, 1)[0]
- parts = localpart.split(DASH)
- if parts[-1] in SUBADDRESS_NAMES:
- listname = DASH.join(parts[:-1])
- subaddress = parts[-1]
- else:
- listname = localpart
- subaddress = None
- return listname, subaddress, domain
-
-
-
-class Channel(smtpd.SMTPChannel):
- """An LMTP channel."""
-
- def __init__(self, server, conn, addr):
- smtpd.SMTPChannel.__init__(self, server, conn, addr)
- # Stash this here since the subclass uses private attributes. :(
- self._server = server
-
- def smtp_LHLO(self, arg):
- """The LMTP greeting, used instead of HELO/EHLO."""
- smtpd.SMTPChannel.smtp_HELO(self, arg)
-
- def smtp_HELO(self, arg):
- """HELO is not a valid LMTP command."""
- self.push(ERR_502)
-
-
-
-class LMTPRunner(Runner, smtpd.SMTPServer):
- # Only __init__ is called on startup. Asyncore is responsible for later
- # connections from the MTA. slice and numslices are ignored and are
- # necessary only to satisfy the API.
- def __init__(self, slice=None, numslices=1):
- localaddr = config.mta.lmtp_host, int(config.mta.lmtp_port)
- # Do not call Runner's constructor because there's no QDIR to create
- qlog.debug('LMTP server listening on %s:%s',
- localaddr[0], localaddr[1])
- smtpd.SMTPServer.__init__(self, localaddr, remoteaddr=None)
-
- def handle_accept(self):
- conn, addr = self.accept()
- Channel(self, conn, addr)
- qlog.debug('LMTP accept from %s', addr)
-
- @txn
- def process_message(self, peer, mailfrom, rcpttos, data):
- try:
- # Refresh the list of list names every time we process a message
- # since the set of mailing lists could have changed.
- listnames = set(getUtility(IListManager).names)
- # Parse the message data. If there are any defects in the
- # message, reject it right away; it's probably spam.
- msg = email.message_from_string(data, Message)
- msg.original_size = len(data)
- if msg.defects:
- return ERR_501
- msg['X-MailFrom'] = mailfrom
- message_id = msg['message-id']
- except Exception:
- elog.exception('LMTP message parsing')
- config.db.abort()
- return CRLF.join(ERR_451 for to in rcpttos)
- # RFC 2033 requires us to return a status code for every recipient.
- status = []
- # Now for each address in the recipients, parse the address to first
- # see if it's destined for a valid mailing list. If so, then queue
- # the message to the appropriate place and record a 250 status for
- # that recipient. If not, record a failure status for that recipient.
- for to in rcpttos:
- try:
- to = parseaddr(to)[1].lower()
- listname, subaddress, domain = split_recipient(to)
- qlog.debug('%s to: %s, list: %s, sub: %s, dom: %s',
- message_id, to, listname, subaddress, domain)
- listname += '@' + domain
- if listname not in listnames:
- status.append(ERR_550)
- continue
- # The recipient is a valid mailing list. Find the subaddress
- # if there is one, and set things up to enqueue to the proper
- # queue runner.
- queue = None
- msgdata = dict(listname=listname,
- original_size=msg.original_size)
- canonical_subaddress = SUBADDRESS_NAMES.get(subaddress)
- queue = SUBADDRESS_QUEUES.get(canonical_subaddress)
- if subaddress is None:
- # The message is destined for the mailing list.
- msgdata['to_list'] = True
- queue = 'in'
- elif canonical_subaddress is None:
- # The subaddress was bogus.
- elog.error('%s unknown sub-address: %s',
- message_id, subaddress)
- status.append(ERR_550)
- continue
- else:
- # A valid subaddress.
- msgdata['subaddress'] = canonical_subaddress
- if canonical_subaddress == 'owner':
- msgdata.update(dict(
- to_owner=True,
- envsender=config.mailman.site_owner,
- ))
- queue = 'in'
- # If we found a valid destination, enqueue the message and add
- # a success status for this recipient.
- if queue is not None:
- config.switchboards[queue].enqueue(msg, msgdata)
- qlog.debug('%s subaddress: %s, queue: %s',
- message_id, canonical_subaddress, queue)
- status.append('250 Ok')
- except Exception:
- elog.exception('Queue detection: %s', msg['message-id'])
- config.db.abort()
- status.append(ERR_550)
- # All done; returning this big status string should give the expected
- # response to the LMTP client.
- return CRLF.join(status)
-
- def run(self):
- """See `IRunner`."""
- asyncore.loop()
-
- def stop(self):
- """See `IRunner`."""
- asyncore.socket_map.clear()
- asyncore.close_all()
- self.close()
diff --git a/src/mailman/queue/maildir.py b/src/mailman/queue/maildir.py
deleted file mode 100644
index 07a89903c..000000000
--- a/src/mailman/queue/maildir.py
+++ /dev/null
@@ -1,193 +0,0 @@
-# Copyright (C) 2002-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Maildir pre-queue runner.
-
-Most MTAs can be configured to deliver messages to a `Maildir'[1]. This
-runner will read messages from a maildir's new/ directory and inject them into
-Mailman's qfiles/in directory for processing in the normal pipeline. This
-delivery mechanism contrasts with mail program delivery, where incoming
-messages end up in qfiles/in via the MTA executing the scripts/post script
-(and likewise for the other -aliases for each mailing list).
-
-The advantage to Maildir delivery is that it is more efficient; there's no
-need to fork an intervening program just to take the message from the MTA's
-standard output, to the qfiles/in directory.
-
-[1] http://cr.yp.to/proto/maildir.html
-
-We're going to use the :info flag == 1, experimental status flag for our own
-purposes. The :1 can be followed by one of these letters:
-
-- P means that MaildirRunner's in the process of parsing and enqueuing the
- message. If successful, it will delete the file.
-
-- X means something failed during the parse/enqueue phase. An error message
- will be logged to log/error and the file will be renamed <filename>:1,X.
- MaildirRunner will never automatically return to this file, but once the
- problem is fixed, you can manually move the file back to the new/ directory
- and MaildirRunner will attempt to re-process it. At some point we may do
- this automatically.
-
-See the variable USE_MAILDIR in Defaults.py.in for enabling this delivery
-mechanism.
-"""
-
-# NOTE: Maildir delivery is experimental in Mailman 2.1.
-
-import os
-import errno
-import logging
-
-from email.parser import Parser
-from email.utils import parseaddr
-
-from mailman.config import config
-from mailman.message import Message
-from mailman.queue import Runner, Switchboard
-
-
-log = logging.getLogger('mailman.error')
-
-# We only care about the listname and the subq as in listname@ or
-# listname-request@
-subqnames = ('admin', 'bounces', 'confirm', 'join', 'leave',
- 'owner', 'request', 'subscribe', 'unsubscribe')
-
-
-def getlistq(address):
- localpart, domain = address.split('@', 1)
- # TK: FIXME I only know configs of Postfix.
- if config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
- p = localpart.split(config.POSTFIX_VIRTUAL_SEPARATOR, 1)
- if len(p) == 2:
- localpart, domain = p
- l = localpart.split('-')
- if l[-1] in subqnames:
- listname = '-'.join(l[:-1])
- subq = l[-1]
- else:
- listname = localpart
- subq = None
- return listname, subq, domain
-
-
-
-class MaildirRunner(Runner):
- # This class is much different than most runners because it pulls files
- # of a different format than what scripts/post and friends leaves. The
- # files this runner reads are just single message files as dropped into
- # the directory by the MTA. This runner will read the file, and enqueue
- # it in the expected qfiles directory for normal processing.
- def __init__(self, slice=None, numslices=1):
- # Don't call the base class constructor, but build enough of the
- # underlying attributes to use the base class's implementation.
- self._stop = 0
- self._dir = os.path.join(config.MAILDIR_DIR, 'new')
- self._cur = os.path.join(config.MAILDIR_DIR, 'cur')
- self._parser = Parser(Message)
-
- def _one_iteration(self):
- # Refresh this each time through the list.
- listnames = list(config.list_manager.names)
- # Cruise through all the files currently in the new/ directory
- try:
- files = os.listdir(self._dir)
- except OSError, e:
- if e.errno <> errno.ENOENT:
- raise
- # Nothing's been delivered yet
- return 0
- for file in files:
- srcname = os.path.join(self._dir, file)
- dstname = os.path.join(self._cur, file + ':1,P')
- xdstname = os.path.join(self._cur, file + ':1,X')
- try:
- os.rename(srcname, dstname)
- except OSError, e:
- if e.errno == errno.ENOENT:
- # Some other MaildirRunner beat us to it
- continue
- log.error('Could not rename maildir file: %s', srcname)
- raise
- # Now open, read, parse, and enqueue this message
- try:
- fp = open(dstname)
- try:
- msg = self._parser.parse(fp)
- finally:
- fp.close()
- # Now we need to figure out which queue of which list this
- # message was destined for. See get_verp() in
- # mailman.app.bounces for why we do things this way.
- vals = []
- for header in ('delivered-to', 'envelope-to', 'apparently-to'):
- vals.extend(msg.get_all(header, []))
- for field in vals:
- to = parseaddr(field)[1].lower()
- if not to:
- continue
- listname, subq, domain = getlistq(to)
- listname = listname + '@' + domain
- if listname in listnames:
- break
- else:
- # As far as we can tell, this message isn't destined for
- # any list on the system. What to do?
- log.error('Message apparently not for any list: %s',
- xdstname)
- os.rename(dstname, xdstname)
- continue
- # BAW: blech, hardcoded
- msgdata = {'listname': listname}
- # -admin is deprecated
- if subq in ('bounces', 'admin'):
- queue = Switchboard('bounces', config.BOUNCEQUEUE_DIR)
- elif subq == 'confirm':
- msgdata['toconfirm'] = 1
- queue = Switchboard('command', config.CMDQUEUE_DIR)
- elif subq in ('join', 'subscribe'):
- msgdata['tojoin'] = 1
- queue = Switchboard('command', config.CMDQUEUE_DIR)
- elif subq in ('leave', 'unsubscribe'):
- msgdata['toleave'] = 1
- queue = Switchboard('command', config.CMDQUEUE_DIR)
- elif subq == 'owner':
- msgdata.update({
- 'toowner': True,
- 'envsender': config.SITE_OWNER_ADDRESS,
- 'pipeline': config.OWNER_PIPELINE,
- })
- queue = Switchboard('in', config.INQUEUE_DIR)
- elif subq is None:
- msgdata['tolist'] = 1
- queue = Switchboard('in', config.INQUEUE_DIR)
- elif subq == 'request':
- msgdata['torequest'] = 1
- queue = Switchboard('command', config.CMDQUEUE_DIR)
- else:
- log.error('Unknown sub-queue: %s', subq)
- os.rename(dstname, xdstname)
- continue
- queue.enqueue(msg, msgdata)
- os.unlink(dstname)
- except Exception, e:
- os.rename(dstname, xdstname)
- log.error('%s', e)
-
- def _clean_up(self):
- pass
diff --git a/src/mailman/queue/news.py b/src/mailman/queue/news.py
deleted file mode 100644
index a3d915244..000000000
--- a/src/mailman/queue/news.py
+++ /dev/null
@@ -1,167 +0,0 @@
-# Copyright (C) 2000-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""NNTP queue runner."""
-
-import re
-import email
-import socket
-import logging
-import nntplib
-
-from cStringIO import StringIO
-from lazr.config import as_host_port
-
-from mailman.config import config
-from mailman.interfaces.nntp import NewsModeration
-from mailman.queue import Runner
-
-COMMASPACE = ', '
-log = logging.getLogger('mailman.error')
-
-
-# Matches our Mailman crafted Message-IDs. See Utils.unique_message_id()
-# XXX The move to email.utils.make_msgid() breaks this.
-mcre = re.compile(r"""
- <mailman. # match the prefix
- \d+. # serial number
- \d+. # time in seconds since epoch
- \d+. # pid
- (?P<listname>[^@]+) # list's internal_name()
- @ # localpart@dom.ain
- (?P<hostname>[^>]+) # list's host_name
- > # trailer
- """, re.VERBOSE)
-
-
-
-class NewsRunner(Runner):
- def _dispose(self, mlist, msg, msgdata):
- # Make sure we have the most up-to-date state
- mlist.Load()
- if not msgdata.get('prepped'):
- prepare_message(mlist, msg, msgdata)
- try:
- # Flatten the message object, sticking it in a StringIO object
- fp = StringIO(msg.as_string())
- conn = None
- try:
- try:
- nntp_host, nntp_port = as_host_port(
- mlist.nntp_host, default_port=119)
- conn = nntplib.NNTP(nntp_host, nntp_port,
- readermode=True,
- user=config.nntp.username,
- password=config.nntp.password)
- conn.post(fp)
- except nntplib.error_temp, e:
- log.error('(NNTPDirect) NNTP error for list "%s": %s',
- mlist.internal_name(), e)
- except socket.error, e:
- log.error('(NNTPDirect) socket error for list "%s": %s',
- mlist.internal_name(), e)
- finally:
- if conn:
- conn.quit()
- except Exception, e:
- # Some other exception occurred, which we definitely did not
- # expect, so set this message up for requeuing.
- self._log(e)
- return True
- return False
-
-
-
-def prepare_message(mlist, msg, msgdata):
- # If the newsgroup is moderated, we need to add this header for the Usenet
- # software to accept the posting, and not forward it on to the n.g.'s
- # moderation address. The posting would not have gotten here if it hadn't
- # already been approved. 1 == open list, mod n.g., 2 == moderated
- if mlist.news_moderation in (NewsModeration.open_moderated,
- NewsModeration.moderated):
- del msg['approved']
- msg['Approved'] = mlist.posting_address
- # Should we restore the original, non-prefixed subject for gatewayed
- # messages? TK: We use stripped_subject (prefix stripped) which was
- # crafted in CookHeaders.py to ensure prefix was stripped from the subject
- # came from mailing list user.
- stripped_subject = msgdata.get('stripped_subject') \
- or msgdata.get('origsubj')
- if not mlist.news_prefix_subject_too and stripped_subject is not None:
- del msg['subject']
- msg['subject'] = stripped_subject
- # Add the appropriate Newsgroups: header
- ngheader = msg['newsgroups']
- if ngheader is not None:
- # See if the Newsgroups: header already contains our linked_newsgroup.
- # If so, don't add it again. If not, append our linked_newsgroup to
- # the end of the header list
- ngroups = [s.strip() for s in ngheader.split(',')]
- if mlist.linked_newsgroup not in ngroups:
- ngroups.append(mlist.linked_newsgroup)
- # Subtitute our new header for the old one.
- del msg['newsgroups']
- msg['Newsgroups'] = COMMASPACE.join(ngroups)
- else:
- # Newsgroups: isn't in the message
- msg['Newsgroups'] = mlist.linked_newsgroup
- # Note: We need to be sure two messages aren't ever sent to the same list
- # in the same process, since message ids need to be unique. Further, if
- # messages are crossposted to two Usenet-gated mailing lists, they each
- # need to have unique message ids or the nntpd will only accept one of
- # them. The solution here is to substitute any existing message-id that
- # isn't ours with one of ours, so we need to parse it to be sure we're not
- # looping.
- #
- # Our Message-ID format is <mailman.secs.pid.listname@hostname>
- msgid = msg['message-id']
- hackmsgid = True
- if msgid:
- mo = mcre.search(msgid)
- if mo:
- lname, hname = mo.group('listname', 'hostname')
- if lname == mlist.internal_name() and hname == mlist.host_name:
- hackmsgid = False
- if hackmsgid:
- del msg['message-id']
- msg['Message-ID'] = email.utils.make_msgid()
- # Lines: is useful
- if msg['Lines'] is None:
- # BAW: is there a better way?
- count = len(list(email.Iterators.body_line_iterator(msg)))
- msg['Lines'] = str(count)
- # Massage the message headers by remove some and rewriting others. This
- # woon't completely sanitize the message, but it will eliminate the bulk
- # of the rejections based on message headers. The NNTP server may still
- # reject the message because of other problems.
- for header in config.nntp.remove_headers.split():
- del msg[header]
- for rewrite_pairs in config.nntp.rewrite_duplicate_headers.splitlines():
- if len(rewrite_pairs.strip()) == 0:
- continue
- header, rewrite = rewrite_pairs.split()
- values = msg.get_all(header, [])
- if len(values) < 2:
- # We only care about duplicates
- continue
- del msg[header]
- # But keep the first one...
- msg[header] = values[0]
- for v in values[1:]:
- msg[rewrite] = v
- # Mark this message as prepared in case it has to be requeued
- msgdata['prepped'] = True
diff --git a/src/mailman/queue/outgoing.py b/src/mailman/queue/outgoing.py
deleted file mode 100644
index ed27f014c..000000000
--- a/src/mailman/queue/outgoing.py
+++ /dev/null
@@ -1,160 +0,0 @@
-# Copyright (C) 2000-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Outgoing queue runner."""
-
-import socket
-import logging
-
-from datetime import datetime
-from lazr.config import as_boolean, as_timedelta
-from zope.component import getUtility
-
-from mailman.config import config
-from mailman.interfaces.bounce import BounceContext, IBounceProcessor
-from mailman.interfaces.mailinglist import Personalization
-from mailman.interfaces.membership import ISubscriptionService
-from mailman.interfaces.mta import SomeRecipientsFailed
-from mailman.interfaces.pending import IPendings
-from mailman.queue import Runner
-from mailman.utilities.datetime import now
-from mailman.utilities.modules import find_name
-
-
-# This controls how often _do_periodic() will try to deal with deferred
-# permanent failures. It is a count of calls to _do_periodic()
-DEAL_WITH_PERMFAILURES_EVERY = 10
-
-log = logging.getLogger('mailman.error')
-smtp_log = logging.getLogger('mailman.smtp')
-
-
-
-class OutgoingRunner(Runner):
- """The outgoing queue runner."""
-
- def __init__(self, slice=None, numslices=1):
- super(OutgoingRunner, self).__init__(slice, numslices)
- # We look this function up only at startup time.
- self._func = find_name(config.mta.outgoing)
- # This prevents smtp server connection problems from filling up the
- # error log. It gets reset if the message was successfully sent, and
- # set if there was a socket.error.
- self._logged = False
- self._retryq = config.switchboards['retry']
-
- def _dispose(self, mlist, msg, msgdata):
- # See if we should retry delivery of this message again.
- deliver_after = msgdata.get('deliver_after', datetime.fromtimestamp(0))
- if now() < deliver_after:
- return True
- # Calculate whether we should VERP this message or not. The results of
- # this set the 'verp' key in the message metadata.
- interval = int(config.mta.verp_delivery_interval)
- if 'verp' in msgdata:
- # Honor existing settings.
- pass
- # If personalization is enabled for this list and we've configured
- # Mailman to always VERP personalized deliveries, then yes we VERP it.
- # Also, if personalization is /not/ enabled, but
- # verp_delivery_interval is set (and we've hit this interval), then
- # again, this message should be VERP'd. Otherwise, no.
- elif mlist.personalize != Personalization.none:
- if as_boolean(config.mta.verp_personalized_deliveries):
- msgdata['verp'] = True
- elif interval == 0:
- # Never VERP.
- msgdata['verp'] = False
- elif interval == 1:
- # VERP every time.
- msgdata['verp'] = True
- else:
- # VERP every 'interval' number of times.
- msgdata['verp'] = (mlist.post_id % interval == 0)
- try:
- self._func(mlist, msg, msgdata)
- self._logged = False
- except socket.error:
- # There was a problem connecting to the SMTP server. Log this
- # once, but crank up our sleep time so we don't fill the error
- # log.
- port = int(config.mta.smtp_port)
- if port == 0:
- port = 'smtp' # Log this just once.
- if not self._logged:
- log.error('Cannot connect to SMTP server %s on port %s',
- config.mta.smtp_host, port)
- self._logged = True
- return True
- except SomeRecipientsFailed as error:
- processor = getUtility(IBounceProcessor)
- # BAW: msg is the original message that failed delivery, not a
- # bounce message. This may be confusing if this is what's sent to
- # the user in the probe message. Maybe we should craft a
- # bounce-like message containing information about the permanent
- # SMTP failure?
- if 'probe_token' in msgdata:
- # This is a failure of our local MTA to deliver to a probe
- # message recipient. Register the bounce event for permanent
- # failures. Start by grabbing and confirming (i.e. removing)
- # the pendable record associated with this bounce token,
- # regardless of what address was actually failing.
- if len(error.permanent_failures) > 0:
- pended = getUtility(IPendings).confirm(
- msgdata['probe_token'])
- # It's possible the token has been confirmed out of the
- # database. Just ignore that.
- if pended is not None:
- member = getUtility(ISubscriptionService).get_member(
- pended['member_id'])
- processor.register(
- mlist, member.address.email, msg,
- BounceContext.probe)
- else:
- # Delivery failed at SMTP time for some or all of the
- # recipients. Permanent failures are registered as bounces,
- # but temporary failures are retried for later.
- for email in error.permanent_failures:
- processor.register(mlist, email, msg, BounceContext.normal)
- # Move temporary failures to the qfiles/retry queue which will
- # occasionally move them back here for another shot at
- # delivery.
- if error.temporary_failures:
- current_time = now()
- recipients = error.temporary_failures
- last_recip_count = msgdata.get('last_recip_count', 0)
- deliver_until = msgdata.get('deliver_until', current_time)
- if len(recipients) == last_recip_count:
- # We didn't make any progress. If we've exceeded the
- # configured retry period, log this failure and
- # discard the message.
- if current_time > deliver_until:
- smtp_log.error('Discarding message with '
- 'persistent temporary failures: '
- '{0}'.format(msg['message-id']))
- return False
- else:
- # We made some progress, so keep trying to delivery
- # this message for a while longer.
- deliver_until = current_time + as_timedelta(
- config.mta.delivery_retry_period)
- msgdata['last_recip_count'] = len(recipients)
- msgdata['deliver_until'] = deliver_until
- msgdata['recipients'] = recipients
- self._retryq.enqueue(msg, msgdata)
- # We've successfully completed handling of this message.
- return False
diff --git a/src/mailman/queue/pipeline.py b/src/mailman/queue/pipeline.py
deleted file mode 100644
index 099ebd032..000000000
--- a/src/mailman/queue/pipeline.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright (C) 2008-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""The pipeline queue runner.
-
-This runner's purpose is to take messages that have been approved for posting
-through the 'preparation pipeline'. This pipeline adds, deletes and modifies
-headers, calculates message recipients, and more.
-"""
-
-from mailman.core.pipelines import process
-from mailman.queue import Runner
-
-
-
-class PipelineRunner(Runner):
- def _dispose(self, mlist, msg, msgdata):
- # Process the message through the mailing list's pipeline.
- process(mlist, msg, msgdata, mlist.pipeline)
- # Do not keep this message queued.
- return False
diff --git a/src/mailman/queue/rest.py b/src/mailman/queue/rest.py
deleted file mode 100644
index 31e840a51..000000000
--- a/src/mailman/queue/rest.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# Copyright (C) 2009-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Start the administrative HTTP server."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'RESTRunner',
- ]
-
-
-import sys
-import errno
-import select
-import signal
-import logging
-
-from mailman.queue import Runner
-from mailman.rest.wsgiapp import make_server
-
-
-log = logging.getLogger('mailman.http')
-
-
-
-class RESTRunner(Runner):
- intercept_signals = False
-
- def run(self):
- log.info('Starting REST server')
- try:
- make_server().serve_forever()
- except KeyboardInterrupt:
- log.info('REST server interrupted')
- sys.exit(signal.SIGTERM)
- except select.error as (errcode, message):
- if errcode == errno.EINTR:
- log.info('REST server exiting')
- sys.exit(signal.SIGTERM)
- raise
- except:
- raise
diff --git a/src/mailman/queue/retry.py b/src/mailman/queue/retry.py
deleted file mode 100644
index 24aa7f82b..000000000
--- a/src/mailman/queue/retry.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright (C) 2003-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Retry delivery."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'RetryRunner',
- ]
-
-
-import time
-
-from mailman.config import config
-from mailman.queue import Runner
-
-
-
-class RetryRunner(Runner):
- """Retry delivery."""
-
- def _dispose(self, mlist, msg, msgdata):
- # Move the message to the out queue for another try.
- config.switchboards['outgoing'].enqueue(msg, msgdata)
- return False
-
- def _snooze(self, filecnt):
- # We always want to snooze.
- time.sleep(self.sleep_float)
diff --git a/src/mailman/queue/tests/__init__.py b/src/mailman/queue/tests/__init__.py
deleted file mode 100644
index e69de29bb..000000000
--- a/src/mailman/queue/tests/__init__.py
+++ /dev/null
diff --git a/src/mailman/queue/tests/test_bounce.py b/src/mailman/queue/tests/test_bounce.py
deleted file mode 100644
index 1946df50c..000000000
--- a/src/mailman/queue/tests/test_bounce.py
+++ /dev/null
@@ -1,236 +0,0 @@
-# Copyright (C) 2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Test the bounce queue runner."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'test_suite',
- ]
-
-
-import unittest
-
-from zope.component import getUtility
-
-from mailman.app.bounces import send_probe
-from mailman.app.lifecycle import create_list
-from mailman.config import config
-from mailman.interfaces.bounce import (
- BounceContext, IBounceProcessor, UnrecognizedBounceDisposition)
-from mailman.interfaces.member import MemberRole
-from mailman.interfaces.usermanager import IUserManager
-from mailman.queue.bounce import BounceRunner
-from mailman.testing.helpers import (
- LogFileMark,
- get_queue_messages,
- make_testable_runner,
- specialized_message_from_string as message_from_string)
-from mailman.testing.layers import ConfigLayer
-
-
-
-class TestBounceQueue(unittest.TestCase):
- """Test the bounce queue runner."""
-
- layer = ConfigLayer
-
- def setUp(self):
- self._mlist = create_list('test@example.com')
- self._bounceq = config.switchboards['bounces']
- self._runner = make_testable_runner(BounceRunner, 'bounces')
- self._anne = getUtility(IUserManager).create_address(
- 'anne@example.com')
- self._member = self._mlist.subscribe(self._anne, MemberRole.member)
- self._msg = message_from_string("""\
-From: mail-daemon@example.com
-To: test-bounces+anne=example.com@example.com
-Message-Id: <first>
-
-""")
- self._msgdata = dict(listname='test@example.com')
- self._processor = getUtility(IBounceProcessor)
- config.push('site owner', """
- [mailman]
- site_owner: postmaster@example.com
- """)
-
- def tearDown(self):
- config.pop('site owner')
-
- def test_does_no_processing(self):
- # If the mailing list does no bounce processing, the messages are
- # simply discarded.
- self._mlist.bounce_processing = False
- self._bounceq.enqueue(self._msg, self._msgdata)
- self._runner.run()
- self.assertEqual(len(get_queue_messages('bounces')), 0)
- self.assertEqual(len(list(self._processor.events)), 0)
-
- def test_verp_detection(self):
- # When we get a VERPd bounce, and we're doing processing, a bounce
- # event will be registered.
- self._bounceq.enqueue(self._msg, self._msgdata)
- self._runner.run()
- self.assertEqual(len(get_queue_messages('bounces')), 0)
- events = list(self._processor.events)
- self.assertEqual(len(events), 1)
- self.assertEqual(events[0].email, 'anne@example.com')
- self.assertEqual(events[0].list_name, 'test@example.com')
- self.assertEqual(events[0].message_id, '<first>')
- self.assertEqual(events[0].context, BounceContext.normal)
- self.assertEqual(events[0].processed, False)
-
- def test_nonfatal_verp_detection(self):
- # A VERPd bounce was received, but the error was nonfatal.
- nonfatal = message_from_string("""\
-From: mail-daemon@example.com
-To: test-bounces+anne=example.com@example.com
-Message-Id: <first>
-Content-Type: multipart/report; report-type=delivery-status; boundary=AAA
-MIME-Version: 1.0
-
---AAA
-Content-Type: message/delivery-status
-
-Action: delayed
-Original-Recipient: rfc822; somebody@example.com
-
---AAA--
-""")
- self._bounceq.enqueue(nonfatal, self._msgdata)
- self._runner.run()
- self.assertEqual(len(get_queue_messages('bounces')), 0)
- events = list(self._processor.events)
- self.assertEqual(len(events), 0)
-
- def test_verp_probe_bounce(self):
- # A VERP probe bounced. The primary difference here is that the
- # registered bounce event will have a different context. The
- # Message-Id will be different too, because of the way we're
- # simulating the probe bounce.
- #
- # Start be simulating a probe bounce.
- send_probe(self._member, self._msg)
- message = get_queue_messages('virgin')[0].msg
- bounce = message_from_string("""\
-To: {0}
-From: mail-daemon@example.com
-Message-Id: <second>
-
-""".format(message['From']))
- self._bounceq.enqueue(bounce, self._msgdata)
- self._runner.run()
- self.assertEqual(len(get_queue_messages('bounces')), 0)
- events = list(self._processor.events)
- self.assertEqual(len(events), 1)
- self.assertEqual(events[0].email, 'anne@example.com')
- self.assertEqual(events[0].list_name, 'test@example.com')
- self.assertEqual(events[0].message_id, '<second>')
- self.assertEqual(events[0].context, BounceContext.probe)
- self.assertEqual(events[0].processed, False)
-
- def test_nonverp_detectable_fatal_bounce(self):
- # Here's a bounce that is not VERPd, but which has a bouncing address
- # that can be parsed from a known bounce format. DSN is as good as
- # any, but we'll make the parsed address different for the fun of it.
- dsn = message_from_string("""\
-From: mail-daemon@example.com
-To: test-bounces@example.com
-Message-Id: <first>
-Content-Type: multipart/report; report-type=delivery-status; boundary=AAA
-MIME-Version: 1.0
-
---AAA
-Content-Type: message/delivery-status
-
-Action: fail
-Original-Recipient: rfc822; bart@example.com
-
---AAA--
-""")
- self._bounceq.enqueue(dsn, self._msgdata)
- self._runner.run()
- self.assertEqual(len(get_queue_messages('bounces')), 0)
- events = list(self._processor.events)
- self.assertEqual(len(events), 1)
- self.assertEqual(events[0].email, 'bart@example.com')
- self.assertEqual(events[0].list_name, 'test@example.com')
- self.assertEqual(events[0].message_id, '<first>')
- self.assertEqual(events[0].context, BounceContext.normal)
- self.assertEqual(events[0].processed, False)
-
- def test_nonverp_detectable_nonfatal_bounce(self):
- # Here's a bounce that is not VERPd, but which has a bouncing address
- # that can be parsed from a known bounce format. The bounce is
- # non-fatal so no bounce event is registered.
- dsn = message_from_string("""\
-From: mail-daemon@example.com
-To: test-bounces@example.com
-Message-Id: <first>
-Content-Type: multipart/report; report-type=delivery-status; boundary=AAA
-MIME-Version: 1.0
-
---AAA
-Content-Type: message/delivery-status
-
-Action: delayed
-Original-Recipient: rfc822; bart@example.com
-
---AAA--
-""")
- self._bounceq.enqueue(dsn, self._msgdata)
- self._runner.run()
- self.assertEqual(len(get_queue_messages('bounces')), 0)
- events = list(self._processor.events)
- self.assertEqual(len(events), 0)
-
- def test_no_detectable_bounce_addresses(self):
- # A bounce message was received, but no addresses could be detected.
- # A message will be logged in the bounce log though, and the message
- # can be forwarded to someone who can do something about it.
- self._mlist.forward_unrecognized_bounces_to = (
- UnrecognizedBounceDisposition.site_owner)
- bogus = message_from_string("""\
-From: mail-daemon@example.com
-To: test-bounces@example.com
-Message-Id: <third>
-
-""")
- self._bounceq.enqueue(bogus, self._msgdata)
- mark = LogFileMark('mailman.bounce')
- self._runner.run()
- self.assertEqual(len(get_queue_messages('bounces')), 0)
- events = list(self._processor.events)
- self.assertEqual(len(events), 0)
- line = mark.readline()
- self.assertEqual(
- line[-51:-1],
- 'Bounce message w/no discernable addresses: <third>')
- # Here's the forwarded message to the site owners.
- forwards = get_queue_messages('virgin')
- self.assertEqual(len(forwards), 1)
- self.assertEqual(forwards[0].msg['to'], 'postmaster@example.com')
-
-
-
-def test_suite():
- suite = unittest.TestSuite()
- suite.addTest(unittest.makeSuite(TestBounceQueue))
- return suite
diff --git a/src/mailman/queue/tests/test_outgoing.py b/src/mailman/queue/tests/test_outgoing.py
deleted file mode 100644
index a0fe407c8..000000000
--- a/src/mailman/queue/tests/test_outgoing.py
+++ /dev/null
@@ -1,549 +0,0 @@
-# Copyright (C) 2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Test the outgoing queue runner."""
-
-from __future__ import absolute_import, unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'test_suite',
- ]
-
-
-import os
-import socket
-import logging
-import unittest
-
-from contextlib import contextmanager
-from datetime import datetime, timedelta
-from lazr.config import as_timedelta
-from zope.component import getUtility
-
-from mailman.app.bounces import send_probe
-from mailman.app.lifecycle import create_list
-from mailman.config import config
-from mailman.interfaces.bounce import BounceContext, IBounceProcessor
-from mailman.interfaces.mailinglist import Personalization
-from mailman.interfaces.member import MemberRole
-from mailman.interfaces.mta import SomeRecipientsFailed
-from mailman.interfaces.pending import IPendings
-from mailman.interfaces.usermanager import IUserManager
-from mailman.queue.outgoing import OutgoingRunner
-from mailman.testing.helpers import (
- LogFileMark,
- get_queue_messages,
- make_testable_runner,
- specialized_message_from_string as message_from_string)
-from mailman.testing.layers import ConfigLayer, SMTPLayer
-from mailman.utilities.datetime import factory, now
-
-
-
-def run_once(qrunner):
- """Predicate for make_testable_runner().
-
- Ensures that the queue runner only runs once.
- """
- return True
-
-
-@contextmanager
-def temporary_config(name, settings):
- """Temporarily set a configuration (use in a with-statement)."""
- config.push(name, settings)
- try:
- yield
- finally:
- config.pop(name)
-
-
-
-class TestOnce(unittest.TestCase):
- """Test outgoing runner message disposition."""
-
- layer = SMTPLayer
-
- def setUp(self):
- self._mlist = create_list('test@example.com')
- self._outq = config.switchboards['out']
- self._runner = make_testable_runner(OutgoingRunner, 'out', run_once)
- self._msg = message_from_string("""\
-From: anne@example.com
-To: test@example.com
-Message-Id: <first>
-
-""")
- self._msgdata = {}
-
- def test_deliver_after(self):
- # When the metadata has a deliver_after key in the future, the queue
- # runner will re-enqueue the message rather than delivering it.
- deliver_after = now() + timedelta(days=10)
- self._msgdata['deliver_after'] = deliver_after
- self._outq.enqueue(self._msg, self._msgdata,
- tolist=True, listname='test@example.com')
- self._runner.run()
- items = get_queue_messages('out')
- self.assertEqual(len(items), 1)
- self.assertEqual(items[0].msgdata['deliver_after'], deliver_after)
- self.assertEqual(items[0].msg['message-id'], '<first>')
-
-
-
-captured_mlist = None
-captured_msg = None
-captured_msgdata = None
-
-def capture(mlist, msg, msgdata):
- global captured_mlist, captured_msg, captured_msgdata
- captured_mlist = mlist
- captured_msg = msg
- captured_msgdata = msgdata
-
-
-class TestVERPSettings(unittest.TestCase):
- """Test the selection of VERP based on various criteria."""
-
- layer = ConfigLayer
-
- def setUp(self):
- global captured_mlist, captured_msg, captured_msgdata
- # Push a config where actual delivery is handled by a dummy function.
- # We generally don't care what this does, since we're just testing the
- # setting of the 'verp' key in the metadata.
- config.push('fake outgoing', """
- [mta]
- outgoing: mailman.queue.tests.test_outgoing.capture
- """)
- # Reset the captured data.
- captured_mlist = None
- captured_msg = None
- captured_msgdata = None
- self._mlist = create_list('test@example.com')
- self._outq = config.switchboards['out']
- self._runner = make_testable_runner(OutgoingRunner, 'out')
- self._msg = message_from_string("""\
-From: anne@example.com
-To: test@example.com
-Message-Id: <first>
-
-""")
-
- def tearDown(self):
- config.pop('fake outgoing')
-
- def test_delivery_callback(self):
- # Test that the configuration variable calls the appropriate callback.
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
- self._runner.run()
- self.assertEqual(captured_mlist, self._mlist)
- self.assertEqual(captured_msg.as_string(), self._msg.as_string())
- # Of course, the message metadata will contain a bunch of keys added
- # by the processing. We don't really care about the details, so this
- # test is a good enough stand-in.
- self.assertEqual(captured_msgdata['listname'], 'test@example.com')
-
- def test_verp_in_metadata(self):
- # Test that if the metadata has a 'verp' key, it is unchanged.
- marker = 'yepper'
- msgdata = dict(verp=marker)
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- self._runner.run()
- self.assertEqual(captured_msgdata['verp'], marker)
-
- def test_personalized_individual_deliveries_verp(self):
- # When deliveries are personalized, and the configuration setting
- # indicates, messages will be VERP'd.
- msgdata = {}
- self._mlist.personalize = Personalization.individual
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- with temporary_config('personalize', """
- [mta]
- verp_personalized_deliveries: yes
- """):
- self._runner.run()
- self.assertTrue(captured_msgdata['verp'])
-
- def test_personalized_full_deliveries_verp(self):
- # When deliveries are personalized, and the configuration setting
- # indicates, messages will be VERP'd.
- msgdata = {}
- self._mlist.personalize = Personalization.full
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- with temporary_config('personalize', """
- [mta]
- verp_personalized_deliveries: yes
- """):
- self._runner.run()
- self.assertTrue(captured_msgdata['verp'])
-
- def test_personalized_deliveries_no_verp(self):
- # When deliveries are personalized, but the configuration setting
- # does not indicate, messages will not be VERP'd.
- msgdata = {}
- self._mlist.personalize = Personalization.full
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- self._runner.run()
- self.assertFalse('verp' in captured_msgdata)
-
- def test_verp_never(self):
- # Never VERP when the interval is zero.
- msgdata = {}
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- with temporary_config('personalize', """
- [mta]
- verp_delivery_interval: 0
- """):
- self._runner.run()
- self.assertEqual(captured_msgdata['verp'], False)
-
- def test_verp_always(self):
- # Always VERP when the interval is one.
- msgdata = {}
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- with temporary_config('personalize', """
- [mta]
- verp_delivery_interval: 1
- """):
- self._runner.run()
- self.assertEqual(captured_msgdata['verp'], True)
-
- def test_verp_on_interval_match(self):
- # VERP every so often, when the post_id matches.
- self._mlist.post_id = 5
- msgdata = {}
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- with temporary_config('personalize', """
- [mta]
- verp_delivery_interval: 5
- """):
- self._runner.run()
- self.assertEqual(captured_msgdata['verp'], True)
-
- def test_no_verp_on_interval_miss(self):
- # VERP every so often, when the post_id matches.
- self._mlist.post_id = 4
- msgdata = {}
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- with temporary_config('personalize', """
- [mta]
- verp_delivery_interval: 5
- """):
- self._runner.run()
- self.assertEqual(captured_msgdata['verp'], False)
-
-
-
-def raise_socket_error(mlist, msg, msgdata):
- raise socket.error
-
-
-class TestSocketError(unittest.TestCase):
- """Test socket.error occurring in the delivery function."""
-
- layer = ConfigLayer
-
- def setUp(self):
- # Push a config where actual delivery is handled by a dummy function.
- # We generally don't care what this does, since we're just testing the
- # setting of the 'verp' key in the metadata.
- config.push('fake outgoing', """
- [mta]
- outgoing: mailman.queue.tests.test_outgoing.raise_socket_error
- """)
- self._mlist = create_list('test@example.com')
- self._outq = config.switchboards['out']
- self._runner = make_testable_runner(OutgoingRunner, 'out', run_once)
- self._msg = message_from_string("""\
-From: anne@example.com
-To: test@example.com
-Message-Id: <first>
-
-""")
-
- def tearDown(self):
- config.pop('fake outgoing')
-
- def test_error_with_port_0(self):
- # Test the code path where a socket.error is raised in the delivery
- # function, and the MTA port is set to zero. The only real effect of
- # that is a log message. Start by opening the error log and reading
- # the current file position.
- error_log = logging.getLogger('mailman.error')
- filename = error_log.handlers[0].filename
- filepos = os.stat(filename).st_size
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
- with temporary_config('port 0', """
- [mta]
- smtp_port: 0
- """):
- self._runner.run()
- with open(filename) as fp:
- fp.seek(filepos)
- line = fp.readline()
- # The log line will contain a variable timestamp, the PID, and a
- # trailing newline. Ignore these.
- self.assertEqual(
- line[-53:-1],
- 'Cannot connect to SMTP server localhost on port smtp')
-
- def test_error_with_numeric_port(self):
- # Test the code path where a socket.error is raised in the delivery
- # function, and the MTA port is set to zero. The only real effect of
- # that is a log message. Start by opening the error log and reading
- # the current file position.
- mark = LogFileMark('mailman.error')
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
- with temporary_config('port 0', """
- [mta]
- smtp_port: 2112
- """):
- self._runner.run()
- line = mark.readline()
- # The log line will contain a variable timestamp, the PID, and a
- # trailing newline. Ignore these.
- self.assertEqual(
- line[-53:-1],
- 'Cannot connect to SMTP server localhost on port 2112')
-
-
-
-temporary_failures = []
-permanent_failures = []
-
-
-def raise_SomeRecipientsFailed(mlist, msg, msgdata):
- raise SomeRecipientsFailed(temporary_failures, permanent_failures)
-
-
-class TestSomeRecipientsFailed(unittest.TestCase):
- """Test socket.error occurring in the delivery function."""
-
- layer = ConfigLayer
-
- def setUp(self):
- global temporary_failures, permanent_failures
- del temporary_failures[:]
- del permanent_failures[:]
- self._processor = getUtility(IBounceProcessor)
- # Push a config where actual delivery is handled by a dummy function.
- # We generally don't care what this does, since we're just testing the
- # setting of the 'verp' key in the metadata.
- config.push('fake outgoing', """
- [mta]
- outgoing: mailman.queue.tests.test_outgoing.raise_SomeRecipientsFailed
- """)
- self._mlist = create_list('test@example.com')
- self._outq = config.switchboards['out']
- self._runner = make_testable_runner(OutgoingRunner, 'out', run_once)
- self._msg = message_from_string("""\
-From: anne@example.com
-To: test@example.com
-Message-Id: <first>
-
-""")
-
- def tearDown(self):
- config.pop('fake outgoing')
-
- def test_probe_failure(self):
- # When a probe message fails during SMTP, a bounce event is recorded
- # with the proper bounce context.
- anne = getUtility(IUserManager).create_address('anne@example.com')
- member = self._mlist.subscribe(anne, MemberRole.member)
- token = send_probe(member, self._msg)
- msgdata = dict(probe_token=token)
- permanent_failures.append('anne@example.com')
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- self._runner.run()
- events = list(self._processor.unprocessed)
- self.assertEqual(len(events), 1)
- event = events[0]
- self.assertEqual(event.list_name, 'test@example.com')
- self.assertEqual(event.email, 'anne@example.com')
- self.assertEqual(event.timestamp, datetime(2005, 8, 1, 7, 49, 23))
- self.assertEqual(event.message_id, '<first>')
- self.assertEqual(event.context, BounceContext.probe)
- self.assertEqual(event.processed, False)
-
- def test_confirmed_probe_failure(self):
- # This time, a probe also fails, but for some reason the probe token
- # has already been confirmed and no longer exists in the database.
- anne = getUtility(IUserManager).create_address('anne@example.com')
- member = self._mlist.subscribe(anne, MemberRole.member)
- token = send_probe(member, self._msg)
- getUtility(IPendings).confirm(token)
- msgdata = dict(probe_token=token)
- permanent_failures.append('anne@example.com')
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- self._runner.run()
- events = list(self._processor.unprocessed)
- self.assertEqual(len(events), 0)
-
- def test_probe_temporary_failure(self):
- # This time, a probe also fails, but the failures are temporary so
- # they are not registered.
- anne = getUtility(IUserManager).create_address('anne@example.com')
- member = self._mlist.subscribe(anne, MemberRole.member)
- token = send_probe(member, self._msg)
- getUtility(IPendings).confirm(token)
- msgdata = dict(probe_token=token)
- temporary_failures.append('anne@example.com')
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- self._runner.run()
- events = list(self._processor.unprocessed)
- self.assertEqual(len(events), 0)
-
- def test_one_permanent_failure(self):
- # Normal (i.e. non-probe) permanent failures just get registered.
- permanent_failures.append('anne@example.com')
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
- self._runner.run()
- events = list(self._processor.unprocessed)
- self.assertEqual(len(events), 1)
- self.assertEqual(events[0].email, 'anne@example.com')
- self.assertEqual(events[0].context, BounceContext.normal)
-
- def test_two_permanent_failures(self):
- # Two normal (i.e. non-probe) permanent failures just get registered.
- permanent_failures.append('anne@example.com')
- permanent_failures.append('bart@example.com')
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
- self._runner.run()
- events = list(self._processor.unprocessed)
- self.assertEqual(len(events), 2)
- self.assertEqual(events[0].email, 'anne@example.com')
- self.assertEqual(events[0].context, BounceContext.normal)
- self.assertEqual(events[1].email, 'bart@example.com')
- self.assertEqual(events[1].context, BounceContext.normal)
-
- def test_one_temporary_failure(self):
- # The first time there are temporary failures, the message just gets
- # put in the retry queue, but with some metadata to prevent infinite
- # retries.
- temporary_failures.append('cris@example.com')
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
- self._runner.run()
- events = list(self._processor.unprocessed)
- self.assertEqual(len(events), 0)
- items = get_queue_messages('retry')
- self.assertEqual(len(items), 1)
- self.assertEqual(self._msg.as_string(), items[0].msg.as_string())
- # The metadata has three keys which are used two decide whether the
- # next temporary failure should be retried.
- self.assertEqual(items[0].msgdata['last_recip_count'], 1)
- deliver_until = (datetime(2005, 8, 1, 7, 49, 23) +
- as_timedelta(config.mta.delivery_retry_period))
- self.assertEqual(items[0].msgdata['deliver_until'], deliver_until)
- self.assertEqual(items[0].msgdata['recipients'], ['cris@example.com'])
-
- def test_two_temporary_failures(self):
- # The first time there are temporary failures, the message just gets
- # put in the retry queue, but with some metadata to prevent infinite
- # retries.
- temporary_failures.append('cris@example.com')
- temporary_failures.append('dave@example.com')
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
- self._runner.run()
- events = list(self._processor.unprocessed)
- self.assertEqual(len(events), 0)
- items = get_queue_messages('retry')
- # There's still only one item in the retry queue, but the metadata
- # contains both temporary failures.
- self.assertEqual(len(items), 1)
- self.assertEqual(items[0].msgdata['last_recip_count'], 2)
- self.assertEqual(items[0].msgdata['recipients'],
- ['cris@example.com', 'dave@example.com'])
-
- def test_mixed_failures(self):
- # Some temporary and some permanent failures.
- permanent_failures.append('elle@example.com')
- permanent_failures.append('fred@example.com')
- temporary_failures.append('gwen@example.com')
- temporary_failures.append('herb@example.com')
- self._outq.enqueue(self._msg, {}, listname='test@example.com')
- self._runner.run()
- # Let's look at the permanent failures.
- events = list(self._processor.unprocessed)
- self.assertEqual(len(events), 2)
- self.assertEqual(events[0].email, 'elle@example.com')
- self.assertEqual(events[0].context, BounceContext.normal)
- self.assertEqual(events[1].email, 'fred@example.com')
- self.assertEqual(events[1].context, BounceContext.normal)
- # Let's look at the temporary failures.
- items = get_queue_messages('retry')
- self.assertEqual(len(items), 1)
- self.assertEqual(items[0].msgdata['recipients'],
- ['gwen@example.com', 'herb@example.com'])
-
- def test_no_progress_on_retries_within_retry_period(self):
- # Temporary failures cause queuing for a retry later on, unless no
- # progress is being made on the retries and we've tried for the
- # specified delivery retry period. This test ensures that even if no
- # progress is made, if the retry period hasn't expired, the message
- # will be requeued.
- temporary_failures.append('iona@example.com')
- temporary_failures.append('jeff@example.com')
- deliver_until = (datetime(2005, 8, 1, 7, 49, 23) +
- as_timedelta(config.mta.delivery_retry_period))
- msgdata = dict(last_recip_count=2,
- deliver_until=deliver_until)
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- self._runner.run()
- # The retry queue should have our message waiting to be retried.
- items = get_queue_messages('retry')
- self.assertEqual(len(items), 1)
- self.assertEqual(items[0].msgdata['deliver_until'], deliver_until)
- self.assertEqual(items[0].msgdata['recipients'],
- ['iona@example.com', 'jeff@example.com'])
-
- def test_no_progress_on_retries_with_expired_retry_period(self):
- # We've had temporary failures with no progress, and the retry period
- # has expired. In that case, a log entry is written and message is
- # discarded. There's nothing more that can be done.
- temporary_failures.append('kira@example.com')
- temporary_failures.append('lonn@example.com')
- retry_period = as_timedelta(config.mta.delivery_retry_period)
- deliver_until = datetime(2005, 8, 1, 7, 49, 23) + retry_period
- msgdata = dict(last_recip_count=2,
- deliver_until=deliver_until)
- self._outq.enqueue(self._msg, msgdata, listname='test@example.com')
- # Before the queue runner runs, several days pass.
- factory.fast_forward(retry_period.days + 1)
- mark = LogFileMark('mailman.smtp')
- self._runner.run()
- # There should be no message in the retry or outgoing queues.
- self.assertEqual(len(get_queue_messages('retry')), 0)
- self.assertEqual(len(get_queue_messages('out')), 0)
- # There should be a log message in the smtp log indicating that the
- # message has been discarded.
- line = mark.readline()
- self.assertEqual(
- line[-63:-1],
- 'Discarding message with persistent temporary failures: <first>')
-
-
-
-def test_suite():
- suite = unittest.TestSuite()
- suite.addTest(unittest.makeSuite(TestOnce))
- suite.addTest(unittest.makeSuite(TestVERPSettings))
- suite.addTest(unittest.makeSuite(TestSocketError))
- suite.addTest(unittest.makeSuite(TestSomeRecipientsFailed))
- return suite
diff --git a/src/mailman/queue/virgin.py b/src/mailman/queue/virgin.py
deleted file mode 100644
index 2dcdca910..000000000
--- a/src/mailman/queue/virgin.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Copyright (C) 1998-2011 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman is free software: you can redistribute it and/or modify it under
-# the terms of the GNU General Public License as published by the Free
-# Software Foundation, either version 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
-# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
-# more details.
-#
-# You should have received a copy of the GNU General Public License along with
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Virgin message queue runner.
-
-This qrunner handles messages that the Mailman system gives virgin birth to.
-E.g. acknowledgement responses to user posts or Replybot messages. They need
-to go through some minimal processing before they can be sent out to the
-recipient.
-"""
-
-from mailman.core.pipelines import process
-from mailman.queue import Runner
-
-
-
-class VirginRunner(Runner):
- def _dispose(self, mlist, msg, msgdata):
- # We need to fast track this message through any pipeline handlers
- # that touch it, e.g. especially cook-headers.
- msgdata['_fasttrack'] = True
- # Use the 'virgin' pipeline.
- process(mlist, msg, msgdata, 'virgin')
- # Do not keep this message queued.
- return False