diff options
Diffstat (limited to 'mailman/core')
| -rw-r--r-- | mailman/core/chains.py | 115 | ||||
| -rw-r--r-- | mailman/core/pipelines.py | 122 | ||||
| -rw-r--r-- | mailman/core/plugins.py | 65 | ||||
| -rw-r--r-- | mailman/core/rules.py | 44 | ||||
| -rw-r--r-- | mailman/core/styles.py | 294 |
5 files changed, 640 insertions, 0 deletions
diff --git a/mailman/core/chains.py b/mailman/core/chains.py new file mode 100644 index 000000000..bebbf5bee --- /dev/null +++ b/mailman/core/chains.py @@ -0,0 +1,115 @@ +# Copyright (C) 2007-2008 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.""" + +__all__ = [ + 'initialize', + 'process', + ] +__metaclass__ = type + + +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.configuration import config +from mailman.interfaces 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 = chain_iter.next() + 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: %s' % 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: %s' % 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/mailman/core/pipelines.py b/mailman/core/pipelines.py new file mode 100644 index 000000000..c790901b2 --- /dev/null +++ b/mailman/core/pipelines.py @@ -0,0 +1,122 @@ +# Copyright (C) 2008 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.""" + +__metaclass__ = type +__all__ = [ + 'initialize', + 'process', + ] + + +from zope.interface import implements +from zope.interface.verify import verifyObject + +from mailman.configuration import config +from mailman.core.plugins import get_plugins +from mailman.i18n import _ +from mailman.interfaces import IHandler, 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 "%s" found in %s' % + (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/mailman/core/plugins.py b/mailman/core/plugins.py new file mode 100644 index 000000000..cf22ad377 --- /dev/null +++ b/mailman/core/plugins.py @@ -0,0 +1,65 @@ +# Copyright (C) 2007-2008 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.""" + +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: %s' % 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: %s' % 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: %s' % 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/mailman/core/rules.py b/mailman/core/rules.py new file mode 100644 index 000000000..2c1e4fb3b --- /dev/null +++ b/mailman/core/rules.py @@ -0,0 +1,44 @@ +# Copyright (C) 2007-2008 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""" + +__metaclass__ = type +__all__ = [ + 'initialize', + ] + + +from zope.interface import implements +from zope.interface.verify import verifyObject + +from mailman.configuration import config +from mailman.core.plugins import get_plugins +from mailman.interfaces 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 "%s" found in %s' % (rule.name, rule_finder)) + config.rules[rule.name] = rule diff --git a/mailman/core/styles.py b/mailman/core/styles.py new file mode 100644 index 000000000..96104c204 --- /dev/null +++ b/mailman/core/styles.py @@ -0,0 +1,294 @@ +# Copyright (C) 2007-2008 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 of list styles to new and existing lists.""" + +__metaclass__ = type +__all__ = [ + 'DefaultStyle', + 'style_manager', + ] + +import datetime + +from operator import attrgetter +from zope.interface import implements +from zope.interface.verify import verifyObject + +from mailman import Utils +from mailman.configuration import config +from mailman.core.plugins import get_plugins +from mailman.i18n import _ +from mailman.interfaces import ( + Action, DuplicateStyleError, IStyle, IStyleManager, NewsModeration, + Personalization) + + + +class DefaultStyle: + """The defalt (i.e. legacy) style.""" + + implements(IStyle) + + name = 'default' + priority = 0 # the lowest priority style + + def apply(self, mailing_list): + """See `IStyle`.""" + # For cut-n-paste convenience. + mlist = mailing_list + # Most of these were ripped from the old MailList.InitVars() method. + mlist.volume = 1 + mlist.post_id = 1 + mlist.new_member_options = config.DEFAULT_NEW_MEMBER_OPTIONS + # This stuff is configurable + mlist.real_name = mlist.list_name.capitalize() + mlist.respond_to_post_requests = True + mlist.advertised = config.DEFAULT_LIST_ADVERTISED + mlist.max_num_recipients = config.DEFAULT_MAX_NUM_RECIPIENTS + mlist.max_message_size = config.DEFAULT_MAX_MESSAGE_SIZE + mlist.reply_goes_to_list = config.DEFAULT_REPLY_GOES_TO_LIST + mlist.reply_to_address = u'' + mlist.first_strip_reply_to = config.DEFAULT_FIRST_STRIP_REPLY_TO + mlist.admin_immed_notify = config.DEFAULT_ADMIN_IMMED_NOTIFY + mlist.admin_notify_mchanges = ( + config.DEFAULT_ADMIN_NOTIFY_MCHANGES) + mlist.require_explicit_destination = ( + config.DEFAULT_REQUIRE_EXPLICIT_DESTINATION) + mlist.acceptable_aliases = config.DEFAULT_ACCEPTABLE_ALIASES + mlist.send_reminders = config.DEFAULT_SEND_REMINDERS + mlist.send_welcome_msg = config.DEFAULT_SEND_WELCOME_MSG + mlist.send_goodbye_msg = config.DEFAULT_SEND_GOODBYE_MSG + mlist.bounce_matching_headers = ( + config.DEFAULT_BOUNCE_MATCHING_HEADERS) + mlist.header_matches = [] + mlist.anonymous_list = config.DEFAULT_ANONYMOUS_LIST + mlist.description = u'' + mlist.info = u'' + mlist.welcome_msg = u'' + mlist.goodbye_msg = u'' + mlist.subscribe_policy = config.DEFAULT_SUBSCRIBE_POLICY + mlist.subscribe_auto_approval = config.DEFAULT_SUBSCRIBE_AUTO_APPROVAL + mlist.unsubscribe_policy = config.DEFAULT_UNSUBSCRIBE_POLICY + mlist.private_roster = config.DEFAULT_PRIVATE_ROSTER + mlist.obscure_addresses = config.DEFAULT_OBSCURE_ADDRESSES + mlist.admin_member_chunksize = config.DEFAULT_ADMIN_MEMBER_CHUNKSIZE + mlist.administrivia = config.DEFAULT_ADMINISTRIVIA + mlist.preferred_language = config.DEFAULT_SERVER_LANGUAGE + mlist.include_rfc2369_headers = True + mlist.include_list_post_header = True + mlist.filter_mime_types = config.DEFAULT_FILTER_MIME_TYPES + mlist.pass_mime_types = config.DEFAULT_PASS_MIME_TYPES + mlist.filter_filename_extensions = ( + config.DEFAULT_FILTER_FILENAME_EXTENSIONS) + mlist.pass_filename_extensions = config.DEFAULT_PASS_FILENAME_EXTENSIONS + mlist.filter_content = config.DEFAULT_FILTER_CONTENT + mlist.collapse_alternatives = config.DEFAULT_COLLAPSE_ALTERNATIVES + mlist.convert_html_to_plaintext = ( + config.DEFAULT_CONVERT_HTML_TO_PLAINTEXT) + mlist.filter_action = config.DEFAULT_FILTER_ACTION + # Digest related variables + mlist.digestable = config.DEFAULT_DIGESTABLE + mlist.digest_is_default = config.DEFAULT_DIGEST_IS_DEFAULT + mlist.mime_is_default_digest = config.DEFAULT_MIME_IS_DEFAULT_DIGEST + mlist.digest_size_threshhold = config.DEFAULT_DIGEST_SIZE_THRESHHOLD + mlist.digest_send_periodic = config.DEFAULT_DIGEST_SEND_PERIODIC + mlist.digest_header = config.DEFAULT_DIGEST_HEADER + mlist.digest_footer = config.DEFAULT_DIGEST_FOOTER + mlist.digest_volume_frequency = config.DEFAULT_DIGEST_VOLUME_FREQUENCY + mlist.one_last_digest = {} + mlist.digest_members = {} + mlist.next_digest_number = 1 + mlist.nondigestable = config.DEFAULT_NONDIGESTABLE + mlist.personalize = Personalization.none + # New sender-centric moderation (privacy) options + mlist.default_member_moderation = ( + config.DEFAULT_DEFAULT_MEMBER_MODERATION) + # Archiver + mlist.archive = config.DEFAULT_ARCHIVE + mlist.archive_private = config.DEFAULT_ARCHIVE_PRIVATE + mlist.archive_volume_frequency = ( + config.DEFAULT_ARCHIVE_VOLUME_FREQUENCY) + mlist.emergency = False + mlist.member_moderation_action = Action.hold + mlist.member_moderation_notice = u'' + mlist.accept_these_nonmembers = [] + mlist.hold_these_nonmembers = [] + mlist.reject_these_nonmembers = [] + mlist.discard_these_nonmembers = [] + mlist.forward_auto_discards = config.DEFAULT_FORWARD_AUTO_DISCARDS + mlist.generic_nonmember_action = ( + config.DEFAULT_GENERIC_NONMEMBER_ACTION) + mlist.nonmember_rejection_notice = u'' + # Ban lists + mlist.ban_list = [] + # Max autoresponses per day. A mapping between addresses and a + # 2-tuple of the date of the last autoresponse and the number of + # autoresponses sent on that date. + mlist.hold_and_cmd_autoresponses = {} + mlist.subject_prefix = _(config.DEFAULT_SUBJECT_PREFIX) + mlist.msg_header = config.DEFAULT_MSG_HEADER + mlist.msg_footer = config.DEFAULT_MSG_FOOTER + # Set this to Never if the list's preferred language uses us-ascii, + # otherwise set it to As Needed + if Utils.GetCharSet(mlist.preferred_language) == 'us-ascii': + mlist.encode_ascii_prefixes = 0 + else: + mlist.encode_ascii_prefixes = 2 + # scrub regular delivery + mlist.scrub_nondigest = config.DEFAULT_SCRUB_NONDIGEST + # automatic discarding + mlist.max_days_to_hold = config.DEFAULT_MAX_DAYS_TO_HOLD + # Autoresponder + mlist.autorespond_postings = False + mlist.autorespond_admin = False + # this value can be + # 0 - no autoresponse on the -request line + # 1 - autorespond, but discard the original message + # 2 - autorespond, and forward the message on to be processed + mlist.autorespond_requests = 0 + mlist.autoresponse_postings_text = u'' + mlist.autoresponse_admin_text = u'' + mlist.autoresponse_request_text = u'' + mlist.autoresponse_graceperiod = datetime.timedelta(days=90) + mlist.postings_responses = {} + mlist.admin_responses = {} + mlist.request_responses = {} + # Bounces + mlist.bounce_processing = config.DEFAULT_BOUNCE_PROCESSING + mlist.bounce_score_threshold = config.DEFAULT_BOUNCE_SCORE_THRESHOLD + mlist.bounce_info_stale_after = config.DEFAULT_BOUNCE_INFO_STALE_AFTER + mlist.bounce_you_are_disabled_warnings = ( + config.DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS) + mlist.bounce_you_are_disabled_warnings_interval = ( + config.DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS_INTERVAL) + mlist.bounce_unrecognized_goes_to_list_owner = ( + config.DEFAULT_BOUNCE_UNRECOGNIZED_GOES_TO_LIST_OWNER) + mlist.bounce_notify_owner_on_disable = ( + config.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_DISABLE) + mlist.bounce_notify_owner_on_removal = ( + config.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_REMOVAL) + # This holds legacy member related information. It's keyed by the + # member address, and the value is an object containing the bounce + # score, the date of the last received bounce, and a count of the + # notifications left to send. + mlist.bounce_info = {} + # New style delivery status + mlist.delivery_status = {} + # NNTP gateway + mlist.nntp_host = config.DEFAULT_NNTP_HOST + mlist.linked_newsgroup = u'' + mlist.gateway_to_news = False + mlist.gateway_to_mail = False + mlist.news_prefix_subject_too = True + # In patch #401270, this was called newsgroup_is_moderated, but the + # semantics weren't quite the same. + mlist.news_moderation = NewsModeration.none + # Topics + # + # `topics' is a list of 4-tuples of the following form: + # + # (name, pattern, description, emptyflag) + # + # name is a required arbitrary string displayed to the user when they + # get to select their topics of interest + # + # pattern is a required verbose regular expression pattern which is + # used as IGNORECASE. + # + # description is an optional description of what this topic is + # supposed to match + # + # emptyflag is a boolean used internally in the admin interface to + # signal whether a topic entry is new or not (new ones which do not + # have a name or pattern are not saved when the submit button is + # pressed). + mlist.topics = [] + mlist.topics_enabled = False + mlist.topics_bodylines_limit = 5 + # This is a mapping between user "names" (i.e. addresses) and + # information about which topics that user is interested in. The + # values are a list of topic names that the user is interested in, + # which should match the topic names in mlist.topics above. + # + # If the user has not selected any topics of interest, then the rule + # is that they will get all messages, and they will not have an entry + # in this dictionary. + mlist.topics_userinterest = {} + # The processing chain that messages coming into this list get + # processed by. + mlist.start_chain = u'built-in' + # The default pipeline to send accepted messages through. + mlist.pipeline = u'built-in' + + def match(self, mailing_list, styles): + """See `IStyle`.""" + # If no other styles have matched, then the default style matches. + if len(styles) == 0: + styles.append(self) + + + +class StyleManager: + """The built-in style manager.""" + + implements(IStyleManager) + + def __init__(self): + """Install all styles from registered plugins, and install them.""" + self._styles = {} + # Install all the styles provided by plugins. + for style_factory in get_plugins('mailman.styles'): + style = style_factory() + # Let DuplicateStyleErrors percolate up. + self.register(style) + + def get(self, name): + """See `IStyleManager`.""" + return self._styles.get(name) + + def lookup(self, mailing_list): + """See `IStyleManager`.""" + matched_styles = [] + for style in self.styles: + style.match(mailing_list, matched_styles) + for style in matched_styles: + yield style + + @property + def styles(self): + """See `IStyleManager`.""" + for style in sorted(self._styles.values(), + key=attrgetter('priority'), + reverse=True): + yield style + + def register(self, style): + """See `IStyleManager`.""" + verifyObject(IStyle, style) + if style.name in self._styles: + raise DuplicateStyleError(style.name) + self._styles[style.name] = style + + def unregister(self, style): + """See `IStyleManager`.""" + # Let KeyErrors percolate up. + del self._styles[style.name] + + + +style_manager = StyleManager() |
