Commit cd158259 authored by toshio's avatar toshio

Merge with upstream mailman

parents e3087545 5f69c4e7
......@@ -43,6 +43,7 @@ Table of Contents
src/mailman/model/docs/*
src/mailman/core/docs/*
src/mailman/app/docs/*
src/mailman/styles/docs/*
src/mailman/runners/docs/*
src/mailman/pipeline/docs/*
src/mailman/rest/docs/*
......
......@@ -35,7 +35,6 @@ It's easy to find out which queues are available.
digest
in
lmtp
maildir
news
out
pipeline
......
......@@ -61,11 +61,6 @@ class: mailman.runners.incoming.IncomingRunner
[runner.lmtp]
class: mailman.runners.lmtp.LMTPRunner
[runner.maildir]
class: mailman.runners.maildir.MaildirRunner
# This is still experimental.
start: no
[runner.news]
class: mailman.runners.news.NewsRunner
......
......@@ -36,6 +36,7 @@ Architecture
`owners_chain`. The default `built-in` chain is renamed to
`default-posting-chain` while the `built-in` pipeline is renamed
`default-posting-pipeline`.
* The experimental `maildir` runner is removed. Use LMTP.
Database
--------
......
......@@ -21,6 +21,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'TestUser',
]
......@@ -36,6 +37,8 @@ from mailman.utilities.datetime import now
class TestUser(unittest.TestCase):
"""Test users."""
layer = ConfigLayer
def setUp(self):
......
# Copyright (C) 2002-2012 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 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, and untested in
# Mailman 3. Instead, use LMTP delivery for Mailman 3.
import os
import errno
import logging
from email.parser import Parser
from email.utils import parseaddr
from mailman.config import config
from mailman.core.runner import Runner
from mailman.core.switchboard import Switchboard
from mailman.message import Message
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
......@@ -2,9 +2,10 @@
List styles
===========
List styles are a way to name and apply a canned collection of attribute
settings. Every style has a name, which must be unique within the context of
a specific style manager. There is usually only one global style manager.
List styles are a way to name and apply a template of attribute settings to
new mailing lists. Every style has a name, which must be unique within the
context of a specific style manager. There is usually only one global style
manager.
Styles also have a priority, which allows you to specify the order in which
multiple styles will be applied. A style has a `match` function which is used
......@@ -22,26 +23,26 @@ Let's start with a vanilla mailing list and a default style manager.
>>> from mailman.styles.manager import StyleManager
>>> style_manager = StyleManager()
>>> style_manager.populate()
>>> sorted(style.name for style in style_manager.styles)
['default']
>>> styles = sorted(style.name for style in style_manager.styles)
>>> len(styles)
1
>>> print styles[0]
default
The default style
=================
There is a default style which implements the legacy application of list
defaults from previous versions of Mailman. This style only matching a
mailing list when no other styles match, and it has the lowest priority. The
low priority means that it is matched last and if it matches, it is applied
last.
There is a default style which implements a legacy style roughly corresponding
to discussion mailing lists. This style matches when no other styles match,
and it has the lowest priority. The low priority means that it is matched
last and if it matches, it is applied last.
>>> default_style = style_manager.get('default')
>>> default_style.name
'default'
>>> print default_style.name
default
>>> default_style.priority
0
>>> sorted(style.name for style in style_manager.styles)
['default']
Given a mailing list, you can ask the style manager to find all the styles
that match the list. The registered styles will be sorted by decreasing
......@@ -49,8 +50,11 @@ priority and each style's ``match()`` method will be called in turn. The
sorted list of matching styles will be returned -- but not applied -- by the
style manager's ``lookup()`` method.
>>> [style.name for style in style_manager.lookup(mlist)]
['default']
>>> matched_styles = [style.name for style in style_manager.lookup(mlist)]
>>> len(matched_styles)
1
>>> print matched_styles[0]
default
Registering styles
......@@ -60,13 +64,13 @@ New styles must implement the ``IStyle`` interface.
>>> from zope.interface import implements
>>> from mailman.interfaces.styles import IStyle
>>> class TestStyle(object):
>>> class TestStyle:
... implements(IStyle)
... name = 'test'
... priority = 10
... def apply(self, mailing_list):
... # Just does something very simple.
... mailing_list.msg_footer = 'test footer'
... mailing_list.style_thing = 'thing 1'
... def match(self, mailing_list, styles):
... # Applies to any test list
... if 'test' in mailing_list.fqdn_listname:
......@@ -76,16 +80,20 @@ You can register a new style with the style manager.
>>> style_manager.register(TestStyle())
And now if you lookup matching styles, you should find only the new test
And now if you look up matching styles, you should find only the new test
style. This is because the default style only gets applied when no other
styles match the mailing list.
>>> sorted(style.name for style in style_manager.lookup(mlist))
[u'test']
>>> matched_styles = sorted(
... style.name for style in style_manager.lookup(mlist))
>>> len(matched_styles)
1
>>> print matched_styles[0]
test
>>> for style in style_manager.lookup(mlist):
... style.apply(mlist)
>>> print mlist.msg_footer
test footer
>>> print mlist.style_thing
thing 1
Style priority
......@@ -101,16 +109,16 @@ applied last.
... priority = 5
... # Use the base class's match() method.
... def apply(self, mailing_list):
... mailing_list.msg_footer = 'another footer'
... mailing_list.style_thing = 'thing 2'
>>> mlist.msg_footer = ''
>>> mlist.msg_footer
u''
>>> mlist.style_thing = 'thing 0'
>>> print mlist.style_thing
thing 0
>>> style_manager.register(AnotherTestStyle())
>>> for style in style_manager.lookup(mlist):
... style.apply(mlist)
>>> print mlist.msg_footer
another footer
>>> print mlist.style_thing
thing 2
You can change the priority of a style, and if you reapply the styles, they
will take effect in the new priority order.
......@@ -121,8 +129,8 @@ will take effect in the new priority order.
>>> style_2.priority = 10
>>> for style in style_manager.lookup(mlist):
... style.apply(mlist)
>>> print mlist.msg_footer
test footer
>>> print mlist.style_thing
thing 1
Unregistering styles
......@@ -131,32 +139,9 @@ Unregistering styles
You can unregister a style, making it unavailable in the future.
>>> style_manager.unregister(style_2)
>>> sorted(style.name for style in style_manager.lookup(mlist))
[u'test']
Corner cases
============
If you register a style with the same name as an already registered style, you
get an exception.
>>> style_manager.register(TestStyle())
Traceback (most recent call last):
...
DuplicateStyleError: test
If you try to register an object that isn't a style, you get an exception.
>>> style_manager.register(object())
Traceback (most recent call last):
...
DoesNotImplement: An object does not implement interface
<InterfaceClass mailman.interfaces.styles.IStyle>
If you try to unregister a style that isn't registered, you get an exception.
>>> style_manager.unregister(style_2)
Traceback (most recent call last):
...
KeyError: u'another'
>>> matched_styles = sorted(
... style.name for style in style_manager.lookup(mlist))
>>> len(matched_styles)
1
>>> print matched_styles[0]
test
# Copyright (C) 2012 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 styles."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'TestStyle',
]
import unittest
from zope.component import getUtility
from zope.interface import implements
from zope.interface.exceptions import DoesNotImplement
from mailman.interfaces.styles import (
DuplicateStyleError, IStyle, IStyleManager)
from mailman.testing.layers import ConfigLayer
class DummyStyle:
implements(IStyle)
name = 'dummy'
priority = 1
def apply(self, mlist):
pass
def match(self, mlist, styles):
styles.append(self)
class TestStyle(unittest.TestCase):
"""Test styles."""
layer = ConfigLayer
def setUp(self):
self.manager = getUtility(IStyleManager)
def test_register_style_again(self):
# Registering a style with the same name as a previous style raises an
# exception.
self.manager.register(DummyStyle())
try:
self.manager.register(DummyStyle())
except DuplicateStyleError:
pass
else:
raise AssertionError('DuplicateStyleError exception expected')
def test_register_a_non_style(self):
# You can't register something that doesn't implement the IStyle
# interface.
try:
self.manager.register(object())
except DoesNotImplement:
pass
else:
raise AssertionError('DoesNotImplement exception expected')
def test_unregister_a_non_registered_style(self):
# You cannot unregister a style that hasn't yet been registered.
try:
self.manager.unregister(DummyStyle())
except KeyError:
pass
else:
raise AssertionError('KeyError expected')
......@@ -45,9 +45,6 @@ max_restarts: 1
[runner.lmtp]
max_restarts: 1
[runner.maildir]
max_restarts: 1
[runner.news]
max_restarts: 1
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment