Commit 211a82dd authored by Barry Warsaw's avatar Barry Warsaw

Support for proper domains by interface. Add a test and update modules as

necessary.  Finally delete DEFAULT_EMAIL_HOST and DEFAULT_URL_HOST.  Now
add_domain() must be called in the confirmation file for every domain.

add_domain() itself is now pretty much just a proxy for the Domain

Add stop() as an alias for pdb.set_trace() to the documentation doctest globs.
parent b42f3204
......@@ -151,8 +151,12 @@ def process_request(doc, cgidata):
# Make sure the url host name matches one of our virtual domains. Then
# calculate the list's posting address.
url_host = Utils.get_request_domain()
email_host = config.get_email_host(url_host)
if not email_host:
# Find the IDomain matching this url_host if there is one.
for email_host, domain in
if domain.url_host == url_host:
email_host = domain.email_host
safehostname = Utils.websafe(url_host)
request_creation(doc, cgidata,
_('Unknown virtual host: $safehostname'))
......@@ -122,40 +122,6 @@ DEFAULT_VAR_DIRECTORY = '/var/mailman'
DEFAULT_DATABASE_URL = 'sqlite:///$DATA_DIR/mailman.db'
# Virtual domains
# Mailman needs to know about at least one domain, called the 'site default
# domain'. If you run only one domain with Mailman, this will generally be
# calculated automatically when you configured Mailman. You can always change
# this in your mailman.cfg file. You can also add additional virtual domains
# in your mailman.cfg file to enable multiple virtual domains. Every mailing
# list will be situated in exactly one virtual domain.
# For Mailman's purposes, a virtual domain associates an email host name with
# a web host name. These may be the same, but often they are different, and
# the list is always referred to by its fully-qualified posting address. For
# example, if you created 'mylist' in the domain, people can post
# to your list via ''. They may refer to the web pages via
# ''. So your email host name is '' and your web
# host name is ''.
# To add a virtual domain, put a call to add_domain(email_host, url_host) in
# your mailman.cfg file. If no add_domain() calls are found, Mailman will
# automatically add a virtual domain for the following defaults. However if
# you explicit add domains, you will need to add these defaults as well.
# These defaults will be filled in by configure.
# Note that you will want to run bin/ to change the domain of an
# existing list. bin/ must be run within the bin/withlist script,
# like so: bin/withlist -l -r bin/ <listname>
# Spam avoidance defaults
......@@ -52,7 +52,7 @@ class MHonArc:
def list_url(mlist):
"""See `IArchiver`."""
# XXX What about private MHonArc archives?
web_host =, mlist.host_name)
web_host =[mlist.host_name].url_host
return Template(config.PUBLIC_ARCHIVE_URL).safe_substitute(
......@@ -59,7 +59,15 @@ class PipermailMailingListAdapter:
def adapt_mailing_list_for_pipermail(iface, obj):
"""Adapt IMailingLists to IPipermailMailingList."""
"""Adapt `IMailingLists` to `IPipermailMailingList`.
:param iface: The interface to adapt to.
:type iface: `zope.interface.Interface`
:param obj: The object being adapted.
:type obj: any object
:return: An `IPipermailMailingList` instance if adaptation succeeded or
None if it didn't.
if IMailingList.providedBy(obj) and iface is IPipermailMailingList:
return PipermailMailingListAdapter(obj)
return None
......@@ -82,7 +90,7 @@ class Pipermail:
if mlist.archive_private:
url = mlist.script_url('private') + '/index.html'
web_host =, mlist.host_name)
web_host =[mlist.host_name].url_host
url = Template(config.PUBLIC_ARCHIVE_URL).safe_substitute(
......@@ -49,8 +49,7 @@ class Prototype:
def list_url(mlist):
"""See `IArchiver`."""
web_host =, mlist.host_name)
return 'http://' + web_host
def permalink(mlist, msg):
......@@ -24,6 +24,7 @@ import errno
from mailman import Defaults
from mailman import Errors
from mailman import version
from mailman.domain import Domain
from mailman.languages import LanguageManager
SPACE = ' '
......@@ -45,8 +46,7 @@ DEFAULT_QRUNNERS = (
class Configuration(object):
def __init__(self): = {} # email host -> web host
self._reverse = None = {} # email host -> IDomain
self.qrunners = {}
self.qrunner_shortcuts = {}
......@@ -150,10 +150,6 @@ class Configuration(object):
del ns['add_qrunner']
del ns['del_qrunner']
# Add the default domain if there are no virtual domains currently
# defined.
if not
self.add_domain(self.DEFAULT_EMAIL_HOST, self.DEFAULT_URL_HOST)
# Enable all specified languages, and promote the language manager to
# a public attribute.
self.languages = self._languages
......@@ -181,36 +177,25 @@ class Configuration(object):
self.pipelines = {}
self.commands = {}
def add_domain(self, email_host, url_host=None):
def add_domain(self, *args, **kws):
"""Add a virtual domain.
:param email_host: The host name for the email interface.
:param url_host: Optional host name for the web interface. If not
given, the email host will be used.
See `Domain`.
if url_host is None:
url_host = email_host
if email_host in
domain = Domain(*args, **kws)
if domain.email_host in
raise Errors.BadDomainSpecificationError(
'Duplicate email host: %s' % email_host)
'Duplicate email host: %s' % domain.email_host)
# Make sure there's only one mapping for the url_host
if url_host in
if domain.url_host in
raise Errors.BadDomainSpecificationError(
'Duplicate url host: %s' % url_host)
'Duplicate url host: %s' % domain.url_host)
# We'll do the reverse mappings on-demand. There shouldn't be too
# many virtual hosts that it will really matter that much.[email_host] = url_host
# Invalidate the reverse mapping cache
self._reverse = None[domain.email_host] = domain
# Given an email host name, the url host name can be looked up directly.
# This does the reverse mapping.
def get_email_host(self, url_host, default=None):
if self._reverse is None:
# XXX Can't use a generator comprehension until Python 2.4 is
# minimum requirement.
self._reverse = dict([(v, k) for k, v in])
return self._reverse.get(url_host, default)
# Proxy the docstring for the above method.
add_domain.__doc__ = Domain.__init__.__doc__
def add_qrunner(self, name, count=1):
"""Convenient interface for adding additional qrunners.
Domains are how Mailman interacts with email host names and web host names.
Generally, new domains are registered in the mailman.cfg configuration file by
calling the add_domain() method.
At a minimum, the email host name must be specified.
>>> from mailman.configuration import config
>>> config.add_domain('')
The domain object can be looked up by email host name.
>>> domain =['']
>>> print domain.email_host
The base url is calculated by default if not given.
>>> print domain.base_url
There is no description by default.
>>> print domain.description
By default, the contact address is the domain's postmaster.
>>> print domain.contact_address
And the url_host is by default the same as the email host.
>>> print domain.url_host
Full specification
The domain can also be much more fully defined.
>>> config.add_domain(
... '', '',
... 'The example domain',
... '')
>>> domain =['']
>>> print domain.email_host
>>> print domain.base_url
>>> print domain.description
The example domain
>>> print domain.contact_address
>>> print domain.url_host
Confirmation tokens
Confirmation tokens can be added to either the email confirmation address...
>>> print domain.confirm_address('xyz')
...or the confirmation url.
>>> print domain.confirm_url('abc')
# Copyright (C) 2008 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
# 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.
__metaclass__ = type
__all__ = [
from urlparse import urljoin, urlparse
from zope.interface import implements
from mailman.interfaces.domain import IDomain
class Domain:
def __init__(self, email_host, base_url=None, description=None,
"""Create and register a domain.
:param email_host: The host name for the email interface.
:type email_host: string
:param base_url: The optional base url for the domain, including
scheme. If not given, it will be constructed from the
`email_host` using the http protocol.
:type base_url: string
:param description: An optional description of the domain.
:type description: string
:type contact_address: The email address to contact a human for this
domain. If not given, postmaster@`email_host` will be used.
self.email_host = email_host
self.base_url = (base_url if base_url is not None else
'http://' + email_host)
self.description = description
self.contact_address = (contact_address
if contact_address is not None
else 'postmaster@' + email_host)
self.url_host = urlparse(self.base_url).netloc
def confirm_address(self, token=''):
"""See `IDomain`."""
return 'confirm-%s@%s' % (token, self.email_host)
def confirm_url(self, token=''):
"""See `IDomain`."""
return urljoin(self.base_url, 'confirm/' + token)
......@@ -24,30 +24,54 @@ from zope.interface import Interface, Attribute
class IDomain(Interface):
"""Interface representing domains."""
domain_name = Attribute(
"""The domain's name, e.g.""")
email_host = Attribute(
"""The host name for email for this domain.
:type: string
url_host = Attribute(
"""The host name for the web interface for this domain.
:type: string
base_url = Attribute(
"""The base url for the Mailman server at this domain.
The base url includes the scheme and host name.
:type: string
description = Attribute(
"""The human readable description of the domain name.
E.g. Python Dot Org or
:type: string
contact_address = Attribute(
"""The contact address for the human at this domain.
base_url = Attribute(
"""The base url for the Mailman server at this domain.
:type: string
def confirm_address(token=''):
"""The address used for various forms of email confirmation."""
"""The address used for various forms of email confirmation.
:param token: The confirmation token to use in the email address.
:type token: string
:return: The email confirmation address.
:rtype: string
def confirm_url(token=''):
"""The url used for various forms of confirmation."""
"""The url used for various forms of confirmation.
:param token: The confirmation token to use in the url.
:type token: string
:return: The confirmation url.
:rtype: string
......@@ -12,6 +12,6 @@ USE_LMTP = Yes
add_domain('', '')
add_domain('', '')
# bin/testall will add additional runtime configuration variables here.
......@@ -53,6 +53,14 @@ def specialized_message_from_string(text):
return message
def stop():
"""Call into pdb.set_trace()"""
# Do the import here so that you get the wacky special hacked pdb instead
# of Python's normal pdb.
import pdb
def setup(testobj):
"""Test setup."""
smtpd = SMTPServer()
......@@ -64,6 +72,10 @@ def setup(testobj):
testobj.globs['message_from_string'] = specialized_message_from_string
testobj.globs['commit'] = config.db.commit
testobj.globs['smtpd'] = smtpd
testobj.globs['stop'] = stop
# Stash the current state of the global domains away for restoration in
# the teardown.
testobj._domains =
......@@ -71,6 +83,8 @@ def cleaning_teardown(testobj):
"""Clear all persistent data at the end of a doctest."""
# Clear the database of all rows.
# Reset the global domains. = testobj._domains
# Remove all but the default style.
for style in style_manager.styles:
if <> 'default':
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment