summaryrefslogtreecommitdiff
path: root/src/mailman/core
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/core')
-rw-r--r--src/mailman/core/__init__.py0
-rw-r--r--src/mailman/core/chains.py118
-rw-r--r--src/mailman/core/errors.py172
-rw-r--r--src/mailman/core/initialize.py125
-rw-r--r--src/mailman/core/logging.py163
-rw-r--r--src/mailman/core/pipelines.py125
-rw-r--r--src/mailman/core/plugins.py74
-rw-r--r--src/mailman/core/rules.py46
8 files changed, 823 insertions, 0 deletions
diff --git a/src/mailman/core/__init__.py b/src/mailman/core/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/mailman/core/__init__.py
diff --git a/src/mailman/core/chains.py b/src/mailman/core/chains.py
new file mode 100644
index 000000000..40b8c779f
--- /dev/null
+++ b/src/mailman/core/chains.py
@@ -0,0 +1,118 @@
+# Copyright (C) 2007-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/>.
+
+"""Application support for chain processing."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'initialize',
+ 'process',
+ ]
+
+
+from mailman.chains.accept import AcceptChain
+from mailman.chains.builtin import BuiltInChain
+from mailman.chains.discard import DiscardChain
+from mailman.chains.headers import HeaderMatchChain
+from mailman.chains.hold import HoldChain
+from mailman.chains.reject import RejectChain
+from mailman.config import config
+from mailman.interfaces.chain import LinkAction
+
+
+
+def process(mlist, msg, msgdata, start_chain='built-in'):
+ """Process the message through a chain.
+
+ :param mlist: the IMailingList for this message.
+ :param msg: The Message object.
+ :param msgdata: The message metadata dictionary.
+ :param start_chain: The name of the chain to start the processing with.
+ """
+ # Set up some bookkeeping.
+ chain_stack = []
+ msgdata['rule_hits'] = hits = []
+ msgdata['rule_misses'] = misses = []
+ # Find the starting chain and begin iterating through its links.
+ chain = config.chains[start_chain]
+ chain_iter = chain.get_links(mlist, msg, msgdata)
+ # Loop until we've reached the end of all processing chains.
+ while chain:
+ # Iterate over all links in the chain. Do this outside a for-loop so
+ # we can capture a chain's link iterator in mid-flight. This supports
+ # the 'detour' link action
+ try:
+ link = next(chain_iter)
+ except StopIteration:
+ # This chain is exhausted. Pop the last chain on the stack and
+ # continue iterating through it. If there's nothing left on the
+ # chain stack then we're completely finished processing.
+ if len(chain_stack) == 0:
+ return
+ chain, chain_iter = chain_stack.pop()
+ continue
+ # Process this link.
+ if link.rule.check(mlist, msg, msgdata):
+ if link.rule.record:
+ hits.append(link.rule.name)
+ # The rule matched so run its action.
+ if link.action is LinkAction.jump:
+ chain = link.chain
+ chain_iter = chain.get_links(mlist, msg, msgdata)
+ continue
+ elif link.action is LinkAction.detour:
+ # Push the current chain so that we can return to it when
+ # the next chain is finished.
+ chain_stack.append((chain, chain_iter))
+ chain = link.chain
+ chain_iter = chain.get_links(mlist, msg, msgdata)
+ continue
+ elif link.action is LinkAction.stop:
+ # Stop all processing.
+ return
+ elif link.action is LinkAction.defer:
+ # Just process the next link in the chain.
+ pass
+ elif link.action is LinkAction.run:
+ link.function(mlist, msg, msgdata)
+ else:
+ raise AssertionError(
+ 'Bad link action: {0}'.format(link.action))
+ else:
+ # The rule did not match; keep going.
+ if link.rule.record:
+ misses.append(link.rule.name)
+
+
+
+def initialize():
+ """Set up chains, both built-in and from the database."""
+ for chain_class in (DiscardChain, HoldChain, RejectChain, AcceptChain):
+ chain = chain_class()
+ assert chain.name not in config.chains, (
+ 'Duplicate chain name: {0}'.format(chain.name))
+ config.chains[chain.name] = chain
+ # Set up a couple of other default chains.
+ chain = BuiltInChain()
+ config.chains[chain.name] = chain
+ # Create and initialize the header matching chain.
+ chain = HeaderMatchChain()
+ config.chains[chain.name] = chain
+ # XXX Read chains from the database and initialize them.
+ pass
diff --git a/src/mailman/core/errors.py b/src/mailman/core/errors.py
new file mode 100644
index 000000000..39401127e
--- /dev/null
+++ b/src/mailman/core/errors.py
@@ -0,0 +1,172 @@
+# Copyright (C) 1998-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/>.
+
+"""Mailman errors."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'AlreadyReceivingDigests',
+ 'AlreadyReceivingRegularDeliveries',
+ 'BadDomainSpecificationError',
+ 'BadPasswordSchemeError',
+ 'CantDigestError',
+ 'DiscardMessage',
+ 'EmailAddressError',
+ 'HandlerError',
+ 'HoldMessage',
+ 'HostileSubscriptionError',
+ 'InvalidEmailAddress',
+ 'LostHeldMessage',
+ 'MailmanError',
+ 'MailmanException',
+ 'MemberError',
+ 'MembershipIsBanned',
+ 'MustDigestError',
+ 'NotAMemberError',
+ 'PasswordError',
+ 'RejectMessage',
+ 'SomeRecipientsFailed',
+ 'SubscriptionError',
+ ]
+
+
+
+# Base class for all exceptions raised in Mailman (XXX except legacy string
+# exceptions).
+class MailmanException(Exception):
+ pass
+
+
+
+# "New" style membership exceptions (new w/ MM2.1)
+class MemberError(MailmanException): pass
+class NotAMemberError(MemberError): pass
+class AlreadyReceivingDigests(MemberError): pass
+class AlreadyReceivingRegularDeliveries(MemberError): pass
+class CantDigestError(MemberError): pass
+class MustDigestError(MemberError): pass
+class MembershipIsBanned(MemberError): pass
+
+
+
+# New style class based exceptions. All the above errors should eventually be
+# converted.
+
+class MailmanError(MailmanException):
+ """Base class for all Mailman errors."""
+ pass
+
+class BadDomainSpecificationError(MailmanError):
+ """The specification of a virtual domain is invalid or duplicated."""
+
+
+
+# Exception hierarchy for bad email address errors that can be raised from
+# Utils.ValidateEmail()
+class EmailAddressError(MailmanError):
+ """Base class for email address validation errors."""
+
+
+class InvalidEmailAddress(EmailAddressError):
+ """Email address is invalid."""
+
+
+
+# Exceptions for admin request database
+class LostHeldMessage(MailmanError):
+ """Held message was lost."""
+ pass
+
+
+
+def _(s):
+ return s
+
+# Exceptions for the Handler subsystem
+class HandlerError(MailmanError):
+ """Base class for all handler errors."""
+
+class HoldMessage(HandlerError):
+ """Base class for all message-being-held short circuits."""
+
+ # funky spelling is necessary to break import loops
+ reason = _('For some unknown reason')
+
+ def reason_notice(self):
+ return self.reason
+
+ # funky spelling is necessary to break import loops
+ rejection = _('Your message was rejected')
+
+ def rejection_notice(self, mlist):
+ return self.rejection
+
+class DiscardMessage(HandlerError):
+ """The message can be discarded with no further action"""
+
+class SomeRecipientsFailed(HandlerError):
+ """Delivery to some or all recipients failed"""
+ def __init__(self, tempfailures, permfailures):
+ HandlerError.__init__(self)
+ self.tempfailures = tempfailures
+ self.permfailures = permfailures
+
+class RejectMessage(HandlerError):
+ """The message will be bounced back to the sender"""
+ def __init__(self, notice=None):
+ super(RejectMessage, self).__init__()
+ if notice is None:
+ notice = _('Your message was rejected')
+ if notice.endswith('\n\n'):
+ pass
+ elif notice.endswith('\n'):
+ notice += '\n'
+ else:
+ notice += '\n\n'
+ self.notice = notice
+
+
+
+# Subscription exceptions
+class SubscriptionError(MailmanError):
+ """Subscription errors base class."""
+
+
+class HostileSubscriptionError(SubscriptionError):
+ """A cross-subscription attempt was made.
+
+ This exception gets raised when an invitee attempts to use the
+ invitation to cross-subscribe to some other mailing list.
+ """
+
+
+
+class PasswordError(MailmanError):
+ """A password related error."""
+
+
+class BadPasswordSchemeError(PasswordError):
+ """A bad password scheme was given."""
+
+ def __init__(self, scheme_name='unknown'):
+ super(BadPasswordSchemeError, self).__init__()
+ self.scheme_name = scheme_name
+
+ def __str__(self):
+ return 'A bad password scheme was given: %s' % self.scheme_name
diff --git a/src/mailman/core/initialize.py b/src/mailman/core/initialize.py
new file mode 100644
index 000000000..bb16f0036
--- /dev/null
+++ b/src/mailman/core/initialize.py
@@ -0,0 +1,125 @@
+# Copyright (C) 2006-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/>.
+
+"""Initialize all global state.
+
+Every entrance into the Mailman system, be it by command line, mail program,
+or cgi, must call the initialize function here in order for the system's
+global state to be set up properly. Typically this is called after command
+line argument parsing, since some of the initialization behavior is controlled
+by the command line arguments.
+"""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'initialize',
+ 'initialize_1',
+ 'initialize_2',
+ 'initialize_3',
+ ]
+
+
+import os
+
+from zope.interface.interface import adapter_hooks
+from zope.interface.verify import verifyObject
+
+import mailman.config.config
+import mailman.core.logging
+
+from mailman.core.plugins import get_plugin
+from mailman.interfaces.database import IDatabase
+
+
+
+# These initialization calls are separated for the testing framework, which
+# needs to do some internal calculations after config file loading and log
+# initialization, but before database initialization. Generally all other
+# code will just call initialize().
+
+def initialize_1(config_path=None, propagate_logs=None):
+ """First initialization step.
+
+ * The configuration system
+ * Run-time directories
+ * The logging subsystem
+
+ :param config_path: The path to the configuration file.
+ :type config_path: string
+ :param propagate_logs: Should the log output propagate to stderr?
+ :type propagate_logs: boolean or None
+ """
+ # By default, set the umask so that only owner and group can read and
+ # write our files. Specifically we must have g+rw and we probably want
+ # o-rwx although I think in most cases it doesn't hurt if other can read
+ # or write the files. Note that the Pipermail archive has more
+ # restrictive permissions in order to handle private archives, but it
+ # handles that correctly.
+ os.umask(007)
+ mailman.config.config.load(config_path)
+ # Create the queue and log directories if they don't already exist.
+ mailman.config.config.ensure_directories_exist()
+ mailman.core.logging.initialize(propagate_logs)
+
+
+def initialize_2(debug=False):
+ """Second initialization step.
+
+ * Rules
+ * Chains
+ * Pipelines
+ * Commands
+
+ :param debug: Should the database layer be put in debug mode?
+ :type debug: boolean
+ """
+ database_plugin = get_plugin('mailman.database')
+ # Instantiate the database plugin, ensure that it's of the right type, and
+ # initialize it. Then stash the object on our configuration object.
+ database = database_plugin()
+ verifyObject(IDatabase, database)
+ database.initialize(debug)
+ mailman.config.config.db = database
+ # 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.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_rules()
+ initialize_chains()
+ initialize_pipelines()
+ initialize_commands()
+
+
+def initialize_3():
+ """Third initialization step.
+
+ * Adapters
+ """
+ from mailman.app.registrar import adapt_domain_to_registrar
+ adapter_hooks.append(adapt_domain_to_registrar)
+
+
+
+def initialize(config_path=None, propagate_logs=None):
+ initialize_1(config_path, propagate_logs)
+ initialize_2()
+ initialize_3()
diff --git a/src/mailman/core/logging.py b/src/mailman/core/logging.py
new file mode 100644
index 000000000..a18065965
--- /dev/null
+++ b/src/mailman/core/logging.py
@@ -0,0 +1,163 @@
+# Copyright (C) 2006-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/>.
+
+"""Logging initialization, using Python's standard logging package."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'initialize',
+ 'reopen',
+ ]
+
+
+import os
+import sys
+import codecs
+import logging
+
+from lazr.config import as_boolean, as_log_level
+
+from mailman.config import config
+
+
+_handlers = {}
+
+
+
+# XXX I would love to simplify things and use Python 2.6's WatchedFileHandler,
+# but there are two problems. First, it's more difficult to handle the test
+# suite's need to reopen the file handler to a different path. Does
+# zope.testing's logger support fix this?
+#
+# The other problem is that WatchedFileHandler doesn't really easily support
+# HUPing the process to reopen the log file. Now, maybe that's not a big deal
+# because the standard logging module would already handle things correctly if
+# the file is moved, but still that's not an interface I'm ready to give up on
+# yet. For now, keep our hack.
+
+class ReopenableFileHandler(logging.Handler):
+ """A file handler that supports reopening."""
+
+ def __init__(self, name, filename):
+ self.name = name
+ self._filename = filename
+ self._stream = self._open()
+ logging.Handler.__init__(self)
+
+ def _open(self):
+ return codecs.open(self._filename, 'a', 'utf-8')
+
+ def flush(self):
+ if self._stream:
+ self._stream.flush()
+
+ def emit(self, record):
+ # It's possible for the stream to have been closed by the time we get
+ # here, due to the shut down semantics. This mostly happens in the
+ # test suite, but be defensive anyway.
+ stream = (self._stream if self._stream else sys.stderr)
+ try:
+ msg = self.format(record)
+ try:
+ stream.write('{0}'.format(msg))
+ except UnicodeError:
+ stream.write('{0}'.format(msg.encode('string-escape')))
+ self.flush()
+ except:
+ self.handleError(record)
+
+ def close(self):
+ self.flush()
+ self._stream.close()
+ self._stream = None
+ logging.Handler.close(self)
+
+ def reopen(self, filename=None):
+ """Reopen the output stream.
+
+ :param filename: If given, this reopens the output stream to a new
+ file. This is used in the test suite.
+ :type filename: string
+ """
+ if filename is not None:
+ self._filename = filename
+ self._stream.close()
+ self._stream = self._open()
+
+
+
+def initialize(propagate=None):
+ """Initialize all logs.
+
+ :param propagate: Flag specifying whether logs should propagate their
+ messages to the root logger. If omitted, propagation is determined
+ from the configuration files.
+ :type propagate: bool or None
+ """
+ # First, find the root logger and configure the logging subsystem.
+ # Initialize the root logger, then create a formatter for all the
+ # sublogs. The root logger should log to stderr.
+ logging.basicConfig(format=config.logging.root.format,
+ datefmt=config.logging.root.datefmt,
+ level=as_log_level(config.logging.root.level),
+ stream=sys.stderr)
+ # Create the subloggers.
+ for logger_config in config.logger_configs:
+ sub_name = logger_config.name.split('.')[-1]
+ if sub_name == 'root':
+ continue
+ logger_name = 'mailman.' + sub_name
+ log = logging.getLogger(logger_name)
+ # Get settings from log configuration file (or defaults).
+ log_format = logger_config.format
+ log_datefmt = logger_config.datefmt
+ # Propagation to the root logger is how we handle logging to stderr
+ # when the qrunners are not run as a subprocess of mailmanctl.
+ log.propagate = (as_boolean(logger_config.propagate)
+ if propagate is None else propagate)
+ # Set the logger's level.
+ log.setLevel(as_log_level(logger_config.level))
+ # Create a formatter for this logger, then a handler, and link the
+ # formatter to the handler.
+ formatter = logging.Formatter(fmt=log_format, datefmt=log_datefmt)
+ path_str = logger_config.path
+ path_abs = os.path.normpath(os.path.join(config.LOG_DIR, path_str))
+ handler = ReopenableFileHandler(sub_name, path_abs)
+ _handlers[sub_name] = handler
+ handler.setFormatter(formatter)
+ log.addHandler(handler)
+
+
+
+def reopen():
+ """Re-open all log files."""
+ for handler in _handlers.values():
+ handler.reopen()
+
+
+
+def get_handler(sub_name):
+ """Return the handler associated with a named logger.
+
+ :param sub_name: The logger name, sans the 'mailman.' prefix.
+ :type sub_name: string
+ :return: The file handler associated with the named logger.
+ :rtype: `ReopenableFileHandler`
+ """
+ return _handlers[sub_name]
diff --git a/src/mailman/core/pipelines.py b/src/mailman/core/pipelines.py
new file mode 100644
index 000000000..8aae5cc25
--- /dev/null
+++ b/src/mailman/core/pipelines.py
@@ -0,0 +1,125 @@
+# 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/>.
+
+"""Pipeline processor."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'initialize',
+ 'process',
+ ]
+
+
+from zope.interface import implements
+from zope.interface.verify import verifyObject
+
+from mailman.config import config
+from mailman.core.plugins import get_plugins
+from mailman.i18n import _
+from mailman.interfaces.handler import IHandler
+from mailman.interfaces.pipeline import IPipeline
+
+
+
+def process(mlist, msg, msgdata, pipeline_name='built-in'):
+ """Process the message through the given pipeline.
+
+ :param mlist: the IMailingList for this message.
+ :param msg: The Message object.
+ :param msgdata: The message metadata dictionary.
+ :param pipeline_name: The name of the pipeline to process through.
+ """
+ pipeline = config.pipelines[pipeline_name]
+ for handler in pipeline:
+ handler.process(mlist, msg, msgdata)
+
+
+
+class BasePipeline:
+ """Base pipeline implementation."""
+
+ implements(IPipeline)
+
+ _default_handlers = ()
+
+ def __init__(self):
+ self._handlers = []
+ for handler_name in self._default_handlers:
+ self._handlers.append(config.handlers[handler_name])
+
+ def __iter__(self):
+ """See `IPipeline`."""
+ for handler in self._handlers:
+ yield handler
+
+
+class BuiltInPipeline(BasePipeline):
+ """The built-in pipeline."""
+
+ name = 'built-in'
+ description = _('The built-in pipeline.')
+
+ _default_handlers = (
+ 'mime-delete',
+ 'scrubber',
+ 'tagger',
+ 'calculate-recipients',
+ 'avoid-duplicates',
+ 'cleanse',
+ 'cleanse-dkim',
+ 'cook-headers',
+ 'to-digest',
+ 'to-archive',
+ 'to-usenet',
+ 'after-delivery',
+ 'acknowledge',
+ 'to-outgoing',
+ )
+
+
+class VirginPipeline(BasePipeline):
+ """The processing pipeline for virgin messages.
+
+ Virgin messages are those that are crafted internally by Mailman.
+ """
+ name = 'virgin'
+ description = _('The virgin queue pipeline.')
+
+ _default_handlers = (
+ 'cook-headers',
+ 'to-outgoing',
+ )
+
+
+
+def initialize():
+ """Initialize the pipelines."""
+ # Find all handlers in the registered plugins.
+ for handler_finder in get_plugins('mailman.handlers'):
+ for handler_class in handler_finder():
+ handler = handler_class()
+ verifyObject(IHandler, handler)
+ assert handler.name not in config.handlers, (
+ 'Duplicate handler "{0}" found in {1}'.format(
+ handler.name, handler_finder))
+ config.handlers[handler.name] = handler
+ # Set up some pipelines.
+ for pipeline_class in (BuiltInPipeline, VirginPipeline):
+ pipeline = pipeline_class()
+ config.pipelines[pipeline.name] = pipeline
diff --git a/src/mailman/core/plugins.py b/src/mailman/core/plugins.py
new file mode 100644
index 000000000..e9ba26571
--- /dev/null
+++ b/src/mailman/core/plugins.py
@@ -0,0 +1,74 @@
+# Copyright (C) 2007-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/>.
+
+"""Get a requested plugin."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ ]
+
+
+import pkg_resources
+
+
+
+def get_plugin(group):
+ """Get the named plugin.
+
+ In general, this returns exactly one plugin. If no plugins have been
+ added to the named group, the 'stock' plugin will be used. If more than
+ one plugin -- other than the stock one -- exists, an exception will be
+ raised.
+
+ :param group: The plugin group name.
+ :return: The loaded plugin.
+ :raises RuntimeError: If more than one plugin overrides the stock plugin
+ for the named group.
+ """
+ entry_points = list(pkg_resources.iter_entry_points(group))
+ if len(entry_points) == 0:
+ raise RuntimeError(
+ 'No entry points found for group: {0}'.format(group))
+ elif len(entry_points) == 1:
+ # Okay, this is the one to use.
+ return entry_points[0].load()
+ elif len(entry_points) == 2:
+ # Find the one /not/ named 'stock'.
+ entry_points = [ep for ep in entry_points if ep.name <> 'stock']
+ if len(entry_points) == 0:
+ raise RuntimeError(
+ 'No stock plugin found for group: {0}'.format(group))
+ elif len(entry_points) == 2:
+ raise RuntimeError('Too many stock plugins defined')
+ else:
+ raise AssertionError('Insanity')
+ return entry_points[0].load()
+ else:
+ raise RuntimeError('Too many plugins for group: {0}'.format(group))
+
+
+
+def get_plugins(group):
+ """Get and return all plugins in the named group.
+
+ :param group: Plugin group name.
+ :return: The loaded plugin.
+ """
+ for entry_point in pkg_resources.iter_entry_points(group):
+ yield entry_point.load()
diff --git a/src/mailman/core/rules.py b/src/mailman/core/rules.py
new file mode 100644
index 000000000..83e24dfa2
--- /dev/null
+++ b/src/mailman/core/rules.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2007-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/>.
+
+"""Various rule helpers"""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'initialize',
+ ]
+
+
+from zope.interface.verify import verifyObject
+
+from mailman.config import config
+from mailman.core.plugins import get_plugins
+from mailman.interfaces.rules import IRule
+
+
+
+def initialize():
+ """Find and register all rules in all plugins."""
+ # Find rules in plugins.
+ for rule_finder in get_plugins('mailman.rules'):
+ for rule_class in rule_finder():
+ rule = rule_class()
+ verifyObject(IRule, rule)
+ assert rule.name not in config.rules, (
+ 'Duplicate rule "{0}" found in {1}'.format(
+ rule.name, rule_finder))
+ config.rules[rule.name] = rule