diff options
| author | Barry Warsaw | 2016-07-16 15:44:07 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2016-07-16 15:44:07 -0400 |
| commit | dbde6231ec897379ed38ed4cd015b8ab20ed5fa1 (patch) | |
| tree | 1226d06a238314262a1d04d0bbf9c4dc0b72c309 /src/mailman/app | |
| parent | 3387791beb7112dbe07664041f117fdcc20df53d (diff) | |
| download | mailman-dbde6231ec897379ed38ed4cd015b8ab20ed5fa1.tar.gz mailman-dbde6231ec897379ed38ed4cd015b8ab20ed5fa1.tar.zst mailman-dbde6231ec897379ed38ed4cd015b8ab20ed5fa1.zip | |
New template system. Closes #249
The new template system is introduced for API 3.1. See
``src/mailman/rest/docs/templates.rst`` for details.
Diffstat (limited to 'src/mailman/app')
| -rw-r--r-- | src/mailman/app/bounces.py | 33 | ||||
| -rw-r--r-- | src/mailman/app/docs/moderator.rst | 6 | ||||
| -rw-r--r-- | src/mailman/app/docs/pipelines.rst | 12 | ||||
| -rw-r--r-- | src/mailman/app/membership.py | 13 | ||||
| -rw-r--r-- | src/mailman/app/moderator.py | 36 | ||||
| -rw-r--r-- | src/mailman/app/notifications.py | 62 | ||||
| -rw-r--r-- | src/mailman/app/registrar.py | 26 | ||||
| -rw-r--r-- | src/mailman/app/subscriptions.py | 13 | ||||
| -rw-r--r-- | src/mailman/app/templates.py | 104 | ||||
| -rw-r--r-- | src/mailman/app/tests/test_bounces.py | 53 | ||||
| -rw-r--r-- | src/mailman/app/tests/test_notifications.py | 32 | ||||
| -rw-r--r-- | src/mailman/app/tests/test_registrar.py | 3 | ||||
| -rw-r--r-- | src/mailman/app/tests/test_subscriptions.py | 3 | ||||
| -rw-r--r-- | src/mailman/app/tests/test_templates.py | 125 |
14 files changed, 150 insertions, 371 deletions
diff --git a/src/mailman/app/bounces.py b/src/mailman/app/bounces.py index 977b21038..98076dc9b 100644 --- a/src/mailman/app/bounces.py +++ b/src/mailman/app/bounces.py @@ -32,9 +32,9 @@ from mailman.interfaces.bounce import UnrecognizedBounceDisposition from mailman.interfaces.listmanager import IListManager from mailman.interfaces.pending import IPendable, IPendings from mailman.interfaces.subscriptions import ISubscriptionService +from mailman.interfaces.template import ITemplateLoader from mailman.utilities.email import split_email -from mailman.utilities.i18n import make -from mailman.utilities.string import oneline +from mailman.utilities.string import expand, oneline, wrap from string import Template from zope.component import getUtility from zope.interface import implementer @@ -184,12 +184,19 @@ def send_probe(member, msg): """ mlist = getUtility(IListManager).get_by_list_id( member.mailing_list.list_id) - text = make('probe.txt', mlist, member.preferred_language.code, - listname=mlist.fqdn_listname, - address=member.address.email, - optionsurl=member.options_url, - owneraddr=mlist.owner_address, - ) + template = getUtility(ITemplateLoader).get( + 'list:user:notice:probe', mlist, + language=member.preferred_language.code, + # For backward compatibility. + code=member.preferred_language.code, + ) + text = wrap(expand(template, mlist, dict( + sender_email=member.subscriber.email, + # For backward compatibility. + address=member.address.email, + email=member.address.email, + owneraddr=mlist.owner_address, + ))) message_id = msg['message-id'] if isinstance(message_id, bytes): message_id = message_id.decode('ascii') @@ -240,11 +247,11 @@ def maybe_forward(mlist, msg): # The notification is either going to go to the list's administrators # (owners and moderators), or to the site administrators. Most of the # notification is exactly the same in either case. - adminurl = mlist.script_url('admin') + '/bounce' subject = _('Uncaught bounce notification') - text = MIMEText( - make('unrecognized.txt', mlist, adminurl=adminurl), - _charset=mlist.preferred_language.charset) + template = getUtility(ITemplateLoader).get( + 'list:admin:notice:unrecognized', mlist) + text = expand(template, mlist) + text_part = MIMEText(text, _charset=mlist.preferred_language.charset) attachment = MIMEMessage(msg) if (mlist.forward_unrecognized_bounces_to is UnrecognizedBounceDisposition.administrators): @@ -258,6 +265,6 @@ def maybe_forward(mlist, msg): # Create the notification and send it. notice = OwnerNotification(mlist, subject, **keywords) notice.set_type('multipart/mixed') - notice.attach(text) + notice.attach(text_part) notice.attach(attachment) notice.send(mlist) diff --git a/src/mailman/app/docs/moderator.rst b/src/mailman/app/docs/moderator.rst index f4339667f..988e8bebc 100644 --- a/src/mailman/app/docs/moderator.rst +++ b/src/mailman/app/docs/moderator.rst @@ -355,9 +355,9 @@ Jeff is a member of the mailing list, and chooses to unsubscribe. Your authorization is required for a mailing list unsubscription request approval: <BLANKLINE> - By: jeff@example.org - From: ant@example.com - ... + For: jeff@example.org + List: ant@example.com + <BLANKLINE> Membership changes diff --git a/src/mailman/app/docs/pipelines.rst b/src/mailman/app/docs/pipelines.rst index 46880b225..4022251f7 100644 --- a/src/mailman/app/docs/pipelines.rst +++ b/src/mailman/app/docs/pipelines.rst @@ -49,14 +49,12 @@ etc. Precedence: list Subject: [Test] My first post List-Id: <test.example.com> - Archived-At: <http://lists.example.com/.../4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB> - List-Archive: <http://lists.example.com/archives/test@example.com> + Archived-At: <http://example.com/.../4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB> + List-Archive: <http://example.com/archives/test@example.com> List-Help: <mailto:test-request@example.com?subject=help> List-Post: <mailto:test@example.com> - List-Subscribe: <http://lists.example.com/listinfo/test@example.com>, - <mailto:test-join@example.com> - List-Unsubscribe: <http://lists.example.com/listinfo/test@example.com>, - <mailto:test-leave@example.com> + List-Subscribe: <mailto:test-join@example.com> + List-Unsubscribe: <mailto:test-leave@example.com> MIME-Version: 1.0 Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit @@ -65,7 +63,6 @@ etc. _______________________________________________ Test mailing list test@example.com - http://lists.example.com/listinfo/test@example.com <BLANKLINE> The message metadata has information about recipients and other stuff. @@ -139,7 +136,6 @@ delivered to end recipients. _______________________________________________ Test mailing list test@example.com - http://lists.example.com/listinfo/test@example.com >>> dump_msgdata(messages[0].msgdata) _parsemsg : False diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py index 8b4d7ff30..534eed15d 100644 --- a/src/mailman/app/membership.py +++ b/src/mailman/app/membership.py @@ -29,9 +29,10 @@ from mailman.interfaces.bans import IBanManager from mailman.interfaces.member import ( AlreadySubscribedError, MemberRole, MembershipIsBannedError, NotAMemberError, SubscriptionEvent) +from mailman.interfaces.template import ITemplateLoader from mailman.interfaces.user import IUser from mailman.interfaces.usermanager import IUserManager -from mailman.utilities.i18n import make +from mailman.utilities.string import expand from zope.component import getUtility @@ -132,11 +133,11 @@ def delete_member(mlist, email, admin_notif=None, userack=None): user = getUtility(IUserManager).get_user(email) display_name = user.display_name subject = _('$mlist.display_name unsubscription notification') - text = make('adminunsubscribeack.txt', - mailing_list=mlist, - listname=mlist.display_name, - member=formataddr((display_name, email)), - ) + text = expand(getUtility(ITemplateLoader).get( + 'list:admin:notice:unsubscribe', mlist), + mlist, dict( + member=formataddr((display_name, email)), + )) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist) diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py index 2f8c19cfc..8ce631941 100644 --- a/src/mailman/app/moderator.py +++ b/src/mailman/app/moderator.py @@ -31,8 +31,9 @@ from mailman.interfaces.listmanager import ListDeletingEvent from mailman.interfaces.member import NotAMemberError from mailman.interfaces.messages import IMessageStore from mailman.interfaces.requests import IListRequests, RequestType +from mailman.interfaces.template import ITemplateLoader from mailman.utilities.datetime import now -from mailman.utilities.i18n import make +from mailman.utilities.string import expand, wrap from zope.component import getUtility @@ -190,12 +191,14 @@ def hold_unsubscription(mlist, email): if mlist.admin_immed_notify: subject = _( 'New unsubscription request from $mlist.display_name by $email') - text = make('unsubauth.txt', - mailing_list=mlist, - email=email, - listname=mlist.fqdn_listname, - admindb_url=mlist.script_url('admindb'), - ) + template = getUtility(ITemplateLoader).get( + 'list:admin:action:unsubscribe', mlist) + text = wrap(expand(template, mlist, dict( + # For backward compatibility. + mailing_list=mlist, + member=email, + email=email, + ))) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. msg = UserNotification( @@ -230,7 +233,7 @@ def handle_unsubscription(mlist, id, action, comment=None): pass slog.info('%s: deleted %s', mlist.fqdn_listname, email) else: - raise AssertionError('Unexpected action: {0}'.format(action)) + raise AssertionError('Unexpected action: {}'.format(action)) # Delete the request from the database. requestdb.delete_request(id) @@ -246,14 +249,15 @@ def send_rejection(mlist, request, recip, comment, origmsg=None, lang=None): lang = (mlist.preferred_language if member is None else member.preferred_language) - text = make('refuse.txt', - mailing_list=mlist, - language=lang.code, - listname=mlist.fqdn_listname, - request=request, - reason=comment, - adminaddr=mlist.owner_address, - ) + template = getUtility(ITemplateLoader).get( + 'list:user:notice:refuse', mlist) + text = wrap(expand(template, mlist, dict( + language=lang.code, + reason=comment, + # For backward compatibility. + request=request, + adminaddr=mlist.owner_address, + ))) with _.using(lang.code): # add in original message, but not wrap/filled if origmsg: diff --git a/src/mailman/app/notifications.py b/src/mailman/app/notifications.py index 04a56b55d..11a21a379 100644 --- a/src/mailman/app/notifications.py +++ b/src/mailman/app/notifications.py @@ -26,33 +26,14 @@ from mailman.config import config from mailman.core.i18n import _ from mailman.email.message import OwnerNotification, UserNotification from mailman.interfaces.member import DeliveryMode -from mailman.interfaces.templates import ITemplateLoader -from mailman.utilities.i18n import make +from mailman.interfaces.template import ITemplateLoader from mailman.utilities.string import expand, wrap -from urllib.error import URLError from zope.component import getUtility log = logging.getLogger('mailman.error') -def _get_message(uri_template, mlist, language): - if not uri_template: - return '' - try: - uri = expand(uri_template, dict( - listname=mlist.fqdn_listname, - language=language.code, - )) - message = getUtility(ITemplateLoader).get(uri) - except URLError: - log.exception('Message URI not found ({0}): {1}'.format( - mlist.fqdn_listname, uri_template)) - return '' - else: - return wrap(message) - - @public def send_welcome_message(mlist, member, language, text=''): """Send a welcome message to a subscriber. @@ -67,29 +48,18 @@ def send_welcome_message(mlist, member, language, text=''): :param language: The language of the response. :type language: ILanguage """ - welcome_message = _get_message(mlist.welcome_message_uri, mlist, language) - options_url = member.options_url - # Try to find a non-empty display name. We first look at the directly - # subscribed record, which will either be the address or the user. That's - # handled automatically by going through member.subscriber. If that - # doesn't give us something useful, try whatever user is linked to the - # subscriber. - if member.subscriber.display_name: - display_name = member.subscriber.display_name - # If an unlinked address is subscribed tehre will be no .user. - elif member.user is not None and member.user.display_name: - display_name = member.user.display_name - else: - display_name = '' + welcome_message = wrap(getUtility(ITemplateLoader).get( + 'list:user:notice:welcome', mlist, language=language.code)) + display_name = member.display_name # Get the text from the template. - text = expand(welcome_message, dict( + text = expand(welcome_message, mlist, dict( + user_name=display_name, + user_email=member.address.email, + # For backward compatibility. + user_address=member.address.email, fqdn_listname=mlist.fqdn_listname, list_name=mlist.display_name, - listinfo_uri=mlist.script_url('listinfo'), list_requests=mlist.request_address, - user_name=display_name, - user_address=member.address.email, - user_options_uri=options_url, )) digmode = ('' # noqa if member.delivery_mode is DeliveryMode.regular @@ -117,8 +87,8 @@ def send_goodbye_message(mlist, address, language): :param language: the language of the response :type language: string """ - goodbye_message = _get_message(mlist.goodbye_message_uri, - mlist, language) + goodbye_message = wrap(getUtility(ITemplateLoader).get( + 'list:user:notice:goodbye', mlist, language=language.code)) msg = UserNotification( address, mlist.bounces_address, _('You have been unsubscribed from the $mlist.display_name ' @@ -140,10 +110,10 @@ def send_admin_subscription_notice(mlist, address, display_name): """ with _.using(mlist.preferred_language.code): subject = _('$mlist.display_name subscription notification') - text = make('adminsubscribeack.txt', - mailing_list=mlist, - listname=mlist.display_name, - member=formataddr((display_name, address)), - ) + text = expand( + getUtility(ITemplateLoader).get('list:admin:notice:subscribe', mlist), + mlist, dict( + member=formataddr((display_name, address)), + )) msg = OwnerNotification(mlist, subject, text, roster=mlist.administrators) msg.send(mlist) diff --git a/src/mailman/app/registrar.py b/src/mailman/app/registrar.py index 541006f4b..7cefbd518 100644 --- a/src/mailman/app/registrar.py +++ b/src/mailman/app/registrar.py @@ -21,13 +21,13 @@ import logging from mailman import public from mailman.app.subscriptions import SubscriptionWorkflow -from mailman.core.i18n import _ from mailman.database.transaction import flush from mailman.email.message import UserNotification from mailman.interfaces.pending import IPendable, IPendings from mailman.interfaces.registrar import ConfirmationNeededEvent, IRegistrar -from mailman.interfaces.templates import ITemplateLoader +from mailman.interfaces.template import ITemplateLoader from mailman.interfaces.workflow import IWorkflowStateManager +from mailman.utilities.string import expand from zope.component import getUtility from zope.interface import implementer @@ -84,18 +84,22 @@ def handle_ConfirmationNeededEvent(event): # encode the token, they can reply to the robot and keep the token in # the Subject header, or they can click on the URL in the body of the # message and confirm through the web. - subject = 'confirm ' + event.token + subject = 'confirm {}'.format(event.token) confirm_address = event.mlist.confirm_address(event.token) - # For i18n interpolation. - confirm_url = event.mlist.domain.confirm_url(event.token) # noqa email_address = event.email - domain_name = event.mlist.domain.mail_host # noqa - contact_address = event.mlist.owner_address # noqa # Send a verification email to the address. template = getUtility(ITemplateLoader).get( - 'mailman:///{}/{}/confirm.txt'.format( - event.mlist.fqdn_listname, - event.mlist.preferred_language.code)) - text = _(template) + 'list:user:action:confirm', event.mlist) + text = expand(template, event.mlist, dict( + token=event.token, + subject=subject, + confirm_email=confirm_address, + user_email=email_address, + # For backward compatibility. + confirm_address=confirm_address, + email_address=email_address, + domain_name=event.mlist.domain.mail_host, + contact_address=event.mlist.owner_address, + )) msg = UserNotification(email_address, confirm_address, subject, text) msg.send(event.mlist, add_precedence=False) diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py index e360d9615..3042623d1 100644 --- a/src/mailman/app/subscriptions.py +++ b/src/mailman/app/subscriptions.py @@ -36,11 +36,12 @@ from mailman.interfaces.pending import IPendable, IPendings from mailman.interfaces.registrar import ConfirmationNeededEvent from mailman.interfaces.subscriptions import ( ISubscriptionService, SubscriptionPendingError, TokenOwner) +from mailman.interfaces.template import ITemplateLoader from mailman.interfaces.user import IUser from mailman.interfaces.usermanager import IUserManager from mailman.interfaces.workflow import IWorkflowStateManager from mailman.utilities.datetime import now -from mailman.utilities.i18n import make +from mailman.utilities.string import expand, wrap from zope.component import getUtility from zope.event import notify from zope.interface import implementer @@ -257,11 +258,11 @@ class SubscriptionWorkflow(Workflow): 'from $self.address.email') username = formataddr( (self.subscriber.display_name, self.address.email)) - text = make('subauth.txt', - mailing_list=self.mlist, - username=username, - listname=self.mlist.fqdn_listname, - ) + template = getUtility(ITemplateLoader).get( + 'list:admin:action:subscribe', self.mlist) + text = wrap(expand(template, self.mlist, dict( + member=username, + ))) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. msg = UserNotification( diff --git a/src/mailman/app/templates.py b/src/mailman/app/templates.py deleted file mode 100644 index c01b9af47..000000000 --- a/src/mailman/app/templates.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright (C) 2012-2016 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 contextlib import closing -from mailman import public -from mailman.interfaces.languages import ILanguageManager -from mailman.interfaces.listmanager import IListManager -from mailman.interfaces.templates import ITemplateLoader -from mailman.utilities.i18n import TemplateNotFoundError, find -from urllib.error import URLError -from urllib.parse import urlparse -from urllib.request import BaseHandler, build_opener, install_opener, urlopen -from urllib.response import addinfourl -from zope.component import getUtility -from zope.interface import implementer - - -class MailmanHandler(BaseHandler): - # Handle internal mailman: URLs. - def mailman_open(self, req): - list_manager = getUtility(IListManager) - # Parse urls of the form: - # - # mailman:///<fqdn_listname|list_id>/<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 = [p for p in parsed.path.split('/') if p] - if len(parts) == 0: - raise 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? This is rather - # tricky because if it's a mailing list, it could be a list-id and - # that will contain dots, as could the language code. - language = getUtility(ILanguageManager).get(part0) - if language is None: - # part0 must be a fqdn-listname or list-id. - mlist = (list_manager.get(part0) - if '@' in part0 else - list_manager.get_by_list_id(part0)) - if mlist is None: - raise URLError('Bad language or list name') - else: - code = language.code - elif len(parts) == 3: - part0, code, template = parts - # part0 could be an fqdn-listname or a list-id. - mlist = (getUtility(IListManager).get(part0) - if '@' in part0 else - getUtility(IListManager).get_by_list_id(part0)) - if mlist is None: - raise URLError('Missing list') - language = getUtility(ILanguageManager).get(code) - if language is None: - raise URLError('No such language') - code = language.code - else: - raise URLError('No such file') - # Find the template, mutating any missing template exception. - try: - path, fp = find(template, mlist, code) - except TemplateNotFoundError: - raise URLError('No such file') - return addinfourl(fp, {}, original_url) - - -@public -@implementer(ITemplateLoader) -class TemplateLoader: - """Loader of templates, with caching and support for mailman:// URIs.""" - - def __init__(self): - opener = build_opener(MailmanHandler()) - install_opener(opener) - - def get(self, uri): - """See `ITemplateLoader`.""" - with closing(urlopen(uri)) as fp: - return fp.read() diff --git a/src/mailman/app/tests/test_bounces.py b/src/mailman/app/tests/test_bounces.py index ffc1cf2b4..685f02907 100644 --- a/src/mailman/app/tests/test_bounces.py +++ b/src/mailman/app/tests/test_bounces.py @@ -177,6 +177,7 @@ class TestSendProbe(unittest.TestCase): """Test sending of the probe message.""" layer = ConfigLayer + maxDiff = None def setUp(self): self._mlist = create_list('test@example.com') @@ -240,13 +241,24 @@ Message-ID: <first> self.assertEqual(notice.get_content_type(), 'text/plain') # The interesting bits are the parts that have been interpolated into # the message. For now the best we can do is know that the - # interpolation values appear in the message. When Python 2.7 is our - # minimum requirement, we can use assertRegexpMatches(). - body = notice.get_payload() - self.assertIn('test@example.com', body) - self.assertIn('anne@example.com', body) - self.assertIn('http://example.com/anne@example.com', body) - self.assertIn('test-owner@example.com', body) + # interpolation values appear in the message. + self.assertMultiLineEqual(notice.get_payload(), """\ +This is a probe message. You can ignore this message. + +The test@example.com mailing list has received a number of bounces +from you, indicating that there may be a problem delivering messages +to anne@example.com. A sample is attached below. Please examine this +message to make sure there are no problems with your email address. +You may want to check with your mail administrator for more help. + +You don't need to do anything to remain an enabled member of the +mailing list. + +If you have any questions or problems, you can contact the mailing +list owner at + + test-owner@example.com +""") def test_headers(self): # Check the headers of the outer message. @@ -285,7 +297,8 @@ Message-ID: <first> self._var_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self._var_dir) xx_template_path = os.path.join( - self._var_dir, 'templates', 'site', 'xx', 'probe.txt') + self._var_dir, 'templates', 'site', 'xx', + 'list:user:notice:probe.txt') os.makedirs(os.path.dirname(xx_template_path)) config.push('xx template dir', """\ [paths.testing] @@ -300,7 +313,6 @@ Message-ID: <first> blah blah blah $listname $address -$optionsurl $owneraddr """, file=fp) @@ -322,7 +334,9 @@ $owneraddr notice = message.get_payload(0).get_payload() self.assertMultiLineEqual(notice, """\ blah blah blah test@example.com anne@example.com -http://example.com/anne@example.com test-owner@example.com""") +test-owner@example.com + +""") class TestProbe(unittest.TestCase): @@ -364,6 +378,7 @@ class TestMaybeForward(unittest.TestCase): """Test forwarding of unrecognized bounces.""" layer = ConfigLayer + maxDiff = None def setUp(self): config.push('test config', """ @@ -425,9 +440,12 @@ Message-ID: <first> payload = msg.get_payload(0) self.assertEqual(payload.get_content_type(), 'text/plain') body = payload.get_payload() - self.assertEqual( - body.splitlines()[-1], - 'http://lists.example.com/admin/test@example.com/bounce') + self.assertMultiLineEqual(body, """\ +The attached message was received as a bounce, but either the bounce format +was not recognized, or no member addresses could be extracted from it. This +mailing list has been configured to send all unrecognized bounce messages to +the list administrator(s). +""") # The second attachment should be a message/rfc822 containing the # original bounce message. payload = msg.get_payload(1) @@ -470,9 +488,12 @@ Message-ID: <first> payload = msg.get_payload(0) self.assertEqual(payload.get_content_type(), 'text/plain') body = payload.get_payload() - self.assertEqual( - body.splitlines()[-1], - 'http://lists.example.com/admin/test@example.com/bounce') + self.assertMultiLineEqual(body, """\ +The attached message was received as a bounce, but either the bounce format +was not recognized, or no member addresses could be extracted from it. This +mailing list has been configured to send all unrecognized bounce messages to +the list administrator(s). +""") # The second attachment should be a message/rfc822 containing the # original bounce message. payload = msg.get_payload(1) diff --git a/src/mailman/app/tests/test_notifications.py b/src/mailman/app/tests/test_notifications.py index 09873f9bb..3230fd892 100644 --- a/src/mailman/app/tests/test_notifications.py +++ b/src/mailman/app/tests/test_notifications.py @@ -18,19 +18,20 @@ """Test notifications.""" import os -import shutil -import tempfile import unittest +from contextlib import ExitStack from mailman.app.lifecycle import create_list from mailman.config import config from mailman.interfaces.languages import ILanguageManager from mailman.interfaces.member import MemberRole +from mailman.interfaces.template import ITemplateManager from mailman.interfaces.usermanager import IUserManager from mailman.testing.helpers import ( get_queue_messages, set_preferred, subscribe) from mailman.testing.layers import ConfigLayer from mailman.utilities.datetime import now +from tempfile import TemporaryDirectory from zope.component import getUtility @@ -41,36 +42,37 @@ class TestNotifications(unittest.TestCase): maxDiff = None def setUp(self): + resources = ExitStack() + self.addCleanup(resources.close) + self.var_dir = resources.enter_context(TemporaryDirectory()) self._mlist = create_list('test@example.com') - self._mlist.welcome_message_uri = 'mailman:///welcome.txt' self._mlist.display_name = 'Test List' - self.var_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.var_dir) + getUtility(ITemplateManager).set( + 'user:ack:welcome', self._mlist.list_id, 'mailman:///welcome.txt') config.push('template config', """\ [paths.testing] template_dir: {}/templates """.format(self.var_dir)) - self.addCleanup(config.pop, 'template config') + resources.callback(config.pop, 'template config') # Populate the template directories with a few fake templates. path = os.path.join(self.var_dir, 'templates', 'site', 'en') os.makedirs(path) - with open(os.path.join(path, 'welcome.txt'), 'w') as fp: + full_path = os.path.join(path, 'list:user:notice:welcome.txt') + with open(full_path, 'w', encoding='utf-8') as fp: print("""\ Welcome to the $list_name mailing list. Posting address: $fqdn_listname Help and other requests: $list_requests Your name: $user_name - Your address: $user_address - Your options: $user_options_uri""", file=fp) + Your address: $user_address""", file=fp) # Write a list-specific welcome message. path = os.path.join(self.var_dir, 'templates', 'lists', 'test@example.com', 'xx') os.makedirs(path) - with open(os.path.join(path, 'welcome.txt'), 'w') as fp: + full_path = os.path.join(path, 'list:user:notice:welcome.txt') + with open(full_path, 'w', encoding='utf-8') as fp: print('You just joined the $list_name mailing list!', file=fp) - # Let assertMultiLineEqual work without bounds. - self.maxDiff = None def test_welcome_message(self): subscribe(self._mlist, 'Anne', email='anne@example.com') @@ -86,13 +88,13 @@ Welcome to the Test List mailing list. Help and other requests: test-request@example.com Your name: Anne Person Your address: anne@example.com - Your options: http://example.com/anne@example.com """) def test_more_specific_welcome_message_nonenglish(self): - # mlist.welcome_message_uri can contain placeholders for the fqdn list + # The welcome message url can contain placeholders for the fqdn list # name and language. - self._mlist.welcome_message_uri = ( + getUtility(ITemplateManager).set( + 'user:ack:welcome', self._mlist.list_id, 'mailman:///$listname/$language/welcome.txt') # Add the xx language and subscribe Anne using it. manager = getUtility(ILanguageManager) diff --git a/src/mailman/app/tests/test_registrar.py b/src/mailman/app/tests/test_registrar.py index cec90c88b..2107f9648 100644 --- a/src/mailman/app/tests/test_registrar.py +++ b/src/mailman/app/tests/test_registrar.py @@ -247,7 +247,8 @@ class TestRegistrar(unittest.TestCase): self.assertEqual(message['To'], 'ant-owner@example.com') self.assertEqual(message['Subject'], 'Ant subscription notification') self.assertEqual(message.get_payload(), """\ -anne@example.com has been successfully subscribed to Ant.""") +anne@example.com has been successfully subscribed to Ant. +""") def test_no_admin_notify_mchanges(self): # Even when a user gets subscribed via the subscription policy diff --git a/src/mailman/app/tests/test_subscriptions.py b/src/mailman/app/tests/test_subscriptions.py index 11512bcfe..9f02593a9 100644 --- a/src/mailman/app/tests/test_subscriptions.py +++ b/src/mailman/app/tests/test_subscriptions.py @@ -450,7 +450,8 @@ Your authorization is required for a mailing list subscription request approval: For: anne@example.com - List: test@example.com""") + List: test@example.com +""") def test_get_moderator_approval_no_notifications(self): # When the subscription is held for moderator approval, and the list diff --git a/src/mailman/app/tests/test_templates.py b/src/mailman/app/tests/test_templates.py deleted file mode 100644 index 59bf74a0b..000000000 --- a/src/mailman/app/tests/test_templates.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright (C) 2012-2016 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.""" - -import os -import shutil -import tempfile -import unittest - -from mailman.app.lifecycle import create_list -from mailman.config import config -from mailman.interfaces.templates import ITemplateLoader -from mailman.testing.layers import ConfigLayer -from urllib.error import URLError -from zope.component import getUtility - - -class TestTemplateLoader(unittest.TestCase): - """Test the template downloader API.""" - - layer = ConfigLayer - - def setUp(self): - self.var_dir = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, self.var_dir) - config.push('template config', """\ - [paths.testing] - var_dir: {0} - """.format(self.var_dir)) - self.addCleanup(config.pop, 'template config') - # 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) - self._mlist = create_list('test@example.com') - - 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:///test@example.com/demo.txt') - self.assertEqual(content, 'Test content') - - def test_mailman_full_uri(self): - content = self._loader.get('mailman:///test@example.com/en/demo.txt') - self.assertEqual(content, 'Test content') - - def test_mailman_full_uri_with_english_fallback(self): - content = self._loader.get('mailman:///test@example.com/it/demo.txt') - self.assertEqual(content, 'Test content') - - def test_uri_not_found(self): - with self.assertRaises(URLError) as cm: - self._loader.get('mailman:///missing.txt') - self.assertEqual(cm.exception.reason, 'No such file') - - def test_shorter_url_error(self): - with self.assertRaises(URLError) as cm: - self._loader.get('mailman:///') - self.assertEqual(cm.exception.reason, 'No template specified') - - def test_short_url_error(self): - with self.assertRaises(URLError) as cm: - self._loader.get('mailman://') - self.assertEqual(cm.exception.reason, 'No template specified') - - def test_bad_language(self): - with self.assertRaises(URLError) as cm: - self._loader.get('mailman:///xx/demo.txt') - self.assertEqual(cm.exception.reason, 'Bad language or list name') - - def test_bad_mailing_list(self): - with self.assertRaises(URLError) as cm: - self._loader.get('mailman:///missing@example.com/demo.txt') - self.assertEqual(cm.exception.reason, 'Bad language or list name') - - def test_too_many_path_components(self): - with self.assertRaises(URLError) as cm: - self._loader.get('mailman:///missing@example.com/en/foo/demo.txt') - self.assertEqual(cm.exception.reason, 'No such file') - - def test_non_ascii(self): - # mailman://demo.txt with non-ascii content. - test_text = b'\xe4\xb8\xad' - path = os.path.join(self.var_dir, 'templates', 'site', 'it') - os.makedirs(path) - with open(os.path.join(path, 'demo.txt'), 'wb') as fp: - fp.write(test_text) - content = self._loader.get('mailman:///it/demo.txt') - self.assertIsInstance(content, str) - self.assertEqual(content, test_text.decode('utf-8')) |
