summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbwarsaw2007-05-30 16:54:17 +0000
committerbwarsaw2007-05-30 16:54:17 +0000
commit1eba70e9d11c8bd6e6e6a6c3ab6ad489896950d5 (patch)
tree7fe361b3c0d1e8b6075466aac579e0319dd1a2f0
parent6423a8c37b8f8b05012f85af972eeadbd5855903 (diff)
downloadmailman-1eba70e9d11c8bd6e6e6a6c3ab6ad489896950d5.tar.gz
mailman-1eba70e9d11c8bd6e6e6a6c3ab6ad489896950d5.tar.zst
mailman-1eba70e9d11c8bd6e6e6a6c3ab6ad489896950d5.zip
Go ahead and remove the Mailman/database/tables directory since all the Elixir
classes live in Mailman/databae/model now. Remove the TestDecorate test class from test_handlers.py and move them into a doctest called decorate.txt (with harness in test_decorate.py). Remove the dependence on SafeDict from the Decorate handler because I can now use string.Template object to safely fill in header and footer templates. Eventually I want to completely remove SafeDict from Mailman, but it's still used in a few other places. This also means that only $-strings will be supported in headers and footers, and the import script will have to convert %-strings to $-strings. Also, '_internal_name' is no longer a supported header/footer substitution variable. Use $real_name or $list_name now. Added $fqdn_listname as a substitution variable. Update the DEFAULT_MSG_FOOTER accordingly.
-rw-r--r--Mailman/Defaults.py.in9
-rw-r--r--Mailman/Handlers/Decorate.py63
-rw-r--r--Mailman/database/Makefile.in2
-rw-r--r--Mailman/database/tables/Makefile.in71
-rw-r--r--Mailman/database/tables/__init__.py0
-rw-r--r--Mailman/database/tables/addresses.py45
-rw-r--r--Mailman/database/tables/languages.py53
-rw-r--r--Mailman/database/tables/listdata.py200
-rw-r--r--Mailman/database/tables/profiles.py77
-rw-r--r--Mailman/database/tables/rosters.py59
-rw-r--r--Mailman/docs/decorate.txt330
-rw-r--r--Mailman/testing/test_decorate.py (renamed from Mailman/database/tables/versions.py)25
-rw-r--r--Mailman/testing/test_handlers.py223
-rw-r--r--Makefile.in2
-rwxr-xr-xconfigure5
-rw-r--r--configure.in4
-rw-r--r--docs/NEWS.txt6
17 files changed, 385 insertions, 789 deletions
diff --git a/Mailman/Defaults.py.in b/Mailman/Defaults.py.in
index 87d5db125..d9ded812c 100644
--- a/Mailman/Defaults.py.in
+++ b/Mailman/Defaults.py.in
@@ -909,10 +909,11 @@ DEFAULT_MAX_MESSAGE_SIZE = 40 # KB
DEFAULT_SUBJECT_PREFIX = "[%(real_name)s] "
# DEFAULT_SUBJECT_PREFIX = "[%(real_name)s %%d]" # for numbering
DEFAULT_MSG_HEADER = ""
-DEFAULT_MSG_FOOTER = """_______________________________________________
-%(real_name)s mailing list
-%(real_name)s@%(host_name)s
-%(web_page_url)slistinfo%(cgiext)s/%(_internal_name)s
+DEFAULT_MSG_FOOTER = """\
+_______________________________________________
+$real_name mailing list
+$fqdn_realname
+${web_page_url}listinfo${cgiext}/${list_name}
"""
# Where to put subject prefix for 'Re:' messages:
diff --git a/Mailman/Handlers/Decorate.py b/Mailman/Handlers/Decorate.py
index 2f4aceb51..984cd9670 100644
--- a/Mailman/Handlers/Decorate.py
+++ b/Mailman/Handlers/Decorate.py
@@ -21,13 +21,13 @@ import re
import logging
from email.MIMEText import MIMEText
+from string import Template
from Mailman import Errors
-from Mailman import mm_cfg
from Mailman import Utils
-from Mailman.i18n import _
from Mailman.Message import Message
-from Mailman.SafeDict import SafeDict
+from Mailman.configuration import config
+from Mailman.i18n import _
log = logging.getLogger('mailman.error')
@@ -42,7 +42,8 @@ def process(mlist, msg, msgdata):
# Calculate the extra personalization dictionary. Note that the
# length of the recips list better be exactly 1.
recips = msgdata.get('recips')
- assert isinstance(recips, list) and len(recips) == 1
+ assert isinstance(recips, list) and len(recips) == 1, (
+ 'The number of intended recipients must be exactly 1')
member = recips[0].lower()
d['user_address'] = member
try:
@@ -56,8 +57,8 @@ def process(mlist, msg, msgdata):
except Errors.NotAMemberError:
pass
# These strings are descriptive for the log file and shouldn't be i18n'd
- header = decorate(mlist, mlist.msg_header, 'non-digest header', d)
- footer = decorate(mlist, mlist.msg_footer, 'non-digest footer', d)
+ header = decorate(mlist, mlist.msg_header, d)
+ footer = decorate(mlist, mlist.msg_footer, d)
# Escape hatch if both the footer and header are empty
if not header and not footer:
return
@@ -185,35 +186,21 @@ def process(mlist, msg, msgdata):
-def decorate(mlist, template, what, extradict={}):
- # `what' is just a descriptive phrase used in the log message
- #
- # BAW: We've found too many situations where Python can be fooled into
- # interpolating too much revealing data into a format string. For
- # example, a footer of "% silly %(real_name)s" would give a header
- # containing all list attributes. While we've previously removed such
- # really bad ones like `password' and `passwords', it's much better to
- # provide a whitelist of known good attributes, then to try to remove a
- # blacklist of known bad ones.
- d = SafeDict({'real_name' : mlist.real_name,
- 'list_name' : mlist.internal_name(),
- # For backwards compatibility
- '_internal_name': mlist.internal_name(),
- 'host_name' : mlist.host_name,
- 'web_page_url' : mlist.web_page_url,
- 'description' : mlist.description,
- 'info' : mlist.info,
- 'cgiext' : mm_cfg.CGIEXT,
- })
- d.update(extradict)
- # Using $-strings?
- if getattr(mlist, 'use_dollar_strings', 0):
- template = Utils.to_percent(template)
- # Interpolate into the template
- try:
- text = re.sub(r' *\r?\n', r'\n', template % d)
- except (ValueError, TypeError), e:
- log.exception('Exception while calculating %s:\n%s', what, e)
- what = what.upper()
- text = template
- return text
+def decorate(mlist, template, extradict=None):
+ # Create a dictionary which includes the default set of interpolation
+ # variables allowed in headers and footers. These will be augmented by
+ # any key/value pairs in the extradict.
+ d = dict(real_name = mlist.real_name,
+ list_name = mlist.list_name,
+ fqdn_listname = mlist.fqdn_listname,
+ host_name = mlist.host_name,
+ web_page_url = mlist.web_page_url,
+ description = mlist.description,
+ info = mlist.info,
+ cgiext = config.CGIEXT,
+ )
+ if extradict is not None:
+ d.update(extradict)
+ text = Template(template).safe_substitute(d)
+ # Turn any \r\n line endings into just \n
+ return re.sub(r' *\r?\n', r'\n', text)
diff --git a/Mailman/database/Makefile.in b/Mailman/database/Makefile.in
index 56fcb3f29..10a689748 100644
--- a/Mailman/database/Makefile.in
+++ b/Mailman/database/Makefile.in
@@ -42,7 +42,7 @@ PACKAGEDIR= $(prefix)/Mailman/database
SHELL= /bin/sh
MODULES= *.py
-SUBDIRS= tables model
+SUBDIRS= model
# Modes for directories and executables created by the install
# process. Default to group-writable directories but
diff --git a/Mailman/database/tables/Makefile.in b/Mailman/database/tables/Makefile.in
deleted file mode 100644
index dd99e125f..000000000
--- a/Mailman/database/tables/Makefile.in
+++ /dev/null
@@ -1,71 +0,0 @@
-# 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.
-
-# NOTE: Makefile.in is converted into Makefile by the configure script
-# in the parent directory. Once configure has run, you can recreate
-# the Makefile by running just config.status.
-
-# Variables set by configure
-
-VPATH= @srcdir@
-srcdir= @srcdir@
-bindir= @bindir@
-prefix= @prefix@
-exec_prefix= @exec_prefix@
-DESTDIR=
-
-CC= @CC@
-CHMOD= @CHMOD@
-INSTALL= @INSTALL@
-
-DEFS= @DEFS@
-
-# Customizable but not set by configure
-
-OPT= @OPT@
-CFLAGS= $(OPT) $(DEFS)
-PACKAGEDIR= $(prefix)/Mailman/database/tables
-SHELL= /bin/sh
-
-MODULES= *.py
-
-# Modes for directories and executables created by the install
-# process. Default to group-writable directories but
-# user-only-writable for executables.
-DIRMODE= 775
-EXEMODE= 755
-FILEMODE= 644
-INSTALL_PROGRAM=$(INSTALL) -m $(EXEMODE)
-
-
-# Rules
-
-all:
-
-install:
- for f in $(MODULES); \
- do \
- $(INSTALL) -m $(FILEMODE) $(srcdir)/$$f $(DESTDIR)$(PACKAGEDIR); \
- done
-
-finish:
-
-clean:
-
-distclean:
- -rm *.pyc
- -rm Makefile
diff --git a/Mailman/database/tables/__init__.py b/Mailman/database/tables/__init__.py
deleted file mode 100644
index e69de29bb..000000000
--- a/Mailman/database/tables/__init__.py
+++ /dev/null
diff --git a/Mailman/database/tables/addresses.py b/Mailman/database/tables/addresses.py
deleted file mode 100644
index 922984646..000000000
--- a/Mailman/database/tables/addresses.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Copyright (C) 2006-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.
-
-"""Email addresses."""
-
-from sqlalchemy import *
-
-
-
-class Address(object):
- pass
-
-
-def make_table(metadata, tables):
- address_table = Table(
- 'Addresses', metadata,
- Column('address_id', Integer, primary_key=True),
- Column('profile_id', Integer, ForeignKey('Profiles.profile_id')),
- Column('address', Unicode),
- Column('verified', Boolean),
- Column('bounce_info', PickleType),
- )
- # Associate Rosters
- address_rosters_table = Table(
- 'AddressRoster', metadata,
- Column('roster_id', Integer, ForeignKey('Rosters.roster_id')),
- Column('address_id', Integer, ForeignKey('Addresses.address_id')),
- )
- mapper(Address, address_table)
- tables.bind(address_table)
- tables.bind(address_rosters_table, 'address_rosters')
diff --git a/Mailman/database/tables/languages.py b/Mailman/database/tables/languages.py
deleted file mode 100644
index fa10974b7..000000000
--- a/Mailman/database/tables/languages.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# 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.
-
-"""Available languages table."""
-
-from sqlalchemy import *
-
-
-
-class Language(object):
- def __init__(self, code):
- self.code = code
-
- def __repr__(self):
- return u'<Language "%s">' % self.code
-
- def __unicode__(self):
- return self.code
-
- __str__ = __unicode__
-
-
-
-def make_table(metadata, tables):
- language_table = Table(
- 'Languages', metadata,
- # Two letter language code
- Column('language_id', Integer, primary_key=True),
- Column('code', Unicode),
- )
- # Associate List
- available_languages_table = Table(
- 'AvailableLanguages', metadata,
- Column('list_id', Integer, ForeignKey('Listdata.list_id')),
- Column('language_id', Integer, ForeignKey('Languages.language_id')),
- )
- mapper(Language, language_table)
- tables.bind(language_table)
- tables.bind(available_languages_table, 'available_languages')
diff --git a/Mailman/database/tables/listdata.py b/Mailman/database/tables/listdata.py
deleted file mode 100644
index fff396980..000000000
--- a/Mailman/database/tables/listdata.py
+++ /dev/null
@@ -1,200 +0,0 @@
-# Copyright (C) 2006-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.
-
-"""SQLAlchemy based list data storage."""
-
-from sqlalchemy import *
-
-
-
-def make_table(metadata, tables):
- table = Table(
- 'Listdata', metadata,
- # Attributes not directly modifiable via the web u/i
- Column('list_id', Integer, primary_key=True),
- Column('list_name', Unicode),
- Column('web_page_url', Unicode),
- Column('admin_member_chunksize', Integer),
- # Foreign keys - XXX ondelete='all, delete=orphan' ??
- Column('owner', Integer, ForeignKey('RosterSets.rosterset_id')),
- Column('moderator', Integer, ForeignKey('RosterSets.rosterset_id')),
- # Attributes which are directly modifiable via the web u/i. The more
- # complicated attributes are currently stored as pickles, though that
- # will change as the schema and implementation is developed.
- Column('next_request_id', Integer),
- Column('next_digest_number', Integer),
- Column('admin_responses', PickleType),
- Column('postings_responses', PickleType),
- Column('request_responses', PickleType),
- Column('digest_last_sent_at', Float),
- Column('one_last_digest', PickleType),
- Column('volume', Integer),
- Column('last_post_time', Float),
- # OldStyleMemberships attributes, temporarily stored as pickles.
- Column('bounce_info', PickleType),
- Column('delivery_status', PickleType),
- Column('digest_members', PickleType),
- Column('language', PickleType),
- Column('members', PickleType),
- Column('passwords', PickleType),
- Column('topics_userinterest', PickleType),
- Column('user_options', PickleType),
- Column('usernames', PickleType),
- # Attributes which are directly modifiable via the web u/i. The more
- # complicated attributes are currently stored as pickles, though that
- # will change as the schema and implementation is developed.
- Column('accept_these_nonmembers', PickleType),
- Column('acceptable_aliases', PickleType),
- Column('admin_immed_notify', Boolean),
- Column('admin_notify_mchanges', Boolean),
- Column('administrivia', Boolean),
- Column('advertised', Boolean),
- Column('anonymous_list', Boolean),
- Column('archive', Boolean),
- Column('archive_private', Boolean),
- Column('archive_volume_frequency', Integer),
- Column('autorespond_admin', Boolean),
- Column('autorespond_postings', Boolean),
- Column('autorespond_requests', Integer),
- Column('autoresponse_admin_text', Unicode),
- Column('autoresponse_graceperiod', Integer),
- Column('autoresponse_postings_text', Unicode),
- Column('autoresponse_request_text', Unicode),
- Column('ban_list', PickleType),
- Column('bounce_info_stale_after', Integer),
- Column('bounce_matching_headers', Unicode),
- Column('bounce_notify_owner_on_disable', Boolean),
- Column('bounce_notify_owner_on_removal', Boolean),
- Column('bounce_processing', Boolean),
- Column('bounce_score_threshold', Integer),
- Column('bounce_unrecognized_goes_to_list_owner', Boolean),
- Column('bounce_you_are_disabled_warnings', Integer),
- Column('bounce_you_are_disabled_warnings_interval', Integer),
- Column('collapse_alternatives', Boolean),
- Column('convert_html_to_plaintext', Boolean),
- Column('default_member_moderation', Boolean),
- Column('description', Unicode),
- Column('digest_footer', Unicode),
- Column('digest_header', Unicode),
- Column('digest_is_default', Boolean),
- Column('digest_send_periodic', Boolean),
- Column('digest_size_threshhold', Integer),
- Column('digest_volume_frequency', Integer),
- Column('digestable', Boolean),
- Column('discard_these_nonmembers', PickleType),
- Column('emergency', Boolean),
- Column('encode_ascii_prefixes', Boolean),
- Column('filter_action', Integer),
- Column('filter_content', Boolean),
- Column('filter_filename_extensions', PickleType),
- Column('filter_mime_types', PickleType),
- Column('first_strip_reply_to', Boolean),
- Column('forward_auto_discards', Boolean),
- Column('gateway_to_mail', Boolean),
- Column('gateway_to_news', Boolean),
- Column('generic_nonmember_action', Integer),
- Column('goodbye_msg', Unicode),
- Column('header_filter_rules', PickleType),
- Column('hold_these_nonmembers', PickleType),
- Column('host_name', Unicode),
- Column('include_list_post_header', Boolean),
- Column('include_rfc2369_headers', Boolean),
- Column('info', Unicode),
- Column('linked_newsgroup', Unicode),
- Column('max_days_to_hold', Integer),
- Column('max_message_size', Integer),
- Column('max_num_recipients', Integer),
- Column('member_moderation_action', Boolean),
- Column('member_moderation_notice', Unicode),
- Column('mime_is_default_digest', Boolean),
- Column('mod_password', Unicode),
- Column('msg_footer', Unicode),
- Column('msg_header', Unicode),
- Column('new_member_options', Integer),
- Column('news_moderation', Boolean),
- Column('news_prefix_subject_too', Boolean),
- Column('nntp_host', Unicode),
- Column('nondigestable', Boolean),
- Column('nonmember_rejection_notice', Unicode),
- Column('obscure_addresses', Boolean),
- Column('pass_filename_extensions', PickleType),
- Column('pass_mime_types', PickleType),
- Column('password', Unicode),
- Column('personalize', Integer),
- Column('post_id', Integer),
- Column('preferred_language', Unicode),
- Column('private_roster', Boolean),
- Column('real_name', Unicode),
- Column('reject_these_nonmembers', PickleType),
- Column('reply_goes_to_list', Boolean),
- Column('reply_to_address', Unicode),
- Column('require_explicit_destination', Boolean),
- Column('respond_to_post_requests', Boolean),
- Column('scrub_nondigest', Boolean),
- Column('send_goodbye_msg', Boolean),
- Column('send_reminders', Boolean),
- Column('send_welcome_msg', Boolean),
- Column('subject_prefix', Unicode),
- Column('subscribe_auto_approval', PickleType),
- Column('subscribe_policy', Integer),
- Column('topics', PickleType),
- Column('topics_bodylines_limit', Integer),
- Column('topics_enabled', Boolean),
- Column('umbrella_list', Boolean),
- Column('umbrella_member_suffix', Unicode),
- Column('unsubscribe_policy', Integer),
- Column('welcome_msg', Unicode),
- )
- # Avoid circular imports
- from Mailman.MailList import MailList
- from Mailman.database.tables.languages import Language
- from Mailman.database.tables.rosters import RosterSet
- # We need to ensure MailList.InitTempVars() is called whenever a MailList
- # instance is created from a row. Use a mapper extension for this.
- props = dict(
- # listdata* <-> language*
- available_languages= relation(Language,
- secondary=tables.available_languages,
- lazy=False))
- mapper(MailList, table,
- # The mapper extension ensures MailList.InitTempVars() is called
- # whenever a MailList instance is created from a row.
- extension=MailListMapperExtension(),
- properties=props)
- tables.bind(table)
-
-
-
-class MailListMapperExtension(MapperExtension):
- def populate_instance(self, mapper, context, row, mlist, ikey, isnew):
- # Michael Bayer on the sqlalchemy mailing list:
- #
- # "isnew" is used to indicate that we are going to populate the
- # instance with data from the database, *and* that this particular row
- # is the first row in the result which has indicated the presence of
- # this entity (i.e. the primary key points to it). this implies that
- # populate_instance() can be called *multiple times* for the instance,
- # if multiple successive rows all contain its particular primary key.
- if isnew:
- # Get the list name and host name -- which are required by
- # InitTempVars() from the row data.
- list_name = row['listdata_list_name']
- host_name = row['listdata_host_name']
- fqdn_name = '%s@%s' % (list_name, host_name)
- mlist.InitTempVars(fqdn_name)
- # In all cases, let SA proceed as normal
- return EXT_PASS
diff --git a/Mailman/database/tables/profiles.py b/Mailman/database/tables/profiles.py
deleted file mode 100644
index 9f65bdf03..000000000
--- a/Mailman/database/tables/profiles.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# 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.
-
-"""Mailman user profile information."""
-
-from sqlalchemy import *
-
-from Mailman import Defaults
-
-
-
-class Profile(object):
- pass
-
-
-
-# Both of these Enum types are stored in the database as integers, and
-# converted back into their enums on retrieval.
-
-class DeliveryModeType(types.TypeDecorator):
- impl = types.Integer
-
- def convert_bind_param(self, value, engine):
- return int(value)
-
- def convert_result_value(self, value, engine):
- return Defaults.DeliveryMode(value)
-
-
-class DeliveryStatusType(types.TypeDecorator):
- impl = types.Integer
-
- def convert_bind_param(self, value, engine):
- return int(value)
-
- def convert_result_value(self, value, engine):
- return Defaults.DeliveryStatus(value)
-
-
-
-def make_table(metadata, tables):
- table = Table(
- 'Profiles', metadata,
- Column('profile_id', Integer, primary_key=True),
- # OldStyleMemberships attributes, temporarily stored as pickles.
- Column('ack', Boolean),
- Column('delivery_mode', DeliveryModeType),
- Column('delivery_status', DeliveryStatusType),
- Column('hide', Boolean),
- Column('language', Unicode),
- Column('nodupes', Boolean),
- Column('nomail', Boolean),
- Column('notmetoo', Boolean),
- Column('password', Unicode),
- Column('realname', Unicode),
- Column('topics', PickleType),
- )
- # Avoid circular references
- from Mailman.database.tables.addresses import Address
- # profile -> address*
- props = dict(addresses=relation(Address, cascade='all, delete-orphan'))
- mapper(Profile, table, properties=props)
- tables.bind(table)
diff --git a/Mailman/database/tables/rosters.py b/Mailman/database/tables/rosters.py
deleted file mode 100644
index eea0cbb39..000000000
--- a/Mailman/database/tables/rosters.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# 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.
-
-"""Collections of email addresses.
-
-Rosters contain email addresses. RosterSets contain Rosters. Most attributes
-on the listdata table take RosterSets so that it's easy to compose just about
-any combination of addresses.
-"""
-
-from sqlalchemy import *
-
-from Mailman.database.tables.addresses import Address
-
-
-
-class Roster(object):
- pass
-
-
-class RosterSet(object):
- pass
-
-
-
-def make_table(metadata, tables):
- table = Table(
- 'Rosters', metadata,
- Column('roster_id', Integer, primary_key=True),
- )
- # roster* <-> address*
- props = dict(addresses=
- relation(Address,
- secondary=tables.address_rosters,
- lazy=False))
- mapper(Roster, table, properties=props)
- tables.bind(table)
- table = Table(
- 'RosterSets', metadata,
- Column('rosterset_id', Integer, primary_key=True),
- )
- # rosterset -> roster*
- props = dict(rosters=relation(Roster, cascade='all, delete=orphan'))
- mapper(RosterSet, table, properties=props)
- tables.bind(table)
diff --git a/Mailman/docs/decorate.txt b/Mailman/docs/decorate.txt
new file mode 100644
index 000000000..1325eb676
--- /dev/null
+++ b/Mailman/docs/decorate.txt
@@ -0,0 +1,330 @@
+Message decoration
+==================
+
+Message decoration is the process of adding headers and footers to the
+original message. A handler module takes care of this based on the settings
+of the mailing list and the type of message being processed.
+
+ >>> from email import message_from_string
+ >>> from Mailman.Handlers.Decorate import process
+ >>> from Mailman.configuration import config
+ >>> from Mailman.database import flush
+ >>> mlist = config.list_manager.create('_xtest@example.com')
+ >>> msg_text = """\
+ ... From: aperson@example.org
+ ...
+ ... Here is a message.
+ ... """
+ >>> msg = message_from_string(msg_text)
+
+
+Short circuiting
+----------------
+
+Digest messages get decorated during the digest creation phase so no extra
+decorations are added for digest messages.
+
+ >>> process(mlist, msg, dict(isdigest=True))
+ >>> print msg.as_string()
+ From: aperson@example.org
+ <BLANKLINE>
+ Here is a message.
+
+ >>> process(mlist, msg, dict(nodecorate=True))
+ >>> print msg.as_string()
+ From: aperson@example.org
+ <BLANKLINE>
+ Here is a message.
+
+
+Decorating simple text messages
+-------------------------------
+
+Text messages that have no declared content type character set are by default,
+encoded in us-ascii. When the mailing list's preferred language is 'en'
+(i.e. English), the character set of the mailing list and of the message will
+match. In this case, and when the header and footer have no interpolation
+placeholder variables, the message's payload will be prepended by the verbatim
+header, and appended with the verbatim footer.
+
+ >>> msg = message_from_string(msg_text)
+ >>> mlist.msg_header = 'header\n'
+ >>> mlist.msg_footer = 'footer'
+ >>> mlist.preferred_language = 'en'
+ >>> flush()
+ >>> process(mlist, msg, {})
+ >>> print msg.as_string()
+ From: aperson@example.org
+ ...
+ <BLANKLINE>
+ header
+ Here is a message.
+ footer
+
+Mailman supports a number of interpolation variables, placeholders in the
+header and footer for information to be filled in with mailing list specific
+data. An example of such information is the mailing list's "real name" (a
+short descriptive name for the mailing list).
+
+ # XXX Remove this line after converting this test
+ >>> mlist.use_dollar_strings = True
+
+ >>> msg = message_from_string(msg_text)
+ >>> mlist.msg_header = '$real_name header\n'
+ >>> mlist.msg_footer = '$real_name footer'
+ >>> mlist.real_name = 'XTest'
+ >>> flush()
+ >>> process(mlist, msg, {})
+ >>> print msg.as_string()
+ From: aperson@example.org
+ ...
+ XTest header
+ Here is a message.
+ XTest footer
+
+You can't just pick any interpolation variable though; if you do, the variable
+will remain in the header or footer unchanged.
+
+ >>> msg = message_from_string(msg_text)
+ >>> mlist.msg_header = '$dummy header\n'
+ >>> mlist.msg_footer = '$dummy footer'
+ >>> flush()
+ >>> process(mlist, msg, {})
+ >>> print msg.as_string()
+ From: aperson@example.org
+ ...
+ $dummy header
+ Here is a message.
+ $dummy footer
+
+
+Handling RFC 3676 'format=flowed' parameters
+--------------------------------------------
+
+RFC 3676 describes a standard by which text/plain messages can marked by
+generating MUAs for better readability in compatible receiving MUAs. The
+'format' parameter on the text/plain Content-Type header gives hints as to how
+the receiving MUA may flow and delete trailing whitespace for better display
+in a proportional font.
+
+When Mailman sees text/plain messages with such RFC 3676 parameters, it
+preserves these parameters when it concatenates headers and footers to the
+message payload.
+
+ >>> mlist.msg_header = 'header'
+ >>> mlist.msg_footer = 'footer'
+ >>> mlist.preferred_language = 'en'
+ >>> mlist.flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.org
+ ... Content-Type: text/plain; format=flowed; delsp=no
+ ...
+ ... Here is a message\x20
+ ... with soft line breaks.
+ ... """)
+ >>> process(mlist, msg, {})
+ >>> # Don't use 'print' here as above because it won't be obvious from the
+ >>> # output that the soft-line break space at the end of the 'Here is a
+ >>> # message' line will be retained in the output.
+ >>> msg['content-type']
+ u'text/plain; format="flowed"; delsp="no"; charset="us-ascii"'
+ >>> [line for line in msg.get_payload().splitlines()]
+ ['header', 'Here is a message ', 'with soft line breaks.', 'footer']
+
+
+Decorating mixed-charset messages
+---------------------------------
+
+When a message has no explicit character set, it is assumed to be us-ascii.
+However, if the mailing list's preferred language has a different character
+set, Mailman will still try to concatenate the header and footer, but it will
+convert the text to utf-8 and base-64 encode the message payload.
+
+ # 'ja' = Japanese; charset = 'euc-jp'
+ >>> mlist.preferred_language = 'ja'
+ >>> mlist.msg_header = '$description header'
+ >>> mlist.msg_footer = '$description footer'
+ >>> mlist.description = u'\u65e5\u672c\u8a9e'
+ >>> flush()
+
+ >>> from email.message import Message
+ >>> msg = Message()
+ >>> msg.set_payload('Fran\xe7aise', 'iso-8859-1')
+ >>> print msg.as_string()
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="iso-8859-1"
+ Content-Transfer-Encoding: quoted-printable
+ <BLANKLINE>
+ Fran=E7aise
+ >>> process(mlist, msg, {})
+ >>> print msg.as_string()
+ MIME-Version: 1.0
+ Content-Type: text/plain; charset="utf-8"
+ Content-Transfer-Encoding: base64
+ <BLANKLINE>
+ 5pel5pys6KqeIGhlYWRlcgpGcmFuw6dhaXNlCuaXpeacrOiqniBmb290ZXI=
+
+
+Sometimes the message even has an unknown character set. In this case,
+Mailman has no choice but to decorate the original message with MIME
+attachments.
+
+ >>> mlist.preferred_language = 'en'
+ >>> mlist.msg_header = 'header'
+ >>> mlist.msg_footer = 'footer'
+ >>> flush()
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.org
+ ... Content-Type: text/plain; charset=unknown
+ ... Content-Transfer-Encoding: 7bit
+ ...
+ ... Here is a message.
+ ... """)
+ >>> process(mlist, msg, {})
+ >>> msg.set_boundary('BOUNDARY')
+ >>> print msg.as_string()
+ From: aperson@example.org
+ Content-Type: multipart/mixed; boundary="BOUNDARY"
+ <BLANKLINE>
+ --BOUNDARY
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ <BLANKLINE>
+ header
+ --BOUNDARY
+ Content-Type: text/plain; charset=unknown
+ Content-Transfer-Encoding: 7bit
+ <BLANKLINE>
+ Here is a message.
+ <BLANKLINE>
+ --BOUNDARY
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ <BLANKLINE>
+ footer
+ --BOUNDARY--
+
+
+Decorating multipart messages
+-----------------------------
+
+Multipart messages have to be decorated differently. The header and footer
+cannot be simply concatenated into the payload because that will break the
+MIME structure of the message. Instead, the header and footer are attached as
+separate MIME subparts.
+
+When the outerpart is multipart/mixed, the header and footer can have a
+Content-Disposition of 'inline' so that MUAs can display these headers as if
+they were simply concatenated.
+
+ >>> mlist.preferred_language = 'en'
+ >>> mlist.msg_header = 'header'
+ >>> mlist.msg_footer = 'footer'
+ >>> flush()
+ >>> part_1 = message_from_string("""\
+ ... From: aperson@example.org
+ ...
+ ... Here is the first message.
+ ... """)
+ >>> part_2 = message_from_string("""\
+ ... From: bperson@example.com
+ ...
+ ... Here is the second message.
+ ... """)
+ >>> from email.mime.multipart import MIMEMultipart
+ >>> msg = MIMEMultipart('mixed', boundary='BOUNDARY',
+ ... _subparts=(part_1, part_2))
+ >>> process(mlist, msg, {})
+ >>> print msg.as_string()
+ Content-Type: multipart/mixed; boundary="BOUNDARY"
+ MIME-Version: 1.0
+ <BLANKLINE>
+ --BOUNDARY
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ <BLANKLINE>
+ header
+ --BOUNDARY
+ From: aperson@example.org
+ <BLANKLINE>
+ Here is the first message.
+ <BLANKLINE>
+ --BOUNDARY
+ From: bperson@example.com
+ <BLANKLINE>
+ Here is the second message.
+ <BLANKLINE>
+ --BOUNDARY
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ <BLANKLINE>
+ footer
+ --BOUNDARY--
+
+
+Decorating other content types
+------------------------------
+
+Non-multipart non-text content types will get wrapped in a multipart/mixed so
+that the header and footer can be added as attachments.
+
+ >>> msg = message_from_string("""\
+ ... From: aperson@example.org
+ ... Content-Type: image/x-beautiful
+ ...
+ ... IMAGEDATAIMAGEDATAIMAGEDATA
+ ... """)
+ >>> process(mlist, msg, {})
+ >>> msg.set_boundary('BOUNDARY')
+ >>> print msg.as_string()
+ From: aperson@example.org
+ ...
+ --BOUNDARY
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ <BLANKLINE>
+ header
+ --BOUNDARY
+ Content-Type: image/x-beautiful
+ <BLANKLINE>
+ IMAGEDATAIMAGEDATAIMAGEDATA
+ <BLANKLINE>
+ --BOUNDARY
+ Content-Type: text/plain; charset="us-ascii"
+ MIME-Version: 1.0
+ Content-Transfer-Encoding: 7bit
+ Content-Disposition: inline
+ <BLANKLINE>
+ footer
+ --BOUNDARY--
+
+
+Personalization
+---------------
+
+A mailing list can be 'personalized', meaning that each message is unique for
+each recipient. When the list is personalized, additional interpolation
+variables are available, however the list of intended recipients must be
+provided in the message data, otherwise an exception occurs.
+
+ >>> process(mlist, None, dict(personalize=True))
+ Traceback (most recent call last):
+ ...
+ AssertionError: The number of intended recipients must be exactly 1
+
+And the number of intended recipients must be exactly 1.
+
+ >>> process(mlist, None, dict(personalize=True, recips=[1, 2, 3]))
+ Traceback (most recent call last):
+ ...
+ AssertionError: The number of intended recipients must be exactly 1
diff --git a/Mailman/database/tables/versions.py b/Mailman/testing/test_decorate.py
index 09fd21bf7..23af0598c 100644
--- a/Mailman/database/tables/versions.py
+++ b/Mailman/testing/test_decorate.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2006-2007 by the Free Software Foundation, Inc.
+# 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
@@ -15,17 +15,18 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
# USA.
-"""Schema versions."""
+"""Doctest harness for testing message decoration."""
-from sqlalchemy import *
+import doctest
+import unittest
+options = (doctest.ELLIPSIS
+ | doctest.NORMALIZE_WHITESPACE
+ | doctest.REPORT_NDIFF)
-
-def make_table(metadata, tables):
- table = Table(
- 'Versions', metadata,
- Column('version_id', Integer, primary_key=True),
- Column('component', String),
- Column('version', Integer),
- )
- tables.bind(table)
+
+def test_suite():
+ suite = unittest.TestSuite()
+ suite.addTest(doctest.DocFileSuite('../docs/decorate.txt',
+ optionflags=options))
+ return suite
diff --git a/Mailman/testing/test_handlers.py b/Mailman/testing/test_handlers.py
index 0e0a1bc54..985002189 100644
--- a/Mailman/testing/test_handlers.py
+++ b/Mailman/testing/test_handlers.py
@@ -42,7 +42,6 @@ from Mailman.Handlers import Approve
from Mailman.Handlers import CalcRecips
from Mailman.Handlers import Cleanse
from Mailman.Handlers import CookHeaders
-from Mailman.Handlers import Decorate
from Mailman.Handlers import FileRecips
from Mailman.Handlers import Hold
from Mailman.Handlers import MimeDel
@@ -786,227 +785,6 @@ From: aperson@example.org
-class TestDecorate(TestBase):
- def test_short_circuit(self):
- msgdata = {'isdigest': 1}
- rtn = Decorate.process(self._mlist, None, msgdata)
- # Not really a great test, but there's little else to assert
- self.assertEqual(rtn, None)
-
- def test_no_multipart(self):
- mlist = self._mlist
- mlist.msg_header = 'header\n'
- mlist.msg_footer = 'footer'
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-Here is a message.
-""")
- Decorate.process(self._mlist, msg, {})
- self.assertEqual(msg.get_payload(), """\
-header
-Here is a message.
-footer""")
-
- def test_no_multipart_template(self):
- mlist = self._mlist
- mlist.msg_header = '%(real_name)s header\n'
- mlist.msg_footer = '%(real_name)s footer'
- mlist.real_name = 'XTest'
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-Here is a message.
-""")
- Decorate.process(self._mlist, msg, {})
- self.assertEqual(msg.get_payload(), """\
-XTest header
-Here is a message.
-XTest footer""")
-
- def test_no_multipart_type_error(self):
- mlist = self._mlist
- mlist.msg_header = '%(real_name) header\n'
- mlist.msg_footer = '%(real_name) footer'
- mlist.real_name = 'XTest'
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-Here is a message.
-""")
- Decorate.process(self._mlist, msg, {})
- self.assertEqual(msg.get_payload(), """\
-%(real_name) header
-Here is a message.
-%(real_name) footer""")
-
- def test_no_multipart_value_error(self):
- mlist = self._mlist
- # These will generate warnings in logs/error
- mlist.msg_header = '%(real_name)p header\n'
- mlist.msg_footer = '%(real_name)p footer'
- mlist.real_name = 'XTest'
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-Here is a message.
-""")
- Decorate.process(self._mlist, msg, {})
- self.assertEqual(msg.get_payload(), """\
-%(real_name)p header
-Here is a message.
-%(real_name)p footer""")
-
- def test_no_multipart_missing_key(self):
- mlist = self._mlist
- mlist.msg_header = '%(spooge)s header\n'
- mlist.msg_footer = '%(spooge)s footer'
- msg = email.message_from_string("""\
-From: aperson@example.org
-
-Here is a message.
-""")
- Decorate.process(self._mlist, msg, {})
- self.assertEqual(msg.get_payload(), """\
-%(spooge)s header
-Here is a message.
-%(spooge)s footer""")
-
- def test_multipart(self):
- eq = self.ndiffAssertEqual
- mlist = self._mlist
- mlist.msg_header = 'header'
- mlist.msg_footer = 'footer'
- msg1 = email.message_from_string("""\
-From: aperson@example.org
-
-Here is the first message.
-""")
- msg2 = email.message_from_string("""\
-From: bperson@example.com
-
-Here is the second message.
-""")
- msg = Message.Message()
- msg.set_type('multipart/mixed')
- msg.set_boundary('BOUNDARY')
- msg.attach(msg1)
- msg.attach(msg2)
- Decorate.process(self._mlist, msg, {})
- eq(msg.as_string(unixfrom=0), """\
-MIME-Version: 1.0
-Content-Type: multipart/mixed; boundary="BOUNDARY"
-
---BOUNDARY
-Content-Type: text/plain; charset="us-ascii"
-MIME-Version: 1.0
-Content-Transfer-Encoding: 7bit
-Content-Disposition: inline
-
-header
---BOUNDARY
-From: aperson@example.org
-
-Here is the first message.
-
---BOUNDARY
-From: bperson@example.com
-
-Here is the second message.
-
---BOUNDARY
-Content-Type: text/plain; charset="us-ascii"
-MIME-Version: 1.0
-Content-Transfer-Encoding: 7bit
-Content-Disposition: inline
-
-footer
---BOUNDARY--""")
-
- def test_image(self):
- eq = self.assertEqual
- mlist = self._mlist
- mlist.msg_header = 'header\n'
- mlist.msg_footer = 'footer'
- msg = email.message_from_string("""\
-From: aperson@example.org
-Content-type: image/x-spooge
-
-IMAGEDATAIMAGEDATAIMAGEDATA
-""")
- Decorate.process(self._mlist, msg, {})
- eq(len(msg.get_payload()), 3)
- self.assertEqual(msg.get_payload(1).get_payload(), """\
-IMAGEDATAIMAGEDATAIMAGEDATA
-""")
-
- def test_personalize_assert(self):
- raises = self.assertRaises
- raises(AssertionError, Decorate.process,
- self._mlist, None, {'personalize': 1})
- raises(AssertionError, Decorate.process,
- self._mlist, None, {'personalize': 1,
- 'recips': [1, 2, 3]})
-
- def test_no_multipart_mixed_charset(self):
- mlist = self._mlist
- mlist.preferred_language = 'ja'
- mlist.msg_header = '%(description)s header'
- mlist.msg_footer = '%(description)s footer'
- mlist.description = u'\u65e5\u672c\u8a9e'
- msg = Message.Message()
- msg.set_payload('Fran\xe7aise', 'iso-8859-1')
- Decorate.process(mlist, msg, {})
- self.assertEqual(msg.as_string(unixfrom=0), """\
-MIME-Version: 1.0
-Content-Type: text/plain; charset="utf-8"
-Content-Transfer-Encoding: base64
-
-5pel5pys6KqeIGhlYWRlcgpGcmFuw6dhaXNlCuaXpeacrOiqniBmb290ZXI=
-""")
-
- def test_no_multipart_unknown_charset(self):
- mlist = self._mlist
- mlist.msg_header = 'header'
- mlist.msg_footer = 'footer'
- msg = email.message_from_string("""\
-From: aperson@example.org
-Content-Type: text/plain; charset=unknown
-Content-Transfer-Encoding: 7bit
-
-Here is a message.
-""")
- Decorate.process(mlist, msg, {})
- self.assertEqual(len(msg.get_payload()), 3)
- self.assertEqual(msg.get_payload()[1].as_string(unixfrom=0),"""\
-Content-Type: text/plain; charset=unknown
-Content-Transfer-Encoding: 7bit
-
-Here is a message.
-""")
-
- def test_no_multipart_flowed(self):
- mlist = self._mlist
- mlist.msg_header = 'header'
- mlist.msg_footer = 'footer'
- msg = email.message_from_string("""\
-From: aperson@example.org
-Content-Type: text/plain; format=flowed; delsp=no
-
-Here is a message
-with soft line break.
-""")
- Decorate.process(mlist, msg, {})
- self.assertEqual(msg.get_param('format'), 'flowed')
- self.assertEqual(msg.get_param('delsp'), 'no')
- self.assertEqual(msg.get_payload(), """\
-header
-Here is a message
-with soft line break.
-footer""")
-
-
-
class TestFileRecips(TestBase):
def test_short_circuit(self):
msgdata = {'recips': 1}
@@ -1934,7 +1712,6 @@ def test_suite():
suite.addTest(unittest.makeSuite(TestCalcRecips))
suite.addTest(unittest.makeSuite(TestCleanse))
suite.addTest(unittest.makeSuite(TestCookHeaders))
- suite.addTest(unittest.makeSuite(TestDecorate))
suite.addTest(unittest.makeSuite(TestFileRecips))
suite.addTest(unittest.makeSuite(TestHold))
suite.addTest(unittest.makeSuite(TestMimeDel))
diff --git a/Makefile.in b/Makefile.in
index 43c44326d..e71349e7b 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -51,7 +51,7 @@ VAR_DIRS= \
ARCH_INDEP_DIRS= \
bin templates scripts cron pythonlib \
Mailman Mailman/bin Mailman/interfaces \
- Mailman/database Mailman/database/tables Mailman/database/model \
+ Mailman/database Mailman/database/model \
Mailman/docs Mailman/ext Mailman/Cgi Mailman/Archiver \
Mailman/Handlers Mailman/Queue Mailman/Queue/tests \
Mailman/Bouncers \
diff --git a/configure b/configure
index 162a34343..1e4c412c7 100755
--- a/configure
+++ b/configure
@@ -1,5 +1,5 @@
#! /bin/sh
-# From configure.in Revision: 8196 .
+# From configure.in Revision: 8224 .
# Guess values for system-dependent variables and create Makefiles.
# Generated by GNU Autoconf 2.61 for GNU Mailman 2.2.0a0.
#
@@ -4841,7 +4841,7 @@ build/contrib/rotatelogs.py:contrib/rotatelogs.py \
# scripts. They're removed on a make distclean, so we make them here.
mkdir -p build/bin build/contrib build/cron
-ac_config_files="$ac_config_files misc/paths.py Mailman/Defaults.py Mailman/mm_cfg.py.dist src/Makefile misc/Makefile bin/Makefile Mailman/bin/Makefile Mailman/Makefile Mailman/Cgi/Makefile Mailman/database/Makefile Mailman/database/tables/Makefile Mailman/database/model/Makefile Mailman/docs/Makefile Mailman/ext/Makefile Mailman/interfaces/Makefile Mailman/Archiver/Makefile Mailman/Commands/Makefile Mailman/Handlers/Makefile Mailman/Bouncers/Makefile Mailman/Queue/Makefile Mailman/Queue/tests/Makefile Mailman/MTA/Makefile Mailman/Gui/Makefile templates/Makefile cron/Makefile scripts/Makefile messages/Makefile cron/crontab.in misc/mailman Makefile Mailman/testing/Makefile Mailman/testing/bounces/Makefile tests/Makefile tests/msgs/Makefile $SCRIPTS"
+ac_config_files="$ac_config_files misc/paths.py Mailman/Defaults.py Mailman/mm_cfg.py.dist src/Makefile misc/Makefile bin/Makefile Mailman/bin/Makefile Mailman/Makefile Mailman/Cgi/Makefile Mailman/database/Makefile Mailman/database/model/Makefile Mailman/docs/Makefile Mailman/ext/Makefile Mailman/interfaces/Makefile Mailman/Archiver/Makefile Mailman/Commands/Makefile Mailman/Handlers/Makefile Mailman/Bouncers/Makefile Mailman/Queue/Makefile Mailman/Queue/tests/Makefile Mailman/MTA/Makefile Mailman/Gui/Makefile templates/Makefile cron/Makefile scripts/Makefile messages/Makefile cron/crontab.in misc/mailman Makefile Mailman/testing/Makefile Mailman/testing/bounces/Makefile tests/Makefile tests/msgs/Makefile $SCRIPTS"
ac_config_commands="$ac_config_commands default"
@@ -5427,7 +5427,6 @@ do
"Mailman/Makefile") CONFIG_FILES="$CONFIG_FILES Mailman/Makefile" ;;
"Mailman/Cgi/Makefile") CONFIG_FILES="$CONFIG_FILES Mailman/Cgi/Makefile" ;;
"Mailman/database/Makefile") CONFIG_FILES="$CONFIG_FILES Mailman/database/Makefile" ;;
- "Mailman/database/tables/Makefile") CONFIG_FILES="$CONFIG_FILES Mailman/database/tables/Makefile" ;;
"Mailman/database/model/Makefile") CONFIG_FILES="$CONFIG_FILES Mailman/database/model/Makefile" ;;
"Mailman/docs/Makefile") CONFIG_FILES="$CONFIG_FILES Mailman/docs/Makefile" ;;
"Mailman/ext/Makefile") CONFIG_FILES="$CONFIG_FILES Mailman/ext/Makefile" ;;
diff --git a/configure.in b/configure.in
index 555cf96c1..dd637d7e6 100644
--- a/configure.in
+++ b/configure.in
@@ -16,7 +16,7 @@
# USA.
dnl Process this file with autoconf to produce a configure script.
-AC_REVISION($Revision: 8224 $)
+AC_REVISION($Revision: 8228 $)
AC_PREREQ(2.0)
AC_INIT([GNU Mailman], [2.2.0a0])
@@ -635,7 +635,7 @@ dnl Output everything
AC_OUTPUT([misc/paths.py Mailman/Defaults.py Mailman/mm_cfg.py.dist
src/Makefile misc/Makefile bin/Makefile Mailman/bin/Makefile
Mailman/Makefile Mailman/Cgi/Makefile
- Mailman/database/Makefile Mailman/database/tables/Makefile
+ Mailman/database/Makefile
Mailman/database/model/Makefile Mailman/docs/Makefile
Mailman/ext/Makefile Mailman/interfaces/Makefile
Mailman/Archiver/Makefile Mailman/Commands/Makefile
diff --git a/docs/NEWS.txt b/docs/NEWS.txt
index 15aab78c4..af31a1142 100644
--- a/docs/NEWS.txt
+++ b/docs/NEWS.txt
@@ -24,6 +24,12 @@ Here is a history of user visible changes to Mailman.
- PUBLIC_ARCHIVE_URL now takes $-string substitutions instead of %-string
substitutions. See documentation in Defaults.py.in for details.
+ - Message headers and footers now only accept $-string substitutions;
+ %-strings are no longer supported. The substitution variable
+ '_internal_name' has been removed; use $list_name or $real_name
+ instead. The substitution variable $fqdn_listname has been added.
+ DEFAULT_MSG_FOOTER in Defaults.py.in hsa been updated accordingly.
+
Architecture
- SQLAlchemy/Elixir based storage for all list and user data, with default