summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2007-08-05 00:32:09 -0400
committerBarry Warsaw2007-08-05 00:32:09 -0400
commit959f34a62e0ec3cbe73da3d43640ccb6791cf3a0 (patch)
treeafcf868061fe6a5b56aeb7493c1e72e215fcce1a
parentec734fab4791c107610caf73931e570b2d1b6bd0 (diff)
downloadmailman-959f34a62e0ec3cbe73da3d43640ccb6791cf3a0.tar.gz
mailman-959f34a62e0ec3cbe73da3d43640ccb6791cf3a0.tar.zst
mailman-959f34a62e0ec3cbe73da3d43640ccb6791cf3a0.zip
-rw-r--r--Mailman/Archiver/Archiver.py6
-rw-r--r--Mailman/Autoresponder.py39
-rw-r--r--Mailman/Bouncer.py25
-rw-r--r--Mailman/Defaults.py22
-rw-r--r--Mailman/Digester.py16
-rw-r--r--Mailman/Errors.py5
-rw-r--r--Mailman/GatewayManager.py39
-rw-r--r--Mailman/MailList.py189
-rw-r--r--Mailman/SecurityManager.py7
-rw-r--r--Mailman/TopicMgr.py58
-rw-r--r--Mailman/Utils.py5
-rw-r--r--Mailman/app/create.py46
-rw-r--r--Mailman/app/plugins.py65
-rw-r--r--Mailman/app/styles.py266
-rw-r--r--Mailman/bin/make_instance.py2
-rw-r--r--Mailman/bin/newlist.py167
-rw-r--r--Mailman/bin/rmlist.py28
-rw-r--r--Mailman/bin/withlist.py11
-rw-r--r--Mailman/configuration.py7
-rw-r--r--Mailman/constants.py18
-rw-r--r--Mailman/database/listmanager.py5
-rw-r--r--Mailman/database/model/mailinglist.py26
-rw-r--r--Mailman/database/types.py27
-rw-r--r--Mailman/docs/listmanager.txt9
-rw-r--r--Mailman/ext/__init__.py0
-rw-r--r--Mailman/initialize.py50
-rw-r--r--Mailman/interfaces/listmanager.py5
-rw-r--r--Mailman/interfaces/styles.py86
-rw-r--r--setup.py4
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.
+ """
diff --git a/setup.py b/setup.py
index 5db399924..a75b96213 100644
--- a/setup.py
+++ b/setup.py
@@ -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 = [