diff options
| author | bwarsaw | 2007-05-30 16:54:17 +0000 |
|---|---|---|
| committer | bwarsaw | 2007-05-30 16:54:17 +0000 |
| commit | 1eba70e9d11c8bd6e6e6a6c3ab6ad489896950d5 (patch) | |
| tree | 7fe361b3c0d1e8b6075466aac579e0319dd1a2f0 | |
| parent | 6423a8c37b8f8b05012f85af972eeadbd5855903 (diff) | |
| download | mailman-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.in | 9 | ||||
| -rw-r--r-- | Mailman/Handlers/Decorate.py | 63 | ||||
| -rw-r--r-- | Mailman/database/Makefile.in | 2 | ||||
| -rw-r--r-- | Mailman/database/tables/Makefile.in | 71 | ||||
| -rw-r--r-- | Mailman/database/tables/__init__.py | 0 | ||||
| -rw-r--r-- | Mailman/database/tables/addresses.py | 45 | ||||
| -rw-r--r-- | Mailman/database/tables/languages.py | 53 | ||||
| -rw-r--r-- | Mailman/database/tables/listdata.py | 200 | ||||
| -rw-r--r-- | Mailman/database/tables/profiles.py | 77 | ||||
| -rw-r--r-- | Mailman/database/tables/rosters.py | 59 | ||||
| -rw-r--r-- | Mailman/docs/decorate.txt | 330 | ||||
| -rw-r--r-- | Mailman/testing/test_decorate.py (renamed from Mailman/database/tables/versions.py) | 25 | ||||
| -rw-r--r-- | Mailman/testing/test_handlers.py | 223 | ||||
| -rw-r--r-- | Makefile.in | 2 | ||||
| -rwxr-xr-x | configure | 5 | ||||
| -rw-r--r-- | configure.in | 4 | ||||
| -rw-r--r-- | docs/NEWS.txt | 6 |
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 \ @@ -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 |
