diff options
Diffstat (limited to 'src/mailman/core')
| -rw-r--r-- | src/mailman/core/__init__.py | 0 | ||||
| -rw-r--r-- | src/mailman/core/chains.py | 118 | ||||
| -rw-r--r-- | src/mailman/core/errors.py | 172 | ||||
| -rw-r--r-- | src/mailman/core/initialize.py | 125 | ||||
| -rw-r--r-- | src/mailman/core/logging.py | 163 | ||||
| -rw-r--r-- | src/mailman/core/pipelines.py | 125 | ||||
| -rw-r--r-- | src/mailman/core/plugins.py | 74 | ||||
| -rw-r--r-- | src/mailman/core/rules.py | 46 |
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 |
