diff options
Diffstat (limited to 'src/mailman/app')
| -rw-r--r-- | src/mailman/app/bounces.py | 2 | ||||
| -rw-r--r-- | src/mailman/app/docs/bans.rst | 167 | ||||
| -rw-r--r-- | src/mailman/app/lifecycle.py | 10 | ||||
| -rw-r--r-- | src/mailman/app/membership.py | 25 | ||||
| -rw-r--r-- | src/mailman/app/moderator.py | 41 | ||||
| -rw-r--r-- | src/mailman/app/notifications.py | 36 | ||||
| -rw-r--r-- | src/mailman/app/registrar.py | 4 | ||||
| -rw-r--r-- | src/mailman/app/tests/__init__.py | 0 | ||||
| -rw-r--r-- | src/mailman/app/tests/test_membership.py | 131 |
9 files changed, 359 insertions, 57 deletions
diff --git a/src/mailman/app/bounces.py b/src/mailman/app/bounces.py index 7504c441b..7f6507a14 100644 --- a/src/mailman/app/bounces.py +++ b/src/mailman/app/bounces.py @@ -30,11 +30,11 @@ import logging from email.mime.message import MIMEMessage from email.mime.text import MIMEText -from mailman.Utils import oneline from mailman.app.finder import find_components from mailman.core.i18n import _ from mailman.email.message import UserNotification from mailman.interfaces.bounce import IBounceDetector +from mailman.utilities.string import oneline log = logging.getLogger('mailman.config') diff --git a/src/mailman/app/docs/bans.rst b/src/mailman/app/docs/bans.rst new file mode 100644 index 000000000..bb6d5902e --- /dev/null +++ b/src/mailman/app/docs/bans.rst @@ -0,0 +1,167 @@ +======================= +Banning email addresses +======================= + +Email addresses can be banned from ever subscribing, either to a specific +mailing list or globally within the Mailman system. Both explicit email +addresses and email address patterns can be banned. + +Bans are managed through the `Ban Manager`. + + >>> from zope.component import getUtility + >>> from mailman.interfaces.bans import IBanManager + >>> ban_manager = getUtility(IBanManager) + +At first, no email addresses are banned, either globally... + + >>> ban_manager.is_banned('anne@example.com') + False + +...or for a specific mailing list. + + >>> ban_manager.is_banned('bart@example.com', 'test@example.com') + False + + +Specific bans +============= + +An email address can be banned from a specific mailing list by adding a ban to +the ban manager. + + >>> ban_manager.ban('cris@example.com', 'test@example.com') + >>> ban_manager.is_banned('cris@example.com', 'test@example.com') + True + >>> ban_manager.is_banned('bart@example.com', 'test@example.com') + False + +However, this is not a global ban. + + >>> ban_manager.is_banned('cris@example.com') + False + + +Global bans +=========== + +An email address can be banned globally, so that it cannot be subscribed to +any mailing list. + + >>> ban_manager.ban('dave@example.com') + +Dave is banned from the test mailing list... + + >>> ban_manager.is_banned('dave@example.com', 'test@example.com') + True + +...and the sample mailing list. + + >>> ban_manager.is_banned('dave@example.com', 'sample@example.com') + True + +Dave is also banned globally. + + >>> ban_manager.is_banned('dave@example.com') + True + +Cris however is not banned globally. + + >>> ban_manager.is_banned('cris@example.com') + False + +Even though Cris is not banned globally, we can add a global ban for her. + + >>> ban_manager.ban('cris@example.com') + >>> ban_manager.is_banned('cris@example.com') + True + +Cris is obviously still banned from specific mailing lists. + + >>> ban_manager.is_banned('cris@example.com', 'test@example.com') + True + >>> ban_manager.is_banned('cris@example.com', 'sample@example.com') + True + +We can remove the global ban to once again just ban her address from the test +list. + + >>> ban_manager.unban('cris@example.com') + >>> ban_manager.is_banned('cris@example.com', 'test@example.com') + True + >>> ban_manager.is_banned('cris@example.com', 'sample@example.com') + False + + +Regular expression bans +======================= + +Entire email address patterns can be banned, both for a specific mailing list +and globally, just as specific addresses can be banned. Use this for example, +when an entire domain is a spam faucet. When using a pattern, the email +address must start with a caret (^). + + >>> ban_manager.ban('^.*@example.org', 'test@example.com') + +Now, no one from example.org can subscribe to the test list. + + >>> ban_manager.is_banned('elle@example.org', 'test@example.com') + True + >>> ban_manager.is_banned('eperson@example.org', 'test@example.com') + True + >>> ban_manager.is_banned('elle@example.com', 'test@example.com') + False + +They are not, however banned globally. + + >>> ban_manager.is_banned('elle@example.org', 'sample@example.com') + False + >>> ban_manager.is_banned('elle@example.org') + False + +Of course, we can ban everyone from example.org globally too. + + >>> ban_manager.ban('^.*@example.org') + >>> ban_manager.is_banned('elle@example.org', 'sample@example.com') + True + >>> ban_manager.is_banned('elle@example.org') + True + +We can remove the mailing list ban on the pattern, though the global ban will +still be in place. + + >>> ban_manager.unban('^.*@example.org', 'test@example.com') + >>> ban_manager.is_banned('elle@example.org', 'test@example.com') + True + >>> ban_manager.is_banned('elle@example.org', 'sample@example.com') + True + >>> ban_manager.is_banned('elle@example.org') + True + +But once the global ban is removed, everyone from example.org can subscribe to +the mailing lists. + + >>> ban_manager.unban('^.*@example.org') + >>> ban_manager.is_banned('elle@example.org', 'test@example.com') + False + >>> ban_manager.is_banned('elle@example.org', 'sample@example.com') + False + >>> ban_manager.is_banned('elle@example.org') + False + + +Adding and removing bans +======================== + +It is not an error to add a ban more than once. These are just ignored. + + >>> ban_manager.ban('fred@example.com', 'test@example.com') + >>> ban_manager.ban('fred@example.com', 'test@example.com') + >>> ban_manager.is_banned('fred@example.com', 'test@example.com') + True + +Nor is it an error to remove a ban more than once. + + >>> ban_manager.unban('fred@example.com', 'test@example.com') + >>> ban_manager.unban('fred@example.com', 'test@example.com') + >>> ban_manager.is_banned('fred@example.com', 'test@example.com') + False diff --git a/src/mailman/app/lifecycle.py b/src/mailman/app/lifecycle.py index 39ec09aa7..b30266f3b 100644 --- a/src/mailman/app/lifecycle.py +++ b/src/mailman/app/lifecycle.py @@ -33,7 +33,7 @@ import logging from zope.component import getUtility from mailman.config import config -from mailman.email.validate import validate +from mailman.interfaces.address import IEmailValidator from mailman.interfaces.domain import ( BadDomainSpecificationError, IDomainManager) from mailman.interfaces.listmanager import IListManager @@ -58,13 +58,15 @@ def create_list(fqdn_listname, owners=None): :type owners: list of string email addresses :return: The new mailing list. :rtype: `IMailingList` - :raises `BadDomainSpecificationError`: when the hostname part of + :raises BadDomainSpecificationError: when the hostname part of `fqdn_listname` does not exist. - :raises `ListAlreadyExistsError`: when the mailing list already exists. + :raises ListAlreadyExistsError: when the mailing list already exists. + :raises InvalidEmailAddressError: when the fqdn email address is invalid. """ if owners is None: owners = [] - validate(fqdn_listname) + # This raises I + getUtility(IEmailValidator).validate(fqdn_listname) # pylint: disable-msg=W0612 listname, domain = fqdn_listname.split('@', 1) if domain not in getUtility(IDomainManager): diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py index 8ea8769a6..fcbedc2f5 100644 --- a/src/mailman/app/membership.py +++ b/src/mailman/app/membership.py @@ -29,15 +29,16 @@ __all__ = [ from email.utils import formataddr from zope.component import getUtility -from mailman import Utils from mailman.app.notifications import send_goodbye_message from mailman.core.i18n import _ from mailman.email.message import OwnerNotification -from mailman.email.validate import validate +from mailman.interfaces.address import IEmailValidator +from mailman.interfaces.bans import IBanManager from mailman.interfaces.member import ( AlreadySubscribedError, MemberRole, MembershipIsBannedError, NotAMemberError) from mailman.interfaces.usermanager import IUserManager +from mailman.utilities.i18n import make @@ -67,14 +68,12 @@ def add_member(mlist, email, realname, password, delivery_mode, language): :raises MembershipIsBannedError: if the membership is not allowed. """ # Let's be extra cautious. - validate(email) + getUtility(IEmailValidator).validate(email) if mlist.members.get_member(email) is not None: raise AlreadySubscribedError( mlist.fqdn_listname, email, MemberRole.member) - # Check for banned email addresses here too for administrative mass - # subscribes and confirmations. - pattern = Utils.get_pattern(email, mlist.ban_list) - if pattern: + # Check to see if the email address is banned. + if getUtility(IBanManager).is_banned(email, mlist.fqdn_listname): raise MembershipIsBannedError(mlist, email) # See if there's already a user linked with the given address. user_manager = getUtility(IUserManager) @@ -104,7 +103,7 @@ def add_member(mlist, email, realname, password, delivery_mode, language): else: # The user exists and is linked to the address. for address in user.addresses: - if address.email == address: + if address.email == email: break else: raise AssertionError( @@ -150,10 +149,10 @@ def delete_member(mlist, address, admin_notif=None, userack=None): user = getUtility(IUserManager).get_user(address) realname = user.real_name subject = _('$mlist.real_name unsubscription notification') - text = Utils.maketext( - 'adminunsubscribeack.txt', - {'listname': mlist.real_name, - 'member' : formataddr((realname, address)), - }, mlist=mlist) + text = make('adminunsubscribeack.txt', + mailing_list=mlist, + listname=mlist.real_name, + member=formataddr((realname, address)), + ) msg = OwnerNotification(mlist, subject, text) msg.send(mlist) diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py index cdfedd44b..a2f838934 100644 --- a/src/mailman/app/moderator.py +++ b/src/mailman/app/moderator.py @@ -35,7 +35,6 @@ from datetime import datetime from email.utils import formataddr, formatdate, getaddresses, make_msgid from zope.component import getUtility -from mailman import Utils from mailman.app.membership import add_member, delete_member from mailman.app.notifications import ( send_admin_subscription_notice, send_welcome_message) @@ -48,6 +47,7 @@ from mailman.interfaces.member import ( AlreadySubscribedError, DeliveryMode, NotAMemberError) from mailman.interfaces.messages import IMessageStore from mailman.interfaces.requests import IRequests, RequestType +from mailman.utilities.i18n import make NL = '\n' @@ -209,12 +209,12 @@ def hold_subscription(mlist, address, realname, password, mode, language): if mlist.admin_immed_notify: subject = _( 'New subscription request to list $mlist.real_name from $address') - text = Utils.maketext( - 'subauth.txt', - {'username' : address, - 'listname' : mlist.fqdn_listname, - 'admindb_url': mlist.script_url('admindb'), - }, mlist=mlist) + text = make('subauth.txt', + mailing_list=mlist, + username=address, + listname=mlist.fqdn_listname, + admindb_url=mlist.script_url('admindb'), + ) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. msg = UserNotification( @@ -281,12 +281,12 @@ def hold_unsubscription(mlist, address): if mlist.admin_immed_notify: subject = _( 'New unsubscription request from $mlist.real_name by $address') - text = Utils.maketext( - 'unsubauth.txt', - {'address' : address, - 'listname' : mlist.fqdn_listname, - 'admindb_url': mlist.script_url('admindb'), - }, mlist=mlist) + text = make('unsubauth.txt', + mailing_list=mlist, + address=address, + listname=mlist.fqdn_listname, + admindb_url=mlist.script_url('admindb'), + ) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. msg = UserNotification( @@ -336,13 +336,14 @@ def _refuse(mlist, request, recip, comment, origmsg=None, lang=None): lang = (mlist.preferred_language if member is None else member.preferred_language) - text = Utils.maketext( - 'refuse.txt', - {'listname' : mlist.fqdn_listname, - 'request' : request, - 'reason' : comment, - 'adminaddr': mlist.owner_address, - }, lang=lang.code, mlist=mlist) + text = make('refuse.txt', + mailing_list=mlist, + language=lang.code, + listname=mlist.fqdn_listname, + request=request, + reason=comment, + 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 985f4eece..8bfbd0934 100644 --- a/src/mailman/app/notifications.py +++ b/src/mailman/app/notifications.py @@ -30,11 +30,12 @@ __all__ = [ from email.utils import formataddr from lazr.config import as_boolean -from mailman import Utils 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.utilities.i18n import make +from mailman.utilities.string import wrap @@ -54,7 +55,7 @@ def send_welcome_message(mlist, address, language, delivery_mode, text=''): :type delivery_mode: DeliveryMode """ if mlist.welcome_msg: - welcome = Utils.wrap(mlist.welcome_msg) + '\n' + welcome = wrap(mlist.welcome_msg) + '\n' else: welcome = '' # Find the IMember object which is subscribed to the mailing list, because @@ -62,15 +63,16 @@ def send_welcome_message(mlist, address, language, delivery_mode, text=''): member = mlist.members.get_member(address) options_url = member.options_url # Get the text from the template. - text += Utils.maketext( - 'subscribeack.txt', { - 'real_name' : mlist.real_name, - 'posting_address' : mlist.fqdn_listname, - 'listinfo_url' : mlist.script_url('listinfo'), - 'optionsurl' : options_url, - 'request_address' : mlist.request_address, - 'welcome' : welcome, - }, lang=language.code, mlist=mlist) + text += make('subscribeack.txt', + mailing_list=mlist, + language=language.code, + real_name=mlist.real_name, + posting_address=mlist.fqdn_listname, + listinfo_url=mlist.script_url('listinfo'), + optionsurl=options_url, + request_address=mlist.request_address, + welcome=welcome, + ) if delivery_mode is not DeliveryMode.regular: digmode = _(' (Digest mode)') else: @@ -98,7 +100,7 @@ def send_goodbye_message(mlist, address, language): :type language: string """ if mlist.goodbye_msg: - goodbye = Utils.wrap(mlist.goodbye_msg) + '\n' + goodbye = wrap(mlist.goodbye_msg) + '\n' else: goodbye = '' msg = UserNotification( @@ -124,10 +126,10 @@ def send_admin_subscription_notice(mlist, address, full_name, language): with _.using(mlist.preferred_language.code): subject = _('$mlist.real_name subscription notification') full_name = full_name.encode(language.charset, 'replace') - text = Utils.maketext( - 'adminsubscribeack.txt', - {'listname' : mlist.real_name, - 'member' : formataddr((full_name, address)), - }, mlist=mlist) + text = make('adminsubscribeack.txt', + mailing_list=mlist, + listname=mlist.real_name, + member=formataddr((full_name, address)), + ) msg = OwnerNotification(mlist, subject, text) msg.send(mlist) diff --git a/src/mailman/app/registrar.py b/src/mailman/app/registrar.py index 181d48126..ec899237a 100644 --- a/src/mailman/app/registrar.py +++ b/src/mailman/app/registrar.py @@ -33,7 +33,7 @@ from zope.interface import implements from mailman.core.i18n import _ from mailman.email.message import UserNotification -from mailman.email.validate import validate +from mailman.interfaces.address import IEmailValidator from mailman.interfaces.listmanager import IListManager from mailman.interfaces.member import MemberRole from mailman.interfaces.pending import IPendable, IPendings @@ -57,7 +57,7 @@ class Registrar: """See `IUserRegistrar`.""" # First, do validation on the email address. If the address is # invalid, it will raise an exception, otherwise it just returns. - validate(email) + getUtility(IEmailValidator).validate(email) # Create a pendable for the registration. pendable = PendableRegistration( type=PendableRegistration.PEND_KEY, diff --git a/src/mailman/app/tests/__init__.py b/src/mailman/app/tests/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/mailman/app/tests/__init__.py diff --git a/src/mailman/app/tests/test_membership.py b/src/mailman/app/tests/test_membership.py new file mode 100644 index 000000000..b0e1bae5d --- /dev/null +++ b/src/mailman/app/tests/test_membership.py @@ -0,0 +1,131 @@ +# Copyright (C) 2011 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/>. + +"""Tests of application level membership functions.""" + +from __future__ import absolute_import, unicode_literals + +__metaclass__ = type +__all__ = [ + 'test_suite', + ] + + +import unittest + +from zope.component import getUtility + +from mailman.app.lifecycle import create_list +from mailman.app.membership import add_member +from mailman.core.constants import system_preferences +from mailman.interfaces.bans import IBanManager +from mailman.interfaces.member import DeliveryMode, MembershipIsBannedError +from mailman.interfaces.usermanager import IUserManager +from mailman.testing.helpers import reset_the_world +from mailman.testing.layers import ConfigLayer + + + +class AddMemberTest(unittest.TestCase): + layer = ConfigLayer + + def setUp(self): + self._mlist = create_list('test@example.com') + + def tearDown(self): + reset_the_world() + + def test_add_member_new_user(self): + # Test subscribing a user to a mailing list when the email address has + # not yet been associated with a user. + member = add_member(self._mlist, 'aperson@example.com', + 'Anne Person', '123', DeliveryMode.regular, + system_preferences.preferred_language) + self.assertEqual(member.address.email, 'aperson@example.com') + self.assertEqual(member.mailing_list, 'test@example.com') + + def test_add_member_existing_user(self): + # Test subscribing a user to a mailing list when the email address has + # already been associated with a user. + user_manager = getUtility(IUserManager) + user_manager.create_user('aperson@example.com', 'Anne Person') + member = add_member(self._mlist, 'aperson@example.com', + 'Anne Person', '123', DeliveryMode.regular, + system_preferences.preferred_language) + self.assertEqual(member.address.email, 'aperson@example.com') + self.assertEqual(member.mailing_list, 'test@example.com') + + def test_add_member_banned(self): + # Test that members who are banned by specific address cannot + # subscribe to the mailing list. + getUtility(IBanManager).ban('anne@example.com', 'test@example.com') + self.assertRaises( + MembershipIsBannedError, + add_member, self._mlist, 'anne@example.com', 'Anne Person', + '123', DeliveryMode.regular, system_preferences.preferred_language) + + def test_add_member_globally_banned(self): + # Test that members who are banned by specific address cannot + # subscribe to the mailing list. + getUtility(IBanManager).ban('anne@example.com') + self.assertRaises( + MembershipIsBannedError, + add_member, self._mlist, 'anne@example.com', 'Anne Person', + '123', DeliveryMode.regular, system_preferences.preferred_language) + + def test_add_member_banned_from_different_list(self): + # Test that members who are banned by specific address cannot + # subscribe to the mailing list. + getUtility(IBanManager).ban('anne@example.com', 'sample@example.com') + member = add_member(self._mlist, 'anne@example.com', + 'Anne Person', '123', DeliveryMode.regular, + system_preferences.preferred_language) + self.assertEqual(member.address.email, 'anne@example.com') + + def test_add_member_banned_by_pattern(self): + # Test that members who are banned by specific address cannot + # subscribe to the mailing list. + getUtility(IBanManager).ban('^.*@example.com', 'test@example.com') + self.assertRaises( + MembershipIsBannedError, + add_member, self._mlist, 'anne@example.com', 'Anne Person', + '123', DeliveryMode.regular, system_preferences.preferred_language) + + def test_add_member_globally_banned_by_pattern(self): + # Test that members who are banned by specific address cannot + # subscribe to the mailing list. + getUtility(IBanManager).ban('^.*@example.com') + self.assertRaises( + MembershipIsBannedError, + add_member, self._mlist, 'anne@example.com', 'Anne Person', + '123', DeliveryMode.regular, system_preferences.preferred_language) + + def test_add_member_banned_from_different_list_by_pattern(self): + # Test that members who are banned by specific address cannot + # subscribe to the mailing list. + getUtility(IBanManager).ban('^.*@example.com', 'sample@example.com') + member = add_member(self._mlist, 'anne@example.com', + 'Anne Person', '123', DeliveryMode.regular, + system_preferences.preferred_language) + self.assertEqual(member.address.email, 'anne@example.com') + + + +def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(AddMemberTest)) + return suite |
