MaildirRunner.py   [plain text]


# Copyright (C) 2002 by the Free Software Foundation, Inc.
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

"""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 re
import errno

from email.Parser import Parser
from email.Utils import parseaddr

from Mailman import mm_cfg
from Mailman import Utils
from Mailman.Message import Message
from Mailman.Queue.Runner import Runner
from Mailman.Queue.sbcache import get_switchboard
from Mailman.Logging.Syslog import syslog

# We only care about the listname and the subq as in listname@ or
# listname-request@
lre = re.compile(r"""
 ^                        # start of string
 (?P<listname>[^-@]+)     # listname@ or listname-subq@
 (?:                      # non-grouping
   -                      # dash separator
   (?P<subq>[^-+@]+)      # everything up to + or - or @
 )?                       # if it exists
 """, re.VERBOSE | re.IGNORECASE)



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(mm_cfg.MAILDIR_DIR, 'new')
        self._cur = os.path.join(mm_cfg.MAILDIR_DIR, 'cur')
        self._parser = Parser(Message)

    def _oneloop(self):
        # Refresh this each time through the list.  BAW: could be too
        # expensive.
        listnames = Utils.list_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
                syslog('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 verp_bounce() in
                # BounceRunner.py 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]
                    if not to:
                        continue
                    mo = lre.match(to)
                    if not mo:
                        # This isn't an address we care about
                        continue
                    listname, subq = mo.group('listname', 'subq')
                    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?
                    syslog('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 = get_switchboard(mm_cfg.BOUNCEQUEUE_DIR)
                elif subq == 'confirm':
                    msgdata['toconfirm'] = 1
                    queue = get_switchboard(mm_cfg.CMDQUEUE_DIR)
                elif subq in ('join', 'subscribe'):
                    msgdata['tojoin'] = 1
                    queue = get_switchboard(mm_cfg.CMDQUEUE_DIR)
                elif subq in ('leave', 'unsubscribe'):
                    msgdata['toleave'] = 1
                    queue = get_switchboard(mm_cfg.CMDQUEUE_DIR)
                elif subq == 'owner':
                    msgdata.update({
                        'toowner': 1,
                        'envsender': Utils.get_site_email(extra='bounces'),
                        'pipeline': mm_cfg.OWNER_PIPELINE,
                        })
                    queue = get_switchboard(mm_cfg.INQUEUE_DIR)
                elif subq is None:
                    msgdata['tolist'] = 1
                    queue = get_switchboard(mm_cfg.INQUEUE_DIR)
                elif subq == 'request':
                    msgdata['torequest'] = 1
                    queue = get_switchboard(mm_cfg.CMDQUEUE_DIR)
                else:
                    syslog('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)
                syslog('error', str(e))

    def _cleanup(self):
        pass