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 | |
It all started by trying to remove MailList.Create() and use the IListManager
interface to create and delete lists. Mostly that's working now, but I need
unit tests for most of the new work contained in this revision.
Implemented a rudimentary 'list styles' subsystem, along with interfaces, but
no tests yet. Moved all of MailList.InitVars() into a DefaultStyle, which is
always available at priority zero. It's used by default if there are no
matching styles for a mailing list.
Because of the list styles, we can now get rid of (almost) all InitVars()
methods. And because of /that/ we can get rid of the mixin clases whose sole
purpose was to provide an InitVars() method. Yay for code removal!
Mixin modules/classes removed: Autoresponder, GatewayManager, TopicManager.
Removed the Mailman/ext crufty extension mechanism. Extensions will now be
done using setuptools plugins. Hopefully this will take us everywhere we need
to go, but I'll add Mailman.ext back if necessary later.
Mailiman.app.create module added to implement a common, higher-level list
creation feature. This is used by bin/newlist now, though some of that
functionality (namely, ensuring the owners exist in the database, and
notifying the owners) should be moved here. The MTA plugins aren't yet
integrated into this, but need to be.
Mailman.app.plugins module added to generalize setuptools plugin management.
Defaults.DEFAULT_REPLY_GOES_TO_LIST now gets initialized with a proper enum.
Also, the duplicate DeliveryMode and DeliveryStatus enums are removed from
Defaults because they're in Mailman.constants.
Added Errors.DuplicateStyleError.
Updated Utils.list_exists() to use the new IListManager.get() interface, which
has been changed to return None if the list doesn't exist (for consistency)
instead of raising an exception. Utils.list_names() also needed to be fixed
to use config.db.list_manager.
bin/make_instance, bin/newlist, bin/rmlist changed to use parser.error()
istead of printing to sys.stderr and sys.exit(1).
bin/newlist and bin/rmlist now works with the IListManager interface, so you
can create and delete lists from the command line again. The CLI for newlist
has been much simplified; it no longer prompts for missing positional
arguments. It now uses a more traditional CLI. newlist also accepts zero to
many owners, and it ensures that the owners are all in the database. It no
longer asks for a list password, because this doesn't make sense any more.
bin/withlist has also been fixed to work with the IListManager interface.
There are lots of XXXs and FIXMEs that need to be resolved before this can
land. Also, we need to test all this stuff before it can land.
Configuration.load() is now taught to search in sys.argv[0] for
var/etc/mailman.cfg since this is where it is for egg development layouts.
Also, VAR_DIR must be abspath'd.
Added an __all__ to Mailman.constants, and added an Action enum.
The listmanager implementation has to set the mlist.created_at time. There's
also a bit of crufty refactoring going on to instantiate the roster objects
whenever the list is created or retrieved from the database.
Several MailingList column types are now set to our custom TimeDeltaType,
which knows how to store a datetime.timedelta. A SQLAlchemny converter type
is added to Mailman.database.types. I also fixed a bug in the EnumType
implementation.
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 = [ |
