diff options
| -rw-r--r-- | buildout.cfg | 2 | ||||
| -rw-r--r-- | mailman/Mailbox.py | 3 | ||||
| -rw-r--r-- | mailman/Message.py | 48 | ||||
| -rw-r--r-- | mailman/Utils.py | 6 | ||||
| -rw-r--r-- | mailman/app/lifecycle.py | 3 | ||||
| -rw-r--r-- | mailman/app/notifications.py | 6 | ||||
| -rw-r--r-- | mailman/app/replybot.py | 6 | ||||
| -rw-r--r-- | mailman/attic/Defaults.py (renamed from mailman/Defaults.py) | 0 | ||||
| -rw-r--r-- | mailman/bin/master.py | 8 | ||||
| -rw-r--r-- | mailman/config/__init__.py | 6 | ||||
| -rw-r--r-- | mailman/config/config.py | 9 | ||||
| -rw-r--r-- | mailman/config/mailman.cfg | 2 | ||||
| -rw-r--r-- | mailman/config/schema.cfg | 218 | ||||
| -rw-r--r-- | mailman/database/mailinglist.py | 6 | ||||
| -rw-r--r-- | mailman/database/pending.py | 4 | ||||
| -rw-r--r-- | mailman/pipeline/cleanse_dkim.py | 8 | ||||
| -rw-r--r-- | mailman/pipeline/scrubber.py | 13 | ||||
| -rw-r--r-- | mailman/pipeline/smtp_direct.py | 16 | ||||
| -rw-r--r-- | mailman/pipeline/to_digest.py | 14 | ||||
| -rw-r--r-- | mailman/pipeline/to_outgoing.py | 10 | ||||
| -rw-r--r-- | mailman/queue/__init__.py | 2 | ||||
| -rw-r--r-- | mailman/styles/__init__.py | 0 | ||||
| -rw-r--r-- | mailman/styles/default.py (renamed from mailman/core/styles.py) | 220 | ||||
| -rw-r--r-- | mailman/testing/layers.py | 7 | ||||
| -rw-r--r-- | setup.py | 1 |
25 files changed, 391 insertions, 227 deletions
diff --git a/buildout.cfg b/buildout.cfg index d345bf142..f502d24e9 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -5,7 +5,7 @@ parts = test unzip = true # bzr branch lp:~barry/lazr.config/megamerge -develop = . /home/barry/projects/lazr/megamerge +develop = . /Users/barry/projects/lazr/megamerge [interpreter] recipe = zc.recipe.egg diff --git a/mailman/Mailbox.py b/mailman/Mailbox.py index a56aeab05..d5902023a 100644 --- a/mailman/Mailbox.py +++ b/mailman/Mailbox.py @@ -25,7 +25,6 @@ import mailbox from email.Errors import MessageParseError from email.Generator import Generator -from mailman import Defaults from mailman.Message import Message @@ -90,7 +89,7 @@ class ArchiverMailbox(Mailbox): # scrub() method, giving the scrubber module a chance to do its thing # before the message is archived. def __init__(self, fp, mlist): - scrubber_module = Defaults.ARCHIVE_SCRUBBER + scrubber_module = config.scrubber.archive_scrubber if scrubber_module: __import__(scrubber_module) self._scrubber = sys.modules[scrubber_module].process diff --git a/mailman/Message.py b/mailman/Message.py index 5ca763b30..ac41a758c 100644 --- a/mailman/Message.py +++ b/mailman/Message.py @@ -17,8 +17,8 @@ """Standard Mailman message object. -This is a subclass of mimeo.Message but provides a slightly extended interface -which is more convenient for use inside Mailman. +This is a subclass of email.message.Message but provides a slightly extended +interface which is more convenient for use inside Mailman. """ import re @@ -28,15 +28,15 @@ import email.utils from email.charset import Charset from email.header import Header +from lazr.config import as_boolean -from mailman import Defaults from mailman import Utils from mailman.config import config COMMASPACE = ', ' mo = re.match(r'([\d.]+)', email.__version__) -VERSION = tuple([int(s) for s in mo.group().split('.')]) +VERSION = tuple(int(s) for s in mo.group().split('.')) @@ -111,7 +111,7 @@ class Message(email.message.Message): self._headers = headers # I think this method ought to eventually be deprecated - def get_sender(self, use_envelope=None, preserve_case=0): + def get_sender(self): """Return the address considered to be the author of the email. This can return either the From: header, the Sender: header or the @@ -124,20 +124,13 @@ class Message(email.message.Message): - Otherwise, the search order is From:, Sender:, unixfrom - The optional argument use_envelope, if given overrides the - config.mailman.use_envelope_sender setting. It should be set to - either True or False (don't use None since that indicates - no-override). - unixfrom should never be empty. The return address is always - lowercased, unless preserve_case is true. + lower cased. This method differs from get_senders() in that it returns one and only one address, and uses a different search order. """ - senderfirst = config.mailman.use_envelope_sender - if use_envelope is not None: - senderfirst = use_envelope + senderfirst = as_boolean(config.mailman.use_envelope_sender) if senderfirst: headers = ('sender', 'from') else: @@ -166,46 +159,41 @@ class Message(email.message.Message): else: # TBD: now what?! address = '' - if not preserve_case: - return address.lower() - return address + return address.lower() - def get_senders(self, preserve_case=0, headers=None): + def get_senders(self): """Return a list of addresses representing the author of the email. The list will contain the following addresses (in order) depending on availability: 1. From: - 2. unixfrom + 2. unixfrom (From_) 3. Reply-To: 4. Sender: - The return addresses are always lower cased, unless `preserve_case' is - true. Optional `headers' gives an alternative search order, with None - meaning, search the unixfrom header. Items in `headers' are field - names without the trailing colon. + The return addresses are always lower cased. """ - if headers is None: - headers = Defaults.SENDER_HEADERS pairs = [] - for h in headers: - if h is None: + for header in config.mailman.sender_headers.split(): + header = header.lower() + if header == 'from_': # get_unixfrom() returns None if there's no envelope - fieldval = self.get_unixfrom() or '' + unix_from = self.get_unixfrom() + fieldval = (unix_from if unix_from is not None else '') try: pairs.append(('', fieldval.split()[1])) except IndexError: # Ignore badly formatted unixfroms pass else: - fieldvals = self.get_all(h) + fieldvals = self.get_all(header) if fieldvals: pairs.extend(email.utils.getaddresses(fieldvals)) authors = [] for pair in pairs: address = pair[1] - if address is not None and not preserve_case: + if address is not None: address = address.lower() authors.append(address) return authors diff --git a/mailman/Utils.py b/mailman/Utils.py index 56f186a17..31848d5c8 100644 --- a/mailman/Utils.py +++ b/mailman/Utils.py @@ -35,11 +35,11 @@ import email.Header import email.Iterators from email.Errors import HeaderParseError +from lazr.config import as_boolean from string import ascii_letters, digits, whitespace, Template import mailman.templates -from mailman import Defaults from mailman import passwords from mailman.config import config from mailman.core import errors @@ -318,8 +318,8 @@ def Secure_MakeRandomPassword(length): def MakeRandomPassword(length=None): if length is None: - length = Defaults.MEMBER_PASSWORD_LENGTH - if Defaults.USER_FRIENDLY_PASSWORDS: + length = int(config.member_password_length) + if as_boolean(config.user_friendly_passwords): password = UserFriendly_MakeRandomPassword(length) else: password = Secure_MakeRandomPassword(length) diff --git a/mailman/app/lifecycle.py b/mailman/app/lifecycle.py index e0d895650..7ea50a055 100644 --- a/mailman/app/lifecycle.py +++ b/mailman/app/lifecycle.py @@ -33,7 +33,6 @@ from mailman import Utils from mailman.Utils import ValidateEmail from mailman.config import config from mailman.core import errors -from mailman.core.styles import style_manager from mailman.interfaces.member import MemberRole @@ -50,7 +49,7 @@ def create_list(fqdn_listname, owners=None): 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): + for style in config.style_manager.lookup(mlist): style.apply(mlist) # Coordinate with the MTA, as defined in the configuration file. module_name, class_name = config.mta.incoming.rsplit('.', 1) diff --git a/mailman/app/notifications.py b/mailman/app/notifications.py index 62c9964c0..5d4b4572d 100644 --- a/mailman/app/notifications.py +++ b/mailman/app/notifications.py @@ -26,8 +26,8 @@ __all__ = [ from email.utils import formataddr +from lazr.config import as_boolean -from mailman import Defaults from mailman import Message from mailman import Utils from mailman import i18n @@ -79,7 +79,7 @@ def send_welcome_message(mlist, address, language, delivery_mode, text=''): _('Welcome to the "$mlist.real_name" mailing list${digmode}'), text, language) msg['X-No-Archive'] = 'yes' - msg.send(mlist, verp=Defaults.VERP_PERSONALIZED_DELIVERIES) + msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries)) @@ -104,7 +104,7 @@ def send_goodbye_message(mlist, address, language): address, mlist.bounces_address, _('You have been unsubscribed from the $mlist.real_name mailing list'), goodbye, language) - msg.send(mlist, verp=Defaults.VERP_PERSONALIZED_DELIVERIES) + msg.send(mlist, verp=as_boolean(config.mta.verp_personalized_deliveries)) diff --git a/mailman/app/replybot.py b/mailman/app/replybot.py index 0a8cdd5b7..ba357e18b 100644 --- a/mailman/app/replybot.py +++ b/mailman/app/replybot.py @@ -29,7 +29,6 @@ __all__ = [ import logging import datetime -from mailman import Defaults from mailman import Utils from mailman import i18n @@ -48,7 +47,8 @@ def autorespond_to_sender(mlist, sender, lang=None): """ if lang is None: lang = mlist.preferred_language - if Defaults.MAX_AUTORESPONSES_PER_DAY == 0: + max_autoresponses_per_day = int(config.mta.max_autoresponses_per_day) + if max_autoresponses_per_day == 0: # Unlimited. return True today = datetime.date.today() @@ -64,7 +64,7 @@ def autorespond_to_sender(mlist, sender, lang=None): # them of this fact, so there's nothing more to do. log.info('-request/hold autoresponse discarded for: %s', sender) return False - if count >= Defaults.MAX_AUTORESPONSES_PER_DAY: + if count >= max_autoresponses_per_day: log.info('-request/hold autoresponse limit hit for: %s', sender) mlist.hold_and_cmd_autoresponses[sender] = (today, -1) # Send this notification message instead. diff --git a/mailman/Defaults.py b/mailman/attic/Defaults.py index 6f72ed535..6f72ed535 100644 --- a/mailman/Defaults.py +++ b/mailman/attic/Defaults.py diff --git a/mailman/bin/master.py b/mailman/bin/master.py index d3676629d..d954bc865 100644 --- a/mailman/bin/master.py +++ b/mailman/bin/master.py @@ -36,7 +36,6 @@ from lazr.config import as_boolean from locknix import lockfile from munepy import Enum -from mailman import Defaults from mailman.config import config from mailman.core.logging import reopen from mailman.i18n import _ @@ -44,7 +43,8 @@ from mailman.options import Options DOT = '.' -LOCK_LIFETIME = Defaults.days(1) + Defaults.hours(6) +LOCK_LIFETIME = timedelta(days=1, hours=6) +SECONDS_IN_A_DAY = 86400 @@ -233,9 +233,9 @@ class Loop: # so this should be plenty. def sigalrm_handler(signum, frame): self._lock.refresh() - signal.alarm(int(Defaults.days(1))) + signal.alarm(SECONDS_IN_A_DAY) signal.signal(signal.SIGALRM, sigalrm_handler) - signal.alarm(int(Defaults.days(1))) + signal.alarm(SECONDS_IN_A_DAY) # SIGHUP tells the qrunners to close and reopen their log files. def sighup_handler(signum, frame): reopen() diff --git a/mailman/config/__init__.py b/mailman/config/__init__.py index 6ac060cb2..0611fc154 100644 --- a/mailman/config/__init__.py +++ b/mailman/config/__init__.py @@ -15,6 +15,12 @@ # You should have received a copy of the GNU General Public License along with # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. +__metaclass__ = type +__all__ = [ + 'config', + ] + + from mailman.config.config import Configuration config = Configuration() diff --git a/mailman/config/config.py b/mailman/config/config.py index cbdea2aea..349202338 100644 --- a/mailman/config/config.py +++ b/mailman/config/config.py @@ -32,11 +32,11 @@ from StringIO import StringIO from lazr.config import ConfigSchema, as_boolean from pkg_resources import resource_string -from mailman import Defaults from mailman import version from mailman.core import errors from mailman.domain import Domain from mailman.languages import LanguageManager +from mailman.styles.manager import StyleManager SPACE = ' ' @@ -149,6 +149,7 @@ class Configuration(object): # Always enable the server default language, which must be defined. self.languages.enable_language(self._config.mailman.default_language) self.ensure_directories_exist() + self.style_manager = StyleManager() @property def logger_configs(self): @@ -189,6 +190,12 @@ class Configuration(object): yield getattr(sys.modules[module_name], class_name)() @property + def style_configs(self): + """Iterate over all the style configuration sections.""" + for section in self._config.getByCategory('style', []): + yield section + + @property def header_matches(self): """Iterate over all spam matching headers. diff --git a/mailman/config/mailman.cfg b/mailman/config/mailman.cfg index d88947975..2bf528bea 100644 --- a/mailman/config/mailman.cfg +++ b/mailman/config/mailman.cfg @@ -65,3 +65,5 @@ start: no [qrunner.virgin] class: mailman.queue.virgin.VirginRunner + +[style.default] diff --git a/mailman/config/schema.cfg b/mailman/config/schema.cfg index 47cf03eeb..654bbaabc 100644 --- a/mailman/config/schema.cfg +++ b/mailman/config/schema.cfg @@ -54,9 +54,37 @@ default_language: en # spoofed messages may get through. use_envelope_sender: no +# Membership tests for posting purposes are usually performed by looking at a +# set of headers, passing the test if any of their values match a member of +# the list. Headers are checked in the order given in this variable. The +# value From_ means to use the envelope sender. Field names are case +# insensitive. This is a space separate list of headers. +sender_headers: from from_ reply-to sender + # Mail command processor will ignore mail command lines after designated max. email_commands_max_lines: 10 +# Default length of time a pending request is live before it is evicted from +# the pending database. +pending_request_life: 3d + + +[passwords] +# When Mailman generates them, this is the default length of member passwords. +member_password_length: 8 + +# Specify the type of passwords to use, when Mailman generates the passwords +# itself, as would be the case for membership requests where the user did not +# fill in a password, or during list creation, when auto-generation of admin +# passwords was selected. +# +# Set this value to 'yes' for classic Mailman user-friendly(er) passwords. +# These generate semi-pronounceable passwords which are easier to remember. +# Set this value to 'no' to use more cryptographically secure, but harder to +# remember, passwords -- if your operating system and Python version support +# the necessary feature (specifically that /dev/urandom be available). +user_friendly_passwords: yes + [qrunner.master] # Define which process queue runners, and how many of them, to start. @@ -248,6 +276,118 @@ smtp_port: 25 lmtp_host: localhost lmtp_port: 8025 +# Ceiling on the number of recipients that can be specified in a single SMTP +# transaction. Set to 0 to submit the entire recipient list in one +# transaction. +max_recipients: 500 + +# Ceiling on the number of SMTP sessions to perform on a single socket +# connection. Some MTAs have limits. Set this to 0 to do as many as we like +# (i.e. your MTA has no limits). Set this to some number great than 0 and +# Mailman will close the SMTP connection and re-open it after this number of +# consecutive sessions. +max_sessions_per_connection: 0 + +# Maximum number of simultaneous subthreads that will be used for SMTP +# delivery. After the recipients list is chunked according to max_recipients, +# each chunk is handed off to the SMTP server by a separate such thread. If +# your Python interpreter was not built for threads, this feature is disabled. +# You can explicitly disable it in all cases by setting max_delivery_threads +# to 0. +max_delivery_threads: 0 + +# These variables control the format and frequency of VERP-like delivery for +# better bounce detection. VERP is Variable Envelope Return Path, defined +# here: +# +# http://cr.yp.to/proto/verp.txt +# +# This involves encoding the address of the recipient as we (Mailman) know it +# into the envelope sender address (i.e. the SMTP `MAIL FROM:' address). +# Thus, no matter what kind of forwarding the recipient has in place, should +# it eventually bounce, we will receive an unambiguous notice of the bouncing +# address. +# +# However, we're technically only "VERP-like" because we're doing the envelope +# sender encoding in Mailman, not in the MTA. We do require cooperation from +# the MTA, so you must be sure your MTA can be configured for extended address +# semantics. +# +# The first variable describes how to encode VERP envelopes. It must contain +# these three string interpolations: +# +# $bounces -- the list-bounces mailbox will be set here +# $mailbox -- the recipient's mailbox will be set here +# $host -- the recipient's host name will be set here +# +# This example uses the default below. +# +# FQDN list address is: mylist@dom.ain +# Recipient is: aperson@a.nother.dom +# +# The envelope sender will be mylist-bounces+aperson=a.nother.dom@dom.ain +# +# Note that your MTA /must/ be configured to deliver such an addressed message +# to mylist-bounces! +verp_delimiter: + +verp_format: ${bounces}+${mailbox}=${host} + +# For nicer confirmation emails, use a VERP-like format which encodes the +# confirmation cookie in the reply address. This lets us put a more user +# friendly Subject: on the message, but requires cooperation from the MTA. +# Format is like verp_format, but with the following substitutions: +# +# $address -- the list-confirm address +# $cookie -- the confirmation cookie +verp_confirm_format: $address+$cookie + +# This is analogous to verp_regexp, but for splitting apart the +# verp_confirm_format. MUAs have been observed that mung +# +# From: local_part@host +# +# into +# +# To: "local_part" <local_part@host> +# +# when replying, so we skip everything up to '<' if any. +verp_confirm_regexp: ^(.*<)?(?P<addr>[^+]+?)\+(?P<cookie>[^@]+)@.*$ + +# Set this to 'yes' to enable VERP-like (more user friendly) confirmations. +verp_confirmations: no + +# Another good opportunity is when regular delivery is personalized. Here +# again, we're already incurring the performance hit for addressing each +# individual recipient. Set this to 'yes' to enable VERPs on all personalized +# regular deliveries (personalized digests aren't supported yet). +verp_personalized_deliveries: no + +# And finally, we can VERP normal, non-personalized deliveries. However, +# because it can be a significant performance hit, we allow you to decide how +# often to VERP regular deliveries. This is the interval, in number of +# messages, to do a VERP recipient address. The same variable controls both +# regular and digest deliveries. Set to 0 to disable occasional VERPs, set to +# 1 to VERP every delivery, or to some number > 1 for only occasional VERPs. +verp_delivery_interval: 0 + +# This is the maximum number of automatic responses sent to an address because +# of -request messages or posting hold messages. This limit prevents response +# loops between Mailman and misconfigured remote email robots. Mailman +# already inhibits automatic replies to any message labeled with a header +# "Precendence: bulk|list|junk". This is a fallback safety valve so it should +# be set fairly high. Set to 0 for no limit (probably useful only for +# debugging). +max_autoresponses_per_day: 10 + +# Some list posts and mail to the -owner address may contain DomainKey or +# DomainKeys Identified Mail (DKIM) signature headers <http://www.dkim.org/>. +# Various list transformations to the message such as adding a list header or +# footer or scrubbing attachments or even reply-to munging can break these +# signatures. It is generally felt that these signatures have value, even if +# broken and even if the outgoing message is resigned. However, some sites +# may wish to remove these headers by setting this to 'yes'. +remove_dkim_headers: no + [archiver.master] # To add new archivers, define a new section based on this one, overriding the @@ -287,3 +427,81 @@ class: mailman.archiving.pipermail.Pipermail [archiver.prototype] # This is a prototypical sample archiver. class: mailman.archiving.prototype.Prototype + + +[style.master] +# The style's priority, with 0 being the lowest priority. +priority: 0 + +# The class implementing the IStyle interface, which applies the style. +class: mailman.styles.default.DefaultStyle + + +[scrubber] +# A filter module that converts from multipart messages to "flat" messages +# (i.e. containing a single payload). This is required for Pipermail, and you +# may want to set it to 0 for external archivers. You can also replace it +# with your own module as long as it contains a process() function that takes +# a MailList object and a Message object. It should raise +# Errors.DiscardMessage if it wants to throw the message away. Otherwise it +# should modify the Message object as necessary. +archive_scrubber: mailman.pipeline.scrubber + +# This variable defines what happens to text/html subparts. They can be +# stripped completely, escaped, or filtered through an external program. The +# legal values are: +# 0 - Strip out text/html parts completely, leaving a notice of the removal in +# the message. If the outer part is text/html, the entire message is +# discarded. +# 1 - Remove any embedded text/html parts, leaving them as HTML-escaped +# attachments which can be separately viewed. Outer text/html parts are +# simply HTML-escaped. +# 2 - Leave it inline, but HTML-escape it +# 3 - Remove text/html as attachments but don't HTML-escape them. Note: this +# is very dangerous because it essentially means anybody can send an HTML +# email to your site containing evil JavaScript or web bugs, or other +# nasty things, and folks viewing your archives will be susceptible. You +# should only consider this option if you do heavy moderation of your list +# postings. +# +# Note: given the current archiving code, it is not possible to leave +# text/html parts inline and un-escaped. I wouldn't think it'd be a good idea +# to do anyway. +# +# The value can also be a string, in which case it is the name of a command to +# filter the HTML page through. The resulting output is left in an attachment +# or as the entirety of the message when the outer part is text/html. The +# format of the string must include a $filename substitution variable which +# will contain the name of the temporary file that the program should operate +# on. It should write the processed message to stdout. Set this to +# HTML_TO_PLAIN_TEXT_COMMAND to specify an HTML to plain text conversion +# program. +archive_html_sanitizer: 1 + +# Control parameter whether the scrubber should use the message attachment's +# filename as is indicated by the filename parameter or use 'attachement-xxx' +# instead. The default is set 'no' because the applications on PC and Mac +# begin to use longer non-ascii filenames. +use_attachment_filename: no + +# Use of attachment filename extension per se is may be dangerous because +# viruses fakes it. You can set this 'yes' if you filter the attachment by +# filename extension. +use_attachment_filename_extension: no + + +[digests] +# Headers which should be kept in both RFC 1153 (plain) and MIME digests. RFC +# 1153 also specifies these headers in this exact order, so order matters. +# These are space separated and case insensitive. +mime_digest_keep_headers: + Date From To Cc Subject Message-ID Keywords + In-Reply-To References Content-Type MIME-Version + Content-Transfer-Encoding Precedence Reply-To + Message + +plain_digest_keep_headers: + Message Date From + Subject To Cc + Message-ID Keywords + Content-Type diff --git a/mailman/database/mailinglist.py b/mailman/database/mailinglist.py index eac8ad05d..56caea296 100644 --- a/mailman/database/mailinglist.py +++ b/mailman/database/mailinglist.py @@ -22,7 +22,6 @@ from storm.locals import * from urlparse import urljoin from zope.interface import implements -from mailman import Defaults from mailman.Utils import fqdn_listname, makedirs, split_listname from mailman.config import config from mailman.database import roster @@ -206,8 +205,7 @@ class MailingList(Model): domain = config.domains[self.host_name] # XXX Handle the case for when context is not None; those would be # relative URLs. - return urljoin(domain.base_url, - target + Defaults.CGIEXT + '/' + self.fqdn_listname) + return urljoin(domain.base_url, target + '/' + self.fqdn_listname) @property def data_path(self): @@ -253,7 +251,7 @@ class MailingList(Model): return '%s-unsubscribe@%s' % (self.list_name, self.host_name) def confirm_address(self, cookie): - template = string.Template(Defaults.VERP_CONFIRM_FORMAT) + template = string.Template(config.mta.verp_confirm_format) local_part = template.safe_substitute( address = '%s-confirm' % self.list_name, cookie = cookie) diff --git a/mailman/database/pending.py b/mailman/database/pending.py index 07e594253..6fe66f5fd 100644 --- a/mailman/database/pending.py +++ b/mailman/database/pending.py @@ -29,11 +29,11 @@ import random import hashlib import datetime +from lazr.config import as_timedelta from storm.locals import * from zope.interface import implements from zope.interface.verify import verifyObject -from mailman import Defaults from mailman.config import config from mailman.database.model import Model from mailman.interfaces.pending import ( @@ -87,7 +87,7 @@ class Pendings: verifyObject(IPendable, pendable) # Calculate the token and the lifetime. if lifetime is None: - lifetime = Defaults.PENDING_REQUEST_LIFE + lifetime = as_timedelta(config.pending_request_life) # Calculate a unique token. Algorithm vetted by the Timbot. time() # has high resolution on Linux, clock() on Windows. random gives us # about 45 bits in Python 2.2, 53 bits on Python 2.3. The time and diff --git a/mailman/pipeline/cleanse_dkim.py b/mailman/pipeline/cleanse_dkim.py index bd7de83dc..d4d8f38ae 100644 --- a/mailman/pipeline/cleanse_dkim.py +++ b/mailman/pipeline/cleanse_dkim.py @@ -26,12 +26,14 @@ originating at the Mailman server for the outgoing message. """ __metaclass__ = type -__all__ = ['CleanseDKIM'] +__all__ = [ + 'CleanseDKIM', + ] +from lazr.config import as_boolean from zope.interface import implements -from mailman import Defaults from mailman.i18n import _ from mailman.interfaces.handler import IHandler @@ -47,7 +49,7 @@ class CleanseDKIM: def process(self, mlist, msg, msgdata): """See `IHandler`.""" - if Defaults.REMOVE_DKIM_HEADERS: + if as_boolean(config.mta.remove_dkim_headers): del msg['domainkey-signature'] del msg['dkim-signature'] del msg['authentication-results'] diff --git a/mailman/pipeline/scrubber.py b/mailman/pipeline/scrubber.py index 7431cec27..f7ffd51e1 100644 --- a/mailman/pipeline/scrubber.py +++ b/mailman/pipeline/scrubber.py @@ -34,11 +34,12 @@ import binascii from email.charset import Charset from email.generator import Generator from email.utils import make_msgid, parsedate +from lazr.config import as_boolean from locknix.lockfile import Lock from mimetypes import guess_all_extensions +from string import Template from zope.interface import implements -from mailman import Defaults from mailman import Utils from mailman.config import config from mailman.core.errors import DiscardMessage @@ -159,7 +160,7 @@ def replace_payload_by_text(msg, text, charset): def process(mlist, msg, msgdata=None): - sanitize = Defaults.ARCHIVE_HTML_SANITIZER + sanitize = int(config.scrubber.archive_html_sanitizer) outer = True if msgdata is None: msgdata = {} @@ -410,7 +411,7 @@ def save_attachment(mlist, msg, dir, filter_html=True): filename, fnext = os.path.splitext(filename) # For safety, we should confirm this is valid ext for content-type # but we can use fnext if we introduce fnext filtering - if Defaults.SCRUBBER_USE_ATTACHMENT_FILENAME_EXTENSION: + if as_boolean(config.scrubber.use_attachment_filename_extension): # HTML message doesn't have filename :-( ext = fnext or guess_extension(ctype, fnext) else: @@ -431,7 +432,8 @@ def save_attachment(mlist, msg, dir, filter_html=True): with Lock(os.path.join(fsdir, 'attachments.lock')): # Now base the filename on what's in the attachment, uniquifying it if # necessary. - if not filename or Defaults.SCRUBBER_DONT_USE_ATTACHMENT_FILENAME: + if (not filename or + not as_boolean(config.scrubber.use_attachment_filename)): filebase = 'attachment' else: # Sanitize the filename given in the message headers @@ -476,7 +478,8 @@ def save_attachment(mlist, msg, dir, filter_html=True): try: fp.write(decodedpayload) fp.close() - cmd = Defaults.ARCHIVE_HTML_SANITIZER % {'filename' : tmppath} + cmd = Template(config.mta.archive_html_sanitizer).safe_substitue( + filename=tmppath) progfp = os.popen(cmd, 'r') decodedpayload = progfp.read() status = progfp.close() diff --git a/mailman/pipeline/smtp_direct.py b/mailman/pipeline/smtp_direct.py index 09b2223d6..b82fb9227 100644 --- a/mailman/pipeline/smtp_direct.py +++ b/mailman/pipeline/smtp_direct.py @@ -44,7 +44,6 @@ from email.Utils import formataddr from string import Template from zope.interface import implements -from mailman import Defaults from mailman import Utils from mailman.config import config from mailman.core import errors @@ -70,7 +69,7 @@ class Connection: port = int(config.mta.smtp_port) log.debug('Connecting to %s:%s', host, port) self.__conn.connect(host, port) - self.__numsessions = Defaults.SMTP_MAX_SESSIONS_PER_CONNECTION + self.__numsessions = int(config.mta.max_sessions_per_connection) def sendmail(self, envsender, recips, msgtext): if self.__conn is None: @@ -126,10 +125,10 @@ def process(mlist, msg, msgdata): chunks = [[recip] for recip in recips] msgdata['personalize'] = 1 deliveryfunc = verpdeliver - elif Defaults.SMTP_MAX_RCPTS <= 0: + elif int(config.mta.max_recipients) <= 0: chunks = [recips] else: - chunks = chunkify(recips, Defaults.SMTP_MAX_RCPTS) + chunks = chunkify(recips, int(config.mta.max_recipients)) # See if this is an unshunted message for which some were undelivered if msgdata.has_key('undelivered'): chunks = msgdata['undelivered'] @@ -316,12 +315,9 @@ def verpdeliver(mlist, msg, msgdata, envsender, failures, conn): # this recipient. log.info('Skipping VERP delivery to unqual recip: %s', recip) continue - d = {'bounces': bmailbox, - 'mailbox': rmailbox, - 'host' : DOT.join(rdomain), - } - envsender = '%s@%s' % ((Defaults.VERP_FORMAT % d), - DOT.join(bdomain)) + envsender = Template(config.mta.verp_format).safe_substitute( + bounces=bmailbox, mailbox=rmailbox, + host=DOT.join(rdomain)) + '@' + DOT.join(bdomain) if mlist.personalize == Personalization.full: # When fully personalizing, we want the To address to point to the # recipient, not to the mailing list diff --git a/mailman/pipeline/to_digest.py b/mailman/pipeline/to_digest.py index d71bc71b0..e56c4a109 100644 --- a/mailman/pipeline/to_digest.py +++ b/mailman/pipeline/to_digest.py @@ -48,7 +48,6 @@ from email.parser import Parser from email.utils import formatdate, getaddresses, make_msgid from zope.interface import implements -from mailman import Defaults from mailman import Message from mailman import Utils from mailman import i18n @@ -268,11 +267,10 @@ def send_i18n_digests(mlist, mboxfp): # headers according to RFC 1153. Later, we'll strip out headers for # for the specific MIME or plain digests. keeper = {} - all_keepers = {} - for header in (Defaults.MIME_DIGEST_KEEP_HEADERS + - Defaults.PLAIN_DIGEST_KEEP_HEADERS): - all_keepers[header] = True - all_keepers = all_keepers.keys() + all_keepers = set( + header for header in + config.digests.mime_digest_keep_headers.split() + + config.digests.plain_digest_keep_headers.split()) for keep in all_keepers: keeper[keep] = msg.get_all(keep, []) # Now remove all unkempt headers :) @@ -283,7 +281,7 @@ def send_i18n_digests(mlist, mboxfp): for field in keeper[keep]: msg[keep] = field # And a bit of extra stuff - msg['Message'] = `msgcount` + msg['Message'] = repr(msgcount) # Get the next message in the digest mailbox msg = mbox.next() # Now we're finished with all the messages in the digest. First do some @@ -326,7 +324,7 @@ def send_i18n_digests(mlist, mboxfp): print >> plainmsg, _('[Message discarded by content filter]') continue # Honor the default setting - for h in Defaults.PLAIN_DIGEST_KEEP_HEADERS: + for h in config.digests.plain_digest_keep_headers.split(): if msg[h]: uh = Utils.wrap('%s: %s' % (h, Utils.oneline(msg[h], in_unicode=True))) diff --git a/mailman/pipeline/to_outgoing.py b/mailman/pipeline/to_outgoing.py index 7d56686b7..9ff7ab88a 100644 --- a/mailman/pipeline/to_outgoing.py +++ b/mailman/pipeline/to_outgoing.py @@ -23,12 +23,14 @@ recipient should just be placed in the out queue directly. """ __metaclass__ = type -__all__ = ['ToOutgoing'] +__all__ = [ + 'ToOutgoing', + ] +from lazr.config import as_boolean from zope.interface import implements -from mailman import Defaults from mailman.config import config from mailman.i18n import _ from mailman.interfaces.handler import IHandler @@ -46,7 +48,7 @@ class ToOutgoing: def process(self, mlist, msg, msgdata): """See `IHandler`.""" - interval = Defaults.VERP_DELIVERY_INTERVAL + interval = int(config.mta.verp_delivery_interval) # Should we VERP this message? If personalization is enabled for this # list and VERP_PERSONALIZED_DELIVERIES is true, then yes we VERP it. # Also, if personalization is /not/ enabled, but @@ -58,7 +60,7 @@ class ToOutgoing: if 'verp' in msgdata: pass elif mlist.personalize <> Personalization.none: - if Defaults.VERP_PERSONALIZED_DELIVERIES: + if as_boolean(config.mta.verp_personalized_deliveries): msgdata['verp'] = True elif interval == 0: # Never VERP diff --git a/mailman/queue/__init__.py b/mailman/queue/__init__.py index 65e31f6f3..e6d39ee1b 100644 --- a/mailman/queue/__init__.py +++ b/mailman/queue/__init__.py @@ -298,7 +298,7 @@ class Runner: # sleep_time is a timedelta; turn it into a float for time.sleep(). self.sleep_float = (86400 * self.sleep_time.days + self.sleep_time.seconds + - self.sleep_time.microseconds / 1000000.0) + self.sleep_time.microseconds / 1.0e6) self.max_restarts = int(section.max_restarts) self.start = as_boolean(section.start) self._stop = False diff --git a/mailman/styles/__init__.py b/mailman/styles/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/mailman/styles/__init__.py diff --git a/mailman/core/styles.py b/mailman/styles/default.py index b375a743c..76b697296 100644 --- a/mailman/core/styles.py +++ b/mailman/styles/default.py @@ -20,25 +20,19 @@ __metaclass__ = type __all__ = [ 'DefaultStyle', - 'style_manager', ] # XXX Styles need to be reconciled with lazr.config. import datetime -from operator import attrgetter from zope.interface import implements -from zope.interface.verify import verifyObject -from mailman import Defaults from mailman import Utils -from mailman.core.plugins import get_plugins from mailman.i18n import _ from mailman.interfaces import Action, NewsModeration -from mailman.interfaces.mailinglist import Personalization -from mailman.interfaces.styles import ( - DuplicateStyleError, IStyle, IStyleManager) +from mailman.interfaces.mailinglist import Personalization, ReplyToMunging +from mailman.interfaces.styles import IStyle @@ -57,77 +51,84 @@ class DefaultStyle: # Most of these were ripped from the old MailList.InitVars() method. mlist.volume = 1 mlist.post_id = 1 - mlist.new_member_options = Defaults.DEFAULT_NEW_MEMBER_OPTIONS + mlist.new_member_options = 256 # This stuff is configurable mlist.real_name = mlist.list_name.capitalize() mlist.respond_to_post_requests = True - mlist.advertised = Defaults.DEFAULT_LIST_ADVERTISED - mlist.max_num_recipients = Defaults.DEFAULT_MAX_NUM_RECIPIENTS - mlist.max_message_size = Defaults.DEFAULT_MAX_MESSAGE_SIZE - mlist.reply_goes_to_list = Defaults.DEFAULT_REPLY_GOES_TO_LIST + mlist.advertised = True + mlist.max_num_recipients = 10 + mlist.max_message_size = 40 # KB + mlist.reply_goes_to_list = ReplyToMunging.no_munging mlist.reply_to_address = u'' - mlist.first_strip_reply_to = Defaults.DEFAULT_FIRST_STRIP_REPLY_TO - mlist.admin_immed_notify = Defaults.DEFAULT_ADMIN_IMMED_NOTIFY - mlist.admin_notify_mchanges = ( - Defaults.DEFAULT_ADMIN_NOTIFY_MCHANGES) - mlist.require_explicit_destination = ( - Defaults.DEFAULT_REQUIRE_EXPLICIT_DESTINATION) - mlist.acceptable_aliases = Defaults.DEFAULT_ACCEPTABLE_ALIASES - mlist.send_reminders = Defaults.DEFAULT_SEND_REMINDERS - mlist.send_welcome_msg = Defaults.DEFAULT_SEND_WELCOME_MSG - mlist.send_goodbye_msg = Defaults.DEFAULT_SEND_GOODBYE_MSG - mlist.bounce_matching_headers = ( - Defaults.DEFAULT_BOUNCE_MATCHING_HEADERS) + mlist.first_strip_reply_to = False + mlist.admin_immed_notify = True + mlist.admin_notify_mchanges = False + mlist.require_explicit_destination = True + mlist.acceptable_aliases = u'' + mlist.send_reminders = True + mlist.send_welcome_msg = True + mlist.send_goodbye_msg = True + mlist.bounce_matching_headers = u""" +# Lines that *start* with a '#' are comments. +to: friend@public.com +message-id: relay.comanche.denmark.eu +from: list@listme.com +from: .*@uplinkpro.com +""" mlist.header_matches = [] - mlist.anonymous_list = Defaults.DEFAULT_ANONYMOUS_LIST + mlist.anonymous_list = False mlist.description = u'' mlist.info = u'' mlist.welcome_msg = u'' mlist.goodbye_msg = u'' - mlist.subscribe_policy = Defaults.DEFAULT_SUBSCRIBE_POLICY - mlist.subscribe_auto_approval = ( - Defaults.DEFAULT_SUBSCRIBE_AUTO_APPROVAL) - mlist.unsubscribe_policy = Defaults.DEFAULT_UNSUBSCRIBE_POLICY - mlist.private_roster = Defaults.DEFAULT_PRIVATE_ROSTER - mlist.obscure_addresses = Defaults.DEFAULT_OBSCURE_ADDRESSES - mlist.admin_member_chunksize = Defaults.DEFAULT_ADMIN_MEMBER_CHUNKSIZE - mlist.administrivia = Defaults.DEFAULT_ADMINISTRIVIA - mlist.preferred_language = Defaults.DEFAULT_SERVER_LANGUAGE + mlist.subscribe_policy = 1 + mlist.subscribe_auto_approval = [] + mlist.unsubscribe_policy = 0 + mlist.private_roster = 1 + mlist.obscure_addresses = True + mlist.admin_member_chunksize = 30 + mlist.administrivia = True + mlist.preferred_language = u'en' mlist.include_rfc2369_headers = True mlist.include_list_post_header = True - mlist.filter_mime_types = Defaults.DEFAULT_FILTER_MIME_TYPES - mlist.pass_mime_types = Defaults.DEFAULT_PASS_MIME_TYPES - mlist.filter_filename_extensions = ( - Defaults.DEFAULT_FILTER_FILENAME_EXTENSIONS) - mlist.pass_filename_extensions = ( - Defaults.DEFAULT_PASS_FILENAME_EXTENSIONS) - mlist.filter_content = Defaults.DEFAULT_FILTER_CONTENT - mlist.collapse_alternatives = Defaults.DEFAULT_COLLAPSE_ALTERNATIVES - mlist.convert_html_to_plaintext = ( - Defaults.DEFAULT_CONVERT_HTML_TO_PLAINTEXT) - mlist.filter_action = Defaults.DEFAULT_FILTER_ACTION + mlist.filter_mime_types = [] + mlist.pass_mime_types = [ + 'multipart/mixed', + 'multipart/alternative', + 'text/plain', + ] + mlist.filter_filename_extensions = [ + 'exe', 'bat', 'cmd', 'com', 'pif', 'scr', 'vbs', 'cpl', + ] + mlist.pass_filename_extensions = [] + mlist.filter_content = False + mlist.collapse_alternatives = True + mlist.convert_html_to_plaintext = True + mlist.filter_action = 0 # Digest related variables - mlist.digestable = Defaults.DEFAULT_DIGESTABLE - mlist.digest_is_default = Defaults.DEFAULT_DIGEST_IS_DEFAULT - mlist.mime_is_default_digest = Defaults.DEFAULT_MIME_IS_DEFAULT_DIGEST - mlist.digest_size_threshold = Defaults.DEFAULT_DIGEST_SIZE_THRESHOLD - mlist.digest_send_periodic = Defaults.DEFAULT_DIGEST_SEND_PERIODIC - mlist.digest_header = Defaults.DEFAULT_DIGEST_HEADER - mlist.digest_footer = Defaults.DEFAULT_DIGEST_FOOTER - mlist.digest_volume_frequency = ( - Defaults.DEFAULT_DIGEST_VOLUME_FREQUENCY) + mlist.digestable = True + mlist.digest_is_default = False + mlist.mime_is_default_digest = False + mlist.digest_size_threshold = 30 # KB + mlist.digest_send_periodic = True + mlist.digest_header = u'' + mlist.digest_footer = u"""\ +_______________________________________________ +$real_name mailing list +$fqdn_listname +${listinfo_page} +""" + mlist.digest_volume_frequency = 1 mlist.one_last_digest = {} mlist.next_digest_number = 1 - mlist.nondigestable = Defaults.DEFAULT_NONDIGESTABLE + mlist.nondigestable = True mlist.personalize = Personalization.none # New sender-centric moderation (privacy) options - mlist.default_member_moderation = ( - Defaults.DEFAULT_DEFAULT_MEMBER_MODERATION) + mlist.default_member_moderation = False # Archiver - mlist.archive = Defaults.DEFAULT_ARCHIVE - mlist.archive_private = Defaults.DEFAULT_ARCHIVE_PRIVATE - mlist.archive_volume_frequency = ( - Defaults.DEFAULT_ARCHIVE_VOLUME_FREQUENCY) + mlist.archive = True + mlist.archive_private = 0 + mlist.archive_volume_frequency = 1 mlist.emergency = False mlist.member_moderation_action = Action.hold mlist.member_moderation_notice = u'' @@ -135,9 +136,8 @@ class DefaultStyle: mlist.hold_these_nonmembers = [] mlist.reject_these_nonmembers = [] mlist.discard_these_nonmembers = [] - mlist.forward_auto_discards = Defaults.DEFAULT_FORWARD_AUTO_DISCARDS - mlist.generic_nonmember_action = ( - Defaults.DEFAULT_GENERIC_NONMEMBER_ACTION) + mlist.forward_auto_discards = True + mlist.generic_nonmember_action = 1 mlist.nonmember_rejection_notice = u'' # Ban lists mlist.ban_list = [] @@ -145,9 +145,14 @@ class DefaultStyle: # 2-tuple of the date of the last autoresponse and the number of # autoresponses sent on that date. mlist.hold_and_cmd_autoresponses = {} - mlist.subject_prefix = _(Defaults.DEFAULT_SUBJECT_PREFIX) - mlist.msg_header = Defaults.DEFAULT_MSG_HEADER - mlist.msg_footer = Defaults.DEFAULT_MSG_FOOTER + mlist.subject_prefix = _(u'[$mlist.real_name] ') + mlist.msg_header = u'' + mlist.msg_footer = u"""\ +_______________________________________________ +$real_name mailing list +$fqdn_listname +${listinfo_page} +""" # 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': @@ -155,9 +160,9 @@ class DefaultStyle: else: mlist.encode_ascii_prefixes = 2 # scrub regular delivery - mlist.scrub_nondigest = Defaults.DEFAULT_SCRUB_NONDIGEST + mlist.scrub_nondigest = False # automatic discarding - mlist.max_days_to_hold = Defaults.DEFAULT_MAX_DAYS_TO_HOLD + mlist.max_days_to_hold = 0 # Autoresponder mlist.autorespond_postings = False mlist.autorespond_admin = False @@ -174,20 +179,15 @@ class DefaultStyle: mlist.admin_responses = {} mlist.request_responses = {} # Bounces - mlist.bounce_processing = Defaults.DEFAULT_BOUNCE_PROCESSING - mlist.bounce_score_threshold = Defaults.DEFAULT_BOUNCE_SCORE_THRESHOLD - mlist.bounce_info_stale_after = ( - Defaults.DEFAULT_BOUNCE_INFO_STALE_AFTER) - mlist.bounce_you_are_disabled_warnings = ( - Defaults.DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS) + mlist.bounce_processing = True + mlist.bounce_score_threshold = 5.0 + mlist.bounce_info_stale_after = datetime.timedelta(days=7) + mlist.bounce_you_are_disabled_warnings = 3 mlist.bounce_you_are_disabled_warnings_interval = ( - Defaults.DEFAULT_BOUNCE_YOU_ARE_DISABLED_WARNINGS_INTERVAL) - mlist.bounce_unrecognized_goes_to_list_owner = ( - Defaults.DEFAULT_BOUNCE_UNRECOGNIZED_GOES_TO_LIST_OWNER) - mlist.bounce_notify_owner_on_disable = ( - Defaults.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_DISABLE) - mlist.bounce_notify_owner_on_removal = ( - Defaults.DEFAULT_BOUNCE_NOTIFY_OWNER_ON_REMOVAL) + datetime.timedelta(days=7)) + mlist.bounce_unrecognized_goes_to_list_owner = True + mlist.bounce_notify_owner_on_disable = True + mlist.bounce_notify_owner_on_removal = True # 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 @@ -196,7 +196,7 @@ class DefaultStyle: # New style delivery status mlist.delivery_status = {} # NNTP gateway - mlist.nntp_host = Defaults.DEFAULT_NNTP_HOST + mlist.nntp_host = u'' mlist.linked_newsgroup = u'' mlist.gateway_to_news = False mlist.gateway_to_mail = False @@ -246,55 +246,3 @@ class DefaultStyle: # If no other styles have matched, then the default style matches. if len(styles) == 0: styles.append(self) - - - -class StyleManager: - """The built-in style manager.""" - - implements(IStyleManager) - - def __init__(self): - """Install all styles from registered plugins, and install them.""" - self._styles = {} - # Install all the styles provided by plugins. - for style_factory in get_plugins('mailman.styles'): - style = style_factory() - # Let DuplicateStyleErrors percolate up. - self.register(style) - - def get(self, name): - """See `IStyleManager`.""" - return self._styles.get(name) - - def lookup(self, mailing_list): - """See `IStyleManager`.""" - matched_styles = [] - for style in self.styles: - style.match(mailing_list, matched_styles) - for style in matched_styles: - yield style - - @property - def styles(self): - """See `IStyleManager`.""" - for style in sorted(self._styles.values(), - key=attrgetter('priority'), - reverse=True): - yield style - - def register(self, style): - """See `IStyleManager`.""" - verifyObject(IStyle, style) - if style.name in self._styles: - raise DuplicateStyleError(style.name) - self._styles[style.name] = style - - def unregister(self, style): - """See `IStyleManager`.""" - # Let KeyErrors percolate up. - del self._styles[style.name] - - - -style_manager = StyleManager() diff --git a/mailman/testing/layers.py b/mailman/testing/layers.py index ef4535038..c9b99cab9 100644 --- a/mailman/testing/layers.py +++ b/mailman/testing/layers.py @@ -37,7 +37,6 @@ from textwrap import dedent from mailman.config import config from mailman.core import initialize from mailman.core.logging import get_handler -from mailman.core.styles import style_manager from mailman.i18n import _ from mailman.testing.helpers import SMTPServer @@ -139,7 +138,7 @@ class ConfigLayer: def testSetUp(cls): # Record the current (default) set of styles so that we can reset them # easily in the tear down. - cls.styles = set(style_manager.styles) + cls.styles = set(config.style_manager.styles) @classmethod def testTearDown(cls): @@ -154,9 +153,9 @@ class ConfigLayer: config.db.message_store.delete_message(message['message-id']) config.db.commit() # Reset the global style manager. - new_styles = set(style_manager.styles) - cls.styles + new_styles = set(config.style_manager.styles) - cls.styles for style in new_styles: - style_manager.unregister(style) + config.style_manager.unregister(style) cls.styles = None # Flag to indicate that loggers should propagate to the console. @@ -95,7 +95,6 @@ case second `m'. Any other spelling is incorrect.""", 'mailman.handlers' : 'default = mailman.pipeline:initialize', 'mailman.rules' : 'default = mailman.rules:initialize', 'mailman.scrubber' : 'stock = mailman.archiving.pipermail:Pipermail', - 'mailman.styles' : 'default = mailman.core.styles:DefaultStyle', }, install_requires = [ 'lazr.config', |
