summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.txt20
-rw-r--r--docs/ACKNOWLEDGMENTS.txt14
-rw-r--r--mailman/Defaults.py16
-rw-r--r--mailman/app/membership.py92
-rw-r--r--mailman/app/moderator.py17
-rw-r--r--mailman/app/notifications.py134
-rw-r--r--mailman/archiving/__init__.py31
-rw-r--r--mailman/archiving/mailarchive.py87
-rw-r--r--mailman/archiving/mhonarc.py95
-rw-r--r--mailman/archiving/pipermail.py (renamed from mailman/app/archiving.py)63
-rw-r--r--mailman/archiving/prototype.py75
-rw-r--r--mailman/bin/docs/master.txt2
-rw-r--r--mailman/bin/set_members.py6
-rw-r--r--mailman/bin/testall.py12
-rw-r--r--mailman/commands/__init__.py1
-rw-r--r--mailman/commands/docs/join.txt60
-rw-r--r--mailman/commands/join.py16
-rw-r--r--mailman/configuration.py1
-rw-r--r--mailman/docs/archivers.txt183
-rw-r--r--mailman/docs/pipelines.txt7
-rw-r--r--mailman/initialize.py2
-rw-r--r--mailman/interfaces/archiver.py29
-rw-r--r--mailman/interfaces/mailinglist.py2
-rw-r--r--mailman/loginit.py1
-rw-r--r--mailman/pipeline/cook_headers.py25
-rw-r--r--mailman/pipeline/docs/archives.txt6
-rw-r--r--mailman/pipeline/docs/cook-headers.txt2
-rw-r--r--mailman/pipeline/docs/digests.txt2
-rw-r--r--mailman/pipeline/scrubber.py4
-rw-r--r--mailman/queue/archive.py3
-rw-r--r--mailman/queue/docs/archiver.txt2
-rw-r--r--mailman/queue/docs/command.txt4
-rw-r--r--mailman/queue/docs/incoming.txt4
-rw-r--r--mailman/queue/docs/lmtp.txt2
-rw-r--r--mailman/queue/docs/outgoing.txt31
-rw-r--r--mailman/testing/__init__.py0
-rw-r--r--mailman/testing/helpers.py (renamed from mailman/tests/helpers.py)8
-rw-r--r--mailman/testing/smtplistener.py (renamed from mailman/tests/smtplistener.py)6
-rw-r--r--mailman/testing/testing.cfg.in (renamed from mailman/tests/testing.cfg.in)5
-rw-r--r--mailman/tests/test_documentation.py11
-rw-r--r--setup.py8
41 files changed, 881 insertions, 208 deletions
diff --git a/README.txt b/README.txt
index a415aa076..a09ed5812 100644
--- a/README.txt
+++ b/README.txt
@@ -126,14 +126,18 @@ FOR MORE INFORMATION
http://www.python.org/cgi-bin/faqw-mm.py
- There is also a wiki for more community-driven information:
+ There is a wiki for more community-driven information:
http://wiki.list.org
- Chris Kolar has made a list owner-oriented manual available from
- the following URL
+ The wiki includes the online FAQ maintained by the Mailman community,
+ which contains a vast amount of information:
- http://www.imsa.edu/~ckolar/mailman/
+ http://wiki.list.org/display/DOC/Frequently+Asked+Questions
+
+ As well as links to further documentation:
+
+ http://wiki.list.org/display/DOC/
There are also several mailing lists that can be used as resources
to help you get going with Mailman.
@@ -145,14 +149,6 @@ FOR MORE INFORMATION
http://mail.python.org/mailman/listinfo/mailman-users
- Listowners
- This mailing list with a non-technical focus, specifically for
- discussions from the perspective of listowners and moderators who do
- not have "shell access" to the mailing list server where the Mailman
- software runs.
-
- http://listowner.org
-
Mailman-Announce
A read-only list for release announcements an other important news.
diff --git a/docs/ACKNOWLEDGMENTS.txt b/docs/ACKNOWLEDGMENTS.txt
index 44e7d5c67..4c7187422 100644
--- a/docs/ACKNOWLEDGMENTS.txt
+++ b/docs/ACKNOWLEDGMENTS.txt
@@ -90,6 +90,7 @@ in answering questions on mailman-users.
Maximillian Dornseif
Fred Drake
Maxim Dzumanenko
+ Piarres Beobide Egaña
Rob Ellis
Kerem Erkan
Fil
@@ -124,6 +125,7 @@ in answering questions on mailman-users.
Matthias Juchem
Tamito KAJIYAMA
Nino Katic
+ SHIGENO Kazutaka
Ashley M. Kirchner
Matthias Klose
Harald Koch
@@ -145,28 +147,28 @@ in answering questions on mailman-users.
Gergely Madarasz
Luca Maranzano
John A. Martin
+ Andrew Martynov
+ Jason R. Mastaler
Michael Mclay
Michael Meltzer
Marc MERLIN
Nigel Metheringham
Dan Mick
Garey Mills
+ Martin Mokrejs
Michael Fischer v. Mollard
David Martínez Moreno
+ Dirk Mueller
Jonas Muerer
Erik Myllymaki
Balazs Nagy
+ Moritz Naumann
Dale Newfield
Hrvoje Niksic
Les Niles
Mike Noyes
David B. O'Donnell
Timothy O'Malley
- Andrew Martynov
- Jason R. Mastaler
- Michael Meltzer
- Martin Mokrejs
- Dirk Mueller
"office"
Dan Ohnesorg
Gerald Oskoboiny
@@ -176,7 +178,6 @@ in answering questions on mailman-users.
Chris Pepper
Tim Peters
Joe Peterson
- Piarres Beobide Egaña
PieterB
Rodolfo Pilas
Skye Poier
@@ -201,7 +202,6 @@ in answering questions on mailman-users.
Bartosz Sawicki
Kai Schaetzl
Karoly Segesdi
- SHIGENO Kazutaka
Gleydson Mazioli da Silva
Pasi Sjöholm
Chris Snell
diff --git a/mailman/Defaults.py b/mailman/Defaults.py
index 43bf7a3c3..6fa457425 100644
--- a/mailman/Defaults.py
+++ b/mailman/Defaults.py
@@ -220,6 +220,22 @@ CGIEXT = ''
# - $fqdn_listname -- the long name of the list being accessed
PUBLIC_ARCHIVE_URL = 'http://$hostname/pipermail/$fqdn_listname'
+# The public Mail-Archive.com service's base url.
+MAIL_ARCHIVE_BASEURL = 'http://go.mail-archive.com/'
+# The posting address for the Mail-Archive.com service
+MAIL_ARCHIVE_RECIPIENT = 'archive@mail-archive.com'
+
+# The command for archiving to a local MHonArc instance.
+MHONARC_COMMAND = """\
+/usr/bin/mhonarc \
+-add \
+-dbfile $PRIVATE_ARCHIVE_FILE_DIR/${listname}.mbox/mhonarc.db \
+-outdir $VAR_DIR/mhonarc/${listname} \
+-stderr $LOG_DIR/mhonarc \
+-stdout $LOG_DIR/mhonarc \
+-spammode \
+-umask 022"""
+
# Are archives on or off by default?
DEFAULT_ARCHIVE = On
diff --git a/mailman/app/membership.py b/mailman/app/membership.py
index 19f1d5e8f..227a85003 100644
--- a/mailman/app/membership.py
+++ b/mailman/app/membership.py
@@ -23,10 +23,8 @@ __metaclass__ = type
__all__ = [
'add_member',
'delete_member',
- 'send_goodbye_message',
- 'send_welcome_message',
]
-
+
from email.utils import formataddr
@@ -34,6 +32,7 @@ from mailman import Errors
from mailman import Message
from mailman import Utils
from mailman import i18n
+from mailman.app.notifications import send_goodbye_message
from mailman.configuration import config
from mailman.interfaces import AlreadySubscribedError, DeliveryMode, MemberRole
@@ -41,26 +40,25 @@ _ = i18n._
-def add_member(mlist, address, realname, password, delivery_mode, language,
- ack=None, admin_notif=None, text=''):
+def add_member(mlist, address, realname, password, delivery_mode, language):
"""Add a member right now.
The member's subscription must be approved by whatever policy the list
enforces.
- ack is a flag that specifies whether the user should get an
- acknowledgement of their being subscribed. Default is to use the
- list's default flag value.
-
- admin_notif is a flag that specifies whether the list owner should get
- an acknowledgement of this subscription. Default is to use the list's
- default flag value.
+ :param mlist: the mailing list to add the member to
+ :type mlist: IMailingList
+ :param address: the address to subscribe
+ :type address: string
+ :param realname: the subscriber's full name
+ :type realname: string
+ :param password: the subscriber's password
+ :type password: string
+ :param delivery_mode: the delivery mode the subscriber has chosen
+ :type delivery_mode: DeliveryMode
+ :param language: the language that the subscriber is going to use
+ :type language: string
"""
- # Set up default flag values
- if ack is None:
- ack = mlist.send_welcome_msg
- if admin_notif is None:
- admin_notif = mlist.admin_notify_mchanges
# Let's be extra cautious.
Utils.ValidateEmail(address)
if mlist.members.get_member(address) is not None:
@@ -110,53 +108,6 @@ def add_member(mlist, address, realname, password, delivery_mode, language,
member.preferences.delivery_mode = delivery_mode
## mlist.setMemberOption(email, config.Moderate,
## mlist.default_member_moderation)
- # Send notifications.
- if ack:
- send_welcome_message(mlist, address, language, delivery_mode, text)
- if admin_notif:
- with i18n.using_language(mlist.preferred_language):
- subject = _('$mlist.real_name subscription notification')
- if isinstance(realname, unicode):
- realname = realname.encode(Utils.GetCharSet(language), 'replace')
- text = Utils.maketext(
- 'adminsubscribeack.txt',
- {'listname' : mlist.real_name,
- 'member' : formataddr((realname, address)),
- }, mlist=mlist)
- msg = Message.OwnerNotification(mlist, subject, text)
- msg.send(mlist)
-
-
-
-def send_welcome_message(mlist, address, language, delivery_mode, text=''):
- if mlist.welcome_msg:
- welcome = Utils.wrap(mlist.welcome_msg) + '\n'
- else:
- welcome = ''
- # Find the IMember object which is subscribed to the mailing list, because
- # from there, we can get the member's options url.
- member = mlist.members.get_member(address)
- options_url = member.options_url
- # Get the text from the template.
- text += Utils.maketext(
- 'subscribeack.txt', {
- 'real_name' : mlist.real_name,
- 'posting_address' : mlist.fqdn_listname,
- 'listinfo_url' : mlist.script_url('listinfo'),
- 'optionsurl' : options_url,
- 'request_address' : mlist.request_address,
- 'welcome' : welcome,
- }, lang=language, mlist=mlist)
- if delivery_mode is not DeliveryMode.regular:
- digmode = _(' (Digest mode)')
- else:
- digmode = ''
- msg = Message.UserNotification(
- address, mlist.request_address,
- _('Welcome to the "$mlist.real_name" mailing list${digmode}'),
- text, language)
- msg['X-No-Archive'] = 'yes'
- msg.send(mlist, verp=config.VERP_PERSONALIZED_DELIVERIES)
@@ -184,16 +135,3 @@ def delete_member(mlist, address, admin_notif=None, userack=None):
}, mlist=mlist)
msg = Message.OwnerNotification(mlist, subject, text)
msg.send(mlist)
-
-
-
-def send_goodbye_message(mlist, address, language):
- if mlist.goodbye_msg:
- goodbye = Utils.wrap(mlist.goodbye_msg) + '\n'
- else:
- goodbye = ''
- msg = Message.UserNotification(
- address, mlist.bounces_address,
- _('You have been unsubscribed from the $mlist.real_name mailing list'),
- goodbye, language)
- msg.send(mlist, verp=config.VERP_PERSONALIZED_DELIVERIES)
diff --git a/mailman/app/moderator.py b/mailman/app/moderator.py
index 8b2db9ef7..6cd32c562 100644
--- a/mailman/app/moderator.py
+++ b/mailman/app/moderator.py
@@ -38,8 +38,11 @@ from mailman import Message
from mailman import Utils
from mailman import i18n
from mailman.app.membership import add_member, delete_member
+from mailman.app.notifications import (
+ send_admin_subscription_notice, send_welcome_message)
from mailman.configuration import config
from mailman.interfaces import Action, DeliveryMode, RequestType
+from mailman.interfaces.member import AlreadySubscribedError
from mailman.queue import Switchboard
_ = i18n._
@@ -237,13 +240,21 @@ def handle_subscription(mlist, id, action, comment=None):
delivery_mode = DeliveryMode(enum_value)
address = data['address']
realname = data['realname']
+ language = data['language']
+ password = data['password']
try:
- add_member(mlist, address, realname, data['password'],
- delivery_mode, data['language'])
- except Errors.AlreadySubscribedError:
+ add_member(mlist, address, realname, password,
+ delivery_mode, language)
+ except AlreadySubscribedError:
# The address got subscribed in some other way after the original
# request was made and accepted.
pass
+ else:
+ if mlist.send_welcome_msg:
+ send_welcome_message(mlist, address, language, delivery_mode)
+ if mlist.admin_notify_mchanges:
+ send_admin_subscription_notice(
+ mlist, address, realname, language)
slog.info('%s: new %s, %s %s', mlist.fqdn_listname,
delivery_mode, formataddr((realname, address)),
'via admin approval')
diff --git a/mailman/app/notifications.py b/mailman/app/notifications.py
new file mode 100644
index 000000000..74068c0c2
--- /dev/null
+++ b/mailman/app/notifications.py
@@ -0,0 +1,134 @@
+# Copyright (C) 2007-2008 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.
+
+"""Sending notifications."""
+
+from __future__ import with_statement
+
+__metaclass__ = type
+__all__ = [
+ 'send_admin_subscription_notice',
+ 'send_goodbye_message',
+ 'send_welcome_message',
+ ]
+
+
+from email.utils import formataddr
+
+from mailman import Message
+from mailman import Utils
+from mailman import i18n
+from mailman.configuration import config
+from mailman.interfaces.member import DeliveryMode
+
+_ = i18n._
+
+
+
+def send_welcome_message(mlist, address, language, delivery_mode, text=''):
+ """Send a welcome message to a subscriber.
+
+ Prepending to the standard welcome message template is the mailing list's
+ welcome message, if there is one.
+
+ :param mlist: the mailing list
+ :type mlist: IMailingList
+ :param address: The address to respond to
+ :type address: string
+ :param language: the language of the response
+ :type language: string
+ :param delivery_mode: the type of delivery the subscriber is getting
+ :type delivery_mode: DeliveryMode
+ """
+ if mlist.welcome_msg:
+ welcome = Utils.wrap(mlist.welcome_msg) + '\n'
+ else:
+ welcome = ''
+ # Find the IMember object which is subscribed to the mailing list, because
+ # from there, we can get the member's options url.
+ member = mlist.members.get_member(address)
+ options_url = member.options_url
+ # Get the text from the template.
+ text += Utils.maketext(
+ 'subscribeack.txt', {
+ 'real_name' : mlist.real_name,
+ 'posting_address' : mlist.fqdn_listname,
+ 'listinfo_url' : mlist.script_url('listinfo'),
+ 'optionsurl' : options_url,
+ 'request_address' : mlist.request_address,
+ 'welcome' : welcome,
+ }, lang=language, mlist=mlist)
+ if delivery_mode is not DeliveryMode.regular:
+ digmode = _(' (Digest mode)')
+ else:
+ digmode = ''
+ msg = Message.UserNotification(
+ address, mlist.request_address,
+ _('Welcome to the "$mlist.real_name" mailing list${digmode}'),
+ text, language)
+ msg['X-No-Archive'] = 'yes'
+ msg.send(mlist, verp=config.VERP_PERSONALIZED_DELIVERIES)
+
+
+
+def send_goodbye_message(mlist, address, language):
+ """Send a goodbye message to a subscriber.
+
+ Prepending to the standard goodbye message template is the mailing list's
+ goodbye message, if there is one.
+
+ :param mlist: the mailing list
+ :type mlist: IMailingList
+ :param address: The address to respond to
+ :type address: string
+ :param language: the language of the response
+ :type language: string
+ """
+ if mlist.goodbye_msg:
+ goodbye = Utils.wrap(mlist.goodbye_msg) + '\n'
+ else:
+ goodbye = ''
+ msg = Message.UserNotification(
+ address, mlist.bounces_address,
+ _('You have been unsubscribed from the $mlist.real_name mailing list'),
+ goodbye, language)
+ msg.send(mlist, verp=config.VERP_PERSONALIZED_DELIVERIES)
+
+
+
+def send_admin_subscription_notice(mlist, address, full_name, language):
+ """Send the list administrators a subscription notice.
+
+ :param mlist: the mailing list
+ :type mlist: IMailingList
+ :param address: the address being subscribed
+ :type address: string
+ :param full_name: the name of the subscriber
+ :type full_name: string
+ :param language: the language of the address's realname
+ :type language: string
+ """
+ with i18n.using_language(mlist.preferred_language):
+ subject = _('$mlist.real_name subscription notification')
+ full_name = full_name.encode(Utils.GetCharSet(language), 'replace')
+ text = Utils.maketext(
+ 'adminsubscribeack.txt',
+ {'listname' : mlist.real_name,
+ 'member' : formataddr((full_name, address)),
+ }, mlist=mlist)
+ msg = Message.OwnerNotification(mlist, subject, text)
+ msg.send(mlist)
diff --git a/mailman/archiving/__init__.py b/mailman/archiving/__init__.py
new file mode 100644
index 000000000..b9ef686a1
--- /dev/null
+++ b/mailman/archiving/__init__.py
@@ -0,0 +1,31 @@
+# Copyright (C) 2008 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.
+
+__metaclass__ = type
+__all__ = [
+ 'initialize',
+ ]
+
+
+from mailman.app.plugins import get_plugins
+from mailman.configuration import config
+
+
+def initialize():
+ """Initialize archivers."""
+ for archiver in get_plugins('mailman.archiver'):
+ config.archivers[archiver.name] = archiver
diff --git a/mailman/archiving/mailarchive.py b/mailman/archiving/mailarchive.py
new file mode 100644
index 000000000..fb78a2257
--- /dev/null
+++ b/mailman/archiving/mailarchive.py
@@ -0,0 +1,87 @@
+# Copyright (C) 2008 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.
+
+"""The Mail-Archive.com archiver."""
+
+__metaclass__ = type
+__all__ = [
+ 'MailArchive',
+ ]
+
+
+import hashlib
+
+from base64 import urlsafe_b64encode
+from urllib import quote
+from urlparse import urljoin
+from zope.interface import implements
+
+from mailman.configuration import config
+from mailman.interfaces.archiver import IArchiver
+from mailman.queue import Switchboard
+
+
+
+class MailArchive:
+ """Public archiver at the Mail-Archive.com.
+
+ Messages get archived at http://go.mail-archive.com.
+ """
+
+ implements(IArchiver)
+
+ name = 'mail-archive'
+ is_enabled = False
+
+ @staticmethod
+ def list_url(mlist):
+ """See `IArchiver`."""
+ if mlist.archive_private:
+ return None
+ return urljoin(config.MAIL_ARCHIVE_BASEURL,
+ quote(mlist.posting_address))
+
+ @staticmethod
+ def permalink(mlist, msg):
+ """See `IArchiver`."""
+ if mlist.archive_private:
+ return None
+ message_id = msg.get('message-id')
+ # It is not the archiver's job to ensure the message has a Message-ID.
+ assert message_id is not None, 'No Message-ID found'
+ # The angle brackets are not part of the Message-ID. See RFC 2822.
+ if message_id.startswith('<') and message_id.endswith('>'):
+ message_id = message_id[1:-1]
+ else:
+ message_id = message_id.strip()
+ sha = hashlib.sha1(message_id)
+ sha.update(str(mlist.posting_address))
+ message_id_hash = urlsafe_b64encode(sha.digest())
+ del msg['x-message-id-hash']
+ msg['X-Message-ID-Hash'] = message_id_hash
+ return urljoin(config.MAIL_ARCHIVE_BASEURL, message_id_hash)
+
+ @staticmethod
+ def archive_message(mlist, msg):
+ """See `IArchiver`."""
+ if mlist.archive_private:
+ return
+ outq = Switchboard(config.OUTQUEUE_DIR)
+ outq.enqueue(
+ msg,
+ listname=mlist.fqdn_listname,
+ recips=[config.MAIL_ARCHIVE_RECIPIENT])
diff --git a/mailman/archiving/mhonarc.py b/mailman/archiving/mhonarc.py
new file mode 100644
index 000000000..cc549dee8
--- /dev/null
+++ b/mailman/archiving/mhonarc.py
@@ -0,0 +1,95 @@
+# Copyright (C) 2008 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.
+
+"""MHonArc archiver."""
+
+__metaclass__ = type
+__all__ = [
+ 'MHonArc',
+ ]
+
+
+import hashlib
+import logging
+import subprocess
+
+from base64 import b32encode
+from string import Template
+from urlparse import urljoin
+from zope.interface import implements
+
+from mailman.configuration import config
+from mailman.interfaces.archiver import IArchiver
+
+
+log = logging.getLogger('mailman.archiver')
+
+
+
+class MHonArc:
+ """Local MHonArc archiver."""
+
+ implements(IArchiver)
+
+ name = 'mhonarc'
+ is_enabled = False
+
+ @staticmethod
+ def list_url(mlist):
+ """See `IArchiver`."""
+ # XXX What about private MHonArc archives?
+ web_host = config.domains.get(mlist.host_name, mlist.host_name)
+ return Template(config.PUBLIC_ARCHIVE_URL).safe_substitute(
+ listname=mlist.fqdn_listname,
+ hostname=web_host,
+ fqdn_listname=mlist.fqdn_listname,
+ )
+
+ @staticmethod
+ def permalink(mlist, msg):
+ """See `IArchiver`."""
+ # XXX What about private MHonArc archives?
+ message_id = msg.get('message-id')
+ # It is not the archiver's job to ensure the message has a Message-ID.
+ assert message_id is not None, 'No Message-ID found'
+ # The angle brackets are not part of the Message-ID. See RFC 2822.
+ if message_id.startswith('<') and message_id.endswith('>'):
+ message_id = message_id[1:-1]
+ else:
+ message_id = message_id.strip()
+ sha = hashlib.sha1(message_id)
+ message_id_hash = b32encode(sha.digest())
+ del msg['x-message-id-hash']
+ msg['X-Message-ID-Hash'] = message_id_hash
+ return urljoin(MHonArc.list_url(mlist), message_id_hash)
+
+ @staticmethod
+ def archive_message(mlist, msg):
+ """See `IArchiver`."""
+ substitutions = config.__dict__.copy()
+ substitutions['listname'] = mlist.fqdn_listname
+ command = Template(config.MHONARC_COMMAND).safe_substitute(
+ substitutions)
+ proc = subprocess.Popen(
+ command, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+ shell=True)
+ stdout, stderr = proc.communicate(msg.as_string())
+ if proc.returncode <> 0:
+ log.error('%s: mhonarc subprocess had non-zero exit code: %s' %
+ (msg['message-id'], proc.returncode))
+ log.info(stdout)
+ log.error(stderr)
diff --git a/mailman/app/archiving.py b/mailman/archiving/pipermail.py
index c790bc3dc..1e8f4f28e 100644
--- a/mailman/app/archiving.py
+++ b/mailman/archiving/pipermail.py
@@ -15,34 +15,34 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-"""Application level archiving support."""
+"""Pipermail archiver."""
__metaclass__ = type
__all__ = [
'Pipermail',
- 'get_primary_archiver',
]
import os
-import pkg_resources
+from cStringIO import StringIO
from string import Template
from zope.interface import implements
-from zope.interface.verify import verifyObject
+from zope.interface.interface import adapter_hooks
-from mailman.app.plugins import get_plugins
from mailman.configuration import config
-from mailman.interfaces import IArchiver
+from mailman.interfaces.archiver import IArchiver, IPipermailMailingList
+from mailman.interfaces.mailinglist import IMailingList
from mailman.Archiver.HyperArch import HyperArchive
-from cStringIO import StringIO
class PipermailMailingListAdapter:
"""An adapter for MailingList objects to work with Pipermail."""
+ implements(IPipermailMailingList)
+
def __init__(self, mlist):
self._mlist = mlist
@@ -50,7 +50,7 @@ class PipermailMailingListAdapter:
return getattr(self._mlist, name)
def archive_dir(self):
- """The directory for storing Pipermail artifacts."""
+ """See `IPipermailMailingList`."""
if self._mlist.archive_private:
basedir = config.PRIVATE_ARCHIVE_FILE_DIR
else:
@@ -58,53 +58,52 @@ class PipermailMailingListAdapter:
return os.path.join(basedir, self._mlist.fqdn_listname)
+def adapt_mailing_list_for_pipermail(iface, obj):
+ """Adapt IMailingLists to IPipermailMailingList."""
+ if IMailingList.providedBy(obj) and iface is IPipermailMailingList:
+ return PipermailMailingListAdapter(obj)
+ return None
+
+adapter_hooks.append(adapt_mailing_list_for_pipermail)
+
+
class Pipermail:
"""The stock Pipermail archiver."""
implements(IArchiver)
- def __init__(self, mlist):
- self._mlist = mlist
+ name = 'pipermail'
+ is_enabled = False
- def get_list_url(self):
+ @staticmethod
+ def list_url(mlist):
"""See `IArchiver`."""
- if self._mlist.archive_private:
- url = self._mlist.script_url('private') + '/index.html'
+ if mlist.archive_private:
+ url = mlist.script_url('private') + '/index.html'
else:
- web_host = config.domains.get(
- self._mlist.host_name, self._mlist.host_name)
+ web_host = config.domains.get(mlist.host_name, mlist.host_name)
url = Template(config.PUBLIC_ARCHIVE_URL).safe_substitute(
- listname=self._mlist.fqdn_listname,
+ listname=mlist.fqdn_listname,
hostname=web_host,
- fqdn_listname=self._mlist.fqdn_listname,
+ fqdn_listname=mlist.fqdn_listname,
)
return url
- def get_message_url(self, message):
+ @staticmethod
+ def permalink(mlist, message):
"""See `IArchiver`."""
# Not currently implemented.
return None
- def archive_message(self, message):
+ @staticmethod
+ def archive_message(mlist, message):
"""See `IArchiver`."""
text = str(message)
fileobj = StringIO(text)
- h = HyperArchive(PipermailMailingListAdapter(self._mlist))
+ h = HyperArchive(IPipermailMailingList(mlist))
h.processUnixMailbox(fileobj)
h.close()
fileobj.close()
# There's no good way to know the url for the archived message.
return None
-
-
-
-def get_primary_archiver(mlist):
- """Return the primary archiver."""
- entry_points = list(pkg_resources.iter_entry_points('mailman.archiver'))
- if len(entry_points) == 0:
- return None
- for ep in entry_points:
- if ep.name == 'default':
- return ep.load()(mlist)
- return None
diff --git a/mailman/archiving/prototype.py b/mailman/archiving/prototype.py
new file mode 100644
index 000000000..deeaaa624
--- /dev/null
+++ b/mailman/archiving/prototype.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2008 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.
+
+"""Prototypical permalinking archiver."""
+
+__metaclass__ = type
+__all__ = [
+ 'Prototype',
+ ]
+
+
+import hashlib
+
+from base64 import b32encode
+from urlparse import urljoin
+from zope.interface import implements
+
+from mailman.configuration import config
+from mailman.interfaces.archiver import IArchiver
+
+
+
+class Prototype:
+ """A prototype of a third party archiver.
+
+ Mailman proposes a draft specification for interoperability between list
+ servers and archivers: <http://wiki.list.org/display/DEV/Stable+URLs>.
+ """
+
+ implements(IArchiver)
+
+ name = 'prototype'
+ is_enabled = False
+
+ @staticmethod
+ def list_url(mlist):
+ """See `IArchiver`."""
+ web_host = config.domains.get(mlist.host_name, mlist.host_name)
+ return 'http://' + web_host
+
+ @staticmethod
+ def permalink(mlist, msg):
+ """See `IArchiver`."""
+ message_id = msg.get('message-id')
+ # It is not the archiver's job to ensure the message has a Message-ID.
+ assert message_id is not None, 'No Message-ID found'
+ # The angle brackets are not part of the Message-ID. See RFC 2822.
+ if message_id.startswith('<') and message_id.endswith('>'):
+ message_id = message_id[1:-1]
+ else:
+ message_id = message_id.strip()
+ digest = hashlib.sha1(message_id).digest()
+ message_id_hash = b32encode(digest)
+ del msg['x-message-id-hash']
+ msg['X-Message-ID-Hash'] = message_id_hash
+ return urljoin(Prototype.list_url(mlist), message_id_hash)
+
+ @staticmethod
+ def archive_message(mlist, message):
+ """See `IArchiver`."""
+ raise NotImplementedError
diff --git a/mailman/bin/docs/master.txt b/mailman/bin/docs/master.txt
index 333893f78..7565e93b1 100644
--- a/mailman/bin/docs/master.txt
+++ b/mailman/bin/docs/master.txt
@@ -7,7 +7,7 @@ is used to start, stop and manage the queue runners. mailmanctl actually is
just a wrapper around the real queue runner watcher script called master.py.
>>> from mailman.configuration import config
- >>> from mailman.tests.helpers import TestableMaster
+ >>> from mailman.testing.helpers import TestableMaster
Start the master in a subthread.
diff --git a/mailman/bin/set_members.py b/mailman/bin/set_members.py
index a97b13df8..4071627a7 100644
--- a/mailman/bin/set_members.py
+++ b/mailman/bin/set_members.py
@@ -25,6 +25,8 @@ from mailman import Utils
from mailman import i18n
from mailman import passwords
from mailman.app.membership import add_member
+from mailman.app.notifications import (
+ send_admin_subscription_notice, send_welcome_message)
from mailman.configuration import config
from mailman.initialize import initialize
from mailman.interfaces import DeliveryMode
@@ -176,6 +178,10 @@ def main():
add_member(mlist, address, real_name, password, delivery_mode,
mlist.preferred_language, send_welcome_msg,
admin_notify)
+ if send_welcome_msg:
+ send_welcome_message(mlist, address, language, delivery_mode)
+ if admin_notify:
+ send_admin_subscription_notice(mlist, address, real_name)
config.db.flush()
diff --git a/mailman/bin/testall.py b/mailman/bin/testall.py
index e4e2cc140..ed90980ae 100644
--- a/mailman/bin/testall.py
+++ b/mailman/bin/testall.py
@@ -31,9 +31,11 @@ import tempfile
import unittest
import pkg_resources
+from mailman import Defaults
from mailman.configuration import config
from mailman.i18n import _
from mailman.initialize import initialize_1, initialize_2
+from mailman.testing.helpers import SMTPServer
from mailman.version import MAILMAN_VERSION
@@ -198,7 +200,7 @@ def main():
enable_logging_cfg = True
cfg_in = pkg_resources.resource_string(
- 'mailman.tests', 'testing.cfg.in')
+ 'mailman.testing', 'testing.cfg.in')
fd, cfg_out = tempfile.mkstemp(suffix='.cfg')
os.close(fd)
with open(cfg_out, 'w') as fp:
@@ -226,10 +228,14 @@ def main():
with open(cfg_out, 'a') as fp:
print >> fp, 'VAR_DIR = "%s"' % var_dir
print >> fp, 'MAILMAN_USER = "%s"' % user_name
- print >> fp, 'MAILMAN_UID = %d' % user_id
+ print >> fp, 'MAILMAN_UID =', user_id
print >> fp, 'MAILMAN_GROUP = "%s"' % group_name
- print >> fp, 'MAILMAN_GID = %d' % group_id
+ print >> fp, 'MAILMAN_GID =', group_id
print >> fp, "LANGUAGES = 'en'"
+ print >> fp, 'SMTPPORT =', SMTPServer.port
+ # A fake MHonArc command, for testing.
+ print >> fp, 'MHONARC_COMMAND = """/bin/echo', \
+ Defaults.MHONARC_COMMAND, '"""'
initialize_1(cfg_out, propagate_logs=parser.options.stderr)
mailman_uid = pwd.getpwnam(config.MAILMAN_USER).pw_uid
diff --git a/mailman/commands/__init__.py b/mailman/commands/__init__.py
index e6263715c..044ffd6a5 100644
--- a/mailman/commands/__init__.py
+++ b/mailman/commands/__init__.py
@@ -18,4 +18,5 @@
__all__ = [
'echo',
'end',
+ 'join',
]
diff --git a/mailman/commands/docs/join.txt b/mailman/commands/docs/join.txt
new file mode 100644
index 000000000..75a8ac8ea
--- /dev/null
+++ b/mailman/commands/docs/join.txt
@@ -0,0 +1,60 @@
+The 'join' command
+==================
+
+The mail command 'join' subscribes an email address to the mailing list.
+'subscribe' is an alias for 'join'.
+
+ >>> from mailman.configuration import config
+ >>> command = config.commands['join']
+ >>> print command.name
+ join
+ >>> print command.description
+ Join this mailing list. You will be asked to confirm your subscription
+ request and you may be issued a provisional password.
+ <BLANKLINE>
+ By using the 'digest' option, you can specify whether you want digest
+ delivery or not. If not specified, the mailing list's default will be
+ used. You can also subscribe an alternative address by using the
+ 'address' option. For example:
+ <BLANKLINE>
+ join address=myotheraddress@example.com
+ <BLANKLINE>
+ >>> print command.argument_description
+ [digest=<yes|no>] [address=<address>]
+
+
+No address to join
+------------------
+
+ >>> from mailman.Message import Message
+ >>> from mailman.app.lifecycle import create_list
+ >>> from mailman.queue.command import Results
+ >>> mlist = create_list(u'test@example.com')
+
+When no address argument is given, the message's From address will be used.
+If that's missing though, then an error is returned.
+
+ >>> results = Results()
+ >>> print command.process(mlist, Message(), {}, (), results)
+ ContinueProcessing.no
+ >>> print unicode(results)
+ The results of your email command are provided below.
+ <BLANKLINE>
+ join: No valid address found to subscribe
+ <BLANKLINE>
+
+The 'subscribe' command is an alias.
+
+ >>> subscribe = config.commands['subscribe']
+ >>> print subscribe.name
+ subscribe
+ >>> results = Results()
+ >>> print subscribe.process(mlist, Message(), {}, (), results)
+ ContinueProcessing.no
+ >>> print unicode(results)
+ The results of your email command are provided below.
+ <BLANKLINE>
+ subscribe: No valid address found to subscribe
+ <BLANKLINE>
+
+
diff --git a/mailman/commands/join.py b/mailman/commands/join.py
index 2aa3922d9..1cbf394e0 100644
--- a/mailman/commands/join.py
+++ b/mailman/commands/join.py
@@ -28,7 +28,6 @@ from email.utils import parseaddr
from zope.interface import implements
from mailman.Utils import MakeRandomPassword
-from mailman.app.membership import confirm_add_member
from mailman.configuration import config
from mailman.i18n import _
from mailman.interfaces import (
@@ -57,15 +56,15 @@ example:
def process(self, mlist, msg, msgdata, arguments, results):
"""See `IEmailCommand`."""
# Parse the arguments.
- address, delivery_mmode = self._parser_arguments(mlist, arguments)
+ address, delivery_mmode = self._parse_arguments(arguments)
if address is None:
realname, address = parseaddr(msg['from'])
# Address could be None or the empty string.
if not address:
address = msg.get_sender()
if not address:
- print >> results, self.name, \
- _('No valid address found to subscribe')
+ print >> results, _(
+ '$self.name: No valid address found to subscribe')
return ContinueProcessing.no
password = MakeRandomPassword()
try:
@@ -122,7 +121,7 @@ example:
address = parts[1]
return address, delivery_mode
-
+def ignore():
# Fill in empty defaults
if digest is None:
digest = mlist.digest_is_default
@@ -184,3 +183,10 @@ at %(listowner)s for review."""))
else:
# Everything is a-ok
res.results.append(_('Subscription request succeeded.'))
+
+
+
+class Subscribe(Join):
+ """The email 'subscribe' command (an alias for 'join')."""
+
+ name = 'subscribe'
diff --git a/mailman/configuration.py b/mailman/configuration.py
index 2f682d114..8a702c457 100644
--- a/mailman/configuration.py
+++ b/mailman/configuration.py
@@ -174,6 +174,7 @@ class Configuration(object):
code = self.DEFAULT_SERVER_LANGUAGE
self.languages.enable_language(code)
# Create various registries.
+ self.archivers = {}
self.chains = {}
self.rules = {}
self.handlers = {}
diff --git a/mailman/docs/archivers.txt b/mailman/docs/archivers.txt
new file mode 100644
index 000000000..c8ddb73e4
--- /dev/null
+++ b/mailman/docs/archivers.txt
@@ -0,0 +1,183 @@
+Archivers
+=========
+
+Mailman supports pluggable archivers, and it comes with several default
+archivers.
+
+ >>> from mailman.app.lifecycle import create_list
+ >>> mlist = create_list(u'test@example.com')
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.org
+ ... To: test@example.com
+ ... Subject: An archived message
+ ... Message-ID: <12345>
+ ...
+ ... Here is an archived message.
+ ... """)
+
+Archivers support an interface which provides the RFC 2369 List-Archive
+header, and one that provides a 'permalink' to the specific message object in
+the archive. This latter is appropriate for the message footer or for the RFC
+5064 Archived-At header.
+
+Pipermail does not support a permalink, so that interface returns None.
+Mailman defines a draft spec for how list servers and archivers can
+interoperate.
+
+ >>> from mailman.configuration import config
+ >>> for archiver_name, archiver in sorted(config.archivers.items()):
+ ... print archiver.name
+ ... print ' ', archiver.list_url(mlist)
+ ... print ' ', archiver.permalink(mlist, msg)
+ mail-archive
+ http://go.mail-archive.dev/test%40example.com
+ http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
+ mhonarc
+ http://www.example.com/.../test@example.com
+ http://www.example.com/.../RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
+ pipermail
+ http://www.example.com/pipermail/test@example.com
+ None
+ prototype
+ http://www.example.com
+ http://www.example.com/RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE
+
+
+Sending the message to the archiver
+-----------------------------------
+
+The archiver is also able to archive the message.
+
+ >>> mlist.web_page_url = u'http://lists.example.com/'
+ >>> config.archivers['pipermail'].archive_message(mlist, msg)
+
+ >>> import os
+ >>> from mailman.interfaces.archiver import IPipermailMailingList
+ >>> pckpath = os.path.join(
+ ... IPipermailMailingList(mlist).archive_dir(),
+ ... 'pipermail.pck')
+ >>> os.path.exists(pckpath)
+ True
+
+Note however that the prototype archiver can't archive messages.
+
+ >>> config.archivers['prototype'].archive_message(mlist, msg)
+ Traceback (most recent call last):
+ ...
+ NotImplementedError
+
+
+The Mail-Archive.com
+--------------------
+
+The Mail-Archive <http://www.mail-archive.com> is a public archiver that can
+be used to archive message for free. Mailman comes with a plugin for this
+archiver; by enabling it messages to public lists will get sent there
+automatically.
+
+ >>> archiver = config.archivers['mail-archive']
+ >>> archiver.list_url(mlist)
+ 'http://go.mail-archive.dev/test%40example.com'
+ >>> archiver.permalink(mlist, msg)
+ 'http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc='
+
+To archive the message, the archiver actually mails the message to a special
+address at the Mail-Archive.
+
+ >>> archiver.archive_message(mlist, msg)
+
+ >>> from mailman.queue.outgoing import OutgoingRunner
+ >>> from mailman.testing.helpers import make_testable_runner
+ >>> outgoing = make_testable_runner(OutgoingRunner)
+ >>> outgoing.run()
+
+ >>> from operator import itemgetter
+ >>> messages = list(smtpd.messages)
+ >>> len(messages)
+ 1
+
+ >>> print messages[0].as_string()
+ From: aperson@example.org
+ To: test@example.com
+ Subject: An archived message
+ Message-ID: <12345>
+ X-Message-ID-Hash: ZaXPPxRMM9_hFZL4vTRlQlBx8pc=
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="us-ascii"
+ Content-Transfer-Encoding: 7bit
+ Sender: test-bounces@example.com
+ Errors-To: test-bounces@example.com
+ X-Peer: 127.0.0.1:...
+ X-MailFrom: test-bounces@example.com
+ X-RcptTo: archive@mail-archive.dev
+ <BLANKLINE>
+ Here is an archived message.
+ _______________________________________________
+ Test mailing list
+ test@example.com
+ http://lists.example.com/listinfo/test@example.com
+
+ >>> smtpd.clear()
+
+However, if the mailing list is not public, the message will never be archived
+at this service.
+
+ >>> mlist.archive_private = True
+ >>> print archiver.list_url(mlist)
+ None
+ >>> print archiver.permalink(mlist, msg)
+ None
+ >>> archiver.archive_message(mlist, msg)
+ >>> list(smtpd.messages)
+ []
+
+Additionally, this archiver can handle malformed Message-IDs.
+
+ >>> mlist.archive_private = False
+ >>> del msg['message-id']
+ >>> msg['Message-ID'] = '12345>'
+ >>> archiver.permalink(mlist, msg)
+ 'http://go.mail-archive.dev/bXvG32YzcDEIVDaDLaUSVQekfo8='
+
+ >>> del msg['message-id']
+ >>> msg['Message-ID'] = '<12345'
+ >>> archiver.permalink(mlist, msg)
+ 'http://go.mail-archive.dev/9rockPrT1Mm-jOsLWS6_hseR_OY='
+
+ >>> del msg['message-id']
+ >>> msg['Message-ID'] = '12345'
+ >>> archiver.permalink(mlist, msg)
+ 'http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc='
+
+ >>> del msg['message-id']
+ >>> msg['Message-ID'] = ' 12345 '
+ >>> archiver.permalink(mlist, msg)
+ 'http://go.mail-archive.dev/ZaXPPxRMM9_hFZL4vTRlQlBx8pc='
+
+
+MHonArc
+-------
+
+The MHonArc archiver <http://www.mhonarc.org> is also available.
+
+ >>> archiver = config.archivers['mhonarc']
+ >>> archiver.name
+ 'mhonarc'
+
+Messages sent to a local MHonArc instance are added to its archive via a
+subprocess call.
+
+ >>> archiver.archive_message(mlist, msg)
+ >>> archive_log = open(os.path.join(config.LOG_DIR, 'archiver'))
+ >>> try:
+ ... contents = archive_log.read()
+ ... finally:
+ ... archive_log.close()
+ >>> print 'LOG:', contents
+ LOG: ... /usr/bin/mhonarc -add
+ -dbfile /.../private/test@example.com.mbox/mhonarc.db
+ -outdir /.../mhonarc/test@example.com
+ -stderr /.../logs/mhonarc
+ -stdout /.../logs/mhonarc
+ -spammode -umask 022
+ ...
diff --git a/mailman/docs/pipelines.txt b/mailman/docs/pipelines.txt
index f475bf88e..c4f8488a1 100644
--- a/mailman/docs/pipelines.txt
+++ b/mailman/docs/pipelines.txt
@@ -21,6 +21,8 @@ Processing a message
Messages hit the pipeline after they've been accepted for posting.
+ >>> from mailman.configuration import config
+ >>> config.archivers['pipermail'].is_enabled = True
>>> msg = message_from_string("""\
... From: aperson@example.com
... To: xtest@example.com
@@ -68,8 +70,7 @@ However there are currently no recipients for this message.
And the message is now sitting in various other processing queues.
- >>> from mailman.tests.helpers import get_queue_messages
- >>> from mailman.configuration import config
+ >>> from mailman.testing.helpers import get_queue_messages
>>> messages = get_queue_messages(config.ARCHQUEUE_DIR)
>>> len(messages)
1
@@ -144,7 +145,7 @@ This is the message that will actually get delivered to end recipients.
There's now one message in the digest mailbox, getting ready to be sent.
- >>> from mailman.tests.helpers import digest_mbox
+ >>> from mailman.testing.helpers import digest_mbox
>>> digest = digest_mbox(mlist)
>>> sum(1 for mboxmsg in digest)
1
diff --git a/mailman/initialize.py b/mailman/initialize.py
index 6c2a5a8f4..d8dc0d69d 100644
--- a/mailman/initialize.py
+++ b/mailman/initialize.py
@@ -65,10 +65,12 @@ def initialize_2(debug=False):
mailman.configuration.config.db = database
# Initialize the rules and chains. Do the imports here so as to avoid
# circular imports.
+ from mailman.archiving import initialize as initialize_archivers
from mailman.app.chains import initialize as initialize_chains
from mailman.app.rules import initialize as initialize_rules
from mailman.app.pipelines import initialize as initialize_pipelines
from mailman.app.commands import initialize as initialize_commands
+ initialize_archivers()
initialize_rules()
initialize_chains()
initialize_pipelines()
diff --git a/mailman/interfaces/archiver.py b/mailman/interfaces/archiver.py
index 3b96c5c53..ac6efcb93 100644
--- a/mailman/interfaces/archiver.py
+++ b/mailman/interfaces/archiver.py
@@ -17,21 +17,32 @@
"""Interface for archiving schemes."""
+__metaclass__ = type
+__all__ = [
+ 'IArchiver',
+ 'IPipermailMailingList',
+ ]
+
from zope.interface import Interface, Attribute
+from mailman.interfaces.mailinglist import IMailingList
class IArchiver(Interface):
"""An interface to the archiver."""
- def get_list_url(mlist):
+ name = Attribute('The name of this archiver')
+
+ is_enabled = Attribute('True if this archiver is enabled.')
+
+ def list_url(mlist):
"""Return the url to the top of the list's archive.
:param mlist: The IMailingList object.
:returns: The url string.
"""
- def get_message_url(mlist, message):
+ def permalink(mlist, message):
"""Return the url to the message in the archive.
This url points directly to the message in the archive. This method
@@ -46,9 +57,6 @@ class IArchiver(Interface):
def archive_message(mlist, message):
"""Send the message to the archiver.
- This uses `get_message_url()` to calculate and return the url to the
- message in the archives.
-
:param mlist: The IMailingList object.
:param message: The message object.
:returns: The url string or None if the message's archive url cannot
@@ -56,3 +64,14 @@ class IArchiver(Interface):
"""
# XXX How to handle attachments?
+
+
+
+class IPipermailMailingList(IMailingList):
+ """An interface that adapts IMailingList as needed for Pipermail."""
+
+ def archive_dir():
+ """The directory for storing Pipermail artifacts.
+
+ Pipermail expects this to be a function, not a property.
+ """
diff --git a/mailman/interfaces/mailinglist.py b/mailman/interfaces/mailinglist.py
index a5f6a9e9a..8a9967e89 100644
--- a/mailman/interfaces/mailinglist.py
+++ b/mailman/interfaces/mailinglist.py
@@ -147,7 +147,7 @@ class IMailingList(Interface):
last_post_date = Attribute(
"""The date and time a message was last posted to the mailing list.""")
- post_number = Attribute(
+ post_id = Attribute(
"""A monotonically increasing integer sequentially assigned to each
list posting.""")
diff --git a/mailman/loginit.py b/mailman/loginit.py
index 844c2543e..68debff8d 100644
--- a/mailman/loginit.py
+++ b/mailman/loginit.py
@@ -35,6 +35,7 @@ FMT = '%(asctime)s (%(process)d) %(message)s'
DATEFMT = '%b %d %H:%M:%S %Y'
LOGGERS = (
+ 'archiver', # All archiver output
'bounce', # All bounce processing logs go here
'config', # Configuration issues
'debug', # Only used for development
diff --git a/mailman/pipeline/cook_headers.py b/mailman/pipeline/cook_headers.py
index c237c171a..ad4728be8 100644
--- a/mailman/pipeline/cook_headers.py
+++ b/mailman/pipeline/cook_headers.py
@@ -30,7 +30,7 @@ from email.Utils import parseaddr, formataddr, getaddresses
from zope.interface import implements
from mailman import Utils
-from mailman.app.archiving import get_primary_archiver
+from mailman.app.plugins import get_plugins
from mailman.configuration import config
from mailman.i18n import _
from mailman.interfaces import IHandler, Personalization, ReplyToMunging
@@ -206,29 +206,26 @@ def process(mlist, msg, msgdata):
'List-Unsubscribe': subfieldfmt % (listinfo, mlist.leave_address),
'List-Subscribe' : subfieldfmt % (listinfo, mlist.join_address),
})
- archiver = get_primary_archiver(mlist)
if msgdata.get('reduced_list_headers'):
headers['X-List-Administrivia'] = 'yes'
else:
# List-Post: is controlled by a separate attribute
if mlist.include_list_post_header:
headers['List-Post'] = '<mailto:%s>' % mlist.posting_address
- # Add this header if we're archiving
+ # Add RFC 2369 and 5064 archiving headers, if archiving is enabled.
if mlist.archive:
- archiveurl = archiver.get_list_url()
- headers['List-Archive'] = '<%s>' % archiveurl
+ for archiver in get_plugins('mailman.archiver'):
+ if not archiver.is_enabled:
+ continue
+ headers['List-Archive'] = '<%s>' % archiver.list_url(mlist)
+ permalink = archiver.permalink(mlist, msg)
+ if permalink is not None:
+ headers['Archived-At'] = permalink
# XXX RFC 2369 also defines a List-Owner header which we are not currently
# supporting, but should.
- #
- # Draft RFC 5064 defines an Archived-At header which contains the pointer
- # directly to the message in the archive. If the currently defined
- # archiver can tell us the URL, go ahead and include this header.
- archived_at = archiver.get_message_url(msg)
- if archived_at is not None:
- headers['Archived-At'] = archived_at
- # First we delete any pre-existing headers because the RFC permits only
- # one copy of each, and we want to be sure it's ours.
for h, v in headers.items():
+ # First we delete any pre-existing headers because the RFC permits
+ # only one copy of each, and we want to be sure it's ours.
del msg[h]
# Wrap these lines if they are too long. 78 character width probably
# shouldn't be hardcoded, but is at least text-MUA friendly. The
diff --git a/mailman/pipeline/docs/archives.txt b/mailman/pipeline/docs/archives.txt
index b7b54f17f..67ad45c89 100644
--- a/mailman/pipeline/docs/archives.txt
+++ b/mailman/pipeline/docs/archives.txt
@@ -7,11 +7,11 @@ delivery processes while messages are archived. This also allows external
archivers to work in a separate process from the main Mailman delivery
processes.
- >>> from mailman.queue import Switchboard
+ >>> from mailman.app.lifecycle import create_list
>>> from mailman.configuration import config
+ >>> from mailman.queue import Switchboard
>>> handler = config.handlers['to-archive']
- >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
- >>> mlist.preferred_language = u'en'
+ >>> mlist = create_list(u'_xtest@example.com')
>>> switchboard = Switchboard(config.ARCHQUEUE_DIR)
A helper function.
diff --git a/mailman/pipeline/docs/cook-headers.txt b/mailman/pipeline/docs/cook-headers.txt
index a85ba9e63..4fbdf58bb 100644
--- a/mailman/pipeline/docs/cook-headers.txt
+++ b/mailman/pipeline/docs/cook-headers.txt
@@ -184,8 +184,10 @@ But normally, a list will include these headers.
>>> mlist.include_rfc2369_headers = True
>>> mlist.include_list_post_header = True
>>> mlist.preferred_language = u'en'
+ >>> config.archivers['pipermail'].is_enabled = True
>>> msg = message_from_string("""\
... From: aperson@example.com
+ ... Message-ID: <12345>
...
... """)
>>> process(mlist, msg, {})
diff --git a/mailman/pipeline/docs/digests.txt b/mailman/pipeline/docs/digests.txt
index d81e173f8..df01379b9 100644
--- a/mailman/pipeline/docs/digests.txt
+++ b/mailman/pipeline/docs/digests.txt
@@ -21,7 +21,7 @@ This is a helper function used to iterate through all the accumulated digest
messages, in the order in which they were posted. This makes it easier to
update the tests when we switch to a different mailbox format.
- >>> from mailman.tests.helpers import digest_mbox
+ >>> from mailman.testing.helpers import digest_mbox
>>> from itertools import count
>>> from string import Template
>>> def makemsg():
diff --git a/mailman/pipeline/scrubber.py b/mailman/pipeline/scrubber.py
index ca1fa37e0..bf6effd3a 100644
--- a/mailman/pipeline/scrubber.py
+++ b/mailman/pipeline/scrubber.py
@@ -40,7 +40,7 @@ from zope.interface import implements
from mailman import Utils
from mailman.Errors import DiscardMessage
-from mailman.app.archiving import get_primary_archiver
+from mailman.app.plugins import get_plugin
from mailman.configuration import config
from mailman.i18n import _
from mailman.interfaces import IHandler
@@ -497,7 +497,7 @@ def save_attachment(mlist, msg, dir, filter_html=True):
fp.write(decodedpayload)
fp.close()
# Now calculate the url to the list's archive.
- baseurl = get_primary_archiver(mlist).get_list_url()
+ baseurl = get_plugin('mailman.scrubber').list_url(mlist)
if not baseurl.endswith('/'):
baseurl += '/'
# Trailing space will definitely be a problem with format=flowed.
diff --git a/mailman/queue/archive.py b/mailman/queue/archive.py
index 52e73d9c8..c24d9478d 100644
--- a/mailman/queue/archive.py
+++ b/mailman/queue/archive.py
@@ -80,6 +80,5 @@ class ArchiveRunner(Runner):
# While a list archiving lock is acquired, archive the message.
with Lock(os.path.join(mlist.data_path, 'archive.lck')):
for archive_factory in get_plugins('mailman.archiver'):
- archiver = archive_factory(mlist)
- archiver.archive_message(msg)
+ archive_factory().archive_message(mlist, msg)
diff --git a/mailman/queue/docs/archiver.txt b/mailman/queue/docs/archiver.txt
index 7737ef7d0..8c34e3537 100644
--- a/mailman/queue/docs/archiver.txt
+++ b/mailman/queue/docs/archiver.txt
@@ -24,7 +24,7 @@ interface. By default, there's a Pipermail archiver.
>>> ignore = archiver_queue.enqueue(msg, {}, listname=mlist.fqdn_listname)
>>> from mailman.queue.archive import ArchiveRunner
- >>> from mailman.tests.helpers import make_testable_runner
+ >>> from mailman.testing.helpers import make_testable_runner
>>> runner = make_testable_runner(ArchiveRunner)
>>> runner.run()
diff --git a/mailman/queue/docs/command.txt b/mailman/queue/docs/command.txt
index 06d2e2117..470a632b7 100644
--- a/mailman/queue/docs/command.txt
+++ b/mailman/queue/docs/command.txt
@@ -28,7 +28,7 @@ sender. The command can be in the Subject header.
>>> from mailman.inject import inject_message
>>> inject_message(mlist, msg, qdir=config.CMDQUEUE_DIR)
>>> from mailman.queue.command import CommandRunner
- >>> from mailman.tests.helpers import make_testable_runner
+ >>> from mailman.testing.helpers import make_testable_runner
>>> command = make_testable_runner(CommandRunner)
>>> command.run()
@@ -38,7 +38,7 @@ And now the response is in the virgin queue.
>>> virgin_queue = Switchboard(config.VIRGINQUEUE_DIR)
>>> len(virgin_queue.files)
1
- >>> from mailman.tests.helpers import get_queue_messages
+ >>> from mailman.testing.helpers import get_queue_messages
>>> item = get_queue_messages(virgin_queue)[0]
>>> print item.msg.as_string()
Subject: The results of your email commands
diff --git a/mailman/queue/docs/incoming.txt b/mailman/queue/docs/incoming.txt
index 024077551..997eac07e 100644
--- a/mailman/queue/docs/incoming.txt
+++ b/mailman/queue/docs/incoming.txt
@@ -42,7 +42,7 @@ queue, but this is an effective simulation.
The incoming queue runner runs until it is empty.
>>> from mailman.queue.incoming import IncomingRunner
- >>> from mailman.tests.helpers import make_testable_runner
+ >>> from mailman.testing.helpers import make_testable_runner
>>> incoming = make_testable_runner(IncomingRunner)
>>> incoming.run()
@@ -56,7 +56,7 @@ And now the message is in the pipeline queue.
>>> incoming_queue = Switchboard(config.INQUEUE_DIR)
>>> len(incoming_queue.files)
0
- >>> from mailman.tests.helpers import get_queue_messages
+ >>> from mailman.testing.helpers import get_queue_messages
>>> item = get_queue_messages(pipeline_queue)[0]
>>> print item.msg.as_string()
From: aperson@example.com
diff --git a/mailman/queue/docs/lmtp.txt b/mailman/queue/docs/lmtp.txt
index 7b25d6a74..bb77203b4 100644
--- a/mailman/queue/docs/lmtp.txt
+++ b/mailman/queue/docs/lmtp.txt
@@ -10,7 +10,7 @@ message is destined for a valid endpoint, e.g. mylist-join@example.com.
Let's start a testable LMTP queue runner.
- >>> from mailman.tests import helpers
+ >>> from mailman.testing import helpers
>>> master = helpers.TestableMaster()
>>> master.start('lmtp')
diff --git a/mailman/queue/docs/outgoing.txt b/mailman/queue/docs/outgoing.txt
index fc9161320..edc806d47 100644
--- a/mailman/queue/docs/outgoing.txt
+++ b/mailman/queue/docs/outgoing.txt
@@ -17,30 +17,18 @@ move messages to the 'retry queue' for handling delivery failures.
>>> from mailman.app.membership import add_member
>>> from mailman.interfaces import DeliveryMode
>>> add_member(mlist, u'aperson@example.com', u'Anne Person',
- ... u'password', DeliveryMode.regular, u'en',
- ... ack=False, admin_notif=False)
+ ... u'password', DeliveryMode.regular, u'en')
>>> add_member(mlist, u'bperson@example.com', u'Bart Person',
- ... u'password', DeliveryMode.regular, u'en',
- ... ack=False, admin_notif=False)
+ ... u'password', DeliveryMode.regular, u'en')
>>> add_member(mlist, u'cperson@example.com', u'Cris Person',
- ... u'password', DeliveryMode.regular, u'en',
- ... ack=False, admin_notif=False)
-
- >>> from mailman.tests.helpers import SMTPServer
- >>> smtpd = SMTPServer()
- >>> smtpd.start()
- >>> from mailman.configuration import config
- >>> old_host = config.SMTPHOST
- >>> old_port = config.SMTPPORT
- >>> config.SMTPHOST = smtpd.host
- >>> config.SMTPPORT = smtpd.port
+ ... u'password', DeliveryMode.regular, u'en')
By setting the mailing list to personalize messages, each recipient will get a
unique copy of the message, with certain headers tailored for that recipient.
>>> from mailman.interfaces import Personalization
>>> mlist.personalize = Personalization.individual
- >>> config.db.commit()
+ >>> commit()
>>> msg = message_from_string("""\
... From: aperson@example.com
@@ -55,6 +43,7 @@ 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.
+ >>> from mailman.configuration import config
>>> msgdata = {}
>>> handler = config.handlers['calculate-recipients']
>>> handler.process(mlist, msg, msgdata)
@@ -70,7 +59,7 @@ Running the outgoing queue runner processes the message, delivering it to the
upstream SMTP, which happens to be our test server.
>>> from mailman.queue.outgoing import OutgoingRunner
- >>> from mailman.tests.helpers import make_testable_runner
+ >>> from mailman.testing.helpers import make_testable_runner
>>> outgoing = make_testable_runner(OutgoingRunner)
>>> outgoing.run()
@@ -86,11 +75,3 @@ Three messages have been delivered to our SMTP server, one for each recipient.
test-bounces+aperson=example.com@example.com
test-bounces+bperson=example.com@example.com
test-bounces+cperson=example.com@example.com
-
-
-Clean up
---------
-
- >>> smtpd.stop()
- >>> config.SMTPHOST = old_host
- >>> config.SMTPPORT = old_port
diff --git a/mailman/testing/__init__.py b/mailman/testing/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/mailman/testing/__init__.py
diff --git a/mailman/tests/helpers.py b/mailman/testing/helpers.py
index d1eda9e34..706ef603f 100644
--- a/mailman/tests/helpers.py
+++ b/mailman/testing/helpers.py
@@ -46,7 +46,7 @@ from datetime import datetime, timedelta
from mailman.bin.master import Loop as Master
from mailman.configuration import config
from mailman.queue import Switchboard
-from mailman.tests.smtplistener import Server
+from mailman.testing.smtplistener import Server
log = logging.getLogger('mailman.debug')
@@ -162,7 +162,7 @@ class SMTPServer:
"""An smtp server for testing."""
host = 'localhost'
- port = 9025
+ port = 10825
def __init__(self):
self._messages = []
@@ -174,6 +174,10 @@ class SMTPServer:
"""Start the smtp server in a thread."""
log.info('test SMTP server starting')
self._thread.start()
+ smtpd = smtplib.SMTP()
+ smtpd.connect(self.host, self.port)
+ smtpd.helo('test.localhost')
+ smtpd.quit()
def stop(self):
"""Stop the smtp server."""
diff --git a/mailman/tests/smtplistener.py b/mailman/testing/smtplistener.py
index 1dc11e3e0..eaf9083e8 100644
--- a/mailman/tests/smtplistener.py
+++ b/mailman/testing/smtplistener.py
@@ -58,12 +58,13 @@ class Server(smtpd.SMTPServer):
def __init__(self, localaddr, queue):
smtpd.SMTPServer.__init__(self, localaddr, None)
+ log.info('[SMTPServer] listening: %s', localaddr)
self._queue = queue
def handle_accept(self):
"""Handle connections by creating our own Channel object."""
conn, addr = self.accept()
- log.info('accepted: %s', addr)
+ log.info('[SMTPServer] accepted: %s', addr)
Channel(self, conn, addr)
def process_message(self, peer, mailfrom, rcpttos, data):
@@ -72,7 +73,8 @@ class Server(smtpd.SMTPServer):
message['X-Peer'] = '%s:%s' % peer
message['X-MailFrom'] = mailfrom
message['X-RcptTo'] = COMMASPACE.join(rcpttos)
- log.info('processed message: %s', message.get('message-id', 'n/a'))
+ log.info('[SMTPServer] processed message: %s',
+ message.get('message-id', 'n/a'))
self._queue.put(message)
def start(self):
diff --git a/mailman/tests/testing.cfg.in b/mailman/testing/testing.cfg.in
index e7b23ac6d..c8e121079 100644
--- a/mailman/tests/testing.cfg.in
+++ b/mailman/testing/testing.cfg.in
@@ -4,11 +4,14 @@
# both the process running the tests and all sub-processes (e.g. qrunners)
# must share the same configuration file.
-SMTPPORT = 10825
MAX_RESTARTS = 1
MTA = None
USE_LMTP = Yes
+# Make sure these goes to fake domains.
+MAIL_ARCHIVE_BASEURL = 'http://go.mail-archive.dev/'
+MAIL_ARCHIVE_RECIPIENT = 'archive@mail-archive.dev'
+
add_domain('example.com', 'www.example.com')
# bin/testall will add additional runtime configuration variables here.
diff --git a/mailman/tests/test_documentation.py b/mailman/tests/test_documentation.py
index e805b10fa..2d42f989d 100644
--- a/mailman/tests/test_documentation.py
+++ b/mailman/tests/test_documentation.py
@@ -29,6 +29,7 @@ import mailman
from mailman.Message import Message
from mailman.app.styles import style_manager
from mailman.configuration import config
+from mailman.testing.helpers import SMTPServer
DOT = '.'
@@ -54,12 +55,15 @@ def specialized_message_from_string(text):
def setup(testobj):
"""Test setup."""
+ smtpd = SMTPServer()
+ smtpd.start()
# In general, I don't like adding convenience functions, since I think
# doctests should do the imports themselves. It makes for better
# documentation that way. However, a few are really useful, or help to
# hide some icky test implementation details.
testobj.globs['message_from_string'] = specialized_message_from_string
testobj.globs['commit'] = config.db.commit
+ testobj.globs['smtpd'] = smtpd
@@ -79,6 +83,13 @@ def cleaning_teardown(testobj):
for message in config.db.message_store.messages:
config.db.message_store.delete_message(message['message-id'])
config.db.commit()
+ # Reset all archivers by disabling them.
+ for archiver in config.archivers.values():
+ archiver.is_enabled = False
+ # Shutdown the smtp server.
+ smtpd = testobj.globs['smtpd']
+ smtpd.clear()
+ smtpd.stop()
diff --git a/setup.py b/setup.py
index 36f13e716..a1cf52064 100644
--- a/setup.py
+++ b/setup.py
@@ -90,7 +90,13 @@ Any other spelling is incorrect.""",
entry_points = {
'console_scripts': list(scripts),
# Entry point for plugging in different database backends.
- 'mailman.archiver' : 'default = mailman.app.archiving:Pipermail',
+ 'mailman.archiver' : [
+ 'mail-archive = mailman.archiving.mailarchive:MailArchive',
+ 'mhonarc = mailman.archiving.mhonarc:MHonArc',
+ 'pipermail = mailman.archiving.pipermail:Pipermail',
+ 'prototype = mailman.archiving.prototype:Prototype',
+ ],
+ 'mailman.scrubber' : 'stock = mailman.archiving.pipermail:Pipermail',
'mailman.commands' : list(commands),
'mailman.database' : 'stock = mailman.database:StockDatabase',
'mailman.mta' : 'stock = mailman.MTA:Manual',