Commit 1bfc7f30 authored by Barry Warsaw's avatar Barry Warsaw

Wow. Put domains into the database.

Add an IDomainManager and a global domain manager which can be gotten by
adapting the global config object.

Add an IDomainCollection interface for exposing the domain manager onto the
API.
parent ac3af231
......@@ -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,
......
......@@ -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):
......
......@@ -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,
......
......@@ -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,
......
......@@ -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,
......
......@@ -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):
......
......@@ -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
......
......@@ -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))
......
......@@ -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
......
......@@ -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>
......@@ -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.
......
......@@ -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()
......@@ -22,34 +22,48 @@ 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.interfaces.domain import IDomain
from mailman.core.errors import BadDomainSpecificationError
from mailman.database.model import Model
from mailman.interfaces.domain import IDomain, IDomainManager
class Domain:
class Domain(Model):
"""Domains."""
implements(IDomain)
def __init__(self, email_host, base_url=None, description=None,
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 description: An optional description of the domain.
:type description: string
:type contact_address: The email address to contact a human for this
: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
......@@ -59,9 +73,12 @@ class Domain:
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
self.url_host = urlparse(self.base_url).netloc
return urlparse(self.base_url).netloc
def confirm_address(self, token=''):
"""See `IDomain`."""
......@@ -70,3 +87,77 @@ class Domain:
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
......@@ -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)
......
......@@ -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,
......
......@@ -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):
......
===============
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
......
=======
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
... """)
>>> 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
>>> 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
Confirmation tokens
-------------------
>>> show_domains()
no domains
Confirmation tokens can be added to either the email confirmation address...
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.
>>> print domain.confirm_address('xyz')
confirm-xyz@example.org
>>> 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>
...or the confirmation url.
We can remove domains too.
>>> manager.remove(u'example.org')
<Domain example.org, base_url: http://example.org,
contact_address: postmaster@example.org>
>>> show_domains()
no domains
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 domain.confirm_url('abc')
https://mail.example.org/confirm/abc
>>> print manager[u'example.net']
<Domain example.net, The example domain,
base_url: http://lists.example.net,
contact_address: postmaster@example.com>
>>> print manager[u'doesnotexist.com']
Traceback (most recent call last):
...
KeyError: u'doesnotexist.com'
Clean up
--------
As with a dictionary, you can also get the domain. If the domain does not
exist, None or a default is returned.
>>> 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
====================
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