summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/app/membership.py36
-rw-r--r--src/mailman/app/tests/test_membership.py26
-rw-r--r--src/mailman/bin/docs/master.rst8
-rw-r--r--src/mailman/bin/mailman.py2
-rw-r--r--src/mailman/bin/runner.py21
-rw-r--r--src/mailman/commands/cli_inject.py2
-rw-r--r--src/mailman/commands/cli_status.py2
-rw-r--r--src/mailman/commands/cli_withlist.py2
-rw-r--r--src/mailman/commands/docs/conf.rst4
-rw-r--r--src/mailman/commands/docs/members.rst4
-rw-r--r--src/mailman/commands/tests/test_create.py4
-rw-r--r--src/mailman/config/config.py8
-rw-r--r--src/mailman/config/schema.cfg8
-rw-r--r--src/mailman/core/logging.py4
-rw-r--r--src/mailman/docs/8-miles-high.rst6
-rw-r--r--src/mailman/docs/NEWS.rst51
-rw-r--r--src/mailman/docs/START.rst7
-rw-r--r--src/mailman/rest/tests/test_membership.py44
18 files changed, 157 insertions, 82 deletions
diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py
index c50169a7c..996493dc4 100644
--- a/src/mailman/app/membership.py
+++ b/src/mailman/app/membership.py
@@ -32,7 +32,8 @@ from mailman.core.i18n import _
from mailman.email.message import OwnerNotification
from mailman.interfaces.bans import IBanManager
from mailman.interfaces.member import (
- MemberRole, MembershipIsBannedError, NotAMemberError, SubscriptionEvent)
+ AlreadySubscribedError, MemberRole, MembershipIsBannedError,
+ NotAMemberError, SubscriptionEvent)
from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.i18n import make
from zope.component import getUtility
@@ -96,15 +97,32 @@ def add_member(mlist, email, display_name, password, delivery_mode, language,
member = mlist.subscribe(address, role)
member.preferences.delivery_mode = delivery_mode
else:
- # The user exists and is linked to the address.
+ # The user exists and is linked to the case-insensitive address.
+ # We're looking for two versions of the email address, the case
+ # preserved version and the case insensitive version. We'll
+ # subscribe the version with matching case if it exists, otherwise
+ # we'll use one of the matching case-insensitively ones. It's
+ # undefined which one we pick.
+ case_preserved = None
+ case_insensitive = None
for address in user.addresses:
- if address.email == email:
- break
- else:
- raise AssertionError(
- 'User should have had linked address: {0}'.format(address))
- # Create the member and set the appropriate preferences.
- member = mlist.subscribe(address, role)
+ if address.original_email == email:
+ case_preserved = address
+ if address.email == email.lower():
+ case_insensitive = address
+ assert case_preserved is not None or case_insensitive is not None, (
+ 'Could not find a linked address for: {}'.format(email))
+ address = (case_preserved if case_preserved is not None
+ else case_insensitive)
+ # Create the member and set the appropriate preferences. It's
+ # possible we're subscribing the lower cased version of the address;
+ # if that's already subscribed re-issue the exception with the correct
+ # email address (i.e. the one passed in here).
+ try:
+ member = mlist.subscribe(address, role)
+ except AlreadySubscribedError as error:
+ raise AlreadySubscribedError(
+ error.fqdn_listname, email, error.role)
member.preferences.preferred_language = language
member.preferences.delivery_mode = delivery_mode
return member
diff --git a/src/mailman/app/tests/test_membership.py b/src/mailman/app/tests/test_membership.py
index 9b42c21d6..cdf0641ea 100644
--- a/src/mailman/app/tests/test_membership.py
+++ b/src/mailman/app/tests/test_membership.py
@@ -166,6 +166,32 @@ class TestAddMember(unittest.TestCase):
self.assertEqual(member_1.role, MemberRole.member)
self.assertEqual(member_2.role, MemberRole.owner)
+ def test_add_member_with_mixed_case_email(self):
+ # LP: #1425359 - Mailman is case-perserving, case-insensitive. This
+ # test subscribes the lower case address and ensures the original
+ # mixed case address can't be subscribed.
+ email = 'APerson@example.com'
+ add_member(self._mlist, email.lower(), 'Ann Person', '123',
+ DeliveryMode.regular, system_preferences.preferred_language)
+ with self.assertRaises(AlreadySubscribedError) as cm:
+ add_member(self._mlist, email, 'Ann Person', '123',
+ DeliveryMode.regular,
+ system_preferences.preferred_language)
+ self.assertEqual(cm.exception.email, email)
+
+ def test_add_member_with_lower_case_email(self):
+ # LP: #1425359 - Mailman is case-perserving, case-insensitive. This
+ # test subscribes the mixed case address and ensures the lower cased
+ # address can't be added.
+ email = 'APerson@example.com'
+ add_member(self._mlist, email, 'Ann Person', '123',
+ DeliveryMode.regular, system_preferences.preferred_language)
+ with self.assertRaises(AlreadySubscribedError) as cm:
+ add_member(self._mlist, email.lower(), 'Ann Person', '123',
+ DeliveryMode.regular,
+ system_preferences.preferred_language)
+ self.assertEqual(cm.exception.email, email.lower())
+
class TestAddMemberPassword(unittest.TestCase):
diff --git a/src/mailman/bin/docs/master.rst b/src/mailman/bin/docs/master.rst
index 02c4bd976..5a3a94da6 100644
--- a/src/mailman/bin/docs/master.rst
+++ b/src/mailman/bin/docs/master.rst
@@ -4,10 +4,10 @@ Mailman runner control
Mailman has a number of *runner subprocesses* which perform long-running tasks
such as listening on an LMTP port, processing REST API requests, or processing
-messages in a queue directory. In normal operation, the ``mailman``
-command is used to start, stop and manage the runners. This is just a wrapper
-around the real master watcher, which handles runner starting, stopping,
-exiting, and log file reopening.
+messages in a queue directory. In normal operation, the ``mailman`` command
+is used to start, stop and manage the runners. This is just a wrapper around
+the real master watcher, which handles runner starting, stopping, exiting, and
+log file reopening.
>>> from mailman.testing.helpers import TestableMaster
diff --git a/src/mailman/bin/mailman.py b/src/mailman/bin/mailman.py
index 1cfff19db..3865fef19 100644
--- a/src/mailman/bin/mailman.py
+++ b/src/mailman/bin/mailman.py
@@ -36,7 +36,7 @@ from zope.interface.verify import verifyObject
def main():
- """mailman"""
+ """The `mailman` command dispatcher."""
# Create the basic parser and add all globally common options.
parser = argparse.ArgumentParser(
description=_("""\
diff --git a/src/mailman/bin/runner.py b/src/mailman/bin/runner.py
index a981694b8..87d11dbe9 100644
--- a/src/mailman/bin/runner.py
+++ b/src/mailman/bin/runner.py
@@ -108,19 +108,18 @@ def main():
description=_("""\
Start a runner
- The runner named on the command line is started, and it can
- either run through its main loop once (for those runners that
- support this) or continuously. The latter is how the master
- runner starts all its subprocesses.
+ The runner named on the command line is started, and it can either run
+ through its main loop once (for those runners that support this) or
+ continuously. The latter is how the master runner starts all its
+ subprocesses.
- -r is required unless -l or -h is given, and its argument must
- be one of the names displayed by the -l switch.
+ -r is required unless -l or -h is given, and its argument must be one
+ of the names displayed by the -l switch.
- Normally, this script should be started from 'mailman
- start'. Running it separately or with -o is generally useful
- only for debugging. When run this way, the environment variable
- $MAILMAN_UNDER_MASTER_CONTROL will be set which subtly changes
- some error handling behavior.
+ Normally, this script should be started from 'mailman start'. Running
+ it separately or with -o is generally useful only for debugging. When
+ run this way, the environment variable $MAILMAN_UNDER_MASTER_CONTROL
+ will be set which subtly changes some error handling behavior.
"""))
parser.add_argument(
'--version',
diff --git a/src/mailman/commands/cli_inject.py b/src/mailman/commands/cli_inject.py
index fa71cbed8..1b7f15f7b 100644
--- a/src/mailman/commands/cli_inject.py
+++ b/src/mailman/commands/cli_inject.py
@@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""mailman inject"""
+"""The `mailman inject` subcommand."""
__all__ = [
'Inject',
diff --git a/src/mailman/commands/cli_status.py b/src/mailman/commands/cli_status.py
index 2e8ff0ba2..7faab7941 100644
--- a/src/mailman/commands/cli_status.py
+++ b/src/mailman/commands/cli_status.py
@@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""mailman status."""
+"""The `mailman status` subcommand."""
__all__ = [
'Status',
diff --git a/src/mailman/commands/cli_withlist.py b/src/mailman/commands/cli_withlist.py
index d5ba35b6e..e3307d7b4 100644
--- a/src/mailman/commands/cli_withlist.py
+++ b/src/mailman/commands/cli_withlist.py
@@ -15,7 +15,7 @@
# You should have received a copy of the GNU General Public License along with
# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
-"""mailman withlist"""
+"""The `mailman shell` subcommand."""
__all__ = [
'Shell',
diff --git a/src/mailman/commands/docs/conf.rst b/src/mailman/commands/docs/conf.rst
index a76691e63..1db85918f 100644
--- a/src/mailman/commands/docs/conf.rst
+++ b/src/mailman/commands/docs/conf.rst
@@ -7,8 +7,8 @@ lets you dump one or more Mailman configuration variables to standard output
or a file.
Mailman's configuration is divided in multiple sections which contain multiple
-key-value pairs. The ``mailman conf`` command allows you to display
-a specific key-value pair, or several key-value pairs.
+key-value pairs. The ``mailman conf`` command allows you to display a
+specific key-value pair, or several key-value pairs.
>>> class FakeArgs:
... key = None
diff --git a/src/mailman/commands/docs/members.rst b/src/mailman/commands/docs/members.rst
index fd1c8b1be..c90418181 100644
--- a/src/mailman/commands/docs/members.rst
+++ b/src/mailman/commands/docs/members.rst
@@ -2,8 +2,8 @@
Managing members
================
-The ``mailman members`` command allows a site administrator to display,
-add, and remove members from a mailing list.
+The ``mailman members`` command allows a site administrator to display, add,
+and remove members from a mailing list.
::
>>> mlist1 = create_list('test1@example.com')
diff --git a/src/mailman/commands/tests/test_create.py b/src/mailman/commands/tests/test_create.py
index a3349fb60..d7e17e5d2 100644
--- a/src/mailman/commands/tests/test_create.py
+++ b/src/mailman/commands/tests/test_create.py
@@ -15,7 +15,7 @@
# 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 `mailman create`."""
+"""Test the `mailman create` subcommand."""
__all__ = [
'TestCreate',
@@ -51,8 +51,6 @@ class FakeParser:
class TestCreate(unittest.TestCase):
- """Test `mailman create`."""
-
layer = ConfigLayer
def setUp(self):
diff --git a/src/mailman/config/config.py b/src/mailman/config/config.py
index 4d0d57cd5..d23bdda13 100644
--- a/src/mailman/config/config.py
+++ b/src/mailman/config/config.py
@@ -149,10 +149,10 @@ class Configuration:
# First, collect all variables in a substitution dictionary. $VAR_DIR
# is taken from the environment or from the configuration file if the
# environment is not set. Because the var_dir setting in the config
- # file could be a relative path, and because 'mailman start'
- # chdirs to $VAR_DIR, without this subprocesses bin/master and
- # bin/runner will create $VAR_DIR hierarchies under $VAR_DIR when that
- # path is relative.
+ # file could be a relative path, and because 'mailman start' chdirs to
+ # $VAR_DIR, without this subprocesses bin/master and bin/runner will
+ # create $VAR_DIR hierarchies under $VAR_DIR when that path is
+ # relative.
var_dir = os.environ.get('MAILMAN_VAR_DIR', category.var_dir)
substitutions = dict(
cwd = os.getcwd(),
diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg
index bd3f4f2dd..f8c3a117e 100644
--- a/src/mailman/config/schema.cfg
+++ b/src/mailman/config/schema.cfg
@@ -66,10 +66,10 @@ filtered_messages_are_preservable: no
[shell]
-# `mailman shell` (also `withlist`) gives you an interactive prompt that
-# you can use to interact with an initialized and configured Mailman system.
-# Use --help for more information. This section allows you to configure
-# certain aspects of this interactive shell.
+# `mailman shell` (also `withlist`) gives you an interactive prompt that you
+# can use to interact with an initialized and configured Mailman system. Use
+# --help for more information. This section allows you to configure certain
+# aspects of this interactive shell.
# Customize the interpreter prompt.
prompt: >>>
diff --git a/src/mailman/core/logging.py b/src/mailman/core/logging.py
index 21c86a725..7c80037f6 100644
--- a/src/mailman/core/logging.py
+++ b/src/mailman/core/logging.py
@@ -103,8 +103,8 @@ def _init_logger(propagate, sub_name, log, logger_config):
# Get settings from log configuration file (or defaults).
log_format = logger_config.format
log_datefmt = logger_config.datefmt
- # Propagation to the root logger is how we handle logging to stderr
- # when the runners are not run as a subprocess of 'mailman start'.
+ # Propagation to the root logger is how we handle logging to stderr when
+ # the runners are not run as a subprocess of 'mailman start'.
log.propagate = (as_boolean(logger_config.propagate)
if propagate is None else propagate)
# Set the logger's level.
diff --git a/src/mailman/docs/8-miles-high.rst b/src/mailman/docs/8-miles-high.rst
index 96ce3c6f8..ae3074e1c 100644
--- a/src/mailman/docs/8-miles-high.rst
+++ b/src/mailman/docs/8-miles-high.rst
@@ -162,9 +162,9 @@ when the Mailman daemon starts, and what queue the Runner manages.
Shell Commands
==============
-`mailman`: This is an ubercommand, with subcommands for all the various
-things admins might want to do, similar to Mailman 2's mailmanctl, but with
-more functionality.
+`mailman`: This is an ubercommand, with subcommands for all the various things
+admins might want to do, similar to Mailman 2's mailmanctl, but with more
+functionality.
`bin/master`: The runner manager: starts, watches, stops the runner
daemons.
diff --git a/src/mailman/docs/NEWS.rst b/src/mailman/docs/NEWS.rst
index 20065b25e..381cd5e03 100644
--- a/src/mailman/docs/NEWS.rst
+++ b/src/mailman/docs/NEWS.rst
@@ -21,6 +21,9 @@ Bugs
(LP: #1418280)
* When deleting a user via REST, make sure all linked addresses are deleted.
Found by Andrew Stuart. (LP: #1419519)
+ * When trying to subscribe an address to a mailing list through the REST API
+ where a case-differing version of the address is already subscribed, return
+ a 409 error instead of a 500 error. Found by Ankush Sharma. (LP: #1425359)
Configuration
-------------
@@ -173,7 +176,7 @@ Configuration
-------------
* Add support for the Exim 4 MTA. [Stephen Turnbull]
* When creating the initial file system layout in ``var``, e.g. via
- ``mailman info``, add an ``var/etc/mailman.cfg`` file if one does not
+ ``bin/mailman info``, add an ``var/etc/mailman.cfg`` file if one does not
already exist. Also, when initializing the system, look for that file as
the configuration file, just after ``./mailman.cfg`` and before
``~/.mailman.cfg``. (LP: #1157861)
@@ -291,10 +294,10 @@ Interfaces
Commands
--------
- * `mailman aliases` loses the `--output`, `--format`, and `--simple`
+ * `bin/mailman aliases` loses the `--output`, `--format`, and `--simple`
arguments, and adds a `--directory` argument. This is necessary to support
the Postfix `relay_domains` support.
- * `mailman start` was passing the wrong relative path to its runner
+ * `bin/mailman start` was passing the wrong relative path to its runner
subprocesses when -C was given. (LP: #982551)
* `bin/runner` command has been simplified and its command line options
reduced. Now, only one `-r/--runner` option may be provided and the
@@ -437,7 +440,7 @@ Bug fixes
(LP: #953497)
* List-Post should be NO when posting is not allowed. (LP: #987563)
* Non-unicode values in msgdata broke pending requests. (LP: #1031391)
- * Show devmode in `mailman info` output. (LP: #1035028)
+ * Show devmode in `bin/mailman info` output. (LP: #1035028)
* Fix residual references to the old `IMailingList` archive variables.
(LP: #1031393)
@@ -546,11 +549,11 @@ Interfaces
Commands
--------
- * IPython support in `mailman shell` contributed by Andrea Crotti.
+ * IPython support in `bin/mailman shell` contributed by Andrea Crotti.
(LP: #949926).
* The `mailman.cfg` configuration file will now automatically be detected if
it exists in an `etc` directory which is a sibling of argv0.
- * `mailman shell` is an alias for `withlist`.
+ * `bin/mailman shell` is an alias for `withlist`.
* The `confirm` email command now properly handles `Re:`-like prefixes, even
if they contain non-ASCII characters. (LP: #685261)
* The `join` email command no longer accepts an `address=` argument. Its
@@ -638,10 +641,10 @@ REST
Commands
--------
* `bin/qrunner` is renamed to `bin/runner`.
- * `mailman aliases` gains `-f` and `-s` options.
- * `mailman create` no longer allows a list to be created with bogus owner
+ * `bin/mailman aliases` gains `-f` and `-s` options.
+ * `bin/mailman create` no longer allows a list to be created with bogus owner
addresses. (LP: #778687)
- * `mailman start --force` option is fixed. (LP: #869317)
+ * `bin/mailman start --force` option is fixed. (LP: #869317)
Documentation
-------------
@@ -715,11 +718,11 @@ Configuration
Commands
--------
- * 'mailman start' does a better job of producing an error when Mailman is
+ * 'bin/mailman start' does a better job of producing an error when Mailman is
already running.
- * 'mailman status' added for providing command line status on the master
+ * 'bin/mailman status' added for providing command line status on the master
queue runner watcher process.
- * 'mailman info' now prints the REST root url and credentials.
+ * 'bin/mailman info' now prints the REST root url and credentials.
* mmsitepass removed; there is no more site password.
REST
@@ -763,8 +766,8 @@ Bugs fixed
Commands
--------
* The functionality of 'bin/list_members' has been moved to
- 'mailman members'.
- * 'mailman info' -v/--verbose output displays the file system
+ 'bin/mailman members'.
+ * 'bin/mailman info' -v/--verbose output displays the file system
layout paths Mailman is currently configured to use.
Configuration
@@ -825,8 +828,8 @@ REST
Commands
--------
- * 'bin/dumpdb' is now 'mailman qfile'
- * 'bin/unshunt' is now 'mailman unshunt'
+ * 'bin/dumpdb' is now 'bin/mailman qfile'
+ * 'bin/unshunt' is now 'bin/mailman unshunt'
* Mailman now properly handles the '-join', '-leave', and '-confirm' email
commands and sub-addresses. '-subscribe' and '-unsubscribe' are aliases
for '-join' and '-leave' respectively.
@@ -850,16 +853,16 @@ Configuration
Commands
--------
- * 'inject' is now 'mailman inject', with some changes
- * 'mailmanctl' is now 'mailman start|stop|reopen|restart'
- * 'mailman version' is added (output same as 'mailman --version')
- * 'mailman members' command line arguments have changed. It also
+ * 'bin/inject' is now 'bin/mailman inject', with some changes
+ * 'bin/mailmanctl' is now 'bin/mailman start|stop|reopen|restart'
+ * 'bin/mailman version' is added (output same as 'bin/mailman --version')
+ * 'bin/mailman members' command line arguments have changed. It also
now ignores blank lines and lines that start with #. It also no longer
quits when it sees an address that's already subscribed.
- * 'bin/withlist' is now 'mailman withlist', and its command line
+ * 'bin/withlist' is now 'bin/mailman withlist', and its command line
arguments have changed.
- * 'mailman lists' command line arguments have changed.
- * 'bin/genaliases' is now 'mailman aliases'
+ * 'bin/mailman lists' command line arguments have changed.
+ * 'bin/genaliases' is now 'bin/mailman aliases'
Architecture
------------
@@ -901,7 +904,7 @@ Configuration
Architecture
------------
- * 'mailman' is a new super-command for managing Mailman from the command
+ * 'bin/mailman' is a new super-command for managing Mailman from the command
line. Some older bin scripts have been converted, with more to come.
* Mailman now has an administrative REST interface which can be used to get
information from and manage Mailman remotely.
diff --git a/src/mailman/docs/START.rst b/src/mailman/docs/START.rst
index c604299e7..3ca4460b4 100644
--- a/src/mailman/docs/START.rst
+++ b/src/mailman/docs/START.rst
@@ -188,10 +188,9 @@ The first existing file found wins.
* ``/etc/mailman.cfg``
* ``argv[0]/../../etc/mailman.cfg``
-Run the ``mailman info`` command to see which configuration file Mailman
-will use, and where it will put its database file. The first time you run
-this, Mailman will also create any necessary run-time directories and log
-files.
+Run the ``mailman info`` command to see which configuration file Mailman will
+use, and where it will put its database file. The first time you run this,
+Mailman will also create any necessary run-time directories and log files.
Try ``mailman --help`` for more details. You can use the commands
``mailman start`` to start the runner subprocess daemons, and of course
diff --git a/src/mailman/rest/tests/test_membership.py b/src/mailman/rest/tests/test_membership.py
index e1bff833b..a77dea3b5 100644
--- a/src/mailman/rest/tests/test_membership.py
+++ b/src/mailman/rest/tests/test_membership.py
@@ -39,6 +39,12 @@ from urllib.error import HTTPError
from zope.component import getUtility
+def _set_preferred(user):
+ preferred = list(user.addresses)[0]
+ preferred.verified_on = now()
+ user.preferred_address = preferred
+
+
class TestMembership(unittest.TestCase):
layer = RESTLayer
@@ -98,6 +104,36 @@ class TestMembership(unittest.TestCase):
self.assertEqual(cm.exception.code, 409)
self.assertEqual(cm.exception.reason, b'Member already subscribed')
+ def test_add_member_with_mixed_case_email(self):
+ # LP: #1425359 - Mailman is case-perserving, case-insensitive. This
+ # test subscribes the lower case address and ensures the original mixed
+ # case address can't be subscribed.
+ with transaction():
+ anne = self._usermanager.create_address('anne@example.com')
+ self._mlist.subscribe(anne)
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/members', {
+ 'list_id': 'test.example.com',
+ 'subscriber': 'ANNE@example.com',
+ })
+ self.assertEqual(cm.exception.code, 409)
+ self.assertEqual(cm.exception.reason, b'Member already subscribed')
+
+ def test_add_member_with_lower_case_email(self):
+ # LP: #1425359 - Mailman is case-perserving, case-insensitive. This
+ # test subscribes the mixed case address and ensures the lower cased
+ # address can't be added.
+ with transaction():
+ anne = self._usermanager.create_address('ANNE@example.com')
+ self._mlist.subscribe(anne)
+ with self.assertRaises(HTTPError) as cm:
+ call_api('http://localhost:9001/3.0/members', {
+ 'list_id': 'test.example.com',
+ 'subscriber': 'anne@example.com',
+ })
+ self.assertEqual(cm.exception.code, 409)
+ self.assertEqual(cm.exception.reason, b'Member already subscribed')
+
def test_join_with_invalid_delivery_mode(self):
with self.assertRaises(HTTPError) as cm:
call_api('http://localhost:9001/3.0/members', {
@@ -129,9 +165,7 @@ class TestMembership(unittest.TestCase):
def test_join_as_user_with_preferred_address(self):
with transaction():
anne = self._usermanager.create_user('anne@example.com')
- preferred = list(anne.addresses)[0]
- preferred.verified_on = now()
- anne.preferred_address = preferred
+ _set_preferred(anne)
self._mlist.subscribe(anne)
content, response = call_api('http://localhost:9001/3.0/members')
self.assertEqual(response.status, 200)
@@ -150,9 +184,7 @@ class TestMembership(unittest.TestCase):
def test_member_changes_preferred_address(self):
with transaction():
anne = self._usermanager.create_user('anne@example.com')
- preferred = list(anne.addresses)[0]
- preferred.verified_on = now()
- anne.preferred_address = preferred
+ _set_preferred(anne)
self._mlist.subscribe(anne)
# Take a look at Anne's current membership.
content, response = call_api('http://localhost:9001/3.0/members')