------------------------------------------------------------
revno: 6665
committer: Barry Warsaw <[email protected]>
branch nick: configs
timestamp: Mon 2009-01-05 00:54:19 -0500
message:
Defaults module is mostly eradicated, converted to lazr.config. The test
suite does not yet work though.
added:
mailman/styles/
mailman/styles/__init__.py
renamed:
mailman/Defaults.py => mailman/attic/Defaults.py
mailman/core/styles.py => mailman/styles/default.py
modified:
buildout.cfg
mailman/Mailbox.py
mailman/Message.py
mailman/Utils.py
mailman/app/lifecycle.py
mailman/app/notifications.py
mailman/app/replybot.py
mailman/bin/master.py
mailman/config/__init__.py
mailman/config/config.py
mailman/config/mailman.cfg
mailman/config/schema.cfg
mailman/database/mailinglist.py
mailman/database/pending.py
mailman/pipeline/cleanse_dkim.py
mailman/pipeline/scrubber.py
mailman/pipeline/smtp_direct.py
mailman/pipeline/to_digest.py
mailman/pipeline/to_outgoing.py
mailman/queue/__init__.py
mailman/testing/layers.py
setup.py
mailman/styles/default.py
=== modified file 'buildout.cfg'
--- a/buildout.cfg 2009-01-05 00:41:05 +0000
+++ b/buildout.cfg 2009-01-05 05:54:19 +0000
@@ -5,7 +5,7 @@
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
=== modified file 'mailman/Mailbox.py'
--- a/mailman/Mailbox.py 2009-01-05 00:41:05 +0000
+++ b/mailman/Mailbox.py 2009-01-05 05:54:19 +0000
@@ -25,7 +25,6 @@
from email.Errors import MessageParseError
from email.Generator import Generator
-from mailman import Defaults
from mailman.Message import Message
@@ -90,7 +89,7 @@
# 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
=== modified file 'mailman/Message.py'
--- a/mailman/Message.py 2009-01-01 22:16:51 +0000
+++ b/mailman/Message.py 2009-01-05 05:54:19 +0000
@@ -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 @@
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 @@
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 @@
- 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 @@
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
=== modified file 'mailman/Utils.py'
--- a/mailman/Utils.py 2009-01-05 00:41:05 +0000
+++ b/mailman/Utils.py 2009-01-05 05:54:19 +0000
@@ -35,11 +35,11 @@
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 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)
=== modified file 'mailman/app/lifecycle.py'
--- a/mailman/app/lifecycle.py 2009-01-04 05:22:08 +0000
+++ b/mailman/app/lifecycle.py 2009-01-05 05:54:19 +0000
@@ -33,7 +33,6 @@
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 @@
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)
=== modified file 'mailman/app/notifications.py'
--- a/mailman/app/notifications.py 2009-01-01 22:16:51 +0000
+++ b/mailman/app/notifications.py 2009-01-05 05:54:19 +0000
@@ -26,8 +26,8 @@
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 @@
_('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 @@
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))
=== modified file 'mailman/app/replybot.py'
--- a/mailman/app/replybot.py 2009-01-01 22:16:51 +0000
+++ b/mailman/app/replybot.py 2009-01-05 05:54:19 +0000
@@ -29,7 +29,6 @@
import logging
import datetime
-from mailman import Defaults
from mailman import Utils
from mailman import i18n
@@ -48,7 +47,8 @@
"""
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 @@
# 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.
=== renamed file 'mailman/Defaults.py' => 'mailman/attic/Defaults.py'
=== modified file 'mailman/bin/master.py'
--- a/mailman/bin/master.py 2009-01-01 22:16:51 +0000
+++ b/mailman/bin/master.py 2009-01-05 05:54:19 +0000
@@ -36,7 +36,6 @@
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 @@
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 @@
# 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()
=== modified file 'mailman/config/__init__.py'
--- a/mailman/config/__init__.py 2009-01-01 22:16:51 +0000
+++ b/mailman/config/__init__.py 2009-01-05 05:54:19 +0000
@@ -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()
=== modified file 'mailman/config/config.py'
--- a/mailman/config/config.py 2009-01-03 10:13:41 +0000
+++ b/mailman/config/config.py 2009-01-05 05:54:19 +0000
@@ -32,11 +32,11 @@
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 @@
# 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 @@
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.
=== modified file 'mailman/config/mailman.cfg'
--- a/mailman/config/mailman.cfg 2009-01-01 22:16:51 +0000
+++ b/mailman/config/mailman.cfg 2009-01-05 05:54:19 +0000
@@ -65,3 +65,5 @@
[qrunner.virgin]
class: mailman.queue.virgin.VirginRunner
+
+[style.default]
=== modified file 'mailman/config/schema.cfg'
--- a/mailman/config/schema.cfg 2009-01-03 14:21:50 +0000
+++ b/mailman/config/schema.cfg 2009-01-05 05:54:19 +0000
@@ -54,9 +54,37 @@
# 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 @@
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: [email protected]
+# Recipient is: [email protected]
+#
+# The envelope sender will be [email protected]
+#
+# 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_p...@host
+#
+# into
+#
+# To: "local_part" <local_p...@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 @@
[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
=== modified file 'mailman/database/mailinglist.py'
--- a/mailman/database/mailinglist.py 2009-01-04 05:22:08 +0000
+++ b/mailman/database/mailinglist.py 2009-01-05 05:54:19 +0000
@@ -22,7 +22,6 @@
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 @@
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 @@
return '%s-unsubscr...@%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)
=== modified file 'mailman/database/pending.py'
--- a/mailman/database/pending.py 2009-01-05 00:41:05 +0000
+++ b/mailman/database/pending.py 2009-01-05 05:54:19 +0000
@@ -29,11 +29,11 @@
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 @@
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
=== modified file 'mailman/pipeline/cleanse_dkim.py'
--- a/mailman/pipeline/cleanse_dkim.py 2009-01-04 05:22:08 +0000
+++ b/mailman/pipeline/cleanse_dkim.py 2009-01-05 05:54:19 +0000
@@ -26,12 +26,14 @@
"""
__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 @@
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']
=== modified file 'mailman/pipeline/scrubber.py'
--- a/mailman/pipeline/scrubber.py 2009-01-04 05:22:08 +0000
+++ b/mailman/pipeline/scrubber.py 2009-01-05 05:54:19 +0000
@@ -34,11 +34,12 @@
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 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 @@
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 @@
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 @@
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()
=== modified file 'mailman/pipeline/smtp_direct.py'
--- a/mailman/pipeline/smtp_direct.py 2009-01-05 00:41:05 +0000
+++ b/mailman/pipeline/smtp_direct.py 2009-01-05 05:54:19 +0000
@@ -44,7 +44,6 @@
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 @@
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 @@
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 @@
# this recipient.
log.info('Skipping VERP delivery to unqual recip: %s', recip)
continue
- d = {'bounces': bmailbox,
- 'mailbox': rmailbox,
- 'host' : DOT.join(rdomain),
- }
- envsender = '%...@%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
=== modified file 'mailman/pipeline/to_digest.py'
--- a/mailman/pipeline/to_digest.py 2009-01-04 05:22:08 +0000
+++ b/mailman/pipeline/to_digest.py 2009-01-05 05:54:19 +0000
@@ -48,7 +48,6 @@
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 @@
# 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 @@
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 @@
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)))
=== modified file 'mailman/pipeline/to_outgoing.py'
--- a/mailman/pipeline/to_outgoing.py 2009-01-04 05:22:08 +0000
+++ b/mailman/pipeline/to_outgoing.py 2009-01-05 05:54:19 +0000
@@ -23,12 +23,14 @@
"""
__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 @@
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 @@
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
=== modified file 'mailman/queue/__init__.py'
--- a/mailman/queue/__init__.py 2009-01-04 21:55:59 +0000
+++ b/mailman/queue/__init__.py 2009-01-05 05:54:19 +0000
@@ -298,7 +298,7 @@
# 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
=== added directory 'mailman/styles'
=== added file 'mailman/styles/__init__.py'
=== renamed file 'mailman/core/styles.py' => 'mailman/styles/default.py'
--- a/mailman/core/styles.py 2009-01-05 00:41:05 +0000
+++ b/mailman/styles/default.py 2009-01-05 05:54:19 +0000
@@ -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 @@
# 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: [email protected]
+message-id: relay.comanche.denmark.eu
+from: [email protected]
+from: [email protected]
+"""
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 @@
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 @@
# 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 @@
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 @@
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 @@
# 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 @@
# 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()
=== modified file 'mailman/testing/layers.py'
--- a/mailman/testing/layers.py 2009-01-01 22:16:51 +0000
+++ b/mailman/testing/layers.py 2009-01-05 05:54:19 +0000
@@ -37,7 +37,6 @@
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 @@
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 @@
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.
=== modified file 'setup.py'
--- a/setup.py 2009-01-03 10:13:41 +0000
+++ b/setup.py 2009-01-05 05:54:19 +0000
@@ -95,7 +95,6 @@
'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',
--
Primary development focus
https://code.launchpad.net/~mailman-coders/mailman/3.0
You are receiving this branch notification because you are subscribed to it.
_______________________________________________
Mailman-checkins mailing list
[email protected]
Unsubscribe:
http://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org