# Copyright (C) 1998-2018 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.
"""Incoming queue runner."""
# 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 (-admin is a deprecated alias)
# 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 in a Majordomo-like fashion (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.
import sys
import os
from cStringIO import StringIO
from Mailman import mm_cfg
from Mailman import Errors
from Mailman import LockFile
from Mailman.Queue.Runner import Runner
from Mailman.Logging.Syslog import syslog
class IncomingRunner(Runner):
QDIR = mm_cfg.INQUEUE_DIR
def _dispose(self, mlist, msg, msgdata):
# Try to get the list lock.
try:
mlist.Lock(timeout=mm_cfg.LIST_LOCK_TIMEOUT)
except LockFile.TimeOutError:
# Oh well, try again later
return 1
# Process the message through a handler pipeline. The handler
# pipeline can actually come from one of three places: the message
# metadata, the mlist, or the global pipeline.
#
# If a message was requeued due to an uncaught exception, its metadata
# will contain the retry pipeline. Use this above all else.
# Otherwise, if the mlist has a `pipeline' attribute, it should be
# used. Final fallback is the global pipeline.
try:
pipeline = self._get_pipeline(mlist, msg, msgdata)
msgdata['pipeline'] = pipeline
more = self._dopipeline(mlist, msg, msgdata, pipeline)
if not more:
del msgdata['pipeline']
mlist.Save()
return more
finally:
mlist.Unlock()
# Overridable
def _get_pipeline(self, mlist, msg, msgdata):
# We must return a copy of the list, otherwise, the first message that
# flows through the pipeline will empty it out!
return msgdata.get('pipeline',
getattr(mlist, 'pipeline',
mm_cfg.GLOBAL_PIPELINE))[:]
def _dopipeline(self, mlist, msg, msgdata, pipeline):
while pipeline:
handler = pipeline.pop(0)
modname = 'Mailman.Handlers.' + handler
__import__(modname)
try:
pid = os.getpid()
sys.modules[modname].process(mlist, msg, msgdata)
# Failsafe -- a child may have leaked through.
if pid <> os.getpid():
syslog('error', 'child process leaked thru: %s', modname)
os._exit(1)
except Errors.DiscardMessage:
# Throw the message away; we need do nothing else with it.
# We do need to push the current handler back in the pipeline
# just in case the syslog call throws an exception and the
# message is shunted.
pipeline.insert(0, handler)
syslog('vette', """Message discarded, msgid: %s'
list: %s,
handler: %s""",
msg.get('message-id', 'n/a'),
mlist.real_name, handler)
return 0
except Errors.HoldMessage:
# Let the approval process take it from here. The message no
# longer needs to be queued.
return 0
except Errors.RejectMessage, e:
# Log this.
# We do need to push the current handler back in the pipeline
# just in case the syslog call or BounceMessage throws an
# exception and the message is shunted.
pipeline.insert(0, handler)
syslog('vette', """Message rejected, msgid: %s
list: %s,
handler: %s,
reason: %s""",
msg.get('message-id', 'n/a'),
mlist.real_name, handler, e.notice())
mlist.BounceMessage(msg, msgdata, e)
return 0
except:
# Push this pipeline module back on the stack, then re-raise
# the exception.
pipeline.insert(0, handler)
raise
# We've successfully completed handling of this message
return 0