summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/Archiver/Archiver.py4
-rw-r--r--src/mailman/app/lifecycle.py3
-rw-r--r--src/mailman/app/moderator.py5
-rw-r--r--src/mailman/archiving/mhonarc.py3
-rw-r--r--src/mailman/archiving/pipermail.py3
-rw-r--r--src/mailman/archiving/prototype.py3
-rw-r--r--src/mailman/commands/docs/join.txt3
-rw-r--r--src/mailman/commands/join.py3
-rw-r--r--src/mailman/config/config.py18
-rw-r--r--src/mailman/config/configure.zcml6
-rw-r--r--src/mailman/config/schema.cfg16
-rw-r--r--src/mailman/constants.py12
-rw-r--r--src/mailman/database/domain.py163
-rw-r--r--src/mailman/database/mailinglist.py5
-rw-r--r--src/mailman/database/mailman.sql20
-rw-r--r--src/mailman/database/member.py4
-rw-r--r--src/mailman/docs/addresses.txt11
-rw-r--r--src/mailman/docs/domains.txt138
-rw-r--r--src/mailman/docs/registration.txt60
-rw-r--r--src/mailman/domain.py72
-rw-r--r--src/mailman/interfaces/domain.py77
-rw-r--r--src/mailman/pipeline/docs/cook-headers.txt8
-rw-r--r--src/mailman/rest/adapters.py28
-rw-r--r--src/mailman/rest/configure.zcml6
-rw-r--r--src/mailman/rest/docs/domains.txt78
-rw-r--r--src/mailman/rest/webservice.py9
-rw-r--r--src/mailman/testing/layers.py7
-rw-r--r--src/mailman/testing/testing.cfg5
-rw-r--r--src/mailman/tests/test_documentation.py8
29 files changed, 515 insertions, 263 deletions
diff --git a/src/mailman/Archiver/Archiver.py b/src/mailman/Archiver/Archiver.py
index 1a4e32623..5fb5b754c 100644
--- a/src/mailman/Archiver/Archiver.py
+++ b/src/mailman/Archiver/Archiver.py
@@ -33,6 +33,7 @@ from string import Template
from mailman import Utils
from mailman.config import config
+from mailman.interfaces.domain import IDomainManager
log = logging.getLogger('mailman.error')
@@ -128,7 +129,8 @@ class Archiver:
if self.archive_private:
url = self.GetScriptURL('private') + '/index.html'
else:
- web_host = config.domains.get(self.host_name, self.host_name)
+ domain = IDomainManager(config).get(self.host_name)
+ web_host = (self.host_name if domain is None else domain.url_host)
url = Template(config.PUBLIC_ARCHIVE_URL).safe_substitute(
listname=self.fqdn_listname,
hostname=web_host,
diff --git a/src/mailman/app/lifecycle.py b/src/mailman/app/lifecycle.py
index 7fdbc9d89..85f08f7f7 100644
--- a/src/mailman/app/lifecycle.py
+++ b/src/mailman/app/lifecycle.py
@@ -33,6 +33,7 @@ import logging
from mailman.config import config
from mailman.core import errors
from mailman.email.validate import validate
+from mailman.interfaces.domain import IDomainManager
from mailman.interfaces.member import MemberRole
from mailman.utilities.modules import call_name
@@ -48,7 +49,7 @@ def create_list(fqdn_listname, owners=None):
validate(fqdn_listname)
# pylint: disable-msg=W0612
listname, domain = fqdn_listname.split('@', 1)
- if domain not in config.domains:
+ if domain not in IDomainManager(config):
raise errors.BadDomainSpecificationError(domain)
mlist = config.db.list_manager.create(fqdn_listname)
for style in config.style_manager.lookup(mlist):
diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py
index f562b3237..7ddeb2acb 100644
--- a/src/mailman/app/moderator.py
+++ b/src/mailman/app/moderator.py
@@ -330,8 +330,9 @@ def _refuse(mlist, request, recip, comment, origmsg=None, lang=None):
realname = mlist.real_name
if lang is None:
member = mlist.members.get_member(recip)
- lang = (member.preferred_language if member
- else mlist.preferred_language)
+ lang = (mlist.preferred_language
+ if member is None
+ else member.preferred_language)
text = Utils.maketext(
'refuse.txt',
{'listname' : mlist.fqdn_listname,
diff --git a/src/mailman/archiving/mhonarc.py b/src/mailman/archiving/mhonarc.py
index 949a79144..b27b7f808 100644
--- a/src/mailman/archiving/mhonarc.py
+++ b/src/mailman/archiving/mhonarc.py
@@ -35,6 +35,7 @@ from zope.interface import implements
from mailman.config import config
from mailman.interfaces.archiver import IArchiver
+from mailman.interfaces.domain import IDomainManager
from mailman.utilities.string import expand
@@ -53,7 +54,7 @@ class MHonArc:
def list_url(mlist):
"""See `IArchiver`."""
# XXX What about private MHonArc archives?
- web_host = config.domains[mlist.host_name].url_host
+ web_host = IDomainManager(config)[mlist.host_name].url_host
return expand(config.archiver.mhonarc.base_url,
dict(listname=mlist.fqdn_listname,
hostname=web_host,
diff --git a/src/mailman/archiving/pipermail.py b/src/mailman/archiving/pipermail.py
index 42a89ea55..07bd6144f 100644
--- a/src/mailman/archiving/pipermail.py
+++ b/src/mailman/archiving/pipermail.py
@@ -35,6 +35,7 @@ from zope.interface.interface import adapter_hooks
from mailman.config import config
from mailman.interfaces.archiver import IArchiver, IPipermailMailingList
+from mailman.interfaces.domain import IDomainManager
from mailman.interfaces.mailinglist import IMailingList
from mailman.utilities.filesystem import makedirs
from mailman.utilities.string import expand
@@ -97,7 +98,7 @@ class Pipermail:
if mlist.archive_private:
url = mlist.script_url('private') + '/index.html'
else:
- web_host = config.domains[mlist.host_name].url_host
+ web_host = IDomainManager(config)[mlist.host_name].url_host
return expand(config.archiver.pipermail.base_url,
dict(listname=mlist.fqdn_listname,
hostname=web_host,
diff --git a/src/mailman/archiving/prototype.py b/src/mailman/archiving/prototype.py
index 81163e184..3434587a2 100644
--- a/src/mailman/archiving/prototype.py
+++ b/src/mailman/archiving/prototype.py
@@ -33,6 +33,7 @@ from zope.interface import implements
from mailman.config import config
from mailman.interfaces.archiver import IArchiver
+from mailman.interfaces.domain import IDomainManager
@@ -50,7 +51,7 @@ class Prototype:
@staticmethod
def list_url(mlist):
"""See `IArchiver`."""
- return config.domains[mlist.host_name].base_url
+ return IDomainManager(config)[mlist.host_name].base_url
@staticmethod
def permalink(mlist, msg):
diff --git a/src/mailman/commands/docs/join.txt b/src/mailman/commands/docs/join.txt
index 471a2a5b6..eaafddc7c 100644
--- a/src/mailman/commands/docs/join.txt
+++ b/src/mailman/commands/docs/join.txt
@@ -119,8 +119,9 @@ Once Anne confirms her registration, she will be made a member of the mailing
list.
>>> token = str(qmsg['subject']).split()[1].strip()
+ >>> from mailman.interfaces.domain import IDomainManager
>>> from mailman.interfaces.registrar import IRegistrar
- >>> registrar = IRegistrar(config.domains['example.com'])
+ >>> registrar = IRegistrar(IDomainManager(config)[u'example.com'])
>>> registrar.confirm(token)
True
diff --git a/src/mailman/commands/join.py b/src/mailman/commands/join.py
index 81a018cff..d1f9dd816 100644
--- a/src/mailman/commands/join.py
+++ b/src/mailman/commands/join.py
@@ -30,6 +30,7 @@ from zope.interface import implements
from mailman.config import config
from mailman.i18n import _
from mailman.interfaces.command import ContinueProcessing, IEmailCommand
+from mailman.interfaces.domain import IDomainManager
from mailman.interfaces.member import DeliveryMode
from mailman.interfaces.registrar import IRegistrar
@@ -66,7 +67,7 @@ example:
print >> results, _(
'$self.name: No valid address found to subscribe')
return ContinueProcessing.no
- domain = config.domains[mlist.host_name]
+ domain = IDomainManager(config)[mlist.host_name]
registrar = IRegistrar(domain)
registrar.register(address, real_name, mlist)
person = formataddr((real_name, address))
diff --git a/src/mailman/config/config.py b/src/mailman/config/config.py
index 5b7f51400..736417b24 100644
--- a/src/mailman/config/config.py
+++ b/src/mailman/config/config.py
@@ -36,7 +36,6 @@ from zope.interface import Interface, implements
from mailman import version
from mailman.core import errors
-from mailman.domain import Domain
from mailman.languages.manager import LanguageManager
from mailman.styles.manager import StyleManager
from mailman.utilities.filesystem import makedirs
@@ -58,7 +57,6 @@ class Configuration:
implements(IConfiguration)
def __init__(self):
- self.domains = {} # email host -> IDomain
self.switchboards = {}
self.languages = LanguageManager()
self.style_manager = StyleManager()
@@ -74,7 +72,6 @@ class Configuration:
def _clear(self):
"""Clear the cached configuration variables."""
- self.domains.clear()
self.switchboards.clear()
self.languages = LanguageManager()
@@ -118,21 +115,6 @@ class Configuration:
def _post_process(self):
"""Perform post-processing after loading the configuration files."""
- # Set up the domains.
- domains = self._config.getByCategory('domain', [])
- for section in domains:
- domain = Domain(section.email_host, section.base_url,
- section.description, section.contact_address)
- if domain.email_host in self.domains:
- raise errors.BadDomainSpecificationError(
- 'Duplicate email host: %s' % domain.email_host)
- # Make sure there's only one mapping for the url_host
- if domain.url_host in self.domains.values():
- raise errors.BadDomainSpecificationError(
- '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.
- self.domains[domain.email_host] = domain
# Set up directories.
self.BIN_DIR = os.path.abspath(os.path.dirname(sys.argv[0]))
self.VAR_DIR = var_dir = self._config.mailman.var_dir
diff --git a/src/mailman/config/configure.zcml b/src/mailman/config/configure.zcml
index 405baa8bf..cc1a9face 100644
--- a/src/mailman/config/configure.zcml
+++ b/src/mailman/config/configure.zcml
@@ -24,4 +24,10 @@
factory="mailman.database.mailinglist.AcceptableAliasSet"
/>
+ <adapter
+ for="mailman.config.config.IConfiguration"
+ provides="mailman.interfaces.domain.IDomainManager"
+ factory="mailman.database.domain.DomainManager"
+ />
+
</configure>
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index 103ffb1de..a3b843cca 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -233,22 +233,6 @@ view_permission: None
show_tracebacks: yes
-[domain.master]
-# Site-wide domain defaults. To configure an individual
-# domain, add a [domain.example_com] section with the overrides.
-
-# This is the host name for the email interface.
-email_host: example.com
-# This is the base url for the domain's web interface. It must include the
-# url scheme.
-base_url: http://example.com
-# The contact address for this domain. This is advertised as the human to
-# contact when users have problems with the lists in this domain.
-contact_address: postmaster@example.com
-# A short description of this domain.
-description: An example domain.
-
-
[language.master]
# Template for language definitions. The section name must be [language.xx]
# where xx is the 2-character ISO code for the language.
diff --git a/src/mailman/constants.py b/src/mailman/constants.py
index 73158d8c7..1701c93f7 100644
--- a/src/mailman/constants.py
+++ b/src/mailman/constants.py
@@ -21,7 +21,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
- 'SystemDefaultPreferences',
+ 'system_preferences',
]
@@ -44,8 +44,16 @@ class SystemDefaultPreferences:
acknowledge_posts = False
hide_address = True
- preferred_language = config.languages['en']
receive_list_copy = True
receive_own_postings = True
delivery_mode = DeliveryMode.regular
delivery_status = DeliveryStatus.enabled
+
+ @property
+ def preferred_language(self):
+ """Return the system preferred language."""
+ return config.languages['en']
+
+
+
+system_preferences = SystemDefaultPreferences()
diff --git a/src/mailman/database/domain.py b/src/mailman/database/domain.py
new file mode 100644
index 000000000..a729cb8e2
--- /dev/null
+++ b/src/mailman/database/domain.py
@@ -0,0 +1,163 @@
+# Copyright (C) 2008-2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Domains."""
+
+from __future__ import unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Domain',
+ 'DomainManager',
+ ]
+
+from urlparse import urljoin, urlparse
+from storm.locals import Int, Unicode
+from zope.interface import implements
+
+from mailman.core.errors import BadDomainSpecificationError
+from mailman.database.model import Model
+from mailman.interfaces.domain import IDomain, IDomainManager
+
+
+
+class Domain(Model):
+ """Domains."""
+
+ implements(IDomain)
+
+ id = Int(primary=True)
+
+ email_host = Unicode()
+ base_url = Unicode()
+ description = Unicode()
+ contact_address = Unicode()
+
+ def __init__(self, email_host,
+ description=None,
+ base_url=None,
+ contact_address=None):
+ """Create and register a domain.
+
+ :param email_host: The host name for the email interface.
+ :type email_host: string
+ :param description: An optional description of the domain.
+ :type description: 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 contact_address: The email address to contact a human for this
+ domain. If not given, postmaster@`email_host` will be used.
+ :type contact_address: string
+ """
+ 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)
+
+ @property
+ def url_host(self):
+ # pylint: disable-msg=E1101
+ # no netloc member; yes it does
+ return urlparse(self.base_url).netloc
+
+ def confirm_address(self, token=''):
+ """See `IDomain`."""
+ return 'confirm-{0}@{1}'.format(token, self.email_host)
+
+ def confirm_url(self, token=''):
+ """See `IDomain`."""
+ return urljoin(self.base_url, 'confirm/' + token)
+
+ def __repr__(self):
+ """repr(a_domain)"""
+ if self.description is None:
+ return ('<Domain {0.email_host}, base_url: {0.base_url}, '
+ 'contact_address: {0.contact_address}>').format(self)
+ else:
+ return ('<Domain {0.email_host}, {0.description}, '
+ 'base_url: {0.base_url}, '
+ 'contact_address: {0.contact_address}>').format(self)
+
+
+
+class DomainManager:
+ """Domain manager."""
+
+ implements(IDomainManager)
+
+ def __init__(self, config):
+ """Create a domain manager.
+
+ :param config: The configuration object.
+ :type config: `IConfiguration`
+ """
+ self.config = config
+ self.store = config.db.store
+
+ def add(self, email_host,
+ description=None,
+ base_url=None,
+ contact_address=None):
+ """See `IDomainManager`."""
+ # Be sure the email_host is not already registered. This is probably
+ # a constraint that should (also) be maintained in the database.
+ if self.get(email_host) is not None:
+ raise BadDomainSpecificationError(
+ 'Duplicate email host: %s' % email_host)
+ domain = Domain(email_host, description, base_url, contact_address)
+ self.store.add(domain)
+ return domain
+
+ def remove(self, email_host):
+ domain = self[email_host]
+ self.store.remove(domain)
+ return domain
+
+ def get(self, email_host, default=None):
+ """See `IDomainManager`."""
+ domains = self.store.find(Domain, email_host=email_host)
+ if domains.count() < 1:
+ return default
+ assert domains.count() == 1, (
+ 'Too many matching domains: %s' % email_host)
+ return domains.one()
+
+ def __getitem__(self, email_host):
+ """See `IDomainManager`."""
+ missing = object()
+ domain = self.get(email_host, missing)
+ if domain is missing:
+ raise KeyError(email_host)
+ return domain
+
+ def __len__(self):
+ return self.store.find(Domain).count()
+
+ def __iter__(self):
+ """See `IDomainManager`."""
+ for domain in self.store.find(Domain):
+ yield domain
+
+ def __contains__(self, email_host):
+ """See `IDomainManager`."""
+ return self.store.find(Domain, email_host=email_host).count() > 0
diff --git a/src/mailman/database/mailinglist.py b/src/mailman/database/mailinglist.py
index f08adfd8b..5384e9895 100644
--- a/src/mailman/database/mailinglist.py
+++ b/src/mailman/database/mailinglist.py
@@ -40,6 +40,7 @@ from mailman.database.digests import OneLastDigest
from mailman.database.mime import ContentFilter
from mailman.database.model import Model
from mailman.database.types import Enum
+from mailman.interfaces.domain import IDomainManager
from mailman.interfaces.mailinglist import (
IAcceptableAlias, IAcceptableAliasSet, IMailingList, Personalization)
from mailman.interfaces.mime import FilterType
@@ -210,12 +211,12 @@ class MailingList(Model):
@property
def web_host(self):
"""See `IMailingList`."""
- return config.domains[self.host_name]
+ return IDomainManager(config)[self.host_name]
def script_url(self, target, context=None):
"""See `IMailingList`."""
# Find the domain for this mailing list.
- domain = config.domains[self.host_name]
+ domain = IDomainManager(config)[self.host_name]
# XXX Handle the case for when context is not None; those would be
# relative URLs.
return urljoin(domain.base_url, target + '/' + self.fqdn_listname)
diff --git a/src/mailman/database/mailman.sql b/src/mailman/database/mailman.sql
index fa772416f..84a906fb1 100644
--- a/src/mailman/database/mailman.sql
+++ b/src/mailman/database/mailman.sql
@@ -67,11 +67,21 @@ CREATE TABLE contentfilter (
CREATE INDEX ix_contentfilter_mailing_list_id
ON contentfilter (mailing_list_id);
+CREATE TABLE domain (
+ id INTEGER NOT NULL,
+ email_host TEXT,
+ base_url TEXT,
+ description TEXT,
+ contact_address TEXT,
+ PRIMARY KEY (id)
+ );
+
CREATE TABLE language (
- id INTEGER NOT NULL,
- code TEXT,
- PRIMARY KEY (id)
-);
+ id INTEGER NOT NULL,
+ code TEXT,
+ PRIMARY KEY (id)
+ );
+
CREATE TABLE mailinglist (
id INTEGER NOT NULL,
-- List identity
@@ -80,7 +90,7 @@ CREATE TABLE mailinglist (
list_id TEXT,
include_list_post_header BOOLEAN,
include_rfc2369_headers BOOLEAN,
- -- Attributes not directly modifiable via the web u/i
+ -- Attributes not directly modifiable via the web u/i
created_at TIMESTAMP,
admin_member_chunksize INTEGER,
next_request_id INTEGER,
diff --git a/src/mailman/database/member.py b/src/mailman/database/member.py
index 22bf042f6..4a158a11e 100644
--- a/src/mailman/database/member.py
+++ b/src/mailman/database/member.py
@@ -28,7 +28,7 @@ from storm.locals import *
from zope.interface import implements
from mailman.config import config
-from mailman.constants import SystemDefaultPreferences
+from mailman.constants import system_preferences
from mailman.database.model import Model
from mailman.database.types import Enum
from mailman.interfaces.member import IMember
@@ -69,7 +69,7 @@ class Member(Model):
pref = getattr(self.address.user.preferences, preference)
if pref is not None:
return pref
- return getattr(SystemDefaultPreferences, preference)
+ return getattr(system_preferences, preference)
@property
def acknowledge_posts(self):
diff --git a/src/mailman/docs/addresses.txt b/src/mailman/docs/addresses.txt
index a8ae24840..1621afa90 100644
--- a/src/mailman/docs/addresses.txt
+++ b/src/mailman/docs/addresses.txt
@@ -1,3 +1,4 @@
+===============
Email addresses
===============
@@ -10,7 +11,7 @@ about. Addresses are subscribed to mailing lists though members.
Creating addresses
-------------------
+==================
Addresses are created directly through the user manager, which starts out with
no addresses.
@@ -82,7 +83,7 @@ And now you can find the associated user.
Deleting addresses
-------------------
+==================
You can remove an unlinked address from the user manager.
@@ -110,7 +111,7 @@ address from the user.
Registration and validation
----------------------------
+===========================
Addresses have two dates, the date the address was registered on and the date
the address was validated on. Neither date is set by default.
@@ -141,7 +142,7 @@ And of course, you can also set the validation date.
Subscriptions
--------------
+=============
Addresses get subscribed to mailing lists, not users. When the address is
subscribed, a role is specified.
@@ -179,7 +180,7 @@ Now Elly is both an owner and a member of the mailing list.
Case-preserved addresses
-------------------------
+========================
Technically speaking, email addresses are case sensitive in the local part.
Mailman preserves the case of addresses and uses the case preserved version
diff --git a/src/mailman/docs/domains.txt b/src/mailman/docs/domains.txt
index b71689520..bd7ff5791 100644
--- a/src/mailman/docs/domains.txt
+++ b/src/mailman/docs/domains.txt
@@ -1,46 +1,122 @@
+=======
Domains
=======
+ # The test framework starts out with an example domain, so let's delete
+ # that first.
+ >>> from mailman.interfaces.domain import IDomainManager
+ >>> manager = IDomainManager(config)
+ >>> manager.remove(u'example.com')
+ <Domain example.com...>
+
Domains are how Mailman interacts with email host names and web host names.
-Generally, new domains are registered in the mailman.cfg configuration file.
-We simulate that here by pushing new configurations.
- >>> config.push('example.org', """
- ... [domain.example_dot_org]
- ... email_host: example.org
- ... base_url: https://mail.example.org
- ... description: The example domain
- ... contact_address: postmaster@mail.example.org
- ... """)
+ >>> from operator import attrgetter
+ >>> def show_domains():
+ ... if len(manager) == 0:
+ ... print 'no domains'
+ ... return
+ ... for domain in sorted(manager, key=attrgetter('email_host')):
+ ... print domain
- >>> domain = config.domains['example.org']
- >>> print domain.email_host
- example.org
- >>> print domain.base_url
- https://mail.example.org
- >>> print domain.description
- The example domain
- >>> print domain.contact_address
- postmaster@mail.example.org
- >>> print domain.url_host
- mail.example.org
+ >>> show_domains()
+ no domains
+Adding a domain requires some basic information, of which the email host name
+is the only required piece. The other parts are inferred from that.
-Confirmation tokens
--------------------
+ >>> manager.add(u'example.org')
+ <Domain example.org, base_url: http://example.org,
+ contact_address: postmaster@example.org>
+ >>> show_domains()
+ <Domain example.org, base_url: http://example.org,
+ contact_address: postmaster@example.org>
-Confirmation tokens can be added to either the email confirmation address...
+We can remove domains too.
- >>> print domain.confirm_address('xyz')
- confirm-xyz@example.org
+ >>> manager.remove(u'example.org')
+ <Domain example.org, base_url: http://example.org,
+ contact_address: postmaster@example.org>
+ >>> show_domains()
+ no domains
-...or the confirmation url.
+Sometimes the email host name is different than the base url for hitting the
+web interface for the domain.
+
+ >>> manager.add(u'example.com', base_url=u'https://mail.example.com')
+ <Domain example.com, base_url: https://mail.example.com,
+ contact_address: postmaster@example.com>
+ >>> show_domains()
+ <Domain example.com, base_url: https://mail.example.com,
+ contact_address: postmaster@example.com>
+
+Domains can have explicit descriptions and contact addresses.
+
+ >>> manager.add(
+ ... u'example.net',
+ ... base_url=u'http://lists.example.net',
+ ... contact_address=u'postmaster@example.com',
+ ... description=u'The example domain')
+ <Domain example.net, The example domain,
+ base_url: http://lists.example.net,
+ contact_address: postmaster@example.com>
+
+ >>> show_domains()
+ <Domain example.com, base_url: https://mail.example.com,
+ contact_address: postmaster@example.com>
+ <Domain example.net, The example domain,
+ base_url: http://lists.example.net,
+ contact_address: postmaster@example.com>
+
+In the global domain manager, domains are indexed by their email host name.
+
+ >>> for domain in sorted(manager, key=attrgetter('email_host')):
+ ... print domain.email_host
+ example.com
+ example.net
+
+ >>> print manager[u'example.net']
+ <Domain example.net, The example domain,
+ base_url: http://lists.example.net,
+ contact_address: postmaster@example.com>
- >>> print domain.confirm_url('abc')
- https://mail.example.org/confirm/abc
+ >>> print manager[u'doesnotexist.com']
+ Traceback (most recent call last):
+ ...
+ KeyError: u'doesnotexist.com'
+As with a dictionary, you can also get the domain. If the domain does not
+exist, None or a default is returned.
-Clean up
---------
+ >>> print manager.get(u'example.net')
+ <Domain example.net, The example domain,
+ base_url: http://lists.example.net,
+ contact_address: postmaster@example.com>
+
+ >>> print manager.get(u'doesnotexist.com')
+ None
+
+ >>> print manager.get(u'doesnotexist.com', u'blahdeblah')
+ blahdeblah
+
+Non-existent domains cannot be removed.
+
+ >>> manager.remove(u'doesnotexist.com')
+ Traceback (most recent call last):
+ ...
+ KeyError: u'doesnotexist.com'
+
+
+Confirmation tokens
+===================
+
+Confirmation tokens can be added to either the email confirmation address...
+
+ >>> domain = manager[u'example.net']
+ >>> print domain.confirm_address(u'xyz')
+ confirm-xyz@example.net
+
+...or the confirmation url.
- >>> config.pop('example.org')
+ >>> print domain.confirm_url(u'abc')
+ http://lists.example.net/confirm/abc
diff --git a/src/mailman/docs/registration.txt b/src/mailman/docs/registration.txt
index c1b29fd05..519b81ba8 100644
--- a/src/mailman/docs/registration.txt
+++ b/src/mailman/docs/registration.txt
@@ -1,11 +1,11 @@
+====================
Address registration
====================
-When a user wants to join a mailing list -- any mailing list -- in the running
-instance, he or she must first register with Mailman. The only thing they
-must supply is an email address, although there is additional information they
-may supply. All registered email addresses must be verified before Mailman
-will send them any list traffic.
+Before users can join a mailing list, they must first register with Mailman.
+The only thing they must supply is an email address, although there is
+additional information they may supply. All registered email addresses must
+be verified before Mailman will send them any list traffic.
>>> from mailman.app.registrar import Registrar
>>> from mailman.interfaces.registrar import IRegistrar
@@ -15,19 +15,11 @@ Specifically, it does not handle verifications, email address syntax validity
checks, etc. The IRegistrar is the interface to the object handling all this
stuff.
-Add a domain, which will provide the context for the verification email
-message.
-
- >>> config.push('mail', """
- ... [domain.mail_example_dot_com]
- ... email_host: mail.example.com
- ... base_url: http://mail.example.com
- ... contact_address: postmaster@mail.example.com
- ... """)
-
- >>> domain = config.domains['mail.example.com']
+ >>> from mailman.interfaces.domain import IDomainManager
+ >>> manager = IDomainManager(config)
+ >>> domain = manager[u'example.com']
-Get a registrar by adapting a context to the interface.
+Get a registrar by adapting a domain.
>>> from zope.interface.verify import verifyObject
>>> registrar = IRegistrar(domain)
@@ -45,14 +37,14 @@ Here is a helper function to check the token strings.
Here is a helper function to extract tokens from confirmation messages.
>>> import re
- >>> cre = re.compile('http://mail.example.com/confirm/(.*)')
+ >>> cre = re.compile('http://lists.example.com/confirm/(.*)')
>>> def extract_token(msg):
... mo = cre.search(qmsg.get_payload())
... return mo.group(1)
Invalid email addresses
------------------------
+=======================
The only piece of information you need to register is the email address.
Some amount of sanity checks are performed on the email address, although
@@ -86,7 +78,7 @@ addresses are rejected outright.
Register an email address
--------------------------
+=========================
Registration of an unknown address creates nothing until the confirmation step
is complete. No IUser or IAddress is created at registration time, but a
@@ -115,7 +107,7 @@ But this address is waiting for confirmation.
Verification by email
----------------------
+=====================
There is also a verification email sitting in the virgin queue now. This
message is sent to the user in order to verify the registered address.
@@ -131,7 +123,7 @@ message is sent to the user in order to verify the registered address.
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: 7bit
Subject: confirm ...
- From: confirm-...@mail.example.com
+ From: confirm-...@example.com
To: aperson@example.com
Message-ID: <...>
Date: ...
@@ -139,7 +131,7 @@ message is sent to the user in order to verify the registered address.
<BLANKLINE>
Email Address Registration Confirmation
<BLANKLINE>
- Hello, this is the GNU Mailman server at mail.example.com.
+ Hello, this is the GNU Mailman server at example.com.
<BLANKLINE>
We have received a registration request for the email address
<BLANKLINE>
@@ -150,13 +142,13 @@ message is sent to the user in order to verify the registered address.
this message, keeping the Subject header intact. Or you can visit this
web page
<BLANKLINE>
- http://mail.example.com/confirm/...
+ http://lists.example.com/confirm/...
<BLANKLINE>
If you do not wish to register this email address simply disregard this
message. If you think you are being maliciously subscribed to the list,
or have any other questions, you may contact
<BLANKLINE>
- postmaster@mail.example.com
+ postmaster@example.com
<BLANKLINE>
>>> dump_msgdata(qdata)
_parsemsg : False
@@ -175,7 +167,7 @@ appear in a URL in the body of the message.
The same token will appear in the From header.
- >>> qmsg['from'] == 'confirm-' + token + '@mail.example.com'
+ >>> qmsg['from'] == 'confirm-' + token + '@example.com'
True
It will also appear in the Subject header.
@@ -207,7 +199,7 @@ IUser linked to this address. The IAddress is verified.
Non-standard registrations
---------------------------
+==========================
If you try to confirm a registration token twice, of course only the first one
will work. The second one is ignored.
@@ -254,7 +246,7 @@ registration sends a confirmation.
Discarding
-----------
+==========
A confirmation token can also be discarded, say if the user changes his or her
mind about registering. When discarded, no IAddress or IUser is created.
@@ -272,7 +264,7 @@ mind about registering. When discarded, no IAddress or IUser is created.
Registering a new address for an existing user
-----------------------------------------------
+==============================================
When a new address for an existing user is registered, there isn't too much
different except that the new address will still need to be verified before it
@@ -306,7 +298,7 @@ can be used.
Corner cases
-------------
+============
If you try to confirm a token that doesn't exist in the pending database, the
confirm method will just return None.
@@ -332,7 +324,7 @@ pending even matched with that token will still be removed.
Registration and subscription
------------------------------
+=============================
Fred registers with Mailman at the same time that he subscribes to a mailing
list.
@@ -353,9 +345,3 @@ But after confirmation, he is.
>>> print mlist.members.get_member(u'fred.person@example.com')
<Member: Fred Person <fred.person@example.com>
on alpha@example.com as MemberRole.member>
-
-
-Clean up
---------
-
- >>> config.pop('mail')
diff --git a/src/mailman/domain.py b/src/mailman/domain.py
deleted file mode 100644
index 39c27a029..000000000
--- a/src/mailman/domain.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# Copyright (C) 2008-2009 by the Free Software Foundation, Inc.
-#
-# This file is part of GNU Mailman.
-#
-# GNU Mailman 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 3 of the License, or (at your option)
-# any later version.
-#
-# GNU Mailman 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
-# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-
-"""Domains."""
-
-from __future__ import unicode_literals
-
-__metaclass__ = type
-__all__ = [
- 'Domain',
- ]
-
-from urlparse import urljoin, urlparse
-from zope.interface import implements
-
-from mailman.interfaces.domain import IDomain
-
-
-
-class Domain:
- """Domains."""
-
- implements(IDomain)
-
- def __init__(self, email_host, base_url=None, description=None,
- contact_address=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)
- # pylint: disable-msg=E1101
- # no netloc member; yes it does
- self.url_host = urlparse(self.base_url).netloc
-
- def confirm_address(self, token=''):
- """See `IDomain`."""
- return 'confirm-{0}@{1}'.format(token, self.email_host)
-
- def confirm_url(self, token=''):
- """See `IDomain`."""
- return urljoin(self.base_url, 'confirm/' + token)
diff --git a/src/mailman/interfaces/domain.py b/src/mailman/interfaces/domain.py
index 1546f9487..f9476fc3d 100644
--- a/src/mailman/interfaces/domain.py
+++ b/src/mailman/interfaces/domain.py
@@ -22,7 +22,8 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'IDomain',
- 'IDomainSet',
+ 'IDomainCollection',
+ 'IDomainManager',
]
@@ -91,8 +92,78 @@ class IDomain(Interface):
-class IDomainSet(Interface):
- """The set of all known domains."""
+class IDomainManager(Interface):
+ """The manager of domains."""
+
+ def add(email_host, description=None, base_url=None, contact_address=None):
+ """Add a new domain.
+
+ :param email_host: The email host name for the domain.
+ :type email_host: string
+ :param description: The description of the domain.
+ :type description: string
+ :param base_url: The base url, including the scheme for the web
+ interface of the domain. If not given, it defaults to
+ http://`email_host`/
+ :type base_url: string
+ :param contact_address: The email contact address for the human
+ managing the domain. If not given, defaults to
+ postmaster@`email_host`
+ :type contact_address: string
+ :return: The new domain object
+ :rtype: `IDomain`
+ :raises `BadDomainSpecificationError`: when the `email_host` is
+ already registered.
+ """
+
+ def remove(email_host):
+ """Remove the domain.
+
+ :param email_host: The email host name of the domain to remove.
+ :type email_host: string
+ :raises KeyError: if the named domain does not exist.
+ """
+
+ def __getitem__(email_host):
+ """Return the named domain.
+
+ :param email_host: The email host name of the domain to remove.
+ :type email_host: string
+ :return: The domain object.
+ :rtype: `IDomain`
+ :raises KeyError: if the named domain does not exist.
+ """
+
+ def get(email_host, default=None):
+ """Return the named domain.
+
+ :param email_host: The email host name of the domain to remove.
+ :type email_host: string
+ :param default: What to return if the named domain does not exist.
+ :type default: object
+ :return: The domain object or None if the named domain does not exist.
+ :rtype: `IDomain`
+ """
+
+ def __iter__():
+ """An iterator over all the domains.
+
+ :return: iterator over `IDomain`.
+ """
+
+ def __contains__(email_host):
+ """Is this a known domain?
+
+ :param email_host: An email host name.
+ :type email_host: string
+ :return: True if this domain is known.
+ :rtype: bool
+ """
+
+
+
+class IDomainCollection(Interface):
+ """The set of domains available via the REST API."""
export_as_webservice_collection(IDomain)
diff --git a/src/mailman/pipeline/docs/cook-headers.txt b/src/mailman/pipeline/docs/cook-headers.txt
index ae276a79a..2c6381c8f 100644
--- a/src/mailman/pipeline/docs/cook-headers.txt
+++ b/src/mailman/pipeline/docs/cook-headers.txt
@@ -221,11 +221,11 @@ header.
---end---
There are some circumstances when the list administrator wants to explicitly
-set the List-ID header.
+set the List-ID header. Start by creating a new domain.
- >>> from mailman.domain import Domain
- >>> domain = Domain(u'mail.example.net')
- >>> config.domains[domain.email_host] = domain
+ >>> from mailman.interfaces.domain import IDomainManager
+ >>> manager = IDomainManager(config)
+ >>> domain = manager.add(u'mail.example.net')
>>> mlist.host_name = u'mail.example.net'
>>> process(mlist, msg, {})
diff --git a/src/mailman/rest/adapters.py b/src/mailman/rest/adapters.py
index d5ae01498..50ef89b18 100644
--- a/src/mailman/rest/adapters.py
+++ b/src/mailman/rest/adapters.py
@@ -21,37 +21,43 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
- 'DomainSet',
+ 'DomainCollection',
]
+from operator import attrgetter
+
from zope.interface import implements
from zope.publisher.interfaces import NotFound
-from mailman.interfaces.domain import IDomainSet
+from mailman.interfaces.domain import IDomainCollection
from mailman.interfaces.rest import IResolvePathNames
-class DomainSet:
+class DomainCollection:
"""Sets of known domains."""
- implements(IDomainSet, IResolvePathNames)
+ implements(IDomainCollection, IResolvePathNames)
__name__ = 'domains'
- def __init__(self, config):
- self._config = config
+ def __init__(self, manager):
+ """Initialize the adapter from an `IDomainManager`.
+
+ :param manager: The domain manager.
+ :type manager: `IDomainManager`.
+ """
+ self._manager = manager
def get_domains(self):
- """See `IDomainSet`."""
- # lazr.restful will not allow this to be a generator.
- domains = self._config.domains
- return [domains[domain] for domain in sorted(domains)]
+ """See `IDomainCollection`."""
+ # lazr.restful requires the return value to be a concrete list.
+ return sorted(self._manager, key=attrgetter('email_host'))
def get(self, name):
"""See `IResolvePathNames`."""
- domain = self._config.domains.get(name)
+ domain = self._manager.get(name)
if domain is None:
raise NotFound(self, name)
return domain
diff --git a/src/mailman/rest/configure.zcml b/src/mailman/rest/configure.zcml
index 164bbd445..8b9d6fb70 100644
--- a/src/mailman/rest/configure.zcml
+++ b/src/mailman/rest/configure.zcml
@@ -13,9 +13,9 @@
<webservice:register module="mailman.interfaces.system" />
<adapter
- for="mailman.config.config.IConfiguration"
- provides="mailman.interfaces.domain.IDomainSet"
- factory="mailman.rest.adapters.DomainSet"
+ for="mailman.interfaces.domain.IDomainManager"
+ provides="mailman.interfaces.domain.IDomainCollection"
+ factory="mailman.rest.adapters.DomainCollection"
/>
<adapter
diff --git a/src/mailman/rest/docs/domains.txt b/src/mailman/rest/docs/domains.txt
index 335916a61..2afdce49d 100644
--- a/src/mailman/rest/docs/domains.txt
+++ b/src/mailman/rest/docs/domains.txt
@@ -2,15 +2,38 @@
Domains
=======
-The REST API can be queried for the set of known domains.
+ # The test framework starts out with an example domain, so let's delete
+ # that first.
+ >>> from mailman.interfaces.domain import IDomainManager
+ >>> manager = IDomainManager(config)
+ >>> manager.remove(u'example.com')
+ <Domain example.com...>
+ >>> commit()
+
+The REST API can be queried for the set of known domains, of which there are
+initially none.
+
+ >>> dump_json('http://localhost:8001/3.0/domains')
+ resource_type_link: https://localhost:8001/3.0/#domains
+ start: None
+ total_size: 0
+
+Once a domain is added though, it is accessible through the API.
+
+ >>> manager.add(u'example.com', u'An example domain',
+ ... u'http://lists.example.com')
+ <Domain example.com, An example domain,
+ base_url: http://lists.example.com,
+ contact_address: postmaster@example.com>
+ >>> commit()
>>> dump_json('http://localhost:8001/3.0/domains')
entry 0:
base_url: http://lists.example.com
contact_address: postmaster@example.com
- description: An example domain.
+ description: An example domain
email_host: example.com
- http_etag: "546791f38192b347db544481f1386d33607ccf3d"
+ http_etag: "..."
resource_type_link: https://localhost:8001/3.0/#domain
self_link: https://localhost:8001/3.0/domains/example.com
url_host: lists.example.com
@@ -18,46 +41,47 @@ The REST API can be queried for the set of known domains.
start: 0
total_size: 1
-All domains are returned.
+At the top level, all domains are returned as separate entries.
- >>> from mailman.config import config
- >>> config.push('test domains', """\
- ... [domain.example_dot_org]
- ... email_host: example.org
- ... base_url: http://mail.example.org
- ... contact_address: listmaster@example.org
- ...
- ... [domain.example_dot_net]
- ... email_host: lists.example.net
- ... base_url: http://example.net
- ... contact_address: porkmaster@example.net
- ... """)
+ >>> manager.add(u'example.org',
+ ... base_url=u'http://mail.example.org',
+ ... contact_address=u'listmaster@example.org')
+ <Domain example.org, base_url: http://mail.example.org,
+ contact_address: listmaster@example.org>
+ >>> manager.add(u'lists.example.net',
+ ... u'Porkmasters',
+ ... u'http://example.net',
+ ... u'porkmaster@example.net')
+ <Domain lists.example.net, Porkmasters,
+ base_url: http://example.net,
+ contact_address: porkmaster@example.net>
+ >>> commit()
>>> dump_json('http://localhost:8001/3.0/domains')
entry 0:
base_url: http://lists.example.com
contact_address: postmaster@example.com
- description: An example domain.
+ description: An example domain
email_host: example.com
- http_etag: "546791f38192b347db544481f1386d33607ccf3d"
+ http_etag: "..."
resource_type_link: https://localhost:8001/3.0/#domain
self_link: https://localhost:8001/3.0/domains/example.com
url_host: lists.example.com
entry 1:
base_url: http://mail.example.org
contact_address: listmaster@example.org
- description: An example domain.
+ description: None
email_host: example.org
- http_etag: "4ff00fefca81b99ce2c7e6c50223107daf0649ff"
+ http_etag: "..."
resource_type_link: https://localhost:8001/3.0/#domain
self_link: https://localhost:8001/3.0/domains/example.org
url_host: mail.example.org
entry 2:
base_url: http://example.net
contact_address: porkmaster@example.net
- description: An example domain.
+ description: Porkmasters
email_host: lists.example.net
- http_etag: "aa5a388197948f21b8a3eb940b6c9725c5f41fac"
+ http_etag: "..."
resource_type_link: https://localhost:8001/3.0/#domain
self_link: https://localhost:8001/3.0/domains/lists.example.net
url_host: example.net
@@ -75,9 +99,9 @@ self_links from the above collection.
>>> dump_json('http://localhost:8001/3.0/domains/lists.example.net')
base_url: http://example.net
contact_address: porkmaster@example.net
- description: An example domain.
+ description: Porkmasters
email_host: lists.example.net
- http_etag: "aa5a388197948f21b8a3eb940b6c9725c5f41fac"
+ http_etag: "..."
resource_type_link: https://localhost:8001/3.0/#domain
self_link: https://localhost:8001/3.0/domains/lists.example.net
url_host: example.net
@@ -88,9 +112,3 @@ But we get a 404 for a non-existent domain.
Traceback (most recent call last):
...
HTTPError: HTTP Error 404: Not Found
-
-
-Clean up
-========
-
- >>> config.pop('test domains')
diff --git a/src/mailman/rest/webservice.py b/src/mailman/rest/webservice.py
index 0469e442b..2f811133a 100644
--- a/src/mailman/rest/webservice.py
+++ b/src/mailman/rest/webservice.py
@@ -41,7 +41,7 @@ from zope.publisher.publish import publish
from mailman.config import config
from mailman.core.system import system
-from mailman.interfaces.domain import IDomainSet
+from mailman.interfaces.domain import IDomainCollection, IDomainManager
from mailman.interfaces.rest import IResolvePathNames
from mailman.rest.publication import AdminWebServicePublication
@@ -82,13 +82,14 @@ class AdminWebServiceApplication:
def get(self, name):
"""Maps root names to resources."""
- log.debug('Getting top level name: %s', name)
top_level = dict(
system=system,
- domains=IDomainSet(config),
+ domains=IDomainCollection(IDomainManager(config)),
lists=config.db.list_manager,
)
- return top_level.get(name)
+ next_step = top_level.get(name)
+ log.debug('Top level name: %s -> %s', name, next_step)
+ return next_step
diff --git a/src/mailman/testing/layers.py b/src/mailman/testing/layers.py
index 1765efc4e..540b2e013 100644
--- a/src/mailman/testing/layers.py
+++ b/src/mailman/testing/layers.py
@@ -43,6 +43,7 @@ from mailman.config import config
from mailman.core import initialize
from mailman.core.logging import get_handler
from mailman.i18n import _
+from mailman.interfaces.domain import IDomainManager
from mailman.testing.helpers import SMTPServer, TestableMaster
from mailman.utilities.datetime import factory
from mailman.utilities.string import expand
@@ -161,7 +162,11 @@ class ConfigLayer(MockAndMonkeyLayer):
@classmethod
def testSetUp(cls):
- pass
+ # Add an example domain.
+ IDomainManager(config).add(
+ 'example.com', 'An example domain.',
+ 'http://lists.example.com', 'postmaster@example.com')
+ config.db.commit()
@classmethod
def testTearDown(cls):
diff --git a/src/mailman/testing/testing.cfg b/src/mailman/testing/testing.cfg
index 107db86ed..2947bbcc7 100644
--- a/src/mailman/testing/testing.cfg
+++ b/src/mailman/testing/testing.cfg
@@ -73,11 +73,6 @@ base_url: http://www.example.com/pipermail/$listname
enable: yes
command: /bin/echo "/usr/bin/mhonarc -add -dbfile $PRIVATE_ARCHIVE_FILE_DIR/${listname}.mbox/mhonarc.db -outdir $VAR_DIR/mhonarc/${listname} -stderr $LOG_DIR/mhonarc -stdout $LOG_DIR/mhonarc -spammode -umask 022"
-[domain.example_dot_com]
-email_host: example.com
-base_url: http://lists.example.com
-contact_address: postmaster@example.com
-
[language.ja]
description: Japanese
charset: euc-jp
diff --git a/src/mailman/tests/test_documentation.py b/src/mailman/tests/test_documentation.py
index 501b13a61..06cd3f8b2 100644
--- a/src/mailman/tests/test_documentation.py
+++ b/src/mailman/tests/test_documentation.py
@@ -174,11 +174,13 @@ def test_suite():
else:
layer = getattr(sys.modules[package_path], 'layer', SMTPLayer)
for filename in os.listdir(docsdir):
+ base, extension = os.path.splitext(filename)
if os.path.splitext(filename)[1] == '.txt':
- doctest_files[filename] = (
+ module_path = package_path + '.' + base
+ doctest_files[module_path] = (
os.path.join(docsdir, filename), layer)
- for filename in sorted(doctest_files):
- path, layer = doctest_files[filename]
+ for module_path in sorted(doctest_files):
+ path, layer = doctest_files[module_path]
test = doctest.DocFileSuite(
path,
package='mailman',