diff options
47 files changed, 378 insertions, 802 deletions
diff --git a/mailman/Defaults.py b/mailman/Defaults.py index 2cbfe8241..e3a351ad1 100644 --- a/mailman/Defaults.py +++ b/mailman/Defaults.py @@ -859,7 +859,7 @@ DEFAULT_MSG_FOOTER = u"""\ _______________________________________________ $real_name mailing list $fqdn_listname -${web_page_url}listinfo${cgiext}/${fqdn_listname} +${listinfo_page} """ # Scrub regular delivery diff --git a/mailman/app/lifecycle.py b/mailman/app/lifecycle.py index 5401c1e12..8bc316b43 100644 --- a/mailman/app/lifecycle.py +++ b/mailman/app/lifecycle.py @@ -33,7 +33,6 @@ from mailman import Utils from mailman.Utils import ValidateEmail from mailman.config import config from mailman.core import errors -from mailman.core.plugins import get_plugin from mailman.core.styles import style_manager from mailman.interfaces import MemberRole @@ -52,15 +51,12 @@ def create_list(fqdn_listname, owners=None): raise errors.BadDomainSpecificationError(domain) mlist = config.db.list_manager.create(fqdn_listname) for style in style_manager.lookup(mlist): - # XXX FIXME. When we get rid of the wrapper object, this hack won't - # be necessary. Until then, setattr on the MailList instance won't - # set the database column values, so pass the underlying database - # object to .apply() instead. style.apply(mlist) - # Coordinate with the MTA, which should be defined by plugins. - # XXX FIXME -## mta_plugin = get_plugin('mailman.mta') -## mta_plugin().create(mlist) + # Coordinate with the MTA, as defined in the configuration file. + module_name, class_name = config.mta.incoming.rsplit('.', 1) + __import__(module_name) + mta = getattr(sys.modules[module_name], class_name) + mta().create(mlist) # Create any owners that don't yet exist, and subscribe all addresses as # owners of the mailing list. usermgr = config.db.user_manager diff --git a/mailman/archiving/__init__.py b/mailman/archiving/__init__.py index 93b7ae3ab..e69de29bb 100644 --- a/mailman/archiving/__init__.py +++ b/mailman/archiving/__init__.py @@ -1,31 +0,0 @@ -# Copyright (C) 2008-2009 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/>. - -__metaclass__ = type -__all__ = [ - 'initialize', - ] - - -from mailman.config import config -from mailman.core.plugins import get_plugins - - -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 index 20a3ea483..334818204 100644 --- a/mailman/archiving/mailarchive.py +++ b/mailman/archiving/mailarchive.py @@ -44,7 +44,6 @@ class MailArchive: implements(IArchiver) name = 'mail-archive' - is_enabled = False @staticmethod def list_url(mlist): @@ -61,7 +60,9 @@ class MailArchive: 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' + # If no Message-ID is available, there is no permalink. + if message_id is None: + return None # 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] diff --git a/mailman/archiving/mhonarc.py b/mailman/archiving/mhonarc.py index 3cc83170c..cc2e0d7e9 100644 --- a/mailman/archiving/mhonarc.py +++ b/mailman/archiving/mhonarc.py @@ -47,7 +47,6 @@ class MHonArc: implements(IArchiver) name = 'mhonarc' - is_enabled = False @staticmethod def list_url(mlist): @@ -66,7 +65,9 @@ class MHonArc: # 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' + # If no Message-ID is available, there is no permalink. + if message_id is None: + return None # 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] diff --git a/mailman/archiving/pipermail.py b/mailman/archiving/pipermail.py index d534f1600..d51b80738 100644 --- a/mailman/archiving/pipermail.py +++ b/mailman/archiving/pipermail.py @@ -87,7 +87,6 @@ class Pipermail: implements(IArchiver) name = 'pipermail' - is_enabled = False @staticmethod def list_url(mlist): diff --git a/mailman/archiving/prototype.py b/mailman/archiving/prototype.py index e17a6f177..8fbbe47c4 100644 --- a/mailman/archiving/prototype.py +++ b/mailman/archiving/prototype.py @@ -44,7 +44,6 @@ class Prototype: implements(IArchiver) name = 'prototype' - is_enabled = False @staticmethod def list_url(mlist): @@ -56,7 +55,9 @@ class Prototype: """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' + # If this header is missing, there is no permalink. + if message_id is None: + return None # 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] diff --git a/mailman/bin/create_list.py b/mailman/bin/create_list.py index 36aa5c1d1..0c6039e45 100644 --- a/mailman/bin/create_list.py +++ b/mailman/bin/create_list.py @@ -17,12 +17,12 @@ import sys -from mailman import errors from mailman import Message from mailman import Utils from mailman import i18n from mailman.app.lifecycle import create_list -from mailman.configuration import config +from mailman.config import config +from mailman.core import errors from mailman.interfaces import ListAlreadyExistsError from mailman.options import SingleMailingListOptions @@ -79,7 +79,7 @@ owner is specified with the -o option..""")) def sanity_check(self): """Set up some defaults we couldn't set up earlier.""" if self.options.language is None: - self.options.language = config.DEFAULT_SERVER_LANGUAGE + self.options.language = unicode(config.mailman.default_language) # Is the language known? if self.options.language not in config.languages.enabled_codes: self.parser.error(_('Unknown language: $opts.language')) @@ -95,6 +95,8 @@ def main(): # Create the mailing list, applying styles as appropriate. fqdn_listname = options.options.listname + if fqdn_listname is None: + options.parser.error(_('--listname is required')) try: mlist = create_list(fqdn_listname, options.options.owners) mlist.preferred_language = options.options.language @@ -107,11 +109,6 @@ def main(): config.db.commit() - # Send notices to the list owners. XXX This should also be moved to the - # Mailman.app.create module. - if not options.options.quiet and not options.options.automate: - print _('Hit enter to notify $fqdn_listname owners...'), - sys.stdin.readline() if not options.options.quiet: d = dict( listname = mlist.fqdn_listname, diff --git a/mailman/bin/dumpdb.py b/mailman/bin/dumpdb.py index 4642ac361..6657602e4 100644 --- a/mailman/bin/dumpdb.py +++ b/mailman/bin/dumpdb.py @@ -18,7 +18,7 @@ import pprint import cPickle -from mailman.configuration import config +from mailman.config import config from mailman.i18n import _ from mailman.interact import interact from mailman.options import Options diff --git a/mailman/bin/genaliases.py b/mailman/bin/genaliases.py index a09bc6318..3fbef0f88 100644 --- a/mailman/bin/genaliases.py +++ b/mailman/bin/genaliases.py @@ -1,5 +1,3 @@ -#! @PYTHON@ -# # Copyright (C) 2001-2009 by the Free Software Foundation, Inc. # # This file is part of GNU Mailman. @@ -17,67 +15,49 @@ # You should have received a copy of the GNU General Public License along with # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. +__metaclass__ = type +__all__ = [ + 'main', + ] + + import sys -import optparse -from mailman import MailList -from mailman.configuration import config +from mailman.config import config +from mailman.core.plugins import get_plugin from mailman.i18n import _ -from mailman.initialize import initialize +from mailman.options import Options from mailman.version import MAILMAN_VERSION -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ +class ScriptOptions(Options): + """Options for the genaliases script.""" + + usage = _("""\ %prog [options] Regenerate the Mailman specific MTA aliases from scratch. The actual output -depends on the value of the 'MTA' variable in your etc/mailman.cfg file.""")) - parser.add_option('-q', '--quiet', - default=False, action='store_true', help=_("""\ +depends on the value of the 'MTA' variable in your etc/mailman.cfg file.""") + + def add_options(self): + super(ScriptOptions, self).add_options() + self.parser.add_option( + '-q', '--quiet', + default=False, action='store_true', help=_("""\ Some MTA output can include more verbose help text. Use this to tone down the verbosity.""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if args: - parser.print_error(_('Unexpected arguments')) - return parser, opts, args + def main(): - parser, opts, args = parseargs() - initialize(opts.config) - - # Import the MTA specific module - modulename = 'mailman.MTA.' + config.MTA - __import__(modulename) - MTA = sys.modules[modulename] + options = ScriptOptions() + options.initialize() - # We need to acquire a lock so nobody tries to update the files while - # we're doing it. - lock = MTA.makelock() - lock.lock() - # Group lists by virtual hostname - mlists = {} - for listname in config.list_manager.names: - mlist = MailList.MailList(listname, lock=False) - mlists.setdefault(mlist.host_name, []).append(mlist) - try: - MTA.clear() - if not mlists: - MTA.create(None, nolock=True, quiet=opts.quiet) - else: - for hostname, vlists in mlists.items(): - for mlist in vlists: - MTA.create(mlist, nolock=True, quiet=opts.quiet) - # Be verbose for only the first printed list - quiet = True - finally: - lock.unlock(unconditionally=True) + # Get the MTA-specific module. + mta = get_plugin('mailman.mta') + mta().regenerate() diff --git a/mailman/bin/list_lists.py b/mailman/bin/list_lists.py index d19044f35..d2aed3c01 100644 --- a/mailman/bin/list_lists.py +++ b/mailman/bin/list_lists.py @@ -16,7 +16,7 @@ # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. from mailman import Defaults -from mailman.configuration import config +from mailman.config import config from mailman.i18n import _ from mailman.options import Options diff --git a/mailman/bin/mailmanctl.py b/mailman/bin/mailmanctl.py index 6d424442a..667a46a70 100644 --- a/mailman/bin/mailmanctl.py +++ b/mailman/bin/mailmanctl.py @@ -27,9 +27,9 @@ import logging from optparse import OptionParser -from mailman.configuration import config +from mailman.config import config +from mailman.core.initialize import initialize from mailman.i18n import _ -from mailman.initialize import initialize from mailman.version import MAILMAN_VERSION diff --git a/mailman/bin/qrunner.py b/mailman/bin/qrunner.py index a71bb2ef5..eeb5c286b 100644 --- a/mailman/bin/qrunner.py +++ b/mailman/bin/qrunner.py @@ -151,9 +151,9 @@ def make_qrunner(name, slice, range, once=False): class Once(qrclass): def _do_periodic(self): self.stop() - qrunner = Once(slice, range) + qrunner = Once(name, slice) else: - qrunner = qrclass(slice, range) + qrunner = qrclass(name, slice) return qrunner diff --git a/mailman/bin/remove_list.py b/mailman/bin/remove_list.py index fb5d4cdbf..57e41c3b3 100644 --- a/mailman/bin/remove_list.py +++ b/mailman/bin/remove_list.py @@ -21,7 +21,7 @@ import shutil from mailman import Utils from mailman.app.lifecycle import remove_list -from mailman.configuration import config +from mailman.config import config from mailman.i18n import _ from mailman.options import MultipleMailingListOptions diff --git a/mailman/bin/unshunt.py b/mailman/bin/unshunt.py index 8a1dba081..97369ab17 100644 --- a/mailman/bin/unshunt.py +++ b/mailman/bin/unshunt.py @@ -15,51 +15,33 @@ # You should have received a copy of the GNU General Public License along with # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. +__metaclass__ = type +__all__ = [ + 'main', + ] + + import sys -import optparse -from mailman.configuration import config +from mailman.config import config from mailman.i18n import _ -from mailman.queue import Switchboard from mailman.version import MAILMAN_VERSION - - - -def parseargs(): - parser = optparse.OptionParser(version=MAILMAN_VERSION, - usage=_("""\ -%%prog [options] [directory] - -Move a message from the shunt queue to the original queue. Optional -`directory' specifies a directory to dequeue from other than qfiles/shunt. -""")) - parser.add_option('-C', '--config', - help=_('Alternative configuration file to use')) - opts, args = parser.parse_args() - if len(args) > 1: - parser.print_help() - print >> sys.stderr, _('Unexpected arguments') - sys.exit(1) - return parser, opts, args +from mailman.options import Options def main(): - parser, opts, args = parseargs() - config.load(opts.config) - if args: - qdir = args[0] - else: - qdir = config.SHUNTQUEUE_DIR + options = Options() + options.initialize() + + switchboard = config.switchboards['shunt'] + switchboard.recover_backup_files() - sb = Switchboard(qdir) - sb.recover_backup_files() - for filebase in sb.files(): + for filebase in switchboard.files: try: - msg, msgdata = sb.dequeue(filebase) - whichq = msgdata.get('whichq', config.INQUEUE_DIR) - tosb = Switchboard(whichq) - tosb.enqueue(msg, msgdata) + msg, msgdata = switchboard.dequeue(filebase) + whichq = msgdata.get('whichq', 'in') + config.switchboards[whichq].enqueue(msg, msgdata) except Exception, e: # If there are any unshunting errors, log them and continue trying # other shunted messages. @@ -67,9 +49,4 @@ def main(): 'Cannot unshunt message $filebase, skipping:\n$e') else: # Unlink the .bak file left by dequeue() - sb.finish(filebase) - - - -if __name__ == '__main__': - main() + switchboard.finish(filebase) diff --git a/mailman/bin/withlist.py b/mailman/bin/withlist.py index 8862501d5..f180f5525 100644 --- a/mailman/bin/withlist.py +++ b/mailman/bin/withlist.py @@ -154,7 +154,10 @@ def main(): global LAST_MLIST, VERBOSE parser, opts, args = parseargs() - initialize(opts.config, not opts.quiet) + config_file = (os.getenv('MAILMAN_CONFIG_FILE') + if opts.config is None + else opts.config) + initialize(config_file, not opts.quiet) VERBOSE = not opts.quiet # The default for interact is true unless -r was given diff --git a/mailman/commands/docs/join.txt b/mailman/commands/docs/join.txt index fd382d0f0..9b85e816c 100644 --- a/mailman/commands/docs/join.txt +++ b/mailman/commands/docs/join.txt @@ -106,7 +106,7 @@ Mailman has sent her the confirmation message. this message, keeping the Subject header intact. Or you can visit this web page <BLANKLINE> - http://www.example.com/confirm/... + http://lists.example.com/confirm/... <BLANKLINE> If you do not wish to register this email address simply disregard this message. If you think you are being maliciously subscribed to the list, or diff --git a/mailman/config/config.py b/mailman/config/config.py index 3f717d3be..cbdea2aea 100644 --- a/mailman/config/config.py +++ b/mailman/config/config.py @@ -54,7 +54,6 @@ class Configuration(object): self._config = None self.filename = None # Create various registries. - self.archivers = {} self.chains = {} self.rules = {} self.handlers = {} @@ -174,10 +173,22 @@ class Configuration(object): @property def qrunner_configs(self): + """Iterate over all the qrunner configuration sections.""" for section in self._config.getByCategory('qrunner', []): yield section @property + def archivers(self): + """Iterate over all the enabled archivers.""" + for section in self._config.getByCategory('archiver', []): + if not as_boolean(section.enable): + continue + class_path = section['class'] + module_name, class_name = class_path.rsplit('.', 1) + __import__(module_name) + yield getattr(sys.modules[module_name], class_name)() + + @property def header_matches(self): """Iterate over all spam matching headers. diff --git a/mailman/config/schema.cfg b/mailman/config/schema.cfg index 3cc4eab79..f540bfef1 100644 --- a/mailman/config/schema.cfg +++ b/mailman/config/schema.cfg @@ -250,6 +250,40 @@ lmtp_port: 8025 [archiver.master] +# To add new archivers, define a new section based on this one, overriding the +# following values. + +# The class implementing the IArchiver interface. +class: mailman.archiving.prototype.Prototype + +# Set this to 'yes' to enable the archiver. +enable: no + +# The base url for the archiver. This is used to to calculate links to +# individual messages in the archive. base_url: http://archive.example.com/ + +# If the archiver works by getting a copy of the message, this is the address +# to send the copy to. recipient: archive@archive.example.com + +# If the archiver works by calling a command on the local machine, this is the +# command to call. command: /bin/echo + + +[archiver.mhonarc] +# This is the stock MHonArc archiver. +class: mailman.archiving.mhonarc.MHonArc + +[archiver.mail_archive] +# This is the stock mail-archive.com archiver. +class: mailman.archiving.mailarchive.MailArchive + +[archiver.pipermail] +# This is the stock Pipermail archiver. +class: mailman.archiving.pipermail.Pipermail + +[archiver.prototype] +# This is a prototypical sample archiver. +class: mailman.archiving.prototype.Prototype diff --git a/mailman/core/initialize.py b/mailman/core/initialize.py index d7a2fde5a..bf38303d5 100644 --- a/mailman/core/initialize.py +++ b/mailman/core/initialize.py @@ -70,7 +70,6 @@ def initialize_1(config_path=None, propagate_logs=None): def initialize_2(debug=False): """Second initialization step. - * Archivers * Rules * Chains * Pipelines @@ -89,12 +88,10 @@ def initialize_2(debug=False): # Initialize the rules and chains. Do the imports here so as to avoid # circular imports. from mailman.app.commands import initialize as initialize_commands - from mailman.archiving import initialize as initialize_archivers from mailman.core.chains import initialize as initialize_chains from mailman.core.pipelines import initialize as initialize_pipelines from mailman.core.rules import initialize as initialize_rules # Order here is somewhat important. - initialize_archivers() initialize_rules() initialize_chains() initialize_pipelines() diff --git a/mailman/core/styles.py b/mailman/core/styles.py index 66b8baaf2..aee61790f 100644 --- a/mailman/core/styles.py +++ b/mailman/core/styles.py @@ -43,7 +43,7 @@ from mailman.interfaces import ( class DefaultStyle: - """The defalt (i.e. legacy) style.""" + """The default (i.e. legacy) style.""" implements(IStyle) diff --git a/mailman/database/mailinglist.py b/mailman/database/mailinglist.py index 641245daf..483cc4749 100644 --- a/mailman/database/mailinglist.py +++ b/mailman/database/mailinglist.py @@ -19,6 +19,7 @@ import os import string from storm.locals import * +from urlparse import urljoin from zope.interface import implements from mailman import Defaults @@ -45,7 +46,6 @@ class MailingList(Model): host_name = Unicode() # Attributes not directly modifiable via the web u/i created_at = DateTime() - web_page_url = Unicode() admin_member_chunksize = Int() hold_and_cmd_autoresponses = Pickle() # Attributes which are directly modifiable via the web u/i. The more @@ -202,9 +202,12 @@ class MailingList(Model): def script_url(self, target, context=None): """See `IMailingList`.""" + # Find the domain for this mailing list. + domain = config.domains[self.host_name] # XXX Handle the case for when context is not None; those would be # relative URLs. - return self.web_page_url + target + '/' + self.fqdn_listname + return urljoin(domain.base_url, + target + Defaults.CGIEXT + '/' + self.fqdn_listname) @property def data_path(self): diff --git a/mailman/database/mailman.sql b/mailman/database/mailman.sql index 3cef32e24..b098ed13b 100644 --- a/mailman/database/mailman.sql +++ b/mailman/database/mailman.sql @@ -30,7 +30,6 @@ CREATE TABLE mailinglist ( list_name TEXT, host_name TEXT, created_at TIMESTAMP, - web_page_url TEXT, admin_member_chunksize INTEGER, hold_and_cmd_autoresponses BLOB, next_request_id INTEGER, diff --git a/mailman/docs/archivers.txt b/mailman/docs/archivers.txt index b8da86809..489e3f15b 100644 --- a/mailman/docs/archivers.txt +++ b/mailman/docs/archivers.txt @@ -24,22 +24,25 @@ Pipermail does not support a permalink, so that interface returns None. Mailman defines a draft spec for how list servers and archivers can interoperate. - >>> for archiver_name, archiver in sorted(config.archivers.items()): + >>> archivers = {} + >>> from operator import attrgetter + >>> for archiver in sorted(config.archivers, key=attrgetter('name')): ... print archiver.name ... print ' ', archiver.list_url(mlist) ... print ' ', archiver.permalink(mlist, msg) + ... archivers[archiver.name] = archiver 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 + http://lists.example.com/.../test@example.com + http://lists.example.com/.../RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE pipermail http://www.example.com/pipermail/test@example.com None prototype - http://www.example.com - http://www.example.com/RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE + http://lists.example.com + http://lists.example.com/RSZCG7IGPHFIRW3EMTVMMDNJMNCVCOLE Sending the message to the archiver @@ -47,8 +50,7 @@ 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) + >>> archivers['pipermail'].archive_message(mlist, msg) >>> import os >>> from mailman.interfaces.archiver import IPipermailMailingList @@ -60,7 +62,7 @@ The archiver is also able to archive the message. Note however that the prototype archiver can't archive messages. - >>> config.archivers['prototype'].archive_message(mlist, msg) + >>> archivers['prototype'].archive_message(mlist, msg) Traceback (most recent call last): ... NotImplementedError @@ -74,7 +76,7 @@ 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 = archivers['mail-archive'] >>> archiver.list_url(mlist) 'http://go.mail-archive.dev/test%40example.com' >>> archiver.permalink(mlist, msg) @@ -159,7 +161,7 @@ MHonArc The MHonArc archiver <http://www.mhonarc.org> is also available. - >>> archiver = config.archivers['mhonarc'] + >>> archiver = archivers['mhonarc'] >>> archiver.name 'mhonarc' diff --git a/mailman/docs/chains.txt b/mailman/docs/chains.txt index 6c2137e27..b9fe45686 100644 --- a/mailman/docs/chains.txt +++ b/mailman/docs/chains.txt @@ -101,7 +101,6 @@ The Hold chain places the message into the admin request database and depending on the list's settings, sends a notification to both the original sender and the list moderators. - >>> mlist.web_page_url = u'http://www.example.com/' >>> chain = config.chains['hold'] >>> verifyObject(IChain, chain) True @@ -149,7 +148,7 @@ This message is addressed to the mailing list moderators. <BLANKLINE> At your convenience, visit: <BLANKLINE> - http://www.example.com/admindb/_xtest@example.com + http://lists.example.com/admindb/_xtest@example.com <BLANKLINE> to approve or deny the request. <BLANKLINE> @@ -209,7 +208,7 @@ This message is addressed to the sender of the message. notification of the moderator's decision. If you would like to cancel this posting, please visit the following URL: <BLANKLINE> - http://www.example.com/confirm/_xtest@example.com/... + http://lists.example.com/confirm/_xtest@example.com/... <BLANKLINE> <BLANKLINE> diff --git a/mailman/docs/pipelines.txt b/mailman/docs/pipelines.txt index 94cc792cd..29888ee0b 100644 --- a/mailman/docs/pipelines.txt +++ b/mailman/docs/pipelines.txt @@ -10,7 +10,6 @@ message once it's started. >>> from mailman.app.lifecycle import create_list >>> mlist = create_list(u'xtest@example.com') - >>> mlist.web_page_url = u'http://lists.example.com/archives/' >>> mlist.pipeline u'built-in' >>> from mailman.core.pipelines import process @@ -21,7 +20,6 @@ Processing a message Messages hit the pipeline after they've been accepted for posting. - >>> config.archivers['pipermail'].is_enabled = True >>> msg = message_from_string("""\ ... From: aperson@example.com ... To: xtest@example.com @@ -45,15 +43,18 @@ etc. X-Mailman-Version: ... Precedence: list List-Id: <xtest.example.com> - List-Unsubscribe: - <http://lists.example.com/archives/listinfo/xtest@example.com>, - <mailto:xtest-leave@example.com> - List-Archive: <http://www.example.com/pipermail/xtest@example.com> + X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB List-Post: <mailto:xtest@example.com> - List-Help: <mailto:xtest-request@example.com?subject=help> List-Subscribe: - <http://lists.example.com/archives/listinfo/xtest@example.com>, - <mailto:xtest-join@example.com> + <http://lists.example.com/listinfo/xtest@example.com>, + <mailto:xtest-join@example.com> + Archived-At: + http://lists.example.com/pipermail/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Unsubscribe: + <http://lists.example.com/listinfo/xtest@example.com>, + <mailto:xtest-leave@example.com> + List-Archive: <http://lists.example.com/pipermail/xtest@example.com> + List-Help: <mailto:xtest-request@example.com?subject=help> <BLANKLINE> First post! <BLANKLINE> @@ -82,15 +83,18 @@ And the message is now sitting in various other processing queues. X-Mailman-Version: ... Precedence: list List-Id: <xtest.example.com> - List-Unsubscribe: - <http://lists.example.com/archives/listinfo/xtest@example.com>, - <mailto:xtest-leave@example.com> - List-Archive: <http://www.example.com/pipermail/xtest@example.com> + X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB List-Post: <mailto:xtest@example.com> - List-Help: <mailto:xtest-request@example.com?subject=help> List-Subscribe: - <http://lists.example.com/archives/listinfo/xtest@example.com>, + <http://lists.example.com/listinfo/xtest@example.com>, <mailto:xtest-join@example.com> + Archived-At: + http://lists.example.com/pipermail/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Unsubscribe: + <http://lists.example.com/listinfo/xtest@example.com>, + <mailto:xtest-leave@example.com> + List-Archive: <http://lists.example.com/pipermail/xtest@example.com> + List-Help: <mailto:xtest-request@example.com?subject=help> <BLANKLINE> First post! <BLANKLINE> @@ -122,15 +126,18 @@ This is the message that will actually get delivered to end recipients. X-Mailman-Version: ... Precedence: list List-Id: <xtest.example.com> - List-Unsubscribe: - <http://lists.example.com/archives/listinfo/xtest@example.com>, - <mailto:xtest-leave@example.com> - List-Archive: <http://www.example.com/pipermail/xtest@example.com> + X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB List-Post: <mailto:xtest@example.com> - List-Help: <mailto:xtest-request@example.com?subject=help> List-Subscribe: - <http://lists.example.com/archives/listinfo/xtest@example.com>, + <http://lists.example.com/listinfo/xtest@example.com>, <mailto:xtest-join@example.com> + Archived-At: + http://lists.example.com/pipermail/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Unsubscribe: + <http://lists.example.com/listinfo/xtest@example.com>, + <mailto:xtest-leave@example.com> + List-Archive: <http://lists.example.com/pipermail/xtest@example.com> + List-Help: <mailto:xtest-request@example.com?subject=help> <BLANKLINE> First post! <BLANKLINE> @@ -157,15 +164,18 @@ There's now one message in the digest mailbox, getting ready to be sent. X-Mailman-Version: ... Precedence: list List-Id: <xtest.example.com> - List-Unsubscribe: - <http://lists.example.com/archives/listinfo/xtest@example.com>, - <mailto:xtest-leave@example.com> - List-Archive: <http://www.example.com/pipermail/xtest@example.com> + X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB List-Post: <mailto:xtest@example.com> - List-Help: <mailto:xtest-request@example.com?subject=help> List-Subscribe: - <http://lists.example.com/archives/listinfo/xtest@example.com>, + <http://lists.example.com/listinfo/xtest@example.com>, <mailto:xtest-join@example.com> + Archived-At: + http://lists.example.com/pipermail/4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB + List-Unsubscribe: + <http://lists.example.com/listinfo/xtest@example.com>, + <mailto:xtest-leave@example.com> + List-Archive: <http://lists.example.com/pipermail/xtest@example.com> + List-Help: <mailto:xtest-request@example.com?subject=help> <BLANKLINE> First post! <BLANKLINE> diff --git a/mailman/docs/requests.txt b/mailman/docs/requests.txt index c67a7e06e..0ebb9ff8d 100644 --- a/mailman/docs/requests.txt +++ b/mailman/docs/requests.txt @@ -424,7 +424,6 @@ queue when the message is held. >>> mlist.admin_immed_notify = True >>> # XXX This will almost certainly change once we've worked out the web >>> # space layout for mailing lists now. - >>> mlist.web_page_url = u'http://www.example.com/' >>> id_4 = moderator.hold_subscription(mlist, ... u'cperson@example.org', u'Claire Person', ... u'{NONE}zyxcba', DeliveryMode.regular, u'en') @@ -451,7 +450,7 @@ queue when the message is held. <BLANKLINE> At your convenience, visit: <BLANKLINE> - http://www.example.com/admindb/alist@example.com + http://lists.example.com/admindb/alist@example.com <BLANKLINE> to process the request. <BLANKLINE> @@ -548,7 +547,7 @@ subscription and the fact that they may need to approve it. <BLANKLINE> At your convenience, visit: <BLANKLINE> - http://www.example.com/admindb/alist@example.com + http://lists.example.com/admindb/alist@example.com <BLANKLINE> to process the request. <BLANKLINE> @@ -604,7 +603,7 @@ The welcome message is sent to the person who just subscribed. <BLANKLINE> General information about the mailing list is at: <BLANKLINE> - http://www.example.com/listinfo/alist@example.com + http://lists.example.com/listinfo/alist@example.com <BLANKLINE> If you ever want to unsubscribe or change your options (eg, switch to or from digest mode, change your password, etc.), visit your @@ -711,7 +710,7 @@ unsubscription holds can send the list's moderators an immediate notification. <BLANKLINE> At your convenience, visit: <BLANKLINE> - http://www.example.com/admindb/alist@example.com + http://lists.example.com/admindb/alist@example.com <BLANKLINE> to process the request. <BLANKLINE> diff --git a/mailman/interfaces/archiver.py b/mailman/interfaces/archiver.py index 0159567cc..c69b13427 100644 --- a/mailman/interfaces/archiver.py +++ b/mailman/interfaces/archiver.py @@ -33,8 +33,6 @@ class IArchiver(Interface): 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. diff --git a/mailman/interfaces/mta.py b/mailman/interfaces/mta.py new file mode 100644 index 000000000..a8f55f961 --- /dev/null +++ b/mailman/interfaces/mta.py @@ -0,0 +1,34 @@ +# Copyright (C) 2009 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/>. + +"""Interface for mail transport agent integration.""" + +from zope.interface import Interface, Attribute + + + +class IMailTransportAgent(Interface): + """Interface to the MTA.""" + + def create(mlist): + """Tell the MTA that the mailing list was created.""" + + def delete(mlist): + """Tell the MTA that the mailing list was deleted.""" + + def regenerate(): + """Regenerate the full aliases file.""" diff --git a/mailman/mta/Manual.py b/mailman/mta/Manual.py deleted file mode 100644 index d0e7c359a..000000000 --- a/mailman/mta/Manual.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright (C) 2001-2009 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/>. - -"""Creation/deletion hooks for manual /etc/aliases files.""" - -import sys -import email.Utils - -from cStringIO import StringIO - -from mailman import Message -from mailman import Utils -from mailman.MTA.Utils import makealiases -from mailman.configuration import config -from mailman.i18n import _ -from mailman.queue import Switchboard - - - -# no-ops for interface compliance -def makelock(): - class Dummy: - def lock(self): - pass - def unlock(self, unconditionally=False): - pass - return Dummy() - - -def clear(): - pass - - - -# nolock argument is ignored, but exists for interface compliance -def create(mlist, cgi=False, nolock=False, quiet=False): - if mlist is None: - return - listname = mlist.internal_name() - fieldsz = len(listname) + len('-unsubscribe') - if cgi: - # If a list is being created via the CGI, the best we can do is send - # an email message to mailman-owner requesting that the proper aliases - # be installed. - sfp = StringIO() - if not quiet: - print >> sfp, _("""\ -The mailing list '$listname' has been created via the through-the-web -interface. In order to complete the activation of this mailing list, the -proper /etc/aliases (or equivalent) file must be updated. The program -'newaliases' may also have to be run. - -Here are the entries for the /etc/aliases file: -""") - outfp = sfp - else: - if not quiet: - print _("""\ -To finish creating your mailing list, you must edit your /etc/aliases (or -equivalent) file by adding the following lines, and possibly running the -'newaliases' program: -""") - print _("""\ -## $listname mailing list""") - outfp = sys.stdout - # Common path - for k, v in makealiases(mlist): - print >> outfp, k + ':', ((fieldsz - len(k)) * ' '), v - # If we're using the command line interface, we're done. For ttw, we need - # to actually send the message to mailman-owner now. - if not cgi: - print >> outfp - return - siteowner = Utils.get_site_noreply() - # Should this be sent in the site list's preferred language? - msg = Message.UserNotification( - siteowner, siteowner, - _('Mailing list creation request for list $listname'), - sfp.getvalue(), config.DEFAULT_SERVER_LANGUAGE) - msg.send(mlist) - - - -def remove(mlist, cgi=False): - listname = mlist.fqdn_listname - fieldsz = len(listname) + len('-unsubscribe') - if cgi: - # If a list is being removed via the CGI, the best we can do is send - # an email message to mailman-owner requesting that the appropriate - # aliases be deleted. - sfp = StringIO() - print >> sfp, _("""\ -The mailing list '$listname' has been removed via the through-the-web -interface. In order to complete the de-activation of this mailing list, the -appropriate /etc/aliases (or equivalent) file must be updated. The program -'newaliases' may also have to be run. - -Here are the entries in the /etc/aliases file that should be removed: -""") - outfp = sfp - else: - print _(""" -To finish removing your mailing list, you must edit your /etc/aliases (or -equivalent) file by removing the following lines, and possibly running the -'newaliases' program: - -## $listname mailing list""") - outfp = sys.stdout - # Common path - for k, v in makealiases(mlist): - print >> outfp, k + ':', ((fieldsz - len(k)) * ' '), v - # If we're using the command line interface, we're done. For ttw, we need - # to actually send the message to mailman-owner now. - if not cgi: - print >> outfp - return - siteowner = Utils.get_site_noreply() - # Should this be sent in the site list's preferred language? - msg = Message.UserNotification( - siteowner, siteowner, - _('Mailing list removal request for list $listname'), - sfp.getvalue(), config.DEFAULT_SERVER_LANGUAGE) - msg['Date'] = email.Utils.formatdate(localtime=True) - outq = Switchboard(config.OUTQUEUE_DIR) - outq.enqueue(msg, recips=[siteowner], nodecorate=True) diff --git a/mailman/mta/Postfix.py b/mailman/mta/Postfix.py deleted file mode 100644 index 901c21089..000000000 --- a/mailman/mta/Postfix.py +++ /dev/null @@ -1,411 +0,0 @@ -# Copyright (C) 2001-2009 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/>. - -"""Creation/deletion hooks for the Postfix MTA.""" - -import os -import grp -import pwd -import time -import errno -import logging - -from locknix.lockfile import Lock -from stat import * - -from mailman import Utils -from mailman.MTA.Utils import makealiases -from mailman.configuration import config -from mailman.i18n import _ - -LOCKFILE = os.path.join(config.LOCK_DIR, 'creator') -ALIASFILE = os.path.join(config.DATA_DIR, 'aliases') -VIRTFILE = os.path.join(config.DATA_DIR, 'virtual-mailman') -TRPTFILE = os.path.join(config.DATA_DIR, 'transport') - -log = logging.getLogger('mailman.error') - - - -def _update_maps(): - msg = 'command failed: %s (status: %s, %s)' - if config.USE_LMTP: - tcmd = config.POSTFIX_MAP_CMD + ' ' + TRPTFILE - status = (os.system(tcmd) >> 8) & 0xff - if status: - errstr = os.strerror(status) - log.error(msg, tcmd, status, errstr) - raise RuntimeError(msg % (tcmd, status, errstr)) - acmd = config.POSTFIX_ALIAS_CMD + ' ' + ALIASFILE - status = (os.system(acmd) >> 8) & 0xff - if status: - errstr = os.strerror(status) - log.error(msg, acmd, status, errstr) - raise RuntimeError(msg % (acmd, status, errstr)) - if os.path.exists(VIRTFILE): - vcmd = config.POSTFIX_MAP_CMD + ' ' + VIRTFILE - status = (os.system(vcmd) >> 8) & 0xff - if status: - errstr = os.strerror(status) - log.error(msg, vcmd, status, errstr) - raise RuntimeError(msg % (vcmd, status, errstr)) - - - -def _zapfile(filename): - # Truncate the file w/o messing with the file permissions, but only if it - # already exists. - if os.path.exists(filename): - fp = open(filename, 'w') - fp.close() - - -def clear(): - _zapfile(ALIASFILE) - _zapfile(VIRTFILE) - _zapfile(TRPTFILE) - - - -def _addlist(mlist, fp): - # Set up the mailman-loop address - loopaddr = Utils.ParseEmail(Utils.get_site_noreply())[0] - loopmbox = os.path.join(config.DATA_DIR, 'owner-bounces.mbox') - # Seek to the end of the text file, but if it's empty write the standard - # disclaimer, and the loop catch address. - fp.seek(0, 2) - if not fp.tell(): - print >> fp, """\ -# This file is generated by Mailman, and is kept in sync with the -# binary hash file aliases.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE -# unless you know what you're doing, and can keep the two files properly -# in sync. If you screw it up, you're on your own. -""" - print >> fp, '# The ultimate loop stopper address' - print >> fp, '%s: %s' % (loopaddr, loopmbox) - print >> fp - # Bootstrapping. bin/genaliases must be run before any lists are created, - # but if no lists exist yet then mlist is None. The whole point of the - # exercise is to get the minimal aliases.db file into existance. - if mlist is None: - return - listname = mlist.internal_name() - hostname = mlist.host_name - fieldsz = len(listname) + len('-unsubscribe') - # The text file entries get a little extra info - print >> fp, '# STANZA START: %s@%s' % (listname, hostname) - print >> fp, '# CREATED:', time.ctime(time.time()) - # Now add all the standard alias entries - for k, v in makealiases(mlist): - l = len(k) - if hostname in config.POSTFIX_STYLE_VIRTUAL_DOMAINS: - k += config.POSTFIX_VIRTUAL_SEPARATOR + hostname - # Format the text file nicely - print >> fp, k + ':', ((fieldsz - l) * ' ') + v - # Finish the text file stanza - print >> fp, '# STANZA END: %s@%s' % (listname, hostname) - print >> fp - - - -def _addvirtual(mlist, fp): - listname = mlist.internal_name() - fieldsz = len(listname) + len('-unsubscribe') - hostname = mlist.host_name - # Set up the mailman-loop address - loopaddr = mlist.no_reply_address - loopdest = Utils.ParseEmail(loopaddr)[0] - # Seek to the end of the text file, but if it's empty write the standard - # disclaimer, and the loop catch address. - fp.seek(0, 2) - if not fp.tell(): - print >> fp, """\ -# This file is generated by Mailman, and is kept in sync with the binary hash -# file virtual-mailman.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you -# know what you're doing, and can keep the two files properly in sync. If you -# screw it up, you're on your own. -# -# Note that you should already have this virtual domain set up properly in -# your Postfix installation. See README.POSTFIX for details. - -# LOOP ADDRESSES START -%s\t%s -# LOOP ADDRESSES END -""" % (loopaddr, loopdest) - # The text file entries get a little extra info - print >> fp, '# STANZA START: %s@%s' % (listname, hostname) - print >> fp, '# CREATED:', time.ctime(time.time()) - # Now add all the standard alias entries - for k, v in makealiases(mlist): - fqdnaddr = '%s@%s' % (k, hostname) - l = len(k) - # Format the text file nicely - if hostname in config.POSTFIX_STYLE_VIRTUAL_DOMAINS: - k += config.POSTFIX_VIRTUAL_SEPARATOR + hostname - print >> fp, fqdnaddr, ((fieldsz - l) * ' '), k - # Finish the text file stanza - print >> fp, '# STANZA END: %s@%s' % (listname, hostname) - print >> fp - - - -# Blech. -def _check_for_virtual_loopaddr(mlist, filename, func): - loopaddr = mlist.no_reply_address - loopdest = Utils.ParseEmail(loopaddr)[0] - if func is _addtransport: - loopdest = 'local:' + loopdest - infp = open(filename) - outfp = open(filename + '.tmp', 'w') - try: - # Find the start of the loop address block - while True: - line = infp.readline() - if not line: - break - outfp.write(line) - if line.startswith('# LOOP ADDRESSES START'): - break - # Now see if our domain has already been written - while True: - line = infp.readline() - if not line: - break - if line.startswith('# LOOP ADDRESSES END'): - # It hasn't - print >> outfp, '%s\t%s' % (loopaddr, loopdest) - outfp.write(line) - break - elif line.startswith(loopaddr): - # We just found it - outfp.write(line) - break - else: - # This isn't our loop address, so spit it out and continue - outfp.write(line) - outfp.writelines(infp.readlines()) - finally: - infp.close() - outfp.close() - os.rename(filename + '.tmp', filename) - - - -def _addtransport(mlist, fp): - # Set up the mailman-loop address - loopaddr = mlist.no_reply_address - loopdest = Utils.ParseEmail(loopaddr)[0] - # create/add postfix transport file for mailman - fp.seek(0, 2) - if not fp.tell(): - print >> fp, """\ -# This file is generated by Mailman, and is kept in sync with the -# binary hash file transport.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE -# unless you know what you're doing, and can keep the two files properly -# in sync. If you screw it up, you're on your own. - -# LOOP ADDRESSES START -%s\tlocal:%s -# LOOP ADDRESSES END -""" % (loopaddr, loopdest) - # List LMTP_ONLY_DOMAINS - if config.LMTP_ONLY_DOMAINS: - print >> fp, '# LMTP ONLY DOMAINS START' - for dom in config.LMTP_ONLY_DOMAINS: - print >> fp, '%s\tlmtp:%s:%s' % (dom, - config.LMTP_HOST, - config.LMTP_PORT) - print >> fp, '# LMTP ONLY DOMAINS END\n' - listname = mlist.internal_name() - hostname = mlist.host_name - # No need of individual local part if the domain is LMTP only - if hostname in config.LMTP_ONLY_DOMAINS: - return - fieldsz = len(listname) + len(hostname) + len('-unsubscribe') + 1 - # The text file entries get a little extra info - print >> fp, '# STANZA START: %s@%s' % (listname, hostname) - print >> fp, '# CREATED:', time.ctime(time.time()) - # Now add transport entries - for k, v in makealiases(mlist): - l = len(k + hostname) + 1 - print >> fp, '%s@%s' % (k, hostname), ((fieldsz - l) * ' ')\ - + 'lmtp:%s:%s' % (config.LMTP_HOST, config.LMTP_PORT) - # - print >> fp, '# STANZA END: %s@%s' % (listname, hostname) - print >> fp - - - -def _do_create(mlist, textfile, func): - # Crack open the plain text file - try: - fp = open(textfile, 'r+') - except IOError, e: - if e.errno <> errno.ENOENT: - raise - fp = open(textfile, 'w+') - try: - func(mlist, fp) - finally: - fp.close() - # Now double check the virtual plain text file - if func in (_addvirtual, _addtransport): - _check_for_virtual_loopaddr(mlist, textfile, func) - - -def create(mlist, cgi=False, nolock=False, quiet=False): - # Acquire the global list database lock. quiet flag is ignored. - lock = None - if not nolock: - # XXX FIXME - lock = makelock() - lock.lock() - # Do the aliases file, which always needs to be done - try: - if config.USE_LMTP: - _do_create(mlist, TRPTFILE, _addtransport) - _do_create(None, ALIASFILE, _addlist) - else: - _do_create(mlist, ALIASFILE, _addlist) - if mlist.host_name in config.POSTFIX_STYLE_VIRTUAL_DOMAINS: - _do_create(mlist, VIRTFILE, _addvirtual) - _update_maps() - finally: - if lock: - lock.unlock(unconditionally=True) - - - -def _do_remove(mlist, textfile): - listname = mlist.internal_name() - hostname = mlist.host_name - # Now do our best to filter out the proper stanza from the text file. - # The text file better exist! - outfp = None - try: - infp = open(textfile) - except IOError, e: - if e.errno <> errno.ENOENT: - raise - # Otherwise, there's no text file to filter so we're done. - return - try: - outfp = open(textfile + '.tmp', 'w') - filteroutp = False - start = '# STANZA START: %s@%s' % (listname, hostname) - end = '# STANZA END: %s@%s' % (listname, hostname) - while 1: - line = infp.readline() - if not line: - break - # If we're filtering out a stanza, just look for the end marker and - # filter out everything in between. If we're not in the middle of - # filtering out a stanza, we're just looking for the proper begin - # marker. - if filteroutp: - if line.strip() == end: - filteroutp = False - # Discard the trailing blank line, but don't worry if - # we're at the end of the file. - infp.readline() - # Otherwise, ignore the line - else: - if line.strip() == start: - # Filter out this stanza - filteroutp = True - else: - outfp.write(line) - # Close up shop, and rotate the files - finally: - infp.close() - outfp.close() - os.rename(textfile+'.tmp', textfile) - - -def remove(mlist, cgi=False): - # Acquire the global list database lock - with Lock(LOCKFILE): - if config.USE_LMTP: - _do_remove(mlist, TRPTFILE) - else: - _do_remove(mlist, ALIASFILE) - if mlist.host_name in config.POSTFIX_STYLE_VIRTUAL_DOMAINS: - _do_remove(mlist, VIRTFILE) - # Regenerate the alias and map files - _update_maps() - config.db.commit() - - - -def checkperms(state): - targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP - for file in ALIASFILE, VIRTFILE, TRPTFILE: - if state.VERBOSE: - print _('checking permissions on %(file)s') - stat = None - try: - stat = os.stat(file) - except OSError, e: - if e.errno <> errno.ENOENT: - raise - if stat and (stat[ST_MODE] & targetmode) <> targetmode: - state.ERRORS += 1 - octmode = oct(stat[ST_MODE]) - print _('%(file)s permissions must be 066x (got %(octmode)s)'), - if state.FIX: - print _('(fixing)') - os.chmod(file, stat[ST_MODE] | targetmode) - else: - print - # Make sure the corresponding .db files are owned by the Mailman user. - # We don't need to check the group ownership of the file, since - # check_perms checks this itself. - dbfile = file + '.db' - stat = None - try: - stat = os.stat(dbfile) - except OSError, e: - if e.errno <> errno.ENOENT: - raise - continue - if state.VERBOSE: - print _('checking ownership of %(dbfile)s') - user = config.MAILMAN_USER - ownerok = stat[ST_UID] == pwd.getpwnam(user)[2] - if not ownerok: - try: - owner = pwd.getpwuid(stat[ST_UID])[0] - except KeyError: - owner = 'uid %d' % stat[ST_UID] - print _('%(dbfile)s owned by %(owner)s (must be owned by %(user)s'), - state.ERRORS += 1 - if state.FIX: - print _('(fixing)') - uid = pwd.getpwnam(user)[2] - gid = grp.getgrnam(config.MAILMAN_GROUP)[2] - os.chown(dbfile, uid, gid) - else: - print - if stat and (stat[ST_MODE] & targetmode) <> targetmode: - state.ERRORS += 1 - octmode = oct(stat[ST_MODE]) - print _('%(dbfile)s permissions must be 066x (got %(octmode)s)'), - if state.FIX: - print _('(fixing)') - os.chmod(dbfile, stat[ST_MODE] | targetmode) - else: - print diff --git a/mailman/mta/postfix.py b/mailman/mta/postfix.py new file mode 100644 index 000000000..4b92d5789 --- /dev/null +++ b/mailman/mta/postfix.py @@ -0,0 +1,122 @@ +# Copyright (C) 2001-2009 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/>. + +"""Creation/deletion hooks for the Postfix MTA.""" + +__metaclass__ = type +__all__ = [ + 'LMTP', + ] + + +import os +import grp +import pwd +import time +import errno +import logging +import datetime + +from locknix.lockfile import Lock +from zope.interface import implements + +from mailman import Defaults +from mailman import Utils +from mailman.config import config +from mailman.interfaces.mta import IMailTransportAgent +from mailman.i18n import _ + +log = logging.getLogger('mailman.error') + +LOCKFILE = os.path.join(config.LOCK_DIR, 'mta') +SUBDESTINATIONS = ( + 'bounces', 'confirm', 'join', 'leave', + 'owner', 'request', 'subscribe', 'unsubscribe', + ) + + + +class LMTP: + """Connect Mailman to Postfix via LMTP.""" + + implements(IMailTransportAgent) + + def create(self, mlist): + """See `IMailTransportAgent`.""" + # Acquire a lock file to prevent other processes from racing us here. + with Lock(LOCKFILE): + # We can ignore the mlist argument because for LMTP delivery, we + # just generate the entire file every time. + self._do_write_file() + + delete = create + + def regenerate(self): + """See `IMailTransportAgent`.""" + # Acquire a lock file to prevent other processes from racing us here. + with Lock(LOCKFILE): + self._do_write_file() + + def _do_write_file(self): + """Do the actual file writes for list creation.""" + # Open up the new alias text file. + path = os.path.join(config.DATA_DIR, 'postfix_lmtp') + # Sort all existing mailing list names first by domain, then my local + # part. For postfix we need a dummy entry for the domain. + by_domain = {} + for mailing_list in config.db.list_manager.mailing_lists: + by_domain.setdefault(mailing_list.host_name, []).append( + mailing_list.list_name) + with open(path + '.new', 'w') as fp: + print >> fp, """\ +# AUTOMATICALLY GENERATED BY MAILMAN ON {0} +# +# This file is generated by Mailman, and is kept in sync with the binary hash +# file. YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you know what you're +# doing, and can keep the two files properly in sync. If you screw it up, +# you're on your own. +""".format(datetime.datetime.now().replace(microsecond=0)) + for domain in sorted(by_domain): + print >> fp, """\ +# Aliases which are visible only in the @{0} domain. +""".format(domain) + for list_name in by_domain[domain]: + # Calculate the field width of the longest alias. 10 == + # len('-subscribe') + '@'. + longest = len(list_name + domain) + 10 + print >> fp, """\ +{0}@{1:{3}}lmtp:{2.mta.lmtp_host}:{2.mta.lmtp_port}""".format( + list_name, domain, config, + # Add 1 because the bare list name has no dash. + longest + 1) + for destination in SUBDESTINATIONS: + print >> fp, """\ +{0}-{1}@{2:{4}}lmtp:{3.mta.lmtp_host}:{3.mta.lmtp_port}""".format( + list_name, destination, domain, config, + longest - len(destination)) + print >> fp + # Move the temporary file into place, then generate the new .db file. + os.rename(path + '.new', path) + # Now that the new aliases file has been written, we must tell Postfix + # to generate a new .db file. + command = Defaults.POSTFIX_MAP_CMD + ' ' + path + status = (os.system(command) >> 8) & 0xff + if status: + msg = 'command failure: %s, %s, %s' + errstr = os.strerror(status) + log.error(msg, command, status, errstr) + raise RuntimeError(msg % (command, status, errstr)) diff --git a/mailman/options.py b/mailman/options.py index 32f80d426..8d752bfb5 100644 --- a/mailman/options.py +++ b/mailman/options.py @@ -24,6 +24,7 @@ __all__ = [ ] +import os import sys from copy import copy @@ -108,7 +109,11 @@ class Options: from the configuration files. :type propagate_logs: bool or None. """ - initialize(self.options.config, propagate_logs=propagate_logs) + # Fall back to using the environment variable if -C is not given. + config_file = (os.getenv('MAILMAN_CONFIG_FILE') + if self.options.config is None + else self.options.config) + initialize(config_file, propagate_logs=propagate_logs) self.sanity_check() diff --git a/mailman/pipeline/cook_headers.py b/mailman/pipeline/cook_headers.py index a37a92a69..18000e01a 100644 --- a/mailman/pipeline/cook_headers.py +++ b/mailman/pipeline/cook_headers.py @@ -216,9 +216,7 @@ def process(mlist, msg, msgdata): headers['List-Post'] = '<mailto:%s>' % mlist.posting_address # Add RFC 2369 and 5064 archiving headers, if archiving is enabled. if mlist.archive: - for archiver in get_plugins('mailman.archiver'): - if not archiver.is_enabled: - continue + for archiver in config.archivers: headers['List-Archive'] = '<%s>' % archiver.list_url(mlist) permalink = archiver.permalink(mlist, msg) if permalink is not None: diff --git a/mailman/pipeline/decorate.py b/mailman/pipeline/decorate.py index 3059bde38..4e4b1b97b 100644 --- a/mailman/pipeline/decorate.py +++ b/mailman/pipeline/decorate.py @@ -202,10 +202,9 @@ def decorate(mlist, template, extradict=None): list_name = mlist.list_name, fqdn_listname = mlist.fqdn_listname, host_name = mlist.host_name, - web_page_url = mlist.web_page_url, + listinfo_page = mlist.script_url('listinfo'), description = mlist.description, info = mlist.info, - cgiext = Defaults.CGIEXT, ) if extradict is not None: d.update(extradict) diff --git a/mailman/pipeline/docs/acknowledge.txt b/mailman/pipeline/docs/acknowledge.txt index d1206b6f3..aabd8196a 100644 --- a/mailman/pipeline/docs/acknowledge.txt +++ b/mailman/pipeline/docs/acknowledge.txt @@ -11,7 +11,6 @@ acknowledgment. >>> mlist.preferred_language = u'en' >>> # XXX This will almost certainly change once we've worked out the web >>> # space layout for mailing lists now. - >>> mlist.web_page_url = u'http://lists.example.com/' >>> # Ensure that the virgin queue is empty, since we'll be checking this >>> # for new auto-response messages. diff --git a/mailman/pipeline/docs/cook-headers.txt b/mailman/pipeline/docs/cook-headers.txt index 985214079..eb1a4e6bc 100644 --- a/mailman/pipeline/docs/cook-headers.txt +++ b/mailman/pipeline/docs/cook-headers.txt @@ -12,9 +12,6 @@ is getting sent through the system. We'll take things one-by-one. >>> mlist.subject_prefix = u'' >>> mlist.include_list_post_header = False >>> mlist.archive = True - >>> # XXX This will almost certainly change once we've worked out the web - >>> # space layout for mailing lists now. - >>> mlist.web_page_url = u'http://lists.example.com/' Saving the original sender @@ -183,7 +180,6 @@ 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> @@ -192,7 +188,7 @@ But normally, a list will include these headers. >>> process(mlist, msg, {}) >>> list_headers(msg) ---start--- - List-Archive: <http://www.example.com/pipermail/_xtest@example.com> + List-Archive: <http://lists.example.com/pipermail/_xtest@example.com> List-Help: <mailto:_xtest-request@example.com?subject=help> List-Id: <_xtest.example.com> List-Post: <mailto:_xtest@example.com> @@ -213,7 +209,7 @@ header. >>> process(mlist, msg, {}) >>> list_headers(msg) ---start--- - List-Archive: <http://www.example.com/pipermail/_xtest@example.com> + List-Archive: <http://lists.example.com/pipermail/_xtest@example.com> List-Help: <mailto:_xtest-request@example.com?subject=help> List-Id: My test mailing list <_xtest.example.com> List-Post: <mailto:_xtest@example.com> @@ -252,7 +248,7 @@ List-Post header, which is reasonable for an announce only mailing list. >>> process(mlist, msg, {}) >>> list_headers(msg) ---start--- - List-Archive: <http://www.example.com/pipermail/_xtest@example.com> + List-Archive: <http://lists.example.com/pipermail/_xtest@example.com> List-Help: <mailto:_xtest-request@example.com?subject=help> List-Id: My test mailing list <_xtest.example.com> List-Subscribe: <http://lists.example.com/listinfo/_xtest@example.com>, diff --git a/mailman/pipeline/docs/digests.txt b/mailman/pipeline/docs/digests.txt index 7ef82382f..e94f9912f 100644 --- a/mailman/pipeline/docs/digests.txt +++ b/mailman/pipeline/docs/digests.txt @@ -9,7 +9,6 @@ digests, although only two are currently supported: MIME digests and RFC 1153 >>> from mailman.pipeline.to_digest import process >>> mlist = config.db.list_manager.create(u'_xtest@example.com') >>> mlist.preferred_language = u'en' - >>> mlist.web_page_url = u'http://www.example.com/' >>> mlist.real_name = u'XTest' >>> mlist.subject_prefix = u'[_XTest] ' >>> mlist.one_last_digest = set() @@ -126,7 +125,7 @@ digest and an RFC 1153 plain text digest. The size threshold is in KB. _xtest@example.com <BLANKLINE> To subscribe or unsubscribe via the World Wide Web, visit - http://www.example.com/listinfo/_xtest@example.com + http://lists.example.com/listinfo/_xtest@example.com or, via email, send a message with subject or body 'help' to _xtest-request@example.com <BLANKLINE> @@ -276,7 +275,7 @@ digest and an RFC 1153 plain text digest. The size threshold is in KB. _xtest@example.com <BLANKLINE> To subscribe or unsubscribe via the World Wide Web, visit - http://www.example.com/listinfo/_xtest@example.com + http://lists.example.com/listinfo/_xtest@example.com or, via email, send a message with subject or body 'help' to _xtest-request@example.com <BLANKLINE> @@ -464,7 +463,7 @@ Set the digest threshold to zero so that the digests will be sent immediately. _xtest@example.com <BLANKLINE> Pour vous (d=E9s)abonner par le web, consultez - http://www.example.com/listinfo/_xtest@example.com + http://lists.example.com/listinfo/_xtest@example.com <BLANKLINE> ou, par courriel, envoyez un message avec =AB=A0help=A0=BB dans le corps ou dans le sujet =E0 diff --git a/mailman/pipeline/docs/replybot.txt b/mailman/pipeline/docs/replybot.txt index f9f824e4e..f3c3281b3 100644 --- a/mailman/pipeline/docs/replybot.txt +++ b/mailman/pipeline/docs/replybot.txt @@ -9,7 +9,6 @@ message or the amount of time since the last auto-response. >>> from mailman.pipeline.replybot import process >>> mlist = config.db.list_manager.create(u'_xtest@example.com') >>> mlist.real_name = u'XTest' - >>> mlist.web_page_url = u'http://www.example.com/' >>> # Ensure that the virgin queue is empty, since we'll be checking this >>> # for new auto-response messages. diff --git a/mailman/queue/__init__.py b/mailman/queue/__init__.py index 52686b3ab..f83fd46e9 100644 --- a/mailman/queue/__init__.py +++ b/mailman/queue/__init__.py @@ -378,7 +378,7 @@ class Runner: # them out of our sight. # # Find out which mailing list this message is destined for. - listname = msgdata.get('listname') + listname = unicode(msgdata.get('listname')) mlist = config.db.list_manager.get(listname) if mlist is None: elog.error('Dequeuing message destined for missing list: %s', diff --git a/mailman/queue/archive.py b/mailman/queue/archive.py index 69ec46f4b..c97bd86fb 100644 --- a/mailman/queue/archive.py +++ b/mailman/queue/archive.py @@ -24,15 +24,17 @@ __all__ = [ import os +import sys import time import logging from datetime import datetime from email.Utils import parsedate_tz, mktime_tz, formatdate +from lazr.config import as_boolean from locknix.lockfile import Lock from mailman import Defaults -from mailman.core.plugins import get_plugins +from mailman.config import config from mailman.queue import Runner log = logging.getLogger('mailman.error') @@ -80,11 +82,10 @@ class ArchiveRunner(Runner): msg['X-List-Received-Date'] = received_time # While a list archiving lock is acquired, archive the message. with Lock(os.path.join(mlist.data_path, 'archive.lck')): - for archive_factory in get_plugins('mailman.archiver'): - # A problem in one archiver should not prevent any other - # archiver from running. + for archiver in config.archivers: + # A problem in one archiver should not prevent other archivers + # from running. try: - archive = archive_factory() - archive.archive_message(mlist, msg) + archiver.archive_message(mlist, msg) except Exception: - log.exception('Broken archiver: %s' % archive.name) + log.exception('Broken archiver: %s' % archiver.name) diff --git a/mailman/queue/docs/archiver.txt b/mailman/queue/docs/archiver.txt index ed7c26d45..601857cd9 100644 --- a/mailman/queue/docs/archiver.txt +++ b/mailman/queue/docs/archiver.txt @@ -6,8 +6,7 @@ interface. By default, there's a Pipermail archiver. >>> from mailman.app.lifecycle import create_list >>> mlist = create_list(u'test@example.com') - >>> mlist.web_page_url = u'http://www.example.com/' - >>> config.db.commit() + >>> commit() >>> msg = message_from_string("""\ ... From: aperson@example.com diff --git a/mailman/queue/docs/incoming.txt b/mailman/queue/docs/incoming.txt index 22b32d828..7402d2aaf 100644 --- a/mailman/queue/docs/incoming.txt +++ b/mailman/queue/docs/incoming.txt @@ -87,7 +87,6 @@ pipeline queue. >>> fp.seek(0, 2) >>> mlist.emergency = True - >>> mlist.web_page_url = u'http://archives.example.com/' >>> inject_message(mlist, msg) >>> file_pos = fp.tell() >>> incoming.run() diff --git a/mailman/queue/lmtp.py b/mailman/queue/lmtp.py index f0895ee1f..1f45732a3 100644 --- a/mailman/queue/lmtp.py +++ b/mailman/queue/lmtp.py @@ -143,13 +143,14 @@ class LMTPRunner(Runner, smtpd.SMTPServer): # Parse the message data. If there are any defects in the # message, reject it right away; it's probably spam. msg = email.message_from_string(data, Message) + msg.original_size = len(data) if msg.defects: return ERR_501 msg['X-MailFrom'] = mailfrom except Exception, e: elog.exception('LMTP message parsing') config.db.abort() - return CRLF.join([ERR_451 for to in rcpttos]) + return CRLF.join(ERR_451 for to in rcpttos) # RFC 2033 requires us to return a status code for every recipient. status = [] # Now for each address in the recipients, parse the address to first @@ -169,7 +170,8 @@ class LMTPRunner(Runner, smtpd.SMTPServer): # The recipient is a valid mailing list; see if it's a valid # sub-address, and if so, enqueue it. queue = None - msgdata = dict(listname=listname) + msgdata = dict(listname=listname, + original_size=msg.original_size) if subaddress in ('bounces', 'admin'): queue = 'bounce' elif subaddress == 'confirm': diff --git a/mailman/rules/docs/emergency.txt b/mailman/rules/docs/emergency.txt index e71566853..9d80fdb40 100644 --- a/mailman/rules/docs/emergency.txt +++ b/mailman/rules/docs/emergency.txt @@ -6,7 +6,6 @@ list are held for moderator approval. >>> from mailman.app.lifecycle import create_list >>> mlist = create_list(u'_xtest@example.com') - >>> mlist.web_page_url = u'http://www.example.com/' >>> msg = message_from_string("""\ ... From: aperson@example.com ... To: _xtest@example.com diff --git a/mailman/testing/testing.cfg b/mailman/testing/testing.cfg index ff9bb89da..107db86ed 100644 --- a/mailman/testing/testing.cfg +++ b/mailman/testing/testing.cfg @@ -19,6 +19,7 @@ [mta] smtp_port: 9025 +incoming: mailman.testing.mta.FakeMTA [qrunner.archive] max_restarts: 1 @@ -56,19 +57,25 @@ max_restarts: 1 [qrunner.virgin] max_restarts: 1 +[archiver.prototype] +enable: yes + [archiver.mail_archive] +enable: yes base_url: http://go.mail-archive.dev/ recipient: archive@mail-archive.dev [archiver.pipermail] +enable: yes base_url: http://www.example.com/pipermail/$listname [archiver.mhonarc] +enable: yes command: /bin/echo "/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" [domain.example_dot_com] email_host: example.com -base_url: http://www.example.com +base_url: http://lists.example.com contact_address: postmaster@example.com [language.ja] @@ -90,20 +90,12 @@ case second `m'. Any other spelling is incorrect.""", include_package_data = True, entry_points = { 'console_scripts': list(scripts), - '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', - 'mailman.styles' : 'default = mailman.core.styles:DefaultStyle', - 'mailman.mta' : 'stock = mailman.MTA:Manual', - 'mailman.rules' : 'default = mailman.rules:initialize', 'mailman.handlers' : 'default = mailman.pipeline:initialize', + 'mailman.rules' : 'default = mailman.rules:initialize', + 'mailman.scrubber' : 'stock = mailman.archiving.pipermail:Pipermail', + 'mailman.styles' : 'default = mailman.core.styles:DefaultStyle', }, install_requires = [ 'lazr.config', |
