summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.bzrignore2
-rw-r--r--Mailman/app/rules.py35
-rw-r--r--Mailman/docs/rules.txt112
-rw-r--r--Mailman/interfaces/__init__.py7
-rw-r--r--Mailman/interfaces/rule.py73
-rwxr-xr-xMailman/rules/__init__.py75
-rw-r--r--Mailman/rules/emergency.py44
-rw-r--r--setup.py7
8 files changed, 349 insertions, 6 deletions
diff --git a/.bzrignore b/.bzrignore
index d17a0df0b..a365285be 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -5,7 +5,7 @@ cron/crontab.in
dist/
mailman.egg-info
misc/mailman
-setuptoolsbzr-*.egg
+setuptools_bzr-*.egg
staging
TAGS
var/
diff --git a/Mailman/app/rules.py b/Mailman/app/rules.py
new file mode 100644
index 000000000..3e83eb60c
--- /dev/null
+++ b/Mailman/app/rules.py
@@ -0,0 +1,35 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Process all rules defined by entry points."""
+
+from Mailman.app.plugins import get_plugins
+
+
+
+def process(mlist, msg, msgdata):
+ """Default rule processing plugin.
+
+ :param msg: The message object.
+ :param msgdata: The message metadata.
+ :return: A set of rule names that matched.
+ """
+ rule_hits = set()
+ for processor_class in get_plugins('mailman.rules'):
+ processor = processor_class()
+ rule_hits |= processor.process(mlist, msg, msgdata)
+ return rule_hits
diff --git a/Mailman/docs/rules.txt b/Mailman/docs/rules.txt
new file mode 100644
index 000000000..6f7c8b680
--- /dev/null
+++ b/Mailman/docs/rules.txt
@@ -0,0 +1,112 @@
+Rules
+=====
+
+The rule processor is used to determine the status of a message. Should the
+message be posted to the list, or held for moderator approval? Should the
+message be discarded or rejected (i.e. bounced back to the original sender)?
+
+Actually, these actions are not part of rule processing! Instead, Mailman
+first runs through a set of rules looking for matches. Then later, the
+matched rules are prioritized and matched to an action. Action matching is
+described elsewhere; this documentation describes only the rule processing
+system.
+
+
+Rule processors
+===============
+
+IRuleProcessor is the interface that describes a rule processor. Mailman can
+be extended by plugging in additional rule processors, but it also comes with
+a default rule processor, called the 'built-in rule processor'.
+
+ >>> from zope.interface.verify import verifyObject
+ >>> from Mailman.interfaces import IRuleProcessor
+ >>> from Mailman.rules import BuiltinRules
+ >>> processor = BuiltinRules()
+ >>> verifyObject(IRuleProcessor, processor)
+ True
+
+You can iterator over all the rules in a rule processor.
+
+ >>> from Mailman.interfaces import IRule
+ >>> rule = None
+ >>> for rule in processor.rules:
+ ... if rule.name == 'emergency':
+ ... break
+ >>> verifyObject(IRule, rule)
+ True
+ >>> rule.name
+ 'emergency'
+ >>> print rule.description
+ The mailing list is in emergency hold and this message was not pre-approved
+ by the list administrator.
+
+You can ask for a rule by name.
+
+ >>> processor['emergency'].name
+ 'emergency'
+ >>> processor.get('emergency').name
+ 'emergency'
+
+Processors act like dictionaries when the rule is missing.
+
+ >>> processor['no such rule']
+ Traceback (most recent call last):
+ ...
+ KeyError: 'no such rule'
+ >>> print processor.get('no such rule')
+ None
+ >>> missing = object()
+ >>> processor.get('no such rule', missing) is missing
+ True
+
+
+Rule checks
+-----------
+
+Individual rules can be checked to see if they match, by running the rule's
+`check()` method. This returns a boolean indicating whether the rule was
+matched or not.
+
+ >>> from Mailman.configuration import config
+ >>> mlist = config.db.list_manager.create(u'_xtest@example.com')
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.com
+ ...
+ ... An important message.
+ ... """)
+
+For example, the emergency rule just checks to see if the emergency flag is
+set on the mailing list, and the message has not been pre-approved by the list
+administrator.
+
+ >>> rule = processor['emergency']
+ >>> rule.name
+ 'emergency'
+ >>> mlist.emergency = False
+ >>> rule.check(mlist, msg, {})
+ False
+ >>> mlist.emergency = True
+ >>> rule.check(mlist, msg, {})
+ True
+ >>> rule.check(mlist, msg, dict(adminapproved=True))
+ False
+
+
+Rule processing
+---------------
+
+Mailman has a global rule processor which will return a set of all the rule
+names that match the current message.
+
+ >>> from Mailman.app.rules import process
+ >>> matches = process(mlist, msg, {})
+ >>> matches & set(['emergency'])
+ set(['emergency'])
+ >>> matches = process(mlist, msg, dict(adminapproved=True))
+ >>> matches & set(['emergency'])
+ set([])
+ >>> mlist.emergency = False
+ >>> matches = process(mlist, msg, {})
+ >>> matches & set(['emergency'])
+ set([])
diff --git a/Mailman/interfaces/__init__.py b/Mailman/interfaces/__init__.py
index c4094e365..20dff7fdf 100644
--- a/Mailman/interfaces/__init__.py
+++ b/Mailman/interfaces/__init__.py
@@ -46,7 +46,12 @@ def _populate():
is_enum = issubclass(obj, Enum)
except TypeError:
is_enum = False
- if IInterface.providedBy(obj) or is_enum:
+ is_interface = IInterface.providedBy(obj)
+ try:
+ is_exception = issubclass(obj, Exception)
+ except TypeError:
+ is_exception = False
+ if is_interface or is_exception or is_enum:
setattr(iface_mod, name, obj)
__all__.append(name)
diff --git a/Mailman/interfaces/rule.py b/Mailman/interfaces/rule.py
new file mode 100644
index 000000000..c0cf71697
--- /dev/null
+++ b/Mailman/interfaces/rule.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Interface describing the basics of rules."""
+
+from zope.interface import Interface, Attribute
+
+
+
+class DuplicateRuleError(Exception):
+ """A rule or rule name is added to a processor more than once."""
+
+
+
+class IRule(Interface):
+ """A basic rule."""
+
+ name = Attribute('Rule name; must be unique.')
+ description = Attribute('A brief description of the rule.')
+
+ def check(mlist, msg, msgdata):
+ """Run the rule.
+
+ :param msg: The message object.
+ :param msgdata: The message metadata.
+ :return: A boolean specifying whether the rule was matched or not.
+ """
+
+
+
+class IRuleProcessor(Interface):
+ """A rule processor."""
+
+ def process(mlist, msg, msgdata):
+ """Run all rules this processor knows about.
+
+ :param mlist: The mailing list this message was posted to.
+ :param msg: The message object.
+ :param msgdata: The message metadata.
+ :return: A set of rule names that matched.
+ """
+
+ rules = Attribute('The set of all rules this processor knows about')
+
+ def __getitem__(rule_name):
+ """Return the named rule.
+
+ :param rule_name: The name of the rule.
+ :return: The IRule given by this name.
+ :raise: KeyError if no such rule is known by this processor.
+ """
+
+ def get(rule_name, default=None):
+ """Return the name rule.
+
+ :param rule_name: The name of the rule.
+ :return: The IRule given by this name, or `default` if no such rule
+ is known by this processor.
+ """
diff --git a/Mailman/rules/__init__.py b/Mailman/rules/__init__.py
new file mode 100755
index 000000000..b7459299b
--- /dev/null
+++ b/Mailman/rules/__init__.py
@@ -0,0 +1,75 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""Built in rule processor."""
+
+__all__ = ['BuiltinRules']
+__metaclass__ = type
+
+
+import os
+import sys
+
+from zope.interface import implements
+from Mailman.interfaces import DuplicateRuleError, IRule, IRuleProcessor
+
+
+
+class BuiltinRules:
+ implements(IRuleProcessor)
+
+ def __init__(self):
+ self._rules = {}
+ rule_set = set()
+ # Find all rules found in all modules inside our package.
+ mypackage = self.__class__.__module__
+ here = os.path.dirname(sys.modules[mypackage].__file__)
+ for filename in os.listdir(here):
+ basename, extension = os.path.splitext(filename)
+ if extension <> '.py':
+ continue
+ module_name = mypackage + '.' + basename
+ __import__(module_name)
+ module = sys.modules[module_name]
+ for name in dir(module):
+ rule = getattr(module, name)
+ if IRule.providedBy(rule):
+ if rule.name in self._rules or rule in rule_set:
+ raise DuplicateRuleError(rule.name)
+ self._rules[rule.name] = rule
+ rule_set.add(rule)
+
+ def process(self, mlist, msg, msgdata):
+ """See `IRuleProcessor`."""
+ hits = set()
+ for rule in self._rules.values():
+ if rule.check(mlist, msg, msgdata):
+ hits.add(rule.name)
+ return hits
+
+ def __getitem__(self, rule_name):
+ """See `IRuleProcessor`."""
+ return self._rules[rule_name]
+
+ def get(self, rule_name, default=None):
+ """See `IRuleProcessor`."""
+ return self._rules.get(rule_name, default)
+
+ @property
+ def rules(self):
+ for rule in self._rules.values():
+ yield rule
diff --git a/Mailman/rules/emergency.py b/Mailman/rules/emergency.py
new file mode 100644
index 000000000..088ba4ec8
--- /dev/null
+++ b/Mailman/rules/emergency.py
@@ -0,0 +1,44 @@
+# Copyright (C) 2007 by the Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+# USA.
+
+"""The emergency hold rule."""
+
+__all__ = ['Emergency']
+__metaclass__ = type
+
+
+from zope.interface import implements
+
+from Mailman.i18n import _
+from Mailman.interfaces import IRule
+
+
+
+class Emergency:
+ implements(IRule)
+
+ name = 'emergency'
+ description = _("""\
+The mailing list is in emergency hold and this message was not pre-approved by
+the list administrator.""")
+
+ def check(self, mlist, msg, msgdata):
+ """See `IRule`."""
+ return mlist.emergency and not msgdata.get('adminapproved')
+
+
+emergency_rule = Emergency()
diff --git a/setup.py b/setup.py
index 22747880f..16a4fd3ef 100644
--- a/setup.py
+++ b/setup.py
@@ -82,15 +82,14 @@ Any other spelling is incorrect.""",
include_package_data = True,
entry_points = {
'console_scripts': list(scripts),
- 'setuptools.file_finders': 'bzr = setuptoolsbzr:find_files_for_bzr',
- # Entry point for plugging in different database backends.
+ 'setuptools.file_finders': 'bzr = setuptools_bzr:find_files_for_bzr',
'mailman.database' : 'stock = Mailman.database:StockDatabase',
'mailman.styles' : 'default = Mailman.app.styles:DefaultStyle',
'mailman.mta' : 'stock = Mailman.MTA:Manual',
+ 'mailman.rules' : 'default = Mailman.rules:BuiltinRules',
},
# Third-party requirements.
install_requires = [
- 'SQLAlchemy',
'locknix',
'munepy',
'storm',
@@ -98,6 +97,6 @@ Any other spelling is incorrect.""",
'zope.interface',
],
setup_requires = [
- 'setuptoolsbzr',
+ 'setuptools_bzr',
],
)