summaryrefslogtreecommitdiff
path: root/src/mailman/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/app')
-rw-r--r--src/mailman/app/bounces.py2
-rw-r--r--src/mailman/app/docs/bans.rst167
-rw-r--r--src/mailman/app/lifecycle.py10
-rw-r--r--src/mailman/app/membership.py25
-rw-r--r--src/mailman/app/moderator.py41
-rw-r--r--src/mailman/app/notifications.py36
-rw-r--r--src/mailman/app/registrar.py4
-rw-r--r--src/mailman/app/tests/__init__.py0
-rw-r--r--src/mailman/app/tests/test_membership.py131
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