Commit 7e5470af authored by Barry Warsaw's avatar Barry Warsaw

* Support downloading templates by URI, including mailman:// URIs. This is

   used in welcome and goodbye messages, and supports both language and
   mailing list specifications.  E.g. mailman:///[email protected]/it/welc.txt

 * Schema changes:
   - welcome_msg -> welcome_message_uri
   - goodbye_msg -> goodbye_message_uri
   - send_welcome_msg -> send_welcome_message
   - send_goodbye_msg -> send_goodbye_message

 * New `ITemplateLoader` utility.
parent e09c1319
......@@ -260,7 +260,7 @@ def handle_subscription(mlist, id, action, comment=None):
# request was made and accepted.
pass
else:
if mlist.send_welcome_msg:
if mlist.send_welcome_message:
send_welcome_message(mlist, address, language, delivery_mode)
if mlist.admin_notify_mchanges:
send_admin_subscription_notice(
......
......@@ -54,8 +54,8 @@ def send_welcome_message(mlist, address, language, delivery_mode, text=''):
:param delivery_mode: the type of delivery the subscriber is getting
:type delivery_mode: DeliveryMode
"""
if mlist.welcome_msg:
welcome = wrap(mlist.welcome_msg) + '\n'
if mlist.welcome_message_uri:
welcome = wrap(mlist.welcome_message_uri) + '\n'
else:
welcome = ''
# Find the IMember object which is subscribed to the mailing list, because
......
# Copyright (C) 2012 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/>.
"""Template loader."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'TemplateLoader',
]
import urllib2
from contextlib import closing
from urllib import addinfourl
from urlparse import urlparse
from zope.component import getUtility
from zope.interface import implements
from mailman.utilities.i18n import TemplateNotFoundError, find
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.templates import ITemplateLoader
class MailmanHandler(urllib2.BaseHandler):
# Handle internal mailman: URLs.
def mailman_open(self, req):
# Parse urls of the form:
#
# mailman:///<fqdn_listname>/<language>/<template_name>
#
# where only the template name is required.
mlist = code = template = None
# Parse the full requested URL and be sure it's something we handle.
original_url = req.get_full_url()
parsed = urlparse(original_url)
assert(parsed.scheme == 'mailman')
# The path can contain one, two, or three components. Since no empty
# path components are legal, filter them out.
parts = filter(None, parsed.path.split('/'))
if len(parts) == 0:
raise urllib2.URLError('No template specified')
elif len(parts) == 1:
template = parts[0]
elif len(parts) == 2:
part0, template = parts
# Is part0 a language code or a mailing list? It better be one or
# the other, and there's no possibility of namespace collisions
# because language codes don't contain @ and mailing list names
# MUST contain @.
language = getUtility(ILanguageManager).get(part0)
mlist = getUtility(IListManager).get(part0)
if language is None and mlist is None:
raise urllib2.URLError('Bad language or list name')
elif mlist is None:
code = language.code
elif len(parts) == 3:
fqdn_listname, code, template = parts
mlist = getUtility(IListManager).get(fqdn_listname)
if mlist is None:
raise urllib2.URLError('Missing list')
language = getUtility(ILanguageManager).get(code)
if language is None:
raise urllib2.URLError('No such language')
code = language.code
else:
raise urllib2.URLError('No such file')
# Find the template, mutating any missing template exception.
try:
path, fp = find(template, mlist, code)
except TemplateNotFoundError:
raise urllib2.URLError('No such file')
return addinfourl(fp, {}, original_url)
class TemplateLoader:
"""Loader of templates, with caching and support for mailman:// URIs."""
implements(ITemplateLoader)
def __init__(self):
opener = urllib2.build_opener(MailmanHandler())
urllib2.install_opener(opener)
def get(self, uri):
"""See `ITemplateLoader`."""
with closing(urllib2.urlopen(uri)) as fp:
return fp.read()
# Copyright (C) 2012 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/>.
"""Test the template downloader API."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'TestTemplateLoader',
]
import os
import shutil
import urllib2
import tempfile
import unittest
from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.templates import ITemplateLoader
from mailman.testing.layers import ConfigLayer
class TestTemplateLoader(unittest.TestCase):
"""Test the template downloader API."""
layer = ConfigLayer
def setUp(self):
self.var_dir = tempfile.mkdtemp()
config.push('template config', """\
[paths.testing]
var_dir: {0}
""".format(self.var_dir))
# Put a demo template in the site directory.
path = os.path.join(self.var_dir, 'templates', 'site', 'en')
os.makedirs(path)
with open(os.path.join(path, 'demo.txt'), 'w') as fp:
print('Test content', end='', file=fp)
self._loader = getUtility(ITemplateLoader)
getUtility(ILanguageManager).add('it', 'utf-8', 'Italian')
self._mlist = create_list('[email protected]')
def tearDown(self):
config.pop('template config')
shutil.rmtree(self.var_dir)
def test_mailman_internal_uris(self):
# mailman://demo.txt
content = self._loader.get('mailman:///demo.txt')
self.assertEqual(content, 'Test content')
def test_mailman_internal_uris_twice(self):
# mailman:///demo.txt
content = self._loader.get('mailman:///demo.txt')
self.assertEqual(content, 'Test content')
content = self._loader.get('mailman:///demo.txt')
self.assertEqual(content, 'Test content')
def test_mailman_uri_with_language(self):
content = self._loader.get('mailman:///en/demo.txt')
self.assertEqual(content, 'Test content')
def test_mailman_uri_with_english_fallback(self):
content = self._loader.get('mailman:///it/demo.txt')
self.assertEqual(content, 'Test content')
def test_mailman_uri_with_list_name(self):
content = self._loader.get('mailman:///[email protected]/demo.txt')
self.assertEqual(content, 'Test content')
def test_mailman_full_uri(self):
content = self._loader.get('mailman:///[email protected]/en/demo.txt')
self.assertEqual(content, 'Test content')
def test_mailman_full_uri_with_english_fallback(self):
content = self._loader.get('mailman:///[email protected]/it/demo.txt')
self.assertEqual(content, 'Test content')
def test_uri_not_found(self):
try:
self._loader.get('mailman:///missing.txt')
except urllib2.URLError as error:
self.assertEqual(error.reason, 'No such file')
else:
raise AssertionError('Exception expected')
def test_shorter_url_error(self):
try:
self._loader.get('mailman:///')
except urllib2.URLError as error:
self.assertEqual(error.reason, 'No template specified')
else:
raise AssertionError('Exception expected')
def test_short_url_error(self):
try:
self._loader.get('mailman://')
except urllib2.URLError as error:
self.assertEqual(error.reason, 'No template specified')
else:
raise AssertionError('Exception expected')
def test_bad_language(self):
try:
self._loader.get('mailman:///xx/demo.txt')
except urllib2.URLError as error:
self.assertEqual(error.reason, 'Bad language or list name')
else:
raise AssertionError('Exception expected')
def test_bad_mailing_list(self):
try:
self._loader.get('mailman:///[email protected]/demo.txt')
except urllib2.URLError as error:
self.assertEqual(error.reason, 'Bad language or list name')
else:
raise AssertionError('Exception expected')
def test_too_many_path_components(self):
try:
self._loader.get('mailman:///[email protected]/en/foo/demo.txt')
except urllib2.URLError as error:
self.assertEqual(error.reason, 'No such file')
else:
raise AssertionError('Exception expected')
......@@ -23,68 +23,73 @@
/>
<utility
factory="mailman.model.bans.BanManager"
provides="mailman.interfaces.bans.IBanManager"
factory="mailman.model.bans.BanManager"
/>
<utility
factory="mailman.model.bounce.BounceProcessor"
provides="mailman.interfaces.bounce.IBounceProcessor"
factory="mailman.model.bounce.BounceProcessor"
/>
<utility
factory="mailman.model.domain.DomainManager"
provides="mailman.interfaces.domain.IDomainManager"
factory="mailman.model.domain.DomainManager"
/>
<utility
factory="mailman.languages.manager.LanguageManager"
provides="mailman.interfaces.languages.ILanguageManager"
factory="mailman.languages.manager.LanguageManager"
/>
<utility
factory="mailman.model.listmanager.ListManager"
provides="mailman.interfaces.listmanager.IListManager"
factory="mailman.model.listmanager.ListManager"
/>
<utility
factory="mailman.mta.aliases.MailTransportAgentAliases"
provides="mailman.interfaces.mta.IMailTransportAgentAliases"
factory="mailman.mta.aliases.MailTransportAgentAliases"
/>
<utility
factory="mailman.model.messagestore.MessageStore"
provides="mailman.interfaces.messages.IMessageStore"
factory="mailman.model.messagestore.MessageStore"
/>
<utility
factory="mailman.model.pending.Pendings"
provides="mailman.interfaces.pending.IPendings"
factory="mailman.model.pending.Pendings"
/>
<utility
factory="mailman.app.registrar.Registrar"
provides="mailman.interfaces.registrar.IRegistrar"
factory="mailman.app.registrar.Registrar"
/>
<utility
factory="mailman.styles.manager.StyleManager"
provides="mailman.interfaces.styles.IStyleManager"
factory="mailman.styles.manager.StyleManager"
/>
<utility
factory="mailman.app.subscriptions.SubscriptionService"
provides="mailman.interfaces.subscriptions.ISubscriptionService"
factory="mailman.app.subscriptions.SubscriptionService"
/>
<utility
factory="mailman.model.usermanager.UserManager"
provides="mailman.interfaces.usermanager.IUserManager"
factory="mailman.model.usermanager.UserManager"
/>
<utility
factory="mailman.email.validate.Validator"
provides="mailman.interfaces.address.IEmailValidator"
factory="mailman.email.validate.Validator"
/>
<utility
provides="mailman.interfaces.templates.ITemplateLoader"
factory="mailman.app.templates.TemplateLoader"
/>
</configure>
......@@ -63,7 +63,7 @@ CREATE TABLE mailinglist (
gateway_to_mail BOOLEAN,
gateway_to_news BOOLEAN,
generic_nonmember_action INTEGER,
goodbye_msg TEXT,
goodbye_message_uri TEXT,
header_matches BYTEA,
hold_these_nonmembers BYTEA,
info TEXT,
......@@ -95,9 +95,9 @@ CREATE TABLE mailinglist (
require_explicit_destination BOOLEAN,
respond_to_post_requests BOOLEAN,
scrub_nondigest BOOLEAN,
send_goodbye_msg BOOLEAN,
send_goodbye_message BOOLEAN,
send_reminders BOOLEAN,
send_welcome_msg BOOLEAN,
send_welcome_message BOOLEAN,
start_chain TEXT,
subject_prefix TEXT,
subscribe_auto_approval BYTEA,
......@@ -106,7 +106,7 @@ CREATE TABLE mailinglist (
topics_bodylines_limit INTEGER,
topics_enabled BOOLEAN,
unsubscribe_policy INTEGER,
welcome_msg TEXT,
welcome_message_uri TEXT,
moderation_callback TEXT,
PRIMARY KEY (id)
);
......
......@@ -159,7 +159,7 @@ CREATE TABLE mailinglist (
gateway_to_mail BOOLEAN,
gateway_to_news BOOLEAN,
generic_nonmember_action INTEGER,
goodbye_msg TEXT,
goodbye_message_uri TEXT,
header_matches BLOB,
hold_these_nonmembers BLOB,
info TEXT,
......@@ -191,9 +191,9 @@ CREATE TABLE mailinglist (
require_explicit_destination BOOLEAN,
respond_to_post_requests BOOLEAN,
scrub_nondigest BOOLEAN,
send_goodbye_msg BOOLEAN,
send_goodbye_message BOOLEAN,
send_reminders BOOLEAN,
send_welcome_msg BOOLEAN,
send_welcome_message BOOLEAN,
start_chain TEXT,
subject_prefix TEXT,
subscribe_auto_approval BLOB,
......@@ -202,7 +202,7 @@ CREATE TABLE mailinglist (
topics_bodylines_limit INTEGER,
topics_enabled BOOLEAN,
unsubscribe_policy INTEGER,
welcome_msg TEXT,
welcome_message_uri TEXT,
PRIMARY KEY (id)
);
......
......@@ -25,6 +25,17 @@ Architecture
is now used when search for all template overrides, site, domain, or
mailing list. The in-tree English templates are used only as a last
fallback.
* Support downloading templates by URI, including mailman:// URIs. This is
used in welcome and goodbye messages, and supports both language and
mailing list specifications. E.g. mailman:///[email protected]/it/welc.txt
Database
--------
* Schema changes:
- welcome_msg -> welcome_message_uri
- goodbye_msg -> goodbye_message_uri
- send_welcome_msg -> send_welcome_message
- send_goodbye_msg -> send_goodbye_message
REST
----
......@@ -46,6 +57,7 @@ Interfaces
`Action.defer` (since the message is already being held).
* `IListRequests.get_request()` now takes an optional `request_type`
argument to narrow the search for the given request.
* New `ITemplateLoader` utility.
Commands
--------
......
......@@ -516,6 +516,62 @@ class IMailingList(Interface):
process_bounces = Attribute(
"""Whether or not the mailing list processes bounces.""")
# Notifications.
send_welcome_message = Attribute(
"""Flag indicating whether a welcome message should be sent.""")
welcome_message_uri = Attribute(
"""URI for the list's welcome message.
This can be any URI supported by `httplib2` with the addition of
`mailman:` URIs, which reference internal default resources. This is
a template which can include the following placeholders:
$listname - the FQDN list name for this mailing list.
$language - the language code, usually the list's preferred language.
The resource will be downloaded and cached whenever the welcome
message is sent. The resource at this URI can contain the following
placeholders, which are also filled in through values on the mailing
list:
$fqdn_listname - the FQDN list name for this mailing list.
$list_name - the human readable name for the mailing list.
$listinfo_uri - the URI to the list's information page.
$list_requests - the address to the list's `-request` address.
$user_name - the name of the subscribing user.
$user_address - the email address of the subscribing user.
$user_options_uri - the URI to this member's options page.
""")
send_goodbye_message = Attribute(
"""Flag indicating whether a goodbye message should be sent.""")
goodbye_message_uri = Attribute(
"""URI for the list's goodbye message.
This can be any URI supported by `httplib2` with the addition of
`mailman:` URIs, which reference internal default resources. This is
a template which can include the following placeholders:
$listname - the FQDN list name for this mailing list.
$language - the language code, usually the list's preferred language.
The resource will be downloaded and cached whenever the welcome
message is sent. The resource at this URI can contain the following
placeholders, which are also filled in through values on the mailing
list:
$fqdn_listname - the FQDN list name for this mailing list.
$list_name - the human readable name for the mailing list.
$listinfo_uri - the URI to the list's information page.
$list_requests - the address to the list's `-request` address.
$user_name - the name of the subscribing user.
$user_address - the email address of the subscribing user.
$user_options_uri - the URI to this member's options page.
""")
class IAcceptableAlias(Interface):
......
# Copyright (C) 2012 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/>.
"""Template downloader with cache."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'ITemplateLoader',
]
from zope.interface import Interface
class ITemplateLoader(Interface):
"""The template downloader utility."""
def get(uri):
"""Download the named URI, and return the response and content.
This API uses `urllib2`_ so consult its documentation for details.
.. _`urllib2`: http://docs.python.org/library/urllib2.html
:param uri: The URI of the resource. These may be any URI supported
by `urllib2` and also `mailman:` URIs for internal resources.
:type uri: string
:return: An open file object as defined by urllib2.
"""
......@@ -540,7 +540,7 @@ subscriber.
The subscription can also be accepted. This subscribes the address to the
mailing list.
>>> mlist.send_welcome_msg = True
>>> mlist.send_welcome_message = True
>>> id_4 = moderator.hold_subscription(mlist,
... '[email protected]', 'Frank Person',
... 'abcxyz', DeliveryMode.regular, 'en')
......
......@@ -147,7 +147,7 @@ class MailingList(Model):
gateway_to_mail = Bool()
gateway_to_news = Bool()
generic_nonmember_action = Int()
goodbye_msg = Unicode()
goodbye_message_uri = Unicode()
header_matches = Pickle()
hold_these_nonmembers = Pickle()
info = Unicode()
......@@ -179,9 +179,9 @@ class MailingList(Model):
require_explicit_destination = Bool()
respond_to_post_requests = Bool()
scrub_nondigest = Bool()
send_goodbye_msg = Bool()
send_goodbye_message = Bool()
send_reminders = Bool()
send_welcome_msg = Bool()
send_welcome_message = Bool()
start_chain = Unicode()
subject_prefix = Unicode()
subscribe_auto_approval = Pickle()
......@@ -190,7 +190,7 @@ class MailingList(Model):
topics_bodylines_limit = Int()
topics_enabled = Bool()
unsubscribe_policy = Int()
welcome_msg = Unicode()
welcome_message_uri = Unicode()
def __init__(self, fqdn_listname):
super(MailingList, self).__init__()
......
......@@ -199,10 +199,10 @@ ATTRIBUTES = dict(
reply_goes_to_list=GetterSetter(enum_validator(ReplyToMunging)),
request_address=GetterSetter(None),
scheme=GetterSetter(None),
send_welcome_msg=GetterSetter(as_boolean),
send_welcome_message=GetterSetter(as_boolean),
volume=GetterSetter(None),
web_host=GetterSetter(None),
welcome_msg=GetterSetter(unicode),
welcome_message_uri=GetterSetter(unicode),
)
......
......@@ -58,10 +58,10 @@ All readable attributes for a list are available on a sub-resource.
reply_goes_to_list: no_munging
request_address: [email protected]
scheme: http
send_welcome_msg: True
send_welcome_message: True
volume: 1
web_host: lists.example.com
welcome_msg:
welcome_message_uri:
Changing the full configuration
......@@ -98,8 +98,8 @@ all the writable attributes in one request.
... convert_html_to_plaintext=True,
... collapse_alternatives=False,
... reply_goes_to_list='point_to_list',
... send_welcome_msg=False,
... welcome_msg='Welcome!',
... send_welcome_message=False,
... welcome_message_uri='Welcome!',
... default_member_action='hold',
... default_nonmember_action='discard',
... generic_nonmember_action=2,
......@@ -146,9 +146,9 @@ These values are changed permanently.
real_name: Fnords
reply_goes_to_list: point_to_list
...
send_welcome_msg: False
send_welcome_message: False
...
welcome_msg: Welcome!
welcome_message_uri: Welcome!
If you use ``PUT`` to change a list's configuration, all writable attributes
must be included. It is an error to leave one or more out...
......@@ -179,8 +179,8 @@ must be included. It is an error to leave one or more out...
... convert_html_to_plaintext=True,
... collapse_alternatives=False,
... reply_goes_to_list='point_to_list',
... send_welcome_msg=True,
... welcome_msg='welcome message',
... send_welcome_message=True,
... welcome_message_uri='welcome message',
... default_member_action='accept',
... default_nonmember_action='accept',
... generic_nonmember_action=2,
......
# Copyright (C) 2012 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/>.
"""Template finder."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'TemplateFinder',
]
import os
from restish import http, resource
from mailman.config import config
from mailman.utilities.i18n import TemplateNotFoundError, find
# Use mimetypes.guess_all_extensions()?
EXTENSIONS = {
'text/plain': '.txt',
'text/html': '.html',
}
class TemplateFinder(resource.Resource):
"""Template finder resource."""
def __init__(self, mlist, template, language, content_type):
self.mlist = mlist
self.template = template
self.language = language
self.content_type = content_type
@resource.GET()
def find_template(self, request):
# XXX We currently only support .txt and .html files.
extension = EXTENSIONS.get(self.content_type)
if extension is None: