diff options
| author | Barry Warsaw | 2007-08-05 00:32:09 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2007-08-05 00:32:09 -0400 |
| commit | 959f34a62e0ec3cbe73da3d43640ccb6791cf3a0 (patch) | |
| tree | afcf868061fe6a5b56aeb7493c1e72e215fcce1a | |
| parent | ec734fab4791c107610caf73931e570b2d1b6bd0 (diff) | |
| download | mailman-959f34a62e0ec3cbe73da3d43640ccb6791cf3a0.tar.gz mailman-959f34a62e0ec3cbe73da3d43640ccb6791cf3a0.tar.zst mailman-959f34a62e0ec3cbe73da3d43640ccb6791cf3a0.zip | |
29 files changed, 632 insertions, 601 deletions
diff --git a/Mailman/Archiver/Archiver.py b/Mailman/Archiver/Archiver.py index fa8839230..4dd63b438 100644 --- a/Mailman/Archiver/Archiver.py +++ b/Mailman/Archiver/Archiver.py @@ -64,12 +64,6 @@ class Archiver: # archive directory for the mailing list # def InitVars(self): - # Configurable - self.archive = config.DEFAULT_ARCHIVE - # 0=public, 1=private: - self.archive_private = config.DEFAULT_ARCHIVE_PRIVATE - self.archive_volume_frequency = \ - config.DEFAULT_ARCHIVE_VOLUME_FREQUENCY # The archive file structure by default is: # # archives/ diff --git a/Mailman/Autoresponder.py b/Mailman/Autoresponder.py deleted file mode 100644 index f611f4f6a..000000000 --- a/Mailman/Autoresponder.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (C) 1998-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. - -"""MailList mixin class managing the autoresponder.""" - - - -class Autoresponder: - def InitVars(self): - # configurable - self.autorespond_postings = False - self.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 - self.autorespond_requests = 0 - self.autoresponse_postings_text = '' - self.autoresponse_admin_text = '' - self.autoresponse_request_text = '' - self.autoresponse_graceperiod = 90 # days - # non-configurable - self.postings_responses = {} - self.admin_responses = {} - self.request_responses = {} diff --git a/Mailman/Bouncer.py b/Mailman/Bouncer.py index 46dd13114..99093b334 100644 --- a/Mailman/Bouncer.py +++ b/Mailman/Bouncer.py @@ -79,31 +79,6 @@ class _BounceInfo: class Bouncer: - def InitVars(self): - # Configurable... - self.bounce_processing = config.DEFAULT_BOUNCE_PROCESSING - self.bounce_score_threshold = config.DEFAULT_BOUNCE_SCORE_THRESHOLD - self.bounce_info_stale_after = config.DEFAULT_BOUNCE_INFO_STALE_AFTER - self.bounce_you_are_disabled_warnings = \ - config.DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS - self.bounce_you_are_disabled_warnings_interval = \ - config.DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS_INTERVAL - self.bounce_unrecognized_goes_to_list_owner = \ - config.DEFAULT_BOUNCE_UNRECOGNIZED_GOES_TO_LIST_OWNER - self.bounce_notify_owner_on_disable = \ - config.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_DISABLE - self.bounce_notify_owner_on_removal = \ - config.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_REMOVAL - # Not configurable... - # - # 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. - self.bounce_info = {} - # New style delivery status - self.delivery_status = {} - def registerBounce(self, member, msg, weight=1.0, day=None): if not self.isMember(member): return diff --git a/Mailman/Defaults.py b/Mailman/Defaults.py index 951b61bfc..b53065da6 100644 --- a/Mailman/Defaults.py +++ b/Mailman/Defaults.py @@ -20,9 +20,10 @@ import os from datetime import timedelta -from munepy import Enum +from Mailman.constants import * + def seconds(s): return timedelta(seconds=s) @@ -994,7 +995,7 @@ from: .*@uplinkpro.com # 0 - Reply-To: not munged # 1 - Reply-To: set back to the list # 2 - Reply-To: set to an explicit value (reply_to_address) -DEFAULT_REPLY_GOES_TO_LIST = 0 +DEFAULT_REPLY_GOES_TO_LIST = ReplyToMunging.no_munging # Mailman can be configured to strip any existing Reply-To: header, or simply # extend any existing Reply-To: with one based on the above setting. @@ -1289,23 +1290,6 @@ Moderate = 128 DontReceiveDuplicates = 256 -class DeliveryMode(Enum): - # Non-digest delivery - Regular = 1 - # Digest delivery modes - MIME = 2 - Plain = 3 - - -class DeliveryStatus(Enum): - Enabled = 0 - # Disabled reason - Unknown = 1 - ByUser = 2 - ByAdmin = 3 - ByBounce = 4 - - # A mapping between short option tags and their flag OPTINFO = {'hide' : ConcealSubscription, 'nomail' : DisableDelivery, diff --git a/Mailman/Digester.py b/Mailman/Digester.py index d2051c86f..c0878ecc7 100644 --- a/Mailman/Digester.py +++ b/Mailman/Digester.py @@ -30,22 +30,6 @@ from Mailman.i18n import _ class Digester: - def InitVars(self): - # Configurable - self.digestable = config.DEFAULT_DIGESTABLE - self.digest_is_default = config.DEFAULT_DIGEST_IS_DEFAULT - self.mime_is_default_digest = config.DEFAULT_MIME_IS_DEFAULT_DIGEST - self.digest_size_threshhold = config.DEFAULT_DIGEST_SIZE_THRESHHOLD - self.digest_send_periodic = config.DEFAULT_DIGEST_SEND_PERIODIC - self.digest_header = config.DEFAULT_DIGEST_HEADER - self.digest_footer = config.DEFAULT_DIGEST_FOOTER - self.digest_volume_frequency = config.DEFAULT_DIGEST_VOLUME_FREQUENCY - # Non-configurable. - self.one_last_digest = {} - self.digest_members = {} - self.next_digest_number = 1 - self.digest_last_sent_at = 0 - def send_digest_now(self): # Note: Handler.ToDigest.send_digests() handles bumping the digest # volume and issue number. diff --git a/Mailman/Errors.py b/Mailman/Errors.py index abfeb5d38..594293caa 100644 --- a/Mailman/Errors.py +++ b/Mailman/Errors.py @@ -237,3 +237,8 @@ class AddressAlreadyLinkedError(AddressError): class AddressNotLinkedError(AddressError): """The address is not linked to the user.""" + + + +class DuplicateStyleError(MailmanError): + """A style with the same name is already registered.""" diff --git a/Mailman/GatewayManager.py b/Mailman/GatewayManager.py deleted file mode 100644 index b962dbf4c..000000000 --- a/Mailman/GatewayManager.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (C) 1998-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. - -"""Mixin class for configuring Usenet gateway. - -All the actual functionality is in Handlers/ToUsenet.py for the mail->news -gateway and cron/gate_news for the news->mail gateway. - -""" - -from Mailman.configuration import config - - - -class GatewayManager: - def InitVars(self): - # Configurable - self.nntp_host = config.DEFAULT_NNTP_HOST - self.linked_newsgroup = '' - self.gateway_to_news = False - self.gateway_to_mail = False - self.news_prefix_subject_too = True - # In patch #401270, this was called newsgroup_is_moderated, but the - # semantics weren't quite the same. - self.news_moderation = False diff --git a/Mailman/MailList.py b/Mailman/MailList.py index c2680835d..8d61f6a20 100644 --- a/Mailman/MailList.py +++ b/Mailman/MailList.py @@ -55,15 +55,12 @@ from Mailman.interfaces import * # Base classes from Mailman.Archiver import Archiver -from Mailman.Autoresponder import Autoresponder from Mailman.Bouncer import Bouncer from Mailman.Deliverer import Deliverer from Mailman.Digester import Digester -from Mailman.GatewayManager import GatewayManager from Mailman.HTMLFormatter import HTMLFormatter from Mailman.ListAdmin import ListAdmin from Mailman.SecurityManager import SecurityManager -from Mailman.TopicMgr import TopicMgr # GUI components package from Mailman import Gui @@ -88,8 +85,7 @@ slog = logging.getLogger('mailman.subscribe') # Use mixins here just to avoid having any one chunk be too large. class MailList(object, HTMLFormatter, Deliverer, ListAdmin, - Archiver, Digester, SecurityManager, Bouncer, GatewayManager, - Autoresponder, TopicMgr): + Archiver, Digester, SecurityManager, Bouncer): implements( IMailingList, @@ -315,139 +311,6 @@ class MailList(object, HTMLFormatter, Deliverer, ListAdmin, continue self._gui.append(getattr(Gui, component)()) - def InitVars(self, name=None, admin='', crypted_password=''): - """Assign default values - some will be overriden by stored state.""" - # Non-configurable list info - if name: - self._internal_name = name - - # When was the list created? - self.created_at = time.time() - - # Must save this state, even though it isn't configurable - self.volume = 1 - self.members = {} # self.digest_members is initted in mm_digest - self.data_version = config.DATA_FILE_VERSION - self.last_post_time = 0 - - self.post_id = 1. # A float so it never has a chance to overflow. - self.user_options = {} - self.language = {} - self.usernames = {} - self.passwords = {} - self.new_member_options = config.DEFAULT_NEW_MEMBER_OPTIONS - - # This stuff is configurable - self.respond_to_post_requests = 1 - self.advertised = config.DEFAULT_LIST_ADVERTISED - self.max_num_recipients = config.DEFAULT_MAX_NUM_RECIPIENTS - self.max_message_size = config.DEFAULT_MAX_MESSAGE_SIZE - # XXX Don't set host_name if the attribute is already set. This is a - # hack to ensure that MailList.Create() doesn't create a bogus private - # archive directory via Archiver.InitVars(). The problem is that - # there's no way to set this value to what Create() knows it should be - # for Archiver.Initvars(), before MailList.InitVar() blows it away. - if not hasattr(self, 'host_name'): - self.host_name = config.DEFAULT_HOST_NAME or \ - config.DEFAULT_EMAIL_HOST - self.web_page_url = ( - config.DEFAULT_URL or - config.DEFAULT_URL_PATTERN % config.DEFAULT_URL_HOST) - self.owner = [admin] - self.moderator = [] - self.reply_goes_to_list = config.DEFAULT_REPLY_GOES_TO_LIST - self.reply_to_address = '' - self.first_strip_reply_to = config.DEFAULT_FIRST_STRIP_REPLY_TO - self.admin_immed_notify = config.DEFAULT_ADMIN_IMMED_NOTIFY - self.admin_notify_mchanges = \ - config.DEFAULT_ADMIN_NOTIFY_MCHANGES - self.require_explicit_destination = \ - config.DEFAULT_REQUIRE_EXPLICIT_DESTINATION - self.acceptable_aliases = config.DEFAULT_ACCEPTABLE_ALIASES - self.umbrella_list = config.DEFAULT_UMBRELLA_LIST - self.umbrella_member_suffix = \ - config.DEFAULT_UMBRELLA_MEMBER_ADMIN_SUFFIX - self.send_reminders = config.DEFAULT_SEND_REMINDERS - self.send_welcome_msg = config.DEFAULT_SEND_WELCOME_MSG - self.send_goodbye_msg = config.DEFAULT_SEND_GOODBYE_MSG - self.bounce_matching_headers = \ - config.DEFAULT_BOUNCE_MATCHING_HEADERS - self.header_filter_rules = [] - self.anonymous_list = config.DEFAULT_ANONYMOUS_LIST - self.real_name = self.internal_name().capitalize() - self.description = '' - self.info = '' - self.welcome_msg = '' - self.goodbye_msg = '' - self.subscribe_policy = config.DEFAULT_SUBSCRIBE_POLICY - self.subscribe_auto_approval = config.DEFAULT_SUBSCRIBE_AUTO_APPROVAL - self.unsubscribe_policy = config.DEFAULT_UNSUBSCRIBE_POLICY - self.private_roster = config.DEFAULT_PRIVATE_ROSTER - self.obscure_addresses = config.DEFAULT_OBSCURE_ADDRESSES - self.admin_member_chunksize = config.DEFAULT_ADMIN_MEMBER_CHUNKSIZE - self.administrivia = config.DEFAULT_ADMINISTRIVIA - self.preferred_language = config.DEFAULT_SERVER_LANGUAGE - self.include_rfc2369_headers = 1 - self.include_list_post_header = 1 - self.filter_mime_types = config.DEFAULT_FILTER_MIME_TYPES - self.pass_mime_types = config.DEFAULT_PASS_MIME_TYPES - self.filter_filename_extensions = \ - config.DEFAULT_FILTER_FILENAME_EXTENSIONS - self.pass_filename_extensions = config.DEFAULT_PASS_FILENAME_EXTENSIONS - self.filter_content = config.DEFAULT_FILTER_CONTENT - self.collapse_alternatives = config.DEFAULT_COLLAPSE_ALTERNATIVES - self.convert_html_to_plaintext = \ - config.DEFAULT_CONVERT_HTML_TO_PLAINTEXT - self.filter_action = config.DEFAULT_FILTER_ACTION - # Analogs to these are initted in Digester.InitVars - self.nondigestable = config.DEFAULT_NONDIGESTABLE - self.personalize = 0 - # New sender-centric moderation (privacy) options - self.default_member_moderation = \ - config.DEFAULT_DEFAULT_MEMBER_MODERATION - # Emergency moderation bit - self.emergency = 0 - # This really ought to default to config.HOLD, but that doesn't work - # with the current GUI description model. So, 0==Hold, 1==Reject, - # 2==Discard - self.member_moderation_action = 0 - self.member_moderation_notice = '' - self.accept_these_nonmembers = [] - self.hold_these_nonmembers = [] - self.reject_these_nonmembers = [] - self.discard_these_nonmembers = [] - self.forward_auto_discards = config.DEFAULT_FORWARD_AUTO_DISCARDS - self.generic_nonmember_action = config.DEFAULT_GENERIC_NONMEMBER_ACTION - self.nonmember_rejection_notice = '' - # Ban lists - self.ban_list = [] - # BAW: This should really be set in SecurityManager.InitVars() - self.password = crypted_password - # 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. - self.hold_and_cmd_autoresponses = {} - # Only one level of mixin inheritance allowed - for baseclass in self.__class__.__bases__: - if hasattr(baseclass, 'InitVars'): - baseclass.InitVars(self) - - # These need to come near the bottom because they're dependent on - # other settings. - self.subject_prefix = config.DEFAULT_SUBJECT_PREFIX % self.__dict__ - self.msg_header = config.DEFAULT_MSG_HEADER - self.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(self.preferred_language) == 'us-ascii': - self.encode_ascii_prefixes = 0 - else: - self.encode_ascii_prefixes = 2 - # scrub regular delivery - self.scrub_nondigest = config.DEFAULT_SCRUB_NONDIGEST - # automatic discarding - self.max_days_to_hold = config.DEFAULT_MAX_DAYS_TO_HOLD - # # Web API support via administrative categories @@ -494,54 +357,6 @@ class MailList(object, HTMLFormatter, Deliverer, ListAdmin, return value - # - # List creation - # - def Create(self, fqdn_listname, admin_email, crypted_password, langs=None): - # Validate the list's posting address, which should be fqdn_listname. - # If that's invalid, do not create any of the mailing list artifacts - # (the subdir in lists/ and the subdirs in archives/public and - # archives/private. Most scripts already catch InvalidEmailAddress as - # exceptions on the admin's email address, so transform the exception. - if '@' not in fqdn_listname: - raise Errors.BadListNameError(fqdn_listname) - listname, email_host = fqdn_listname.split('@', 1) - if email_host not in config.domains: - raise Errors.BadDomainSpecificationError(email_host) - try: - Utils.ValidateEmail(fqdn_listname) - except Errors.InvalidEmailAddress: - raise Errors.BadListNameError(fqdn_listname) - # See if the mailing list already exists. - if Utils.list_exists(fqdn_listname): - raise Errors.MMListAlreadyExistsError(fqdn_listname) - # Validate the admin's email address - Utils.ValidateEmail(admin_email) - self._internal_name = self.list_name = listname - self._full_path = os.path.join(config.LIST_DATA_DIR, fqdn_listname) - Utils.makedirs(self._full_path) - # Don't use Lock() since that tries to load the non-existant - # config.pck. However, it's likely that the current lock is a site - # lock because the MailList was instantiated with no name. Blow away - # the current lock object and re-instantiate it. - self._make_lock(fqdn_listname, lock=True) - # We need to set this attribute before calling InitVars() because - # Archiver.InitVars() depends on this to calculate and create the - # archive directory. Without this, Archiver will create a bogus - # archive directory using the default host name. - self.host_name = email_host - self.InitVars(listname, admin_email, crypted_password) - self.CheckValues() - if langs is None: - langs = [self.preferred_language] - for language_code in langs: - self.set_languages(*langs) - url_host = config.domains[email_host] - self.web_page_url = config.DEFAULT_URL_PATTERN % url_host - database.add_list(self) - - - def Save(self): # Refresh the lock, just to let other processes know we're still # interested in it. This will raise a NotLockedError if we don't have @@ -565,8 +380,6 @@ class MailList(object, HTMLFormatter, Deliverer, ListAdmin, """Auto-update schema if necessary.""" if self.data_version >= Version.DATA_FILE_VERSION: return - # Initialize any new variables - self.InitVars() # Then reload the database (but don't recurse). Force a reload even # if we have the most up-to-date state. self.Load(self.fqdn_listname, check_version=False) diff --git a/Mailman/SecurityManager.py b/Mailman/SecurityManager.py index 9b7401e59..17cb870cb 100644 --- a/Mailman/SecurityManager.py +++ b/Mailman/SecurityManager.py @@ -72,13 +72,6 @@ SLASH = '/' class SecurityManager: - def InitVars(self): - # self.password is really a SecurityManager attribute, but it's set in - # MailList.InitVars(). - self.mod_password = None - # Non configurable - self.passwords = {} - def AuthContextInfo(self, authcontext, user=None): # authcontext may be one of AuthUser, AuthListModerator, # AuthListAdmin, AuthSiteAdmin. Not supported is the AuthCreator diff --git a/Mailman/TopicMgr.py b/Mailman/TopicMgr.py deleted file mode 100644 index 23f27d3ce..000000000 --- a/Mailman/TopicMgr.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright (C) 2001-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. - -"""This class mixes in topic feature configuration for mailing lists.""" - -import re - - - -class TopicMgr: - def InitVars(self): - # Configurable - # - # `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). - self.topics = [] - self.topics_enabled = False - self.topics_bodylines_limit = 5 - # Non-configurable - # - # 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 self.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. - self.topics_userinterest = {} diff --git a/Mailman/Utils.py b/Mailman/Utils.py index 4034bfdd8..fdc1d2c44 100644 --- a/Mailman/Utils.py +++ b/Mailman/Utils.py @@ -66,14 +66,13 @@ log = logging.getLogger('mailman.error') def list_exists(fqdn_listname): """Return true iff list `fqdn_listname' exists.""" - listname, hostname = split_listname(fqdn_listname) - return bool(config.list_manager.find_list(listname, hostname)) + return config.db.list_manager.get(fqdn_listname) is not None def list_names(): """Return the fqdn names of all lists in default list directory.""" return ['%s@%s' % (listname, hostname) - for listname, hostname in config.list_manager.get_list_names()] + for listname, hostname in config.db.list_manager.get_list_names()] def split_listname(listname): diff --git a/Mailman/app/create.py b/Mailman/app/create.py new file mode 100644 index 000000000..4e94a3e44 --- /dev/null +++ b/Mailman/app/create.py @@ -0,0 +1,46 @@ +# 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. + +"""Application level list creation.""" + +from Mailman import Errors +from Mailman import Utils +from Mailman.Utils import ValidateEmail +from Mailman.app.plugins import get_plugin +from Mailman.app.styles import style_manager +from Mailman.configuration import config + + + +def create_list(fqdn_listname): + """Create the named list and apply styles.""" + ValidateEmail(fqdn_listname) + listname, domain = Utils.split_listname(fqdn_listname) + if domain not in config.domains: + 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._data) + # Coordinate with the MTA, which should be defined by plugins. + # XXX FIXME +## mta_plugin = get_plugin('mailman.mta') +## mta_plugin().create(mlist) + return mlist diff --git a/Mailman/app/plugins.py b/Mailman/app/plugins.py new file mode 100644 index 000000000..d086bd037 --- /dev/null +++ b/Mailman/app/plugins.py @@ -0,0 +1,65 @@ +# 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. + +"""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/app/styles.py b/Mailman/app/styles.py new file mode 100644 index 000000000..55d8aa690 --- /dev/null +++ b/Mailman/app/styles.py @@ -0,0 +1,266 @@ +# 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. + +"""Application of list styles to new and existing lists.""" + +import datetime + +from operator import attrgetter +from zope.interface import implements + +from Mailman import Utils +from Mailman.Errors import DuplicateStyleError +from Mailman.app.plugins import get_plugins +from Mailman.configuration import config +from Mailman.constants import Action +from Mailman.interfaces import IStyle, IStyleManager + + +__metaclass__ = type +__all__ = [ + 'DefaultStyle', + 'style_manager', + ] + + + +class DefaultStyle: + implements(IStyle) + + name = 'default' + priority = 0 # the lowest priority style + + def apply(self, 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.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 = '' + 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_filter_rules = [] + mlist.anonymous_list = config.DEFAULT_ANONYMOUS_LIST + mlist.description = '' + mlist.info = '' + mlist.welcome_msg = '' + mlist.goodbye_msg = '' + 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 = False + # 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 = '' + 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 = '' + # 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 = {} + # XXX FIXME + #mlist.subject_prefix = config.DEFAULT_SUBJECT_PREFIX % mlist.__dict__ + 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 = '' + mlist.autoresponse_admin_text = '' + mlist.autoresponse_request_text = '' + 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 = '' + 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 = False + # 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 = {} + + def match(self, mailing_list, styles): + # If no other styles have matched, then the default style matches. + if len(styles) == 0: + styles.append(self) + + + +class StyleManager: + implements(IStyleManager) + + def __init__(self): + 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): + return self._styles.get(name) + + def lookup(self, mailing_list): + matched_styles = [] + for style in self.styles: + style.match(mailing_list, matched_styles) + for style in sorted(matched_styles, key=attrgetter('priority')): + yield style + + @property + def styles(self): + for style in sorted(self._styles.values(), key=attrgetter('priority')): + yield style + + def register(self, style): + if style.name in self._styles: + raise DuplicateStyleError(style.name) + self._styles[style.name] = style + + + +style_manager = StyleManager() diff --git a/Mailman/bin/make_instance.py b/Mailman/bin/make_instance.py index e82c12cde..8ec0d13b5 100644 --- a/Mailman/bin/make_instance.py +++ b/Mailman/bin/make_instance.py @@ -79,7 +79,7 @@ available languages.""")) opts, args = parser.parse_args() if args: unexpected = SPACE.join(args) - parser.print_error(_('Unexpected arguments: $unexpected')) + parser.error(_('Unexpected arguments: $unexpected')) return parser, opts, args diff --git a/Mailman/bin/newlist.py b/Mailman/bin/newlist.py index 1bbe818ff..9692b4fd0 100644 --- a/Mailman/bin/newlist.py +++ b/Mailman/bin/newlist.py @@ -18,6 +18,7 @@ import sha import sys import getpass +import datetime import optparse from Mailman import Errors @@ -26,13 +27,12 @@ from Mailman import Message from Mailman import Utils from Mailman import Version from Mailman import i18n -from Mailman import passwords -from Mailman.bin.mmsitepass import check_password_scheme +from Mailman.app.create import create_list from Mailman.configuration import config +from Mailman.constants import MemberRole from Mailman.initialize import initialize _ = i18n._ - __i18n_templates__ = True @@ -40,20 +40,13 @@ __i18n_templates__ = True def parseargs(): parser = optparse.OptionParser(version=Version.MAILMAN_VERSION, usage=_("""\ -%prog [options] [listname [listadmin-addr [admin-password]]] - -Create a new empty mailing list. - -You can specify as many of the arguments as you want on the command line: -you will be prompted for the missing ones. +%prog [options] fqdn_listname -Every Mailman mailing list is situated in a domain, and Mailman supports -multiple virtual domains. 'listname' is required, and if it contains an '@', -it should specify the posting address for your mailing list and the right-hand -side of the email address must be an existing domain. +Create a new mailing list. -If 'listname' does not have an '@', the list will be situated in the default -domain, which Mailman created when you configured the system. +fqdn_listname is the 'fully qualified list name', basically the posting +address of the list. It must be a valid email address and the domain must be +registered with Mailman. Note that listnames are forced to lowercase.""")) parser.add_option('-l', '--language', @@ -61,6 +54,14 @@ Note that listnames are forced to lowercase.""")) help=_("""\ Make the list's preferred language LANGUAGE, which must be a two letter language code.""")) + parser.add_option('-o', '--owner', + type='string', action='append', default=[], + dest='owners', help=_("""\ +Specific a listowner email address. If the address is not currently +registered with Mailman, the address is registered and linked to a user. +Mailman will send a confirmation message to the address, but it will also send +a list creation notice to the address. More than one owner can be +specified.""")) parser.add_option('-q', '--quiet', default=False, action='store_true', help=_("""\ @@ -72,26 +73,13 @@ notification.""")) help=_("""\ This option suppresses the prompt prior to administrator notification but still sends the notification. It can be used to make newlist totally -non-interactive but still send the notification, assuming listname, -listadmin-addr and admin-password are all specified on the command line.""")) - parser.add_option('-p', '--password-scheme', - default='', type='string', help=_("""\ -Specify the RFC 2307 style hashing scheme for passwords included in the -output. Use -P to get a list of supported schemes, which are -case-insensitive.""")) - parser.add_option('-P', '--list-hash-schemes', - default=False, action='store_true', help=_("""\ -List the supported password hashing schemes and exit. The scheme labels are -case-insensitive.""")) +non-interactive but still send the notification, assuming at least one list +owner is specified with the -o option..""")) parser.add_option('-C', '--config', help=_('Alternative configuration file to use')) opts, args = parser.parse_args() - if opts.list_hash_schemes: - for label in passwords.Schemes: - print str(label).upper() - sys.exit(0) - # Can't verify opts.language here because the configuration isn't loaded - # yet. + # We can't verify opts.language here because the configuration isn't + # loaded yet. return parser, opts, args @@ -105,96 +93,51 @@ def main(): opts.language = config.DEFAULT_SERVER_LANGUAGE # Is the language known? if opts.language not in config.languages.enabled_codes: - parser.print_help() - print >> sys.stderr, _('Unknown language: $opts.language') - sys.exit(1) - + parser.error(_('Unknown language: $opts.language')) # Handle variable number of positional arguments - if args: - listname = args.pop(0) - else: - listname = raw_input(_('Enter the name of the list: ')) - - listname = listname.lower() - if '@' in listname: - fqdn_listname = listname - listname, email_host = listname.split('@', 1) - url_host = config.domains.get(email_host) - if not url_host: - print >> sys.stderr, _('Undefined domain: $email_host') - sys.exit(1) - else: - email_host = config.DEFAULT_EMAIL_HOST - url_host = config.DEFAULT_URL_HOST - fqdn_listname = '%s@%s' % (listname, email_host) - web_page_url = config.DEFAULT_URL_PATTERN % url_host - # Even though MailList.Create() will check to make sure the list doesn't - # yet exist, do it now for better usability. - if Utils.list_exists(fqdn_listname): - parser.print_help() - print >> sys.stderr, _('List already exists: $listname') + if len(args) == 0: + parser.error(_('You must supply a mailing list name')) + elif len(args) == 1: + fqdn_listname = args[0].lower() + elif len(args) > 1: + parser.error(_('Unexpected arguments')) - if args: - owner_mail = args.pop(0) - else: - owner_mail = raw_input( - _('Enter the email of the person running the list: ')) - - if args: - listpasswd = args.pop(0) - else: - while True: - listpasswd = getpass.getpass(_('Initial $listname password: ')) - confirm = getpass.getpass(_('Confirm $listname password: ')) - if listpasswd == confirm: - break - print _('Passwords did not match, try again (Ctrl-C to quit)') - - # List passwords cannot be empty - listpasswd = listpasswd.strip() - if not listpasswd: - parser.print_help() - print >> sys.stderr, _('The list password cannot be empty') - - mlist = MailList.MailList() - # Assign the preferred language before Create() since that will use it to - # set available_languages. - mlist.preferred_language = opts.language + # Create the mailing list, applying styles as appropriate. try: - scheme = check_password_scheme(parser, opts.password_scheme) - pw = passwords.make_secret(listpasswd, scheme) - try: - mlist.Create(fqdn_listname, owner_mail, pw) - except Errors.BadListNameError, s: - parser.print_help() - print >> sys.stderr, _('Illegal list name: $s') - sys.exit(1) - except Errors.EmailAddressError, s: - parser.print_help() - print >> sys.stderr, _('Bad owner email address: $s') - sys.exit(1) - except Errors.MMListAlreadyExistsError: - parser.print_help() - print >> sys.stderr, _('List already exists: $listname') - sys.exit(1) - mlist.Save() - finally: - mlist.Unlock() + mlist = create_list(fqdn_listname) + mlist.preferred_language = opts.language + except Errors.BadListNameError: + parser.error(_('Illegal list name: $fqdn_listname')) + except Errors.MMListAlreadyExistsError: + parser.error(_('List already exists: $fqdn_listname')) + except Errors.BadDomainSpecificationError, domain: + parser.error(_('Undefined domain: $domain')) + except Errors.MMListAlreadyExistsError: + parser.error(_('List already exists: $fqdn_listname')) + + # Create any owners that don't yet exist, and subscribe all addresses as + # owners of the mailing list. + usermgr = config.db.user_manager + for owner_address in opts.owners: + addr = usermgr.get_address(owner_address) + if addr is None: + # XXX Make this use an IRegistrar instead, but that requires + # sussing out the IDomain stuff. For now, fake it. + user = usermgr.create_user(owner_address) + addr = list(user.addresses)[0] + addr.verified_on = datetime.datetime.now() + addr.subscribe(mlist, MemberRole.owner) - # Now do the MTA-specific list creation tasks - if config.MTA: - modname = 'Mailman.MTA.' + config.MTA - __import__(modname) - sys.modules[modname].create(mlist) + config.db.flush() - # And send the notice to the list owner + # Send notices to the list owners. XXX This should also be moved to the + # Mailman.app.create module. if not opts.quiet and not opts.automate: - print _('Hit enter to notify $listname owner...'), + print _('Hit enter to notify $fqdn_listname owners...'), sys.stdin.readline() if not opts.quiet: d = dict( listname = listname, - password = listpasswd, admin_url = mlist.GetScriptURL('admin', absolute=True), listinfo_url = mlist.GetScriptURL('listinfo', absolute=True), requestaddr = mlist.GetRequestEmail(), diff --git a/Mailman/bin/rmlist.py b/Mailman/bin/rmlist.py index fcc47b3f0..c51ab7fdf 100644 --- a/Mailman/bin/rmlist.py +++ b/Mailman/bin/rmlist.py @@ -23,7 +23,6 @@ import optparse from Mailman import Errors from Mailman import Utils from Mailman import Version -from Mailman import database from Mailman.MailList import MailList from Mailman.configuration import config from Mailman.i18n import _ @@ -54,7 +53,7 @@ def delete_list(listname, mlist=None, archives=True, quiet=False): removeables = [] if mlist: # Remove the list from the database - database.remove_list(mlist) + config.db.list_manager.delete(mlist) # Do the MTA-specific list deletion tasks if config.MTA: modname = 'Mailman.MTA.' + config.MTA @@ -106,13 +105,9 @@ remove any residual archives.""")) help=_('Alternative configuration file to use')) opts, args = parser.parse_args() if not args: - parser.print_help() - print >> sys.stderr, _('Missing listname') - sys.exit(1) + parser.error(_('Missing listname')) if len(args) > 1: - parser.print_help() - print >> sys.stderr, _('Unexpected arguments') - sys.exit(1) + parser.error(_('Unexpected arguments')) return parser, opts, args @@ -121,23 +116,22 @@ def main(): parser, opts, args = parseargs() initialize(opts.config) - listname = Utils.fqdn_listname(args[0]) - try: - mlist = MailList(listname, lock=False) - except Errors.MMUnknownListError: + fqdn_listname = args[0] + mlist = config.db.list_manager.get(fqdn_listname) + if mlist is None: if not opts.archives: print >> sys.stderr, _( - 'No such list (or list already deleted): $listname') + 'No such list (or list already deleted): $fqdn_listname') sys.exit(1) else: - print _( - 'No such list: ${listname}. Removing its residual archives.') - mlist = None + print _("""\ +No such list: ${fqdn_listname}. Removing its residual archives.""") if not opts.archives: print _('Not removing archives. Reinvoke with -a to remove them.') - delete_list(listname, mlist, opts.archives) + delete_list(fqdn_listname, mlist, opts.archives) + config.db.flush() diff --git a/Mailman/bin/withlist.py b/Mailman/bin/withlist.py index 5e4655100..ba643af81 100644 --- a/Mailman/bin/withlist.py +++ b/Mailman/bin/withlist.py @@ -62,14 +62,13 @@ def do_list(listname, args, func): else: print >> sys.stderr, _('(unlocked)') - try: - mlist = MailList.MailList(listname, lock=LOCK) + mlist = config.db.list_manager.get(listname) + if mlist is None: + print >> sys.stderr, _('Unknown list: $listname') + else: atexit.register(exitfunc, mlist) LAST_MLIST = mlist - except Errors.MMUnknownListError: - print >> sys.stderr, _('Unknown list: $listname') - - # try to import the module and run the callable + # Try to import the module and run the callable. if func: return func(mlist, *args) return None diff --git a/Mailman/configuration.py b/Mailman/configuration.py index 52537e9ac..bd76e5fca 100644 --- a/Mailman/configuration.py +++ b/Mailman/configuration.py @@ -76,10 +76,13 @@ class Configuration(object): # The values in that namespace are exposed as attributes on this # Configuration instance. self.filename = None + bin_dir = os.path.dirname(sys.argv[0]) + dev_dir = join(os.path.dirname(bin_dir)) paths = [ # Development directories. - join(os.getcwd(), 'etc', 'mailman.cfg'), + join(dev_dir, 'var', 'etc', 'mailman.cfg'), join(os.getcwd(), 'var', 'etc', 'mailman.cfg'), + join(os.getcwd(), 'etc', 'mailman.cfg'), # Standard installation directories. join('/etc', 'mailman.cfg'), join(Defaults.DEFAULT_VAR_DIRECTORY, 'etc', 'mailman.cfg'), @@ -108,7 +111,7 @@ class Configuration(object): if ns['USE_LMTP']: self.add_qrunner('LMTP') # Pull out the defaults. - VAR_DIR = ns['VAR_DIR'] + VAR_DIR = os.path.abspath(ns['VAR_DIR']) # Now that we've loaded all the configuration files we're going to # load, set up some useful directories. self.LIST_DATA_DIR = join(VAR_DIR, 'lists') diff --git a/Mailman/constants.py b/Mailman/constants.py index fcf5e9678..55a258af1 100644 --- a/Mailman/constants.py +++ b/Mailman/constants.py @@ -22,6 +22,15 @@ from zope.interface import implements from Mailman.interfaces import IPreferences +__all__ = [ + 'Action', + 'DeliveryMode', + 'DeliveryStatus', + 'MemberRole', + 'ReplyToMunging', + 'SystemDefaultPreferences', + ] + class DeliveryMode(Enum): @@ -75,3 +84,12 @@ class ReplyToMunging(Enum): point_to_list = 1 # An explicit Reply-To header is added explicit_header = 2 + + + +class Action(Enum): + hold = 0 + reject = 1 + discard = 2 + accept = 3 + defer = 4 diff --git a/Mailman/database/listmanager.py b/Mailman/database/listmanager.py index b53bb44b3..d5a6303e6 100644 --- a/Mailman/database/listmanager.py +++ b/Mailman/database/listmanager.py @@ -18,6 +18,7 @@ """SQLAlchemy/Elixir based provider of IListManager.""" import weakref +import datetime from elixir import * from zope.interface import implements @@ -43,6 +44,7 @@ class ListManager(object): if mlist: raise Errors.MMListAlreadyExistsError(fqdn_listname) mlist = MailingList(fqdn_listname) + mlist.created_at = datetime.datetime.now() # Wrap the database model object in an application MailList object and # return the latter. Keep track of the wrapper so we can clean it up # when we're done with it. @@ -62,7 +64,8 @@ class ListManager(object): mlist = MailingList.get_by(list_name=listname, host_name=hostname) if not mlist: - raise Errors.MMUnknownListError(fqdn_listname) + return None + mlist._restore() from Mailman.MailList import MailList wrapper = self._objectmap.setdefault(mlist, MailList(mlist)) return wrapper diff --git a/Mailman/database/model/mailinglist.py b/Mailman/database/model/mailinglist.py index fce73cf25..11deb28c6 100644 --- a/Mailman/database/model/mailinglist.py +++ b/Mailman/database/model/mailinglist.py @@ -21,7 +21,7 @@ from zope.interface import implements from Mailman.Utils import fqdn_listname, split_listname from Mailman.configuration import config from Mailman.interfaces import * -from Mailman.database.types import EnumType +from Mailman.database.types import EnumType, TimeDeltaType @@ -38,6 +38,7 @@ class MailingList(Entity): has_field('list_name', Unicode), has_field('host_name', Unicode), # Attributes not directly modifiable via the web u/i + has_field('created_at', DateTime), has_field('web_page_url', Unicode), has_field('admin_member_chunksize', Integer), has_field('hold_and_cmd_autoresponses', PickleType), @@ -70,11 +71,11 @@ class MailingList(Entity): has_field('autorespond_postings', Boolean), has_field('autorespond_requests', Integer), has_field('autoresponse_admin_text', Unicode), - has_field('autoresponse_graceperiod', Integer), + has_field('autoresponse_graceperiod', TimeDeltaType), has_field('autoresponse_postings_text', Unicode), has_field('autoresponse_request_text', Unicode), has_field('ban_list', PickleType), - has_field('bounce_info_stale_after', Integer), + has_field('bounce_info_stale_after', TimeDeltaType), has_field('bounce_matching_headers', Unicode), has_field('bounce_notify_owner_on_disable', Boolean), has_field('bounce_notify_owner_on_removal', Boolean), @@ -82,7 +83,7 @@ class MailingList(Entity): has_field('bounce_score_threshold', Integer), has_field('bounce_unrecognized_goes_to_list_owner', Boolean), has_field('bounce_you_are_disabled_warnings', Integer), - has_field('bounce_you_are_disabled_warnings_interval', Integer), + has_field('bounce_you_are_disabled_warnings_interval', TimeDeltaType), has_field('collapse_alternatives', Boolean), has_field('convert_html_to_plaintext', Boolean), has_field('default_member_moderation', Boolean), @@ -152,8 +153,6 @@ class MailingList(Entity): has_field('topics', PickleType), has_field('topics_bodylines_limit', Integer), has_field('topics_enabled', Boolean), - has_field('umbrella_list', Boolean), - has_field('umbrella_member_suffix', Unicode), has_field('unsubscribe_policy', Integer), has_field('welcome_msg', Unicode), # Relationships @@ -170,8 +169,15 @@ class MailingList(Entity): self.host_name = hostname # For the pending database self.next_request_id = 1 - # Create several rosters for filtering out or querying the membership - # table. + self._restore() + # 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. + self.hold_and_cmd_autoresponses = {} + + # XXX FIXME + def _restore(self): + # Avoid circular imports. from Mailman.database.model import roster self.owners = roster.OwnerRoster(self) self.moderators = roster.ModeratorRoster(self) @@ -179,10 +185,6 @@ class MailingList(Entity): self.members = roster.MemberRoster(self) self.regular_members = roster.RegularMemberRoster(self) self.digest_members = roster.DigestMemberRoster(self) - # 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. - self.hold_and_cmd_autoresponses = {} @property def fqdn_listname(self): diff --git a/Mailman/database/types.py b/Mailman/database/types.py index 79ea8767d..0f0e46fa3 100644 --- a/Mailman/database/types.py +++ b/Mailman/database/types.py @@ -17,11 +17,12 @@ import sys +from datetime import timedelta from sqlalchemy import types -# SQLAlchemy custom type for storing enums in the database. +# SQLAlchemy custom type for storing munepy Enums in the database. class EnumType(types.TypeDecorator): # Enums can be stored as strings of the form: # full.path.to.Enum:intval @@ -30,7 +31,7 @@ class EnumType(types.TypeDecorator): def convert_bind_param(self, value, engine): if value is None: return None - return '%s:%s.%d' % (value.enumclass.__module__, + return '%s.%s:%d' % (value.enumclass.__module__, value.enumclass.__name__, int(value)) @@ -38,7 +39,27 @@ class EnumType(types.TypeDecorator): if value is None: return None path, intvalue = value.rsplit(':', 1) - modulename, classname = intvalue.rsplit('.', 1) + modulename, classname = path.rsplit('.', 1) __import__(modulename) cls = getattr(sys.modules[modulename], classname) return cls[int(intvalue)] + + + +class TimeDeltaType(types.TypeDecorator): + # timedeltas are stored as the string representation of three integers, + # separated by colons. The values represent the three timedelta + # attributes days, seconds, microseconds. + impl = types.String + + def convert_bind_param(self, value, engine): + if value is None: + return None + return '%s:%s:%s' % (value.days, value.seconds, value.microseconds) + + def convert_result_value(self, value, engine): + if value is None: + return None + parts = value.split(':') + assert len(parts) == 3, 'Bad timedelta representation: %s' % value + return timedelta(*(int(value) for value in parts)) diff --git a/Mailman/docs/listmanager.txt b/Mailman/docs/listmanager.txt index 653cf40f2..1f4586e35 100644 --- a/Mailman/docs/listmanager.txt +++ b/Mailman/docs/listmanager.txt @@ -87,13 +87,10 @@ always get the same object back. >>> mlist_2 is mlist True -Don't try to get a list that doesn't exist yet though, or you will get an -exception. +If you try to get a list that doesn't existing yet, you get None. - >>> listmgr.get('_xtest_2@example.com') - Traceback (most recent call last): - ... - MMUnknownListError: _xtest_2@example.com + >>> print listmgr.get('_xtest_2@example.com') + None Iterating over all mailing lists diff --git a/Mailman/ext/__init__.py b/Mailman/ext/__init__.py deleted file mode 100644 index e69de29bb..000000000 --- a/Mailman/ext/__init__.py +++ /dev/null diff --git a/Mailman/initialize.py b/Mailman/initialize.py index 650aa925f..b17e3a9e2 100644 --- a/Mailman/initialize.py +++ b/Mailman/initialize.py @@ -31,9 +31,9 @@ import pkg_resources from zope.interface.verify import verifyObject import Mailman.configuration -import Mailman.ext import Mailman.loginit +from Mailman.app.plugins import get_plugin from Mailman.interfaces import IDatabase @@ -43,7 +43,7 @@ from Mailman.interfaces import IDatabase # initialization, but before database initialization. Generally all other # code will just call initialize(). -def initialize_1(config, propagate_logs): +def initialize_1(config_path, propagate_logs): # 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 @@ -51,48 +51,22 @@ def initialize_1(config, propagate_logs): # restrictive permissions in order to handle private archives, but it # handles that correctly. os.umask(007) - Mailman.configuration.config.load(config) + Mailman.configuration.config.load(config_path) # Create the queue and log directories if they don't already exist. Mailman.configuration.config.ensure_directories_exist() Mailman.loginit.initialize(propagate_logs) - # Set up site extensions directory - Mailman.ext.__path__.append(Mailman.configuration.config.EXT_DIR) def initialize_2(): - # Find all declared entry points in the mailman.database group. There - # must be exactly one or two such entry points defined. If there are two, - # then we remove the one called 'stock' since that's the one that we - # distribute and it's obviously being overridden. If we're still left - # with more than one after we filter out the stock one, it is an error. - entrypoints = list(pkg_resources.iter_entry_points('mailman.database')) - if len(entrypoints) == 0: - raise RuntimeError('No database entry points found') - elif len(entrypoints) == 1: - # Okay, this is the one to use. - entrypoint = entrypoints[0] - elif len(database) == 2: - # Find the one /not/ named 'stock'. - entrypoints = [ep for ep in entrypoints if ep.name <> 'stock'] - if len(entrypoints) == 0: - raise RuntimeError('No database entry points found') - elif len(entrypoints) == 2: - raise RuntimeError('Too many database entry points defined') - else: - assert len(entrypoints) == 1, 'Insanity' - entrypoint = entrypoint[0] - else: - raise RuntimeError('Too many database entry points defined') - # Instantiate the database entry point, ensure that it's of the right - # type, and initialize it. Then stash the object on our configuration - # object. - ep_object = entrypoint.load() - db = ep_object() - verifyObject(IDatabase, db) - db.initialize() - Mailman.configuration.config.db = db + 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() + Mailman.configuration.config.db = database -def initialize(config=None, propagate_logs=False): - initialize_1(config, propagate_logs) +def initialize(config_path=None, propagate_logs=False): + initialize_1(config_path, propagate_logs) initialize_2() diff --git a/Mailman/interfaces/listmanager.py b/Mailman/interfaces/listmanager.py index 113ca35af..53cb0b4da 100644 --- a/Mailman/interfaces/listmanager.py +++ b/Mailman/interfaces/listmanager.py @@ -48,8 +48,9 @@ class IListManager(Interface): def get(fqdn_listname): """Find the IMailingList with the matching fully qualified list name. - Returns the matching IMailList instance or None if there was no - matching mailing list. fqdn_listname + :param fqdn_listname: Fully qualified list name to get. + :return: The matching IMailingList or None if there was no such + matching mailing list. """ mailing_lists = Attribute( diff --git a/Mailman/interfaces/styles.py b/Mailman/interfaces/styles.py new file mode 100644 index 000000000..7b4207bae --- /dev/null +++ b/Mailman/interfaces/styles.py @@ -0,0 +1,86 @@ +# 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. + +"""Interfaces for list styles.""" + +from zope.interface import Interface, Attribute + + + +class IStyle(Interface): + """Application of a style to an existing mailing list.""" + + name = Attribute( + """The name of this style. Must be unique.""") + + priority = Attribute( + """The priority of this style, as an integer.""") + + def apply(mailing_list): + """Apply the style to the mailing list. + + :param mailing_list: an IMailingList. + """ + + def match(mailing_list, styles): + """Give this list a chance to match the mailing list. + + If the style's internal matching rules match the `mailing_list`, then + the style may append itself to the `styles` list. This list will be + ordered when returned from `IStyleManager.lookup()`. + + :param mailing_list: an IMailingList. + :param styles: ordered list of IStyles matched so far. + """ + + + +class IStyleManager(Interface): + """A manager of styles and style chains.""" + + def get(name): + """Return the named style or None. + + :param name: A style name. + :return: an IStyle or None. + """ + + def lookup(mailing_list): + """Return a list of styles for the given mailing list. + + Use various registered rules to find an IStyle for the given mailing + list. The returned styles are ordered by their priority. + + Style matches can be registered and reordered by plugins. + + :param mailing_list: An IMailingList. + :return: ordered list of IStyles. Zero is the lowest priority. + """ + + styles = Attribute( + """An iterator over all the styles known by this manager. + + Styles are ordered by their priority, which may be changed. + """) + + def register(style): + """Register a style with this manager. + + :param style: an IStyle. + :raises DuplicateStyleError: if a style with the same name was already + registered. + """ @@ -84,7 +84,9 @@ Any other spelling is incorrect.""", 'console_scripts': list(scripts), 'setuptools.file_finders': 'bzr = setuptoolsbzr:find_files_for_bzr', # Entry point for plugging in different database backends. - 'mailman.database': 'stock = Mailman.database:StockDatabase', + 'mailman.database' : 'stock = Mailman.database:StockDatabase', + 'mailman.styles' : 'default = Mailman.app.styles:DefaultStyle', + 'mailman.mta' : 'stock = Mailman.MTA:Manual', }, # Third-party requirements. install_requires = [ |
