Commit d5aac006 authored by Barry Warsaw's avatar Barry Warsaw

Several internal improvements:

* New events:
  - ConfirmationNeededEvent is triggered when a pendable requiring
    confirmation is created.  This allows us to define an event handler for
    this event which sends the user notification.
  - SubscriptionEvent is triggered when a member is added to a mailing list.
    This lets us define an event handler which sends the welcome message.
* send_welcome_message() now takes a member parameter instead of an address,
  which lets us directly access the member's delivery mode and user display
  name (if the member has a user, which it might not in some cases).
* Use the list id in the pendable record instead of the list name for
  robustness (the latter can change but the former is permanent).
* Test more registration conditions.
* In the bin/runner command line switch handling, default `verbose` to None
  instead of False.  This makes it work better with nose's -E switch (log to
  stderr).
* In call_api(), if a POST, PUT, or PATCH method is used and data is None,
  encode the empty dictionary; seems like the behavior of urlencode() has
  changed, so this is safer.
* Fix style and pyflakes warnings.
parent 2fa21e92
......@@ -196,7 +196,7 @@ def send_probe(member, msg):
member.mailing_list.list_id)
text = make('probe.txt', mlist, member.preferred_language.code,
listname=mlist.fqdn_listname,
address= member.address.email,
address=member.address.email,
optionsurl=member.options_url,
owneraddr=mlist.owner_address,
)
......
......@@ -27,7 +27,8 @@ __all__ = [
from zope import event
from mailman.app import domain, moderator, subscriptions
from mailman.app import (
domain, membership, moderator, registrar, subscriptions)
from mailman.core import i18n, switchboard
from mailman.languages import manager as language_manager
from mailman.styles import manager as style_manager
......@@ -39,11 +40,13 @@ def initialize():
"""Initialize global event subscribers."""
event.subscribers.extend([
domain.handle_DomainDeletingEvent,
i18n.handle_ConfigurationUpdatedEvent,
language_manager.handle_ConfigurationUpdatedEvent,
membership.handle_SubscriptionEvent,
moderator.handle_ListDeletingEvent,
passwords.handle_ConfigurationUpdatedEvent,
registrar.handle_ConfirmationNeededEvent,
style_manager.handle_ConfigurationUpdatedEvent,
subscriptions.handle_ListDeletingEvent,
switchboard.handle_ConfigurationUpdatedEvent,
i18n.handle_ConfigurationUpdatedEvent,
style_manager.handle_ConfigurationUpdatedEvent,
language_manager.handle_ConfigurationUpdatedEvent,
])
......@@ -23,20 +23,22 @@ __metaclass__ = type
__all__ = [
'add_member',
'delete_member',
'handle_SubscriptionEvent',
]
from email.utils import formataddr
from zope.component import getUtility
from mailman.app.notifications import send_goodbye_message
from mailman.app.notifications import (
send_goodbye_message, send_welcome_message)
from mailman.config import config
from mailman.core.i18n import _
from mailman.email.message import OwnerNotification
from mailman.interfaces.address import IEmailValidator
from mailman.interfaces.bans import IBanManager
from mailman.interfaces.member import (
MemberRole, MembershipIsBannedError, NotAMemberError)
MemberRole, MembershipIsBannedError, NotAMemberError, SubscriptionEvent)
from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.i18n import make
......@@ -156,3 +158,23 @@ def delete_member(mlist, email, admin_notif=None, userack=None):
msg = OwnerNotification(mlist, subject, text,
roster=mlist.administrators)
msg.send(mlist)
def handle_SubscriptionEvent(event):
if not isinstance(event, SubscriptionEvent):
return
# Only send a notification message if the mailing list is configured to do
# so, and the member being added is a list member (as opposed to a
# moderator, non-member, or owner).
member = event.member
if member.role is not MemberRole.member:
return
mlist = member.mailing_list
if not mlist.send_welcome_message:
return
# What language should the welcome message be sent in?
language = member.preferred_language
if language is None:
language = mlist.preferred_language
send_welcome_message(mlist, member, language)
......@@ -38,8 +38,7 @@ from email.utils import formataddr, formatdate, getaddresses, make_msgid
from zope.component import getUtility
from mailman.app.membership import add_member, delete_member
from mailman.app.notifications import (
send_admin_subscription_notice, send_welcome_message)
from mailman.app.notifications import send_admin_subscription_notice
from mailman.config import config
from mailman.core.i18n import _
from mailman.email.message import UserNotification
......@@ -259,8 +258,6 @@ def handle_subscription(mlist, id, action, comment=None):
# request was made and accepted.
pass
else:
if mlist.send_welcome_message:
send_welcome_message(mlist, address, language, delivery_mode)
if mlist.admin_notify_mchanges:
send_admin_subscription_notice(
mlist, address, display_name, language)
......
......@@ -65,44 +65,36 @@ def _get_message(uri_template, mlist, language):
def send_welcome_message(mlist, address, language, delivery_mode, text=''):
def send_welcome_message(mlist, member, language, text=''):
"""Send a welcome message to a subscriber.
Prepending to the standard welcome message template is the mailing list's
welcome message, if there is one.
:param mlist: the mailing list
:param mlist: The mailing list.
:type mlist: IMailingList
:param address: The address to respond to
:type address: string
:param language: the language of the response
:param member: The member to send the welcome message to.
:param address: IMember
:param language: The language of the response.
:type language: ILanguage
:param delivery_mode: the type of delivery the subscriber is getting
:type delivery_mode: DeliveryMode
"""
welcome_message = _get_message(mlist.welcome_message_uri,
mlist, language)
# Find the IMember object which is subscribed to the mailing list, because
# from there, we can get the member's options url.
member = mlist.members.get_member(address)
user_name = member.user.display_name
welcome_message = _get_message(mlist.welcome_message_uri, mlist, language)
options_url = member.options_url
# Get the text from the template.
display_name = ('' if member.user is None else member.user.display_name)
text = expand(welcome_message, dict(
fqdn_listname=mlist.fqdn_listname,
list_name=mlist.display_name,
listinfo_uri=mlist.script_url('listinfo'),
list_requests=mlist.request_address,
user_name=user_name,
user_address=address,
user_name=display_name,
user_address=member.address.email,
user_options_uri=options_url,
))
if delivery_mode is not DeliveryMode.regular:
digmode = _(' (Digest mode)')
else:
digmode = ''
digmode = ('' if member.delivery_mode is DeliveryMode.regular
else _(' (Digest mode)'))
msg = UserNotification(
formataddr((user_name, address)),
formataddr((display_name, member.address.email)),
mlist.request_address,
_('Welcome to the "$mlist.display_name" mailing list${digmode}'),
text, language)
......
......@@ -22,22 +22,23 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'Registrar',
'handle_ConfirmationNeededEvent',
]
import logging
from zope.component import getUtility
from zope.event import notify
from zope.interface import implementer
from mailman.app.notifications import send_welcome_message
from mailman.core.i18n import _
from mailman.email.message import UserNotification
from mailman.interfaces.address import IEmailValidator
from mailman.interfaces.listmanager import IListManager
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.pending import IPendable, IPendings
from mailman.interfaces.registrar import IRegistrar
from mailman.interfaces.registrar import ConfirmationNeededEvent, IRegistrar
from mailman.interfaces.templates import ITemplateLoader
from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.datetime import now
......@@ -69,29 +70,13 @@ class Registrar:
type=PendableRegistration.PEND_KEY,
email=email,
display_name=display_name,
delivery_mode=delivery_mode.name)
pendable['list_name'] = mlist.fqdn_listname
delivery_mode=delivery_mode.name,
list_id=mlist.list_id)
token = getUtility(IPendings).add(pendable)
# There are three ways for a user to confirm their subscription. They
# can reply to the original message and let the VERP'd return address
# 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 ' + token
confirm_address = mlist.confirm_address(token)
# For i18n interpolation.
confirm_url = mlist.domain.confirm_url(token)
email_address = email
domain_name = mlist.domain.mail_host
contact_address = mlist.domain.contact_address
# Send a verification email to the address.
template = getUtility(ITemplateLoader).get(
'mailman:///{0}/{1}/confirm.txt'.format(
mlist.fqdn_listname,
mlist.preferred_language.code))
text = _(template)
msg = UserNotification(email, confirm_address, subject, text)
msg.send(mlist)
# We now have everything we need to begin the confirmation dance.
# Trigger the event to start the ball rolling, and return the
# generated token.
notify(ConfirmationNeededEvent(mlist, pendable, token))
return token
def confirm(self, token):
......@@ -103,7 +88,6 @@ class Registrar:
missing = object()
email = pendable.get('email', missing)
display_name = pendable.get('display_name', missing)
list_name = pendable.get('list_name', missing)
pended_delivery_mode = pendable.get('delivery_mode', 'regular')
try:
delivery_mode = DeliveryMode[pended_delivery_mode]
......@@ -151,20 +135,43 @@ class Registrar:
pass
address.verified_on = now()
# If this registration is tied to a mailing list, subscribe the person
# to the list right now, and possibly send a welcome message.
list_name = pendable.get('list_name')
if list_name is not None:
mlist = getUtility(IListManager).get(list_name)
if mlist:
# to the list right now. That will generate a SubscriptionEvent,
# which can be used to send a welcome message.
list_id = pendable.get('list_id')
if list_id is not None:
mlist = getUtility(IListManager).get_by_list_id(list_id)
if mlist is not None:
member = mlist.subscribe(address, MemberRole.member)
member.preferences.delivery_mode = delivery_mode
if mlist.send_welcome_message:
send_welcome_message(mlist,
address.email,
mlist.preferred_language,
delivery_mode)
return True
def discard(self, token):
# Throw the record away.
getUtility(IPendings).confirm(token)
def handle_ConfirmationNeededEvent(event):
if not isinstance(event, ConfirmationNeededEvent):
return
# There are three ways for a user to confirm their subscription. They
# can reply to the original message and let the VERP'd return address
# 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
mlist = getUtility(IListManager).get_by_list_id(event.pendable['list_id'])
confirm_address = mlist.confirm_address(event.token)
# For i18n interpolation.
confirm_url = mlist.domain.confirm_url(event.token)
email_address = event.pendable['email']
domain_name = mlist.domain.mail_host
contact_address = mlist.domain.contact_address
# Send a verification email to the address.
template = getUtility(ITemplateLoader).get(
'mailman:///{0}/{1}/confirm.txt'.format(
mlist.fqdn_listname,
mlist.preferred_language.code))
text = _(template)
msg = UserNotification(email_address, confirm_address, subject, text)
msg.send(mlist)
......@@ -198,6 +198,7 @@ class TestSendProbe(unittest.TestCase):
def setUp(self):
self._mlist = create_list('[email protected]')
self._mlist.send_welcome_message = False
self._member = add_member(self._mlist, '[email protected]',
'Anne Person', 'xxx',
DeliveryMode.regular, 'en')
......@@ -355,6 +356,7 @@ class TestProbe(unittest.TestCase):
def setUp(self):
self._mlist = create_list('[email protected]')
self._mlist.send_welcome_message = False
self._member = add_member(self._mlist, '[email protected]',
'Anne Person', 'xxx',
DeliveryMode.regular, 'en')
......@@ -395,6 +397,7 @@ class TestMaybeForward(unittest.TestCase):
site_owner: [email protected]
""")
self._mlist = create_list('[email protected]')
self._mlist.send_welcome_message = False
self._msg = mfs("""\
From: [email protected]
To: [email protected]
......
......@@ -33,10 +33,9 @@ from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.app.membership import add_member
from mailman.app.notifications import send_welcome_message
from mailman.config import config
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.member import DeliveryMode
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.testing.helpers import get_queue_messages
from mailman.testing.layers import ConfigLayer
......@@ -82,11 +81,8 @@ Welcome to the $list_name mailing list.
shutil.rmtree(self.var_dir)
def test_welcome_message(self):
en = getUtility(ILanguageManager).get('en')
add_member(self._mlist, '[email protected]', 'Anne Person',
'password', DeliveryMode.regular, 'en')
send_welcome_message(self._mlist, '[email protected]', en,
DeliveryMode.regular)
# Now there's one message in the virgin queue.
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 1)
......@@ -110,16 +106,42 @@ Welcome to the Test List mailing list.
'mailman:///$listname/$language/welcome.txt')
# Add the xx language and subscribe Anne using it.
manager = getUtility(ILanguageManager)
xx = manager.add('xx', 'us-ascii', 'Xlandia')
manager.add('xx', 'us-ascii', 'Xlandia')
add_member(self._mlist, '[email protected]', 'Anne Person',
'password', DeliveryMode.regular, 'xx')
send_welcome_message(self._mlist, '[email protected]', xx,
DeliveryMode.regular)
# Now there's one message in the virgin queue.
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 1)
message = messages[0].msg
self.assertEqual(str(message['subject']),
'Welcome to the "Test List" mailing list')
self.assertEqual(message.get_payload(),
'You just joined the Test List mailing list!')
self.assertMultiLineEqual(
message.get_payload(),
'You just joined the Test List mailing list!')
def test_no_welcome_message_to_owners(self):
# Welcome messages go only to mailing list members, not to owners.
add_member(self._mlist, '[email protected]', 'Anne Person',
'password', DeliveryMode.regular, 'xx',
MemberRole.owner)
# There is no welcome message in the virgin queue.
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 0)
def test_no_welcome_message_to_nonmembers(self):
# Welcome messages go only to mailing list members, not to nonmembers.
add_member(self._mlist, '[email protected]', 'Anne Person',
'password', DeliveryMode.regular, 'xx',
MemberRole.nonmember)
# There is no welcome message in the virgin queue.
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 0)
def test_no_welcome_message_to_moderators(self):
# Welcome messages go only to mailing list members, not to moderators.
add_member(self._mlist, '[email protected]', 'Anne Person',
'password', DeliveryMode.regular, 'xx',
MemberRole.moderator)
# There is no welcome message in the virgin queue.
messages = get_queue_messages('virgin')
self.assertEqual(len(messages), 0)
# 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 email address registration."""
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
'TestEmailValidation',
'TestRegistration',
]
import unittest
from zope.component import getUtility
from mailman.app.lifecycle import create_list
from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.pending import IPendings
from mailman.interfaces.registrar import ConfirmationNeededEvent, IRegistrar
from mailman.testing.helpers import event_subscribers
from mailman.testing.layers import ConfigLayer
class TestEmailValidation(unittest.TestCase):
"""Test basic email validation."""
layer = ConfigLayer
def setUp(self):
self.registrar = getUtility(IRegistrar)
self.mlist = create_list('[email protected]')
def test_empty_string_is_invalid(self):
self.assertRaises(InvalidEmailAddressError,
self.registrar.register, self.mlist,
'')
def test_no_spaces_allowed(self):
self.assertRaises(InvalidEmailAddressError,
self.registrar.register, self.mlist,
'some [email protected]')
def test_no_angle_brackets(self):
self.assertRaises(InvalidEmailAddressError,
self.registrar.register, self.mlist,
'<script>@example.com')
def test_ascii_only(self):
self.assertRaises(InvalidEmailAddressError,
self.registrar.register, self.mlist,
'\xa0@example.com')
def test_domain_required(self):
self.assertRaises(InvalidEmailAddressError,
self.registrar.register, self.mlist,
'noatsign')
def test_full_domain_required(self):
self.assertRaises(InvalidEmailAddressError,
self.registrar.register, self.mlist,
'[email protected]')
class TestRegistration(unittest.TestCase):
"""Test registration."""
layer = ConfigLayer
def setUp(self):
self.registrar = getUtility(IRegistrar)
self.mlist = create_list('[email protected]')
def test_confirmation_event_received(self):
# Registering an email address generates an event.
def capture_event(event):
self.assertIsInstance(event, ConfirmationNeededEvent)
with event_subscribers(capture_event):
self.registrar.register(self.mlist, '[email protected]')
def test_event_mlist(self):
# The event has a reference to the mailing list being subscribed to.
def capture_event(event):
self.assertIs(event.mlist, self.mlist)
with event_subscribers(capture_event):
self.registrar.register(self.mlist, '[email protected]')
def test_event_pendable(self):
# The event has an IPendable which contains additional information.
def capture_event(event):
pendable = event.pendable
self.assertEqual(pendable['type'], 'registration')
self.assertEqual(pendable['email'], '[email protected]')
# The key is present, but the value is None.
self.assertIsNone(pendable['display_name'])
# The default is regular delivery.
self.assertEqual(pendable['delivery_mode'], 'regular')
self.assertEqual(pendable['list_id'], 'alpha.example.com')
with event_subscribers(capture_event):
self.registrar.register(self.mlist, '[email protected]')
def test_token(self):
# Registering the email address returns a token, and this token links
# back to the pendable.
captured_events = []
def capture_event(event):
captured_events.append(event)
with event_subscribers(capture_event):
token = self.registrar.register(self.mlist, '[email protected]')
self.assertEqual(len(captured_events), 1)
event = captured_events[0]
self.assertEqual(event.token, token)
pending = getUtility(IPendings).confirm(token)
self.assertEqual(pending, event.pendable)
......@@ -156,7 +156,7 @@ def main():
cannot be run once."""))
parser.add_argument(
'-l', '--list',
default=False, action='store_true',
default=None, action='store_true',
help=_('List the available runner names and exit.'))
parser.add_argument(
'-v', '--verbose',
......
......@@ -55,11 +55,10 @@ class TestConfirm(unittest.TestCase):
def tearDown(self):
reset_the_world()
def test_welcome_message(self):
# A confirmation causes a welcome message to be sent to the member, if
# enabled by the mailing list.
#
status = self._command.process(
self._mlist, Message(), {}, (self._token,), Results())
self.assertEqual(status, ContinueProcessing.yes)
......@@ -68,7 +67,7 @@ class TestConfirm(unittest.TestCase):
self.assertEqual(len(messages), 1)
# Grab the welcome message.
welcome = messages[0].msg
self.assertEqual(welcome['subject'],
self.assertEqual(welcome['subject'],
'Welcome to the "Test" mailing list')
self.assertEqual(welcome['to'], 'Anne Person <[email protected]>')
......
......@@ -115,7 +115,7 @@ def initialize_1(config_path=None):
# write our files. Specifically we must have g+rw and we probably want
# o-rwx although I think in most cases it doesn't hurt if other can read
# or write the files.
os.umask(007)
os.umask(0o007)
# Initialize configuration event subscribers. This must be done before
# setting up the configuration system.
from mailman.app.events import initialize as initialize_events
......
......@@ -10,6 +10,7 @@ acknowledgment.
>>> mlist = create_list('[email protected]')
>>> mlist.display_name = 'Test'
>>> mlist.preferred_language = 'en'
>>> mlist.send_welcome_message = False
>>> # XXX This will almost certainly change once we've worked out the web
>>> # space layout for mailing lists now.
......
......@@ -26,6 +26,7 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'ConfirmationNeededEvent',
'IRegistrar',
]
......@@ -33,6 +34,29 @@ __all__ = [
from zope.interface import Interface
class ConfirmationNeededEvent:
"""Triggered when an address needs confirmation.
Addresses must be verified before they can receive messages or post to
mailing list. When an address is registered with Mailman, via the
`IRegistrar` interface, an `IPendable` is created which represents the
pending registration. This pending registration is stored in the
database, keyed by a token. Then this event is triggered.
There may be several ways to confirm an email address. On some sites,
registration may immediately produce a verification, e.g. because it is on
a known intranet. Or verification may occur via external database lookup
(e.g. LDAP). On most public mailing lists, a mail-back confirmation is
sent to the address, and only if they reply to the mail-back, or click on
an embedded link, is the registered address confirmed.
"""
def __init__(self, mlist, pendable, token):
self.mlist = mlist
self.pendable = pendable
self.token = token
class IRegistrar(Interface):
"""Interface for registering and verifying email addresses and users.
......
......@@ -104,7 +104,7 @@ But this address is waiting for confirmation.
delivery_mode: regular
display_name : Anne Person
email : [email protected]
list_name : [email protected]example.com
list_id : alpha.example.com
type : registration
......
......@@ -17,7 +17,7 @@
"""Basic WSGI Application object for REST server."""
from __future__ import absolute_import, unicode_literals
from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
......
......@@ -10,6 +10,7 @@ starts by a number of messages being posted to the mailing list.
>>> mlist.digest_size_threshold = 0.6
>>> mlist.volume = 1
>>> mlist.next_digest_number = 1
>>> mlist.send_welcome_message = False
>>> from string import Template
>>> process = config.handlers['to-digest'].process
......
......@@ -57,6 +57,7 @@ class TestBounceRunner(unittest.TestCase):
def setUp(self):
self._mlist = create_list('[email protected]')
self._mlist.send_welcome_message = False
self._bounceq = config.switchboards['bounces']
self._runner = make_testable_runner(BounceRunner, 'bounces')
self._anne = getUtility(IUserManager).create_address(
......
......@@ -52,6 +52,7 @@ class TestJoin(unittest.TestCase):
def setUp(self):
self._mlist = create_list('[email protected]')
self._mlist.send_welcome_message = False
self._commandq = config.switchboards['command']
self._runner = make_testable_runner(CommandRunner, 'command')
......
......@@ -327,6 +327,8 @@ def call_api(url, data=None, method=None, username=None, password=None):
else:
method = 'POST'
method = method.upper()
if method in ('POST', 'PUT', 'PATCH') and data is None:
data = urlencode({}, doseq=True)
basic_auth = '{0}:{1}'.format(
(config.webservice.admin_user if username is None else username),
(config.webservice.admin_pass if password is None else password))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment