summaryrefslogtreecommitdiff
path: root/src/mailman/model
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/model')
-rw-r--r--src/mailman/model/address.py12
-rw-r--r--src/mailman/model/autorespond.py10
-rw-r--r--src/mailman/model/bans.py8
-rw-r--r--src/mailman/model/bounce.py8
-rw-r--r--src/mailman/model/digests.py10
-rw-r--r--src/mailman/model/docs/addresses.rst20
-rw-r--r--src/mailman/model/docs/domains.rst14
-rw-r--r--src/mailman/model/docs/languages.rst4
-rw-r--r--src/mailman/model/docs/listmanager.rst16
-rw-r--r--src/mailman/model/docs/mailinglist.rst32
-rw-r--r--src/mailman/model/docs/membership.rst36
-rw-r--r--src/mailman/model/docs/messagestore.rst24
-rw-r--r--src/mailman/model/docs/pending.rst10
-rw-r--r--src/mailman/model/docs/registration.rst35
-rw-r--r--src/mailman/model/docs/usermanager.rst2
-rw-r--r--src/mailman/model/docs/users.rst77
-rw-r--r--src/mailman/model/domain.py12
-rw-r--r--src/mailman/model/language.py8
-rw-r--r--src/mailman/model/listmanager.py8
-rw-r--r--src/mailman/model/mailinglist.py26
-rw-r--r--src/mailman/model/member.py13
-rw-r--r--src/mailman/model/message.py11
-rw-r--r--src/mailman/model/messagestore.py21
-rw-r--r--src/mailman/model/mime.py10
-rw-r--r--src/mailman/model/pending.py52
-rw-r--r--src/mailman/model/preferences.py10
-rw-r--r--src/mailman/model/requests.py24
-rw-r--r--src/mailman/model/roster.py8
-rw-r--r--src/mailman/model/tests/test_address.py25
-rw-r--r--src/mailman/model/tests/test_bounce.py7
-rw-r--r--src/mailman/model/tests/test_domain.py22
-rw-r--r--src/mailman/model/tests/test_listmanager.py31
-rw-r--r--src/mailman/model/tests/test_mailinglist.py45
-rw-r--r--src/mailman/model/tests/test_member.py3
-rw-r--r--src/mailman/model/tests/test_messagestore.py71
-rw-r--r--src/mailman/model/tests/test_registrar.py64
-rw-r--r--src/mailman/model/tests/test_requests.py3
-rw-r--r--src/mailman/model/tests/test_roster.py6
-rw-r--r--src/mailman/model/tests/test_uid.py4
-rw-r--r--src/mailman/model/tests/test_user.py44
-rw-r--r--src/mailman/model/uid.py6
-rw-r--r--src/mailman/model/user.py16
-rw-r--r--src/mailman/model/usermanager.py6
43 files changed, 420 insertions, 454 deletions
diff --git a/src/mailman/model/address.py b/src/mailman/model/address.py
index 5d1994567..5ded77dd8 100644
--- a/src/mailman/model/address.py
+++ b/src/mailman/model/address.py
@@ -17,26 +17,22 @@
"""Model for addresses."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Address',
]
from email.utils import formataddr
+from mailman.database.model import Model
+from mailman.interfaces.address import (
+ AddressVerificationEvent, IAddress, IEmailValidator)
+from mailman.utilities.datetime import now
from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode
from sqlalchemy.orm import relationship, backref
from zope.component import getUtility
from zope.event import notify
from zope.interface import implementer
-from mailman.database.model import Model
-from mailman.interfaces.address import (
- AddressVerificationEvent, IAddress, IEmailValidator)
-from mailman.utilities.datetime import now
-
@implementer(IAddress)
diff --git a/src/mailman/model/autorespond.py b/src/mailman/model/autorespond.py
index cfb9e017d..332d04521 100644
--- a/src/mailman/model/autorespond.py
+++ b/src/mailman/model/autorespond.py
@@ -17,25 +17,21 @@
"""Module stuff."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AutoResponseRecord',
'AutoResponseSet',
]
-from sqlalchemy import Column, Date, ForeignKey, Integer, desc
-from sqlalchemy.orm import relationship
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.database.types import Enum
from mailman.interfaces.autorespond import (
IAutoResponseRecord, IAutoResponseSet, Response)
from mailman.utilities.datetime import today
+from sqlalchemy import Column, Date, ForeignKey, Integer, desc
+from sqlalchemy.orm import relationship
+from zope.interface import implementer
diff --git a/src/mailman/model/bans.py b/src/mailman/model/bans.py
index 8678fc1e7..3ad11cbf6 100644
--- a/src/mailman/model/bans.py
+++ b/src/mailman/model/bans.py
@@ -17,9 +17,6 @@
"""Ban manager."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'BanManager',
]
@@ -27,12 +24,11 @@ __all__ = [
import re
-from sqlalchemy import Column, Integer, Unicode
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.interfaces.bans import IBan, IBanManager
+from sqlalchemy import Column, Integer, Unicode
+from zope.interface import implementer
diff --git a/src/mailman/model/bounce.py b/src/mailman/model/bounce.py
index 26ebbe0c6..585a92594 100644
--- a/src/mailman/model/bounce.py
+++ b/src/mailman/model/bounce.py
@@ -17,9 +17,6 @@
"""Bounce support."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'BounceEvent',
'BounceProcessor',
@@ -27,15 +24,14 @@ __all__ = [
-from sqlalchemy import Boolean, Column, DateTime, Integer, Unicode
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.database.types import Enum
from mailman.interfaces.bounce import (
BounceContext, IBounceEvent, IBounceProcessor)
from mailman.utilities.datetime import now
+from sqlalchemy import Boolean, Column, DateTime, Integer, Unicode
+from zope.interface import implementer
diff --git a/src/mailman/model/digests.py b/src/mailman/model/digests.py
index 7bfd512b6..8e8f7dedd 100644
--- a/src/mailman/model/digests.py
+++ b/src/mailman/model/digests.py
@@ -17,22 +17,18 @@
"""One last digest."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'OneLastDigest',
]
-from sqlalchemy import Column, Integer, ForeignKey
-from sqlalchemy.orm import relationship
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.types import Enum
from mailman.interfaces.digests import IOneLastDigest
from mailman.interfaces.member import DeliveryMode
+from sqlalchemy import Column, Integer, ForeignKey
+from sqlalchemy.orm import relationship
+from zope.interface import implementer
diff --git a/src/mailman/model/docs/addresses.rst b/src/mailman/model/docs/addresses.rst
index 795afe43c..9f34efdec 100644
--- a/src/mailman/model/docs/addresses.rst
+++ b/src/mailman/model/docs/addresses.rst
@@ -205,23 +205,9 @@ case-preserved version are available on attributes of the `IAddress` object.
FPERSON@example.com
Because addresses are case-insensitive for all other purposes, you cannot
-create an address that differs only in case.
-
- >>> user_manager.create_address('fperson@example.com')
- Traceback (most recent call last):
- ...
- ExistingAddressError: FPERSON@example.com
- >>> user_manager.create_address('fperson@EXAMPLE.COM')
- Traceback (most recent call last):
- ...
- ExistingAddressError: FPERSON@example.com
- >>> user_manager.create_address('FPERSON@example.com')
- Traceback (most recent call last):
- ...
- ExistingAddressError: FPERSON@example.com
-
-You can get the address using either the lower cased version or case-preserved
-version. In fact, searching for an address is case insensitive.
+create an address that differs only in case. You can get the address using
+either the lower cased version or case-preserved version. In fact, searching
+for an address is case insensitive.
>>> print(user_manager.get_address('fperson@example.com').email)
fperson@example.com
diff --git a/src/mailman/model/docs/domains.rst b/src/mailman/model/docs/domains.rst
index 153f6c19d..abb594a62 100644
--- a/src/mailman/model/docs/domains.rst
+++ b/src/mailman/model/docs/domains.rst
@@ -108,12 +108,7 @@ In the global domain manager, domains are indexed by their email host name.
base_url: http://lists.example.net,
contact_address: postmaster@example.com>
- >>> print(manager['doesnotexist.com'])
- Traceback (most recent call last):
- ...
- KeyError: u'doesnotexist.com'
-
-As with a dictionary, you can also get the domain. If the domain does not
+As with dictionaries, you can also get the domain. If the domain does not
exist, ``None`` or a default is returned.
::
@@ -128,13 +123,6 @@ exist, ``None`` or a default is returned.
>>> print(manager.get('doesnotexist.com', 'blahdeblah'))
blahdeblah
-Non-existent domains cannot be removed.
-
- >>> manager.remove('doesnotexist.com')
- Traceback (most recent call last):
- ...
- KeyError: u'doesnotexist.com'
-
Confirmation tokens
===================
diff --git a/src/mailman/model/docs/languages.rst b/src/mailman/model/docs/languages.rst
index fedea0e6e..4ed1f8ef2 100644
--- a/src/mailman/model/docs/languages.rst
+++ b/src/mailman/model/docs/languages.rst
@@ -62,7 +62,7 @@ You can iterate over all the known language codes.
>>> mgr.add('pl', 'iso-8859-2', 'Polish')
<Language [pl] Polish>
>>> sorted(mgr.codes)
- [u'en', u'it', u'pl']
+ ['en', 'it', 'pl']
You can iterate over all the known languages.
@@ -89,7 +89,7 @@ You can get a particular language by its code.
>>> print(mgr['xx'].code)
Traceback (most recent call last):
...
- KeyError: u'xx'
+ KeyError: 'xx'
>>> print(mgr.get('it').description)
Italian
>>> print(mgr.get('xx'))
diff --git a/src/mailman/model/docs/listmanager.rst b/src/mailman/model/docs/listmanager.rst
index 151bee1fe..8ff6ad3b0 100644
--- a/src/mailman/model/docs/listmanager.rst
+++ b/src/mailman/model/docs/listmanager.rst
@@ -34,22 +34,6 @@ the mailing list to the system.
>>> print(mlist.list_id)
test.example.com
-If you try to create a mailing list with the same name as an existing list,
-you will get an exception.
-
- >>> list_manager.create('test@example.com')
- Traceback (most recent call last):
- ...
- ListAlreadyExistsError: test@example.com
-
-It is an error to create a mailing list that isn't a fully qualified list name
-(i.e. posting address).
-
- >>> list_manager.create('foo')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: foo
-
Deleting a mailing list
=======================
diff --git a/src/mailman/model/docs/mailinglist.rst b/src/mailman/model/docs/mailinglist.rst
index 3d01710c5..00a01662b 100644
--- a/src/mailman/model/docs/mailinglist.rst
+++ b/src/mailman/model/docs/mailinglist.rst
@@ -114,7 +114,8 @@ Subscribing users
An alternative way of subscribing to a mailing list is as a user with a
preferred address. This way the user can change their subscription address
just by changing their preferred address.
-::
+
+The user must have a preferred address.
>>> from mailman.utilities.datetime import now
>>> user = user_manager.create_user('dperson@example.com', 'Dave Person')
@@ -122,6 +123,8 @@ just by changing their preferred address.
>>> address.verified_on = now()
>>> user.preferred_address = address
+The preferred address is used in the subscription.
+
>>> mlist.subscribe(user)
<Member: Dave Person <dperson@example.com> on aardvark@example.com
as MemberRole.member>
@@ -132,6 +135,10 @@ just by changing their preferred address.
<Member: Dave Person <dperson@example.com> on aardvark@example.com
as MemberRole.member>
+If the user's preferred address changes, their subscribed email address also
+changes automatically.
+::
+
>>> new_address = user.register('dave.person@example.com')
>>> new_address.verified_on = now()
>>> user.preferred_address = new_address
@@ -143,31 +150,12 @@ just by changing their preferred address.
<Member: dave.person@example.com on aardvark@example.com
as MemberRole.member>
-A user is not allowed to subscribe more than once to the mailing list.
-
- >>> mlist.subscribe(user)
- Traceback (most recent call last):
- ...
- AlreadySubscribedError: <User "Dave Person" (1) at ...>
- is already a MemberRole.member of mailing list aardvark@example.com
-
-However, they are allowed to subscribe again with a specific address, even if
-this address is their preferred address.
+A user is allowed to explicitly subscribe again with a specific address, even
+if this address is their preferred address.
>>> mlist.subscribe(user.preferred_address)
<Member: dave.person@example.com
on aardvark@example.com as MemberRole.member>
-A user cannot subscribe to a mailing list without a preferred address.
-
- >>> user = user_manager.create_user('eperson@example.com', 'Elly Person')
- >>> address = list(user.addresses)[0]
- >>> address.verified_on = now()
- >>> mlist.subscribe(user)
- Traceback (most recent call last):
- ...
- MissingPreferredAddressError: User must have a preferred address:
- <User "Elly Person" (2) at ...>
-
.. _`RFC 2369`: http://www.faqs.org/rfcs/rfc2369.html
diff --git a/src/mailman/model/docs/membership.rst b/src/mailman/model/docs/membership.rst
index 2a6c99fc0..60ccd1ac1 100644
--- a/src/mailman/model/docs/membership.rst
+++ b/src/mailman/model/docs/membership.rst
@@ -2,10 +2,10 @@
List memberships
================
-Users represent people in Mailman. Users control email addresses, and rosters
-are collections of members. A member gives an email address a role, such as
-`member`, `administrator`, or `moderator`. Even nonmembers are represented by
-a roster.
+Users represent people in Mailman, members represent subscriptions. Users
+control email addresses, and rosters are collections of members. A member
+ties a subscribed email address to a role, such as `member`, `administrator`,
+or `moderator`. Even non-members are represented by a roster.
Roster sets are collections of rosters and a mailing list has a single roster
set that contains all its members, regardless of that member's role.
@@ -228,18 +228,6 @@ regardless of their role.
fperson@example.com MemberRole.nonmember
-Double subscriptions
-====================
-
-It is an error to subscribe someone to a list with the same role twice.
-
- >>> mlist.subscribe(address_1, MemberRole.owner)
- Traceback (most recent call last):
- ...
- AlreadySubscribedError: aperson@example.com is already a MemberRole.owner
- of mailing list ant@example.com
-
-
Moderation actions
==================
@@ -276,7 +264,7 @@ Changing subscriptions
When a user is subscribed to a mailing list via a specific address they
control (as opposed to being subscribed with their preferred address), they
can change their delivery address by setting the appropriate parameter. Note
-though that the address their changing to must be verified.
+though that the address they're changing to must be verified.
>>> bee = create_list('bee@example.com')
>>> gwen = user_manager.create_user('gwen@example.com')
@@ -290,20 +278,6 @@ Gwen gets a email address.
>>> new_address = gwen.register('gperson@example.com')
-She wants to change her membership in the `test` mailing list to use her new
-address, but the address is not yet verified.
-
- >>> gwen_member.address = new_address
- Traceback (most recent call last):
- ...
- UnverifiedAddressError: gperson@example.com
-
-Her membership has not changed.
-
- >>> for m in bee.members.members:
- ... print(m.member_id.int, m.mailing_list.list_id, m.address.email)
- 7 bee.example.com gwen@example.com
-
Gwen verifies her email address, and updates her membership.
>>> from mailman.utilities.datetime import now
diff --git a/src/mailman/model/docs/messagestore.rst b/src/mailman/model/docs/messagestore.rst
index f2f2ca9d2..933ca5619 100644
--- a/src/mailman/model/docs/messagestore.rst
+++ b/src/mailman/model/docs/messagestore.rst
@@ -6,28 +6,20 @@ The message store is a collection of messages keyed off of ``Message-ID`` and
``X-Message-ID-Hash`` headers. Either of these values can be combined with
the message's ``List-Archive`` header to create a globally unique URI to the
message object in the internet facing interface of the message store. The
-``X-Message-ID-Hash`` is the Base32 SHA1 hash of the ``Message-ID``.
+``X-Message-ID-Hash`` is the base-32 SHA1 hash of the ``Message-ID``.
>>> from mailman.interfaces.messages import IMessageStore
>>> from zope.component import getUtility
>>> message_store = getUtility(IMessageStore)
-If you try to add a message to the store which is missing the ``Message-ID``
-header, you will get an exception.
+A message with a ``Message-ID`` header can be stored.
>>> msg = message_from_string("""\
... Subject: An important message
+ ... Message-ID: <87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>
...
... This message is very important.
... """)
- >>> message_store.add(msg)
- Traceback (most recent call last):
- ...
- ValueError: Exactly one Message-ID header required
-
-However, if the message has a ``Message-ID`` header, it can be stored.
-
- >>> msg['Message-ID'] = '<87myycy5eh.fsf@uwakimon.sk.tsukuba.ac.jp>'
>>> x_message_id_hash = message_store.add(msg)
>>> print(x_message_id_hash)
AGDWSNXXKCWEILKKNYTBOHRDQGOX3Y35
@@ -97,15 +89,7 @@ Deleting messages from the store
================================
You delete a message from the storage service by providing the ``Message-ID``
-for the message you want to delete. If you try to delete a ``Message-ID``
-that isn't in the store, you get an exception.
-
- >>> message_store.delete_message('nothing')
- Traceback (most recent call last):
- ...
- LookupError: nothing
-
-But if you delete an existing message, it really gets deleted.
+for the message you want to delete.
>>> message_id = message['message-id']
>>> message_store.delete_message(message_id)
diff --git a/src/mailman/model/docs/pending.rst b/src/mailman/model/docs/pending.rst
index d8206b264..a634322a1 100644
--- a/src/mailman/model/docs/pending.rst
+++ b/src/mailman/model/docs/pending.rst
@@ -33,12 +33,12 @@ token that can be used in urls and such.
>>> len(token)
40
-There's not much you can do with tokens except to `confirm` them, which
-basically means returning the ``IPendable`` structure (as a dictionary) from
-the database that matches the token. If the token isn't in the database, None
-is returned.
+There's not much you can do with tokens except to *confirm* them, which
+basically means returning the `IPendable` structure (as a dictionary) from the
+database that matches the token. If the token isn't in the database, None is
+returned.
- >>> pendable = pendingdb.confirm(bytes('missing'))
+ >>> pendable = pendingdb.confirm(b'missing')
>>> print(pendable)
None
>>> pendable = pendingdb.confirm(token)
diff --git a/src/mailman/model/docs/registration.rst b/src/mailman/model/docs/registration.rst
index 32ee27316..55e99f23a 100644
--- a/src/mailman/model/docs/registration.rst
+++ b/src/mailman/model/docs/registration.rst
@@ -8,7 +8,7 @@ additional information they may supply. All registered email addresses must
be verified before Mailman will send them any list traffic.
The ``IUserManager`` manages users, but it does so at a fairly low level.
-Specifically, it does not handle verifications, email address syntax validity
+Specifically, it does not handle verification, email address syntax validity
checks, etc. The ``IRegistrar`` is the interface to the object handling all
this stuff.
@@ -19,7 +19,7 @@ this stuff.
Here is a helper function to check the token strings.
>>> def check_token(token):
- ... assert isinstance(token, basestring), 'Not a string'
+ ... assert isinstance(token, str), 'Not a string'
... assert len(token) == 40, 'Unexpected length: %d' % len(token)
... assert token.isalnum(), 'Not alphanumeric'
... print('ok')
@@ -47,31 +47,6 @@ Some amount of sanity checks are performed on the email address, although
honestly, not as much as probably should be done. Still, some patently bad
addresses are rejected outright.
- >>> registrar.register(mlist, '')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError
- >>> registrar.register(mlist, 'some name@example.com')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: some name@example.com
- >>> registrar.register(mlist, '<script>@example.com')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: <script>@example.com
- >>> registrar.register(mlist, '\xa0@example.com')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: \xa0@example.com
- >>> registrar.register(mlist, 'noatsign')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: noatsign
- >>> registrar.register(mlist, 'nodom@ain')
- Traceback (most recent call last):
- ...
- InvalidEmailAddressError: nodom@ain
-
Register an email address
=========================
@@ -149,9 +124,9 @@ message is sent to the user in order to verify the registered address.
<BLANKLINE>
>>> dump_msgdata(items[0].msgdata)
_parsemsg : False
- listname : alpha@example.com
+ listid : alpha.example.com
nodecorate : True
- recipients : set([u'aperson@example.com'])
+ recipients : {'aperson@example.com'}
reduced_list_headers: True
version : 3
@@ -312,7 +287,7 @@ Corner cases
If you try to confirm a token that doesn't exist in the pending database, the
confirm method will just return False.
- >>> registrar.confirm(bytes('no token'))
+ >>> registrar.confirm(bytes(b'no token'))
False
Likewise, if you try to confirm, through the `IUserRegistrar` interface, a
diff --git a/src/mailman/model/docs/usermanager.rst b/src/mailman/model/docs/usermanager.rst
index 9a8c35c00..ba328b54b 100644
--- a/src/mailman/model/docs/usermanager.rst
+++ b/src/mailman/model/docs/usermanager.rst
@@ -44,7 +44,7 @@ A user can be assigned a real name.
A user can be assigned a password.
- >>> user.password = b'secret'
+ >>> user.password = 'secret'
>>> dump_list(user.password for user in user_manager.users)
secret
diff --git a/src/mailman/model/docs/users.rst b/src/mailman/model/docs/users.rst
index 2e7333944..0b926d6a7 100644
--- a/src/mailman/model/docs/users.rst
+++ b/src/mailman/model/docs/users.rst
@@ -20,7 +20,7 @@ User data
Users may have a real name and a password.
>>> user_1 = user_manager.create_user()
- >>> user_1.password = b'my password'
+ >>> user_1.password = 'my password'
>>> user_1.display_name = 'Zoe Person'
>>> dump_list(user.display_name for user in user_manager.users)
Zoe Person
@@ -30,7 +30,7 @@ Users may have a real name and a password.
The password and real name can be changed at any time.
>>> user_1.display_name = 'Zoe X. Person'
- >>> user_1.password = b'another password'
+ >>> user_1.password = 'another password'
>>> dump_list(user.display_name for user in user_manager.users)
Zoe X. Person
>>> dump_list(user.password for user in user_manager.users)
@@ -44,7 +44,7 @@ When the user's password is changed, an event is triggered.
... saved_event = event
>>> from mailman.testing.helpers import event_subscribers
>>> with event_subscribers(save_event):
- ... user_1.password = b'changed again'
+ ... user_1.password = 'changed again'
>>> print(saved_event)
<PasswordChangeEvent Zoe X. Person>
@@ -59,20 +59,13 @@ The event holds a reference to the `IUser` that changed their password.
Basic user identification
=========================
-Although rarely visible to users, every user has a unique ID in Mailman, which
-never changes. This ID is generated randomly at the time the user is created,
-and is represented by a UUID.
+Although rarely visible to users, every user has a unique immutable ID. This
+ID is generated randomly at the time the user is created, and is represented
+by a UUID.
>>> print(user_1.user_id)
00000000-0000-0000-0000-000000000001
-The user id cannot change.
-
- >>> user_1.user_id = 'foo'
- Traceback (most recent call last):
- ...
- AttributeError: can't set attribute
-
User records also have a date on which they where created.
# The test suite uses a predictable timestamp.
@@ -84,8 +77,8 @@ Users addresses
===============
One of the pieces of information that a user links to is a set of email
-addresses they control, in the form of IAddress objects. A user can control
-many addresses, but addresses may be controlled by only one user.
+addresses they control, in the form of ``IAddress`` objects. A user can
+control many addresses, but addresses may be linked to only one user.
The easiest way to link a user to an address is to just register the new
address on a user object.
@@ -114,14 +107,6 @@ You can also create the address separately and then link it to the user.
<BLANKLINE>
Zoe Person
-But don't try to link an address to more than one user.
-
- >>> another_user = user_manager.create_user()
- >>> another_user.link(address_1)
- Traceback (most recent call last):
- ...
- AddressAlreadyLinkedError: zperson@example.net
-
You can also ask whether a given user controls a given address.
>>> user_1.controls(address_1.email)
@@ -149,17 +134,6 @@ Addresses can also be unlinked from a user.
>>> print(user_manager.get_user('aperson@example.net'))
None
-But don't try to unlink the address from a user it's not linked to.
-
- >>> user_1.unlink(address_1)
- Traceback (most recent call last):
- ...
- AddressNotLinkedError: zperson@example.net
- >>> another_user.unlink(address_1)
- Traceback (most recent call last):
- ...
- AddressNotLinkedError: zperson@example.net
-
Preferred address
=================
@@ -183,20 +157,10 @@ preferred address.
>>> print(user_2.preferred_address)
None
-The preferred address must be explicitly registered, however only verified
-address may be registered as preferred.
-
- >>> anne
- <Address: Anne Person <anne@example.com> [not verified] at ...>
- >>> user_2.preferred_address = anne
- Traceback (most recent call last):
- ...
- UnverifiedAddressError: Anne Person <anne@example.com>
-
-Once the address has been verified though, it can be set as the preferred
-address, but only if the address is either controlled by the user or
-uncontrolled. In the latter case, setting it as the preferred address makes
-it controlled by the user.
+Once the address has been verified, it can be set as the preferred address,
+but only if the address is either controlled by the user or uncontrolled. In
+the latter case, setting it as the preferred address makes it controlled by
+the user.
::
>>> from mailman.utilities.datetime import now
@@ -217,17 +181,6 @@ it controlled by the user.
>>> user_2.controls(aperson.email)
True
- >>> zperson = user_manager.get_address('zperson@example.com')
- >>> zperson.verified_on = now()
- >>> user_2.controls(zperson.email)
- False
- >>> user_1.controls(zperson.email)
- True
- >>> user_2.preferred_address = zperson
- Traceback (most recent call last):
- ...
- AddressAlreadyLinkedError: Zoe Person <zperson@example.com>
-
A user can disavow their preferred address.
>>> user_2.preferred_address
@@ -328,11 +281,11 @@ membership role.
>>> from zope.interface.verify import verifyObject
>>> verifyObject(IRoster, memberships)
True
- >>> members = sorted(memberships.members)
- >>> len(members)
- 4
>>> def sortkey(member):
... return member.address.email, member.mailing_list, member.role.value
+ >>> members = sorted(memberships.members, key=sortkey)
+ >>> len(members)
+ 4
>>> for member in sorted(members, key=sortkey):
... print(member.address.email, member.mailing_list.list_id,
... member.role)
diff --git a/src/mailman/model/domain.py b/src/mailman/model/domain.py
index 8290cb755..b9d2c88ab 100644
--- a/src/mailman/model/domain.py
+++ b/src/mailman/model/domain.py
@@ -17,26 +17,22 @@
"""Domains."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Domain',
'DomainManager',
]
-from sqlalchemy import Column, Integer, Unicode
-from urlparse import urljoin, urlparse
-from zope.event import notify
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.interfaces.domain import (
BadDomainSpecificationError, DomainCreatedEvent, DomainCreatingEvent,
DomainDeletedEvent, DomainDeletingEvent, IDomain, IDomainManager)
from mailman.model.mailinglist import MailingList
+from six.moves.urllib_parse import urljoin, urlparse
+from sqlalchemy import Column, Integer, Unicode
+from zope.event import notify
+from zope.interface import implementer
diff --git a/src/mailman/model/language.py b/src/mailman/model/language.py
index f4d48fc97..7317b6328 100644
--- a/src/mailman/model/language.py
+++ b/src/mailman/model/language.py
@@ -17,19 +17,15 @@
"""Model for languages."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Language',
]
-from sqlalchemy import Column, Integer, Unicode
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.interfaces.languages import ILanguage
+from sqlalchemy import Column, Integer, Unicode
+from zope.interface import implementer
diff --git a/src/mailman/model/listmanager.py b/src/mailman/model/listmanager.py
index 261490a92..7c228bcb9 100644
--- a/src/mailman/model/listmanager.py
+++ b/src/mailman/model/listmanager.py
@@ -17,17 +17,11 @@
"""A mailing list manager."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ListManager',
]
-from zope.event import notify
-from zope.interface import implementer
-
from mailman.database.transaction import dbconnection
from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.listmanager import (
@@ -36,6 +30,8 @@ from mailman.interfaces.listmanager import (
from mailman.model.mailinglist import MailingList
from mailman.model.mime import ContentFilter
from mailman.utilities.datetime import now
+from zope.event import notify
+from zope.interface import implementer
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py
index 761a78b94..ea3317bb6 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -17,9 +17,6 @@
"""Model for mailing lists."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MailingList',
]
@@ -27,16 +24,6 @@ __all__ = [
import os
-from sqlalchemy import (
- Boolean, Column, DateTime, Float, ForeignKey, Integer, Interval,
- LargeBinary, PickleType, Unicode)
-from sqlalchemy.event import listen
-from sqlalchemy.orm import relationship
-from urlparse import urljoin
-from zope.component import getUtility
-from zope.event import notify
-from zope.interface import implementer
-
from mailman.config import config
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
@@ -65,6 +52,15 @@ from mailman.model.mime import ContentFilter
from mailman.model.preferences import Preferences
from mailman.utilities.filesystem import makedirs
from mailman.utilities.string import expand
+from six.moves.urllib_parse import urljoin
+from sqlalchemy import (
+ Boolean, Column, DateTime, Float, ForeignKey, Integer, Interval,
+ LargeBinary, PickleType, Unicode)
+from sqlalchemy.event import listen
+from sqlalchemy.orm import relationship
+from zope.component import getUtility
+from zope.event import notify
+from zope.interface import implementer
SPACE = ' '
@@ -482,7 +478,9 @@ class MailingList(Model):
Member._user == subscriber).first()
if member:
raise AlreadySubscribedError(
- self.fqdn_listname, subscriber, role)
+ self.fqdn_listname,
+ subscriber.preferred_address.email,
+ role)
else:
raise ValueError('subscriber must be an address or user')
member = Member(role=role,
diff --git a/src/mailman/model/member.py b/src/mailman/model/member.py
index 9da9d5d0d..19a30074e 100644
--- a/src/mailman/model/member.py
+++ b/src/mailman/model/member.py
@@ -17,18 +17,10 @@
"""Model for members."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Member',
]
-from sqlalchemy import Column, ForeignKey, Integer, Unicode
-from sqlalchemy.orm import relationship
-from zope.component import getUtility
-from zope.event import notify
-from zope.interface import implementer
from mailman.core.constants import system_preferences
from mailman.database.model import Model
@@ -42,6 +34,11 @@ from mailman.interfaces.member import (
from mailman.interfaces.user import IUser, UnverifiedAddressError
from mailman.interfaces.usermanager import IUserManager
from mailman.utilities.uid import UniqueIDFactory
+from sqlalchemy import Column, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship
+from zope.component import getUtility
+from zope.event import notify
+from zope.interface import implementer
uid_factory = UniqueIDFactory(context='members')
diff --git a/src/mailman/model/message.py b/src/mailman/model/message.py
index 691861d46..105066daa 100644
--- a/src/mailman/model/message.py
+++ b/src/mailman/model/message.py
@@ -17,19 +17,16 @@
"""Model for messages."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Message',
]
-from sqlalchemy import Column, Integer, LargeBinary, Unicode
-from zope.interface import implementer
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.interfaces.messages import IMessage
+from sqlalchemy import Column, Integer, Unicode
+from zope.interface import implementer
@@ -42,8 +39,8 @@ class Message(Model):
id = Column(Integer, primary_key=True)
# This is a Messge-ID field representation, not a database row id.
message_id = Column(Unicode)
- message_id_hash = Column(LargeBinary)
- path = Column(LargeBinary)
+ message_id_hash = Column(Unicode)
+ path = Column(Unicode)
@dbconnection
def __init__(self, store, message_id, message_id_hash, path):
diff --git a/src/mailman/model/messagestore.py b/src/mailman/model/messagestore.py
index 12b2aef46..05069119c 100644
--- a/src/mailman/model/messagestore.py
+++ b/src/mailman/model/messagestore.py
@@ -17,9 +17,6 @@
"""Model for message stores."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'MessageStore',
]
@@ -28,16 +25,15 @@ __all__ = [
import os
import errno
import base64
+import pickle
import hashlib
-import cPickle as pickle
-
-from zope.interface import implementer
from mailman.config import config
from mailman.database.transaction import dbconnection
from mailman.interfaces.messages import IMessageStore
from mailman.model.message import Message
from mailman.utilities.filesystem import makedirs
+from zope.interface import implementer
# It could be very bad if you have already stored files and you change this
@@ -68,8 +64,8 @@ class MessageStore:
raise ValueError(
'Message ID already exists in message store: {0}'.format(
message_id))
- shaobj = hashlib.sha1(message_id)
- hash32 = base64.b32encode(shaobj.digest())
+ shaobj = hashlib.sha1(message_id.encode('utf-8'))
+ hash32 = base64.b32encode(shaobj.digest()).decode('utf-8')
del message['X-Message-ID-Hash']
message['X-Message-ID-Hash'] = hash32
# Calculate the path on disk where we're going to store this message
@@ -94,7 +90,7 @@ class MessageStore:
# them and try again.
while True:
try:
- with open(path, 'w') as fp:
+ with open(path, 'wb') as fp:
# -1 says to use the highest protocol available.
pickle.dump(message, fp, -1)
break
@@ -106,7 +102,7 @@ class MessageStore:
def _get_message(self, row):
path = os.path.join(config.MESSAGES_DIR, row.path)
- with open(path) as fp:
+ with open(path, 'rb') as fp:
return pickle.load(fp)
@dbconnection
@@ -118,11 +114,6 @@ class MessageStore:
@dbconnection
def get_message_by_hash(self, store, message_id_hash):
- # It's possible the hash came from a message header, in which case it
- # will be a Unicode. However when coming from source code, it may be
- # bytes object. Coerce to the latter if necessary; it must be ASCII.
- if not isinstance(message_id_hash, bytes):
- message_id_hash = message_id_hash.encode('ascii')
row = store.query(Message).filter_by(
message_id_hash=message_id_hash).first()
if row is None:
diff --git a/src/mailman/model/mime.py b/src/mailman/model/mime.py
index dc6a54437..240fd6e2b 100644
--- a/src/mailman/model/mime.py
+++ b/src/mailman/model/mime.py
@@ -17,21 +17,17 @@
"""The content filter."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'ContentFilter'
]
-from sqlalchemy import Column, ForeignKey, Integer, Unicode
-from sqlalchemy.orm import relationship
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.types import Enum
from mailman.interfaces.mime import IContentFilter, FilterType
+from sqlalchemy import Column, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship
+from zope.interface import implementer
diff --git a/src/mailman/model/pending.py b/src/mailman/model/pending.py
index 49b12c16a..05cea4e29 100644
--- a/src/mailman/model/pending.py
+++ b/src/mailman/model/pending.py
@@ -17,33 +17,28 @@
"""Implementations of the IPendable and IPending interfaces."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Pended',
'Pendings',
]
+import json
import time
import random
import hashlib
from lazr.config import as_timedelta
-from sqlalchemy import (
- Column, DateTime, ForeignKey, Integer, LargeBinary, Unicode)
-from sqlalchemy.orm import relationship
-from zope.interface import implementer
-from zope.interface.verify import verifyObject
-
from mailman.config import config
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.interfaces.pending import (
IPendable, IPended, IPendedKeyValue, IPendings)
from mailman.utilities.datetime import now
-from mailman.utilities.modules import call_name
+from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship
+from zope.interface import implementer
+from zope.interface.verify import verifyObject
@@ -71,7 +66,7 @@ class Pended(Model):
__tablename__ = 'pended'
id = Column(Integer, primary_key=True)
- token = Column(LargeBinary)
+ token = Column(Unicode)
expiration_date = Column(DateTime)
key_values = relationship('PendedKeyValue')
@@ -108,33 +103,26 @@ class Pendings:
right_now = time.time()
x = random.random() + right_now % 1.0 + time.clock() % 1.0
# Use sha1 because it produces shorter strings.
- token = hashlib.sha1(repr(x)).hexdigest()
+ token = hashlib.sha1(repr(x).encode('utf-8')).hexdigest()
# In practice, we'll never get a duplicate, but we'll be anal
# about checking anyway.
if store.query(Pended).filter_by(token=token).count() == 0:
break
else:
- raise AssertionError('Could not find a valid pendings token')
+ raise RuntimeError('Could not find a valid pendings token')
# Create the record, and then the individual key/value pairs.
pending = Pended(
token=token,
expiration_date=now() + lifetime)
for key, value in pendable.items():
+ # Both keys and values must be strings.
if isinstance(key, bytes):
key = key.decode('utf-8')
if isinstance(value, bytes):
- value = value.decode('utf-8')
- elif type(value) is int:
- value = '__builtin__.int\1%s' % value
- elif type(value) is float:
- value = '__builtin__.float\1%s' % value
- elif type(value) is bool:
- value = '__builtin__.bool\1%s' % value
- elif type(value) is list:
- # We expect this to be a list of strings.
- value = ('mailman.model.pending.unpack_list\1' +
- '\2'.join(value))
- keyval = PendedKeyValue(key=key, value=value)
+ # Make sure we can turn this back into a bytes.
+ value = dict(__encoding__='utf-8',
+ value=value.decode('utf-8'))
+ keyval = PendedKeyValue(key=key, value=json.dumps(value))
pending.key_values.append(keyval)
store.add(pending)
return token
@@ -155,11 +143,10 @@ class Pendings:
entries = store.query(PendedKeyValue).filter(
PendedKeyValue.pended_id == pending.id)
for keyvalue in entries:
- if keyvalue.value is not None and '\1' in keyvalue.value:
- type_name, value = keyvalue.value.split('\1', 1)
- pendable[keyvalue.key] = call_name(type_name, value)
- else:
- pendable[keyvalue.key] = keyvalue.value
+ value = json.loads(keyvalue.value)
+ if isinstance(value, dict) and '__encoding__' in value:
+ value = value['value'].encode(value['__encoding__'])
+ pendable[keyvalue.key] = value
if expunge:
store.delete(keyvalue)
if expunge:
@@ -178,8 +165,3 @@ class Pendings:
for keyvalue in q:
store.delete(keyvalue)
store.delete(pending)
-
-
-
-def unpack_list(value):
- return value.split('\2')
diff --git a/src/mailman/model/preferences.py b/src/mailman/model/preferences.py
index 1278f80b7..8cec6036e 100644
--- a/src/mailman/model/preferences.py
+++ b/src/mailman/model/preferences.py
@@ -17,23 +17,19 @@
"""Model for preferences."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'Preferences',
]
-from sqlalchemy import Boolean, Column, Integer, Unicode
-from zope.component import getUtility
-from zope.interface import implementer
-
from mailman.database.model import Model
from mailman.database.types import Enum
from mailman.interfaces.languages import ILanguageManager
from mailman.interfaces.member import DeliveryMode, DeliveryStatus
from mailman.interfaces.preferences import IPreferences
+from sqlalchemy import Boolean, Column, Integer, Unicode
+from zope.component import getUtility
+from zope.interface import implementer
diff --git a/src/mailman/model/requests.py b/src/mailman/model/requests.py
index 6b130196d..9d9692b30 100644
--- a/src/mailman/model/requests.py
+++ b/src/mailman/model/requests.py
@@ -17,25 +17,25 @@
"""Implementations of the pending requests interfaces."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'DataPendable',
+ 'ListRequests',
]
-from cPickle import dumps, loads
-from datetime import timedelta
-from sqlalchemy import Column, ForeignKey, Integer, LargeBinary, Unicode
-from sqlalchemy.orm import relationship
-from zope.component import getUtility
-from zope.interface import implementer
+import six
+from datetime import timedelta
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.database.types import Enum
from mailman.interfaces.pending import IPendable, IPendings
from mailman.interfaces.requests import IListRequests, RequestType
+from six.moves.cPickle import dumps, loads
+from sqlalchemy import Column, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship
+from zope.component import getUtility
+from zope.interface import implementer
@@ -50,8 +50,8 @@ class DataPendable(dict):
# such a way that it will be properly reconstituted when unpended.
clean_mapping = {}
for key, value in mapping.items():
- assert isinstance(key, basestring)
- if not isinstance(value, unicode):
+ assert isinstance(key, six.string_types)
+ if not isinstance(value, six.text_type):
key = '_pck_' + key
value = dumps(value).decode('raw-unicode-escape')
clean_mapping[key] = value
@@ -154,7 +154,7 @@ class _Request(Model):
id = Column(Integer, primary_key=True)
key = Column(Unicode)
request_type = Column(Enum(RequestType))
- data_hash = Column(LargeBinary)
+ data_hash = Column(Unicode)
mailing_list_id = Column(Integer, ForeignKey('mailinglist.id'), index=True)
mailing_list = relationship('MailingList')
diff --git a/src/mailman/model/roster.py b/src/mailman/model/roster.py
index 54bc11617..7ea3ad2a4 100644
--- a/src/mailman/model/roster.py
+++ b/src/mailman/model/roster.py
@@ -22,9 +22,6 @@ the ones that fit a particular role. These are used as the member, owner,
moderator, and administrator roster filters.
"""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'AdministratorRoster',
'DigestMemberRoster',
@@ -37,14 +34,13 @@ __all__ = [
]
-from sqlalchemy import and_, or_
-from zope.interface import implementer
-
from mailman.database.transaction import dbconnection
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.roster import IRoster
from mailman.model.address import Address
from mailman.model.member import Member
+from sqlalchemy import and_, or_
+from zope.interface import implementer
diff --git a/src/mailman/model/tests/test_address.py b/src/mailman/model/tests/test_address.py
index 130ec3bae..29b32f542 100644
--- a/src/mailman/model/tests/test_address.py
+++ b/src/mailman/model/tests/test_address.py
@@ -17,9 +17,6 @@
"""Test addresses."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestAddress',
]
@@ -28,8 +25,11 @@ __all__ = [
import unittest
from mailman.email.validate import InvalidEmailAddressError
+from mailman.interfaces.address import ExistingAddressError
+from mailman.interfaces.usermanager import IUserManager
from mailman.model.address import Address
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
@@ -38,6 +38,25 @@ class TestAddress(unittest.TestCase):
layer = ConfigLayer
+ def setUp(self):
+ self._usermgr = getUtility(IUserManager)
+ self._address = self._usermgr.create_address('FPERSON@example.com')
+
def test_invalid_email_string_raises_exception(self):
with self.assertRaises(InvalidEmailAddressError):
Address('not_a_valid_email_string', '')
+
+ def test_local_part_differs_only_by_case(self):
+ with self.assertRaises(ExistingAddressError) as cm:
+ self._usermgr.create_address('fperson@example.com')
+ self.assertEqual(cm.exception.address, 'FPERSON@example.com')
+
+ def test_domain_part_differs_only_by_case(self):
+ with self.assertRaises(ExistingAddressError) as cm:
+ self._usermgr.create_address('fperson@EXAMPLE.COM')
+ self.assertEqual(cm.exception.address, 'FPERSON@example.com')
+
+ def test_mixed_case_exact_match(self):
+ with self.assertRaises(ExistingAddressError) as cm:
+ self._usermgr.create_address('FPERSON@example.com')
+ self.assertEqual(cm.exception.address, 'FPERSON@example.com')
diff --git a/src/mailman/model/tests/test_bounce.py b/src/mailman/model/tests/test_bounce.py
index a22da4416..2929747bc 100644
--- a/src/mailman/model/tests/test_bounce.py
+++ b/src/mailman/model/tests/test_bounce.py
@@ -17,24 +17,21 @@
"""Test bounce model objects."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'TestBounceEvents',
]
import unittest
from datetime import datetime
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.database.transaction import transaction
from mailman.interfaces.bounce import BounceContext, IBounceProcessor
from mailman.testing.helpers import (
specialized_message_from_string as message_from_string)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
diff --git a/src/mailman/model/tests/test_domain.py b/src/mailman/model/tests/test_domain.py
index f9d1ff202..a483d9567 100644
--- a/src/mailman/model/tests/test_domain.py
+++ b/src/mailman/model/tests/test_domain.py
@@ -17,9 +17,6 @@
"""Test domains."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestDomainLifecycleEvents',
'TestDomainManager',
@@ -28,8 +25,6 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.interfaces.domain import (
DomainCreatedEvent, DomainCreatingEvent, DomainDeletedEvent,
@@ -37,6 +32,7 @@ from mailman.interfaces.domain import (
from mailman.interfaces.listmanager import IListManager
from mailman.testing.helpers import event_subscribers
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
@@ -45,6 +41,7 @@ class TestDomainManager(unittest.TestCase):
def setUp(self):
self._events = []
+ self._manager = getUtility(IDomainManager)
def _record_event(self, event):
self._events.append(event)
@@ -53,7 +50,7 @@ class TestDomainManager(unittest.TestCase):
# Test that creating a domain in the domain manager propagates the
# expected events.
with event_subscribers(self._record_event):
- domain = getUtility(IDomainManager).add('example.org')
+ domain = self._manager.add('example.org')
self.assertEqual(len(self._events), 2)
self.assertTrue(isinstance(self._events[0], DomainCreatingEvent))
self.assertEqual(self._events[0].mail_host, 'example.org')
@@ -63,15 +60,24 @@ class TestDomainManager(unittest.TestCase):
def test_delete_domain_event(self):
# Test that deleting a domain in the domain manager propagates the
# expected event.
- domain = getUtility(IDomainManager).add('example.org')
+ domain = self._manager.add('example.org')
with event_subscribers(self._record_event):
- getUtility(IDomainManager).remove('example.org')
+ self._manager.remove('example.org')
self.assertEqual(len(self._events), 2)
self.assertTrue(isinstance(self._events[0], DomainDeletingEvent))
self.assertEqual(self._events[0].domain, domain)
self.assertTrue(isinstance(self._events[1], DomainDeletedEvent))
self.assertEqual(self._events[1].mail_host, 'example.org')
+ def test_lookup_missing_domain(self):
+ # Like dictionaries, getitem syntax raises KeyError on missing domain.
+ with self.assertRaises(KeyError):
+ self._manager['doesnotexist.com']
+
+ def test_delete_missing_domain(self):
+ # Trying to delete a missing domain gives you a KeyError.
+ self.assertRaises(KeyError, self._manager.remove, 'doesnotexist.com')
+
class TestDomainLifecycleEvents(unittest.TestCase):
diff --git a/src/mailman/model/tests/test_listmanager.py b/src/mailman/model/tests/test_listmanager.py
index b290138f3..a28698eb1 100644
--- a/src/mailman/model/tests/test_listmanager.py
+++ b/src/mailman/model/tests/test_listmanager.py
@@ -17,9 +17,6 @@
"""Test the ListManager."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestListCreation',
'TestListLifecycleEvents',
@@ -29,14 +26,13 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.app.moderator import hold_message
from mailman.config import config
+from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.listmanager import (
- IListManager, ListCreatedEvent, ListCreatingEvent, ListDeletedEvent,
- ListDeletingEvent)
+ IListManager, ListAlreadyExistsError, ListCreatedEvent, ListCreatingEvent,
+ ListDeletedEvent, ListDeletingEvent)
from mailman.interfaces.messages import IMessageStore
from mailman.interfaces.requests import IListRequests
from mailman.interfaces.subscriptions import ISubscriptionService
@@ -45,6 +41,7 @@ from mailman.model.mime import ContentFilter
from mailman.testing.helpers import (
event_subscribers, specialized_message_from_string)
from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
@@ -157,11 +154,23 @@ Message-ID: <argon>
class TestListCreation(unittest.TestCase):
layer = ConfigLayer
+ def setUp(self):
+ self._manager = getUtility(IListManager)
+
def test_create_list_case_folding(self):
# LP: #1117176 describes a problem where list names created in upper
# case are not actually usable by the LMTP server.
- manager = getUtility(IListManager)
- manager.create('my-LIST@example.com')
- self.assertIsNone(manager.get('my-LIST@example.com'))
- mlist = manager.get('my-list@example.com')
+ self._manager.create('my-LIST@example.com')
+ self.assertIsNone(self._manager.get('my-LIST@example.com'))
+ mlist = self._manager.get('my-list@example.com')
self.assertEqual(mlist.list_id, 'my-list.example.com')
+
+ def test_cannot_create_a_list_twice(self):
+ self._manager.create('ant@example.com')
+ self.assertRaises(ListAlreadyExistsError,
+ self._manager.create, 'ant@example.com')
+
+ def test_list_name_must_be_fully_qualified(self):
+ with self.assertRaises(InvalidEmailAddressError) as cm:
+ self._manager.create('foo')
+ self.assertEqual(cm.exception.email, 'foo')
diff --git a/src/mailman/model/tests/test_mailinglist.py b/src/mailman/model/tests/test_mailinglist.py
index 9d6177b54..6e7c11fe6 100644
--- a/src/mailman/model/tests/test_mailinglist.py
+++ b/src/mailman/model/tests/test_mailinglist.py
@@ -17,12 +17,10 @@
"""Test MailingLists and related model objects.."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
- 'TestListArchiver',
'TestDisabledListArchiver',
+ 'TestListArchiver',
+ 'TestMailingList',
]
@@ -31,8 +29,47 @@ import unittest
from mailman.app.lifecycle import create_list
from mailman.config import config
from mailman.interfaces.mailinglist import IListArchiverSet
+from mailman.interfaces.member import (
+ AlreadySubscribedError, MemberRole, MissingPreferredAddressError)
+from mailman.interfaces.usermanager import IUserManager
from mailman.testing.helpers import configuration
from mailman.testing.layers import ConfigLayer
+from mailman.utilities.datetime import now
+from zope.component import getUtility
+
+
+
+class TestMailingList(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._mlist = create_list('ant@example.com')
+
+ def test_no_duplicate_subscriptions(self):
+ # A user is not allowed to subscribe more than once to the mailing
+ # list with the same role.
+ anne = getUtility(IUserManager).create_user('anne@example.com')
+ # Give the user a preferred address.
+ preferred = list(anne.addresses)[0]
+ preferred.verified_on = now()
+ anne.preferred_address = preferred
+ # Subscribe Anne to the mailing list as a regular member.
+ member = self._mlist.subscribe(anne)
+ self.assertEqual(member.address, preferred)
+ self.assertEqual(member.role, MemberRole.member)
+ # A second subscription with the same role will fail.
+ with self.assertRaises(AlreadySubscribedError) as cm:
+ self._mlist.subscribe(anne)
+ self.assertEqual(cm.exception.fqdn_listname, 'ant@example.com')
+ self.assertEqual(cm.exception.email, 'anne@example.com')
+ self.assertEqual(cm.exception.role, MemberRole.member)
+
+ def test_subscribing_user_must_have_preferred_address(self):
+ # A user object cannot be subscribed to a mailing list without a
+ # preferred address.
+ anne = getUtility(IUserManager).create_user('anne@example.com')
+ self.assertRaises(MissingPreferredAddressError,
+ self._mlist.subscribe, anne)
diff --git a/src/mailman/model/tests/test_member.py b/src/mailman/model/tests/test_member.py
index 5bd3d1594..38f36acde 100644
--- a/src/mailman/model/tests/test_member.py
+++ b/src/mailman/model/tests/test_member.py
@@ -17,9 +17,6 @@
"""Test members."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestMember',
]
diff --git a/src/mailman/model/tests/test_messagestore.py b/src/mailman/model/tests/test_messagestore.py
new file mode 100644
index 000000000..39d1d97ed
--- /dev/null
+++ b/src/mailman/model/tests/test_messagestore.py
@@ -0,0 +1,71 @@
+# Copyright (C) 2014 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Test the message store."""
+
+__all__ = [
+ 'TestMessageStore',
+ ]
+
+
+import unittest
+
+from mailman.interfaces.messages import IMessageStore
+from mailman.testing.helpers import (
+ specialized_message_from_string as mfs)
+from mailman.testing.layers import ConfigLayer
+from mailman.utilities.email import add_message_hash
+from zope.component import getUtility
+
+
+
+class TestMessageStore(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ self._store = getUtility(IMessageStore)
+
+ def test_message_id_required(self):
+ # The Message-ID header is required in order to add it to the store.
+ message = mfs("""\
+Subject: An important message
+
+This message is very important.
+""")
+ self.assertRaises(ValueError, self._store.add, message)
+
+ def test_get_message_by_hash(self):
+ # Messages have an X-Message-ID-Hash header, the value of which can be
+ # used to look the message up in the message store.
+ message = mfs("""\
+Subject: An important message
+Message-ID: <ant>
+
+This message is very important.
+""")
+ add_message_hash(message)
+ self._store.add(message)
+ self.assertEqual(message['x-message-id-hash'],
+ 'V3YEHAFKE2WVJNK63Z7RFP4JMHISI2RG')
+ found = self._store.get_message_by_hash(
+ 'V3YEHAFKE2WVJNK63Z7RFP4JMHISI2RG')
+ self.assertEqual(found['message-id'], '<ant>')
+ self.assertEqual(found['x-message-id-hash'],
+ 'V3YEHAFKE2WVJNK63Z7RFP4JMHISI2RG')
+
+ def test_cannot_delete_missing_message(self):
+ self.assertRaises(LookupError, self._store.delete_message, 'missing')
diff --git a/src/mailman/model/tests/test_registrar.py b/src/mailman/model/tests/test_registrar.py
new file mode 100644
index 000000000..8d7c00e78
--- /dev/null
+++ b/src/mailman/model/tests/test_registrar.py
@@ -0,0 +1,64 @@
+# Copyright (C) 2014 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 `IRegistrar`."""
+
+__all__ = [
+ 'TestRegistrar',
+ ]
+
+
+import unittest
+
+from functools import partial
+from mailman.app.lifecycle import create_list
+from mailman.interfaces.address import InvalidEmailAddressError
+from mailman.interfaces.registrar import IRegistrar
+from mailman.testing.layers import ConfigLayer
+from zope.component import getUtility
+
+
+
+class TestRegistrar(unittest.TestCase):
+ layer = ConfigLayer
+
+ def setUp(self):
+ mlist = create_list('test@example.com')
+ self._register = partial(getUtility(IRegistrar).register, mlist)
+
+ def test_invalid_empty_string(self):
+ self.assertRaises(InvalidEmailAddressError, self._register, '')
+
+ def test_invalid_space_in_name(self):
+ self.assertRaises(InvalidEmailAddressError, self._register,
+ 'some name@example.com')
+
+ def test_invalid_funky_characters(self):
+ self.assertRaises(InvalidEmailAddressError, self._register,
+ '<script>@example.com')
+
+ def test_invalid_nonascii(self):
+ self.assertRaises(InvalidEmailAddressError, self._register,
+ '\xa0@example.com')
+
+ def test_invalid_no_at_sign(self):
+ self.assertRaises(InvalidEmailAddressError, self._register,
+ 'noatsign')
+
+ def test_invalid_no_domain(self):
+ self.assertRaises(InvalidEmailAddressError, self._register,
+ 'nodom@ain')
diff --git a/src/mailman/model/tests/test_requests.py b/src/mailman/model/tests/test_requests.py
index 419c6077f..c47c61013 100644
--- a/src/mailman/model/tests/test_requests.py
+++ b/src/mailman/model/tests/test_requests.py
@@ -17,9 +17,6 @@
"""Test the various pending requests interfaces."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestRequests',
]
diff --git a/src/mailman/model/tests/test_roster.py b/src/mailman/model/tests/test_roster.py
index 5bd06f485..8cf189e08 100644
--- a/src/mailman/model/tests/test_roster.py
+++ b/src/mailman/model/tests/test_roster.py
@@ -17,9 +17,6 @@
"""Test rosters."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestMailingListRoster',
'TestMembershipsRoster',
@@ -28,13 +25,12 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
from mailman.interfaces.member import DeliveryMode, MemberRole
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.layers import ConfigLayer
from mailman.utilities.datetime import now
+from zope.component import getUtility
diff --git a/src/mailman/model/tests/test_uid.py b/src/mailman/model/tests/test_uid.py
index 4c541205a..dd61ccc51 100644
--- a/src/mailman/model/tests/test_uid.py
+++ b/src/mailman/model/tests/test_uid.py
@@ -17,10 +17,8 @@
"""Test the UID model class."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
+ 'TestUID',
]
diff --git a/src/mailman/model/tests/test_user.py b/src/mailman/model/tests/test_user.py
index 17d4d24ff..ba5ba116f 100644
--- a/src/mailman/model/tests/test_user.py
+++ b/src/mailman/model/tests/test_user.py
@@ -17,9 +17,6 @@
"""Test users."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'TestUser',
]
@@ -27,12 +24,14 @@ __all__ = [
import unittest
-from zope.component import getUtility
-
from mailman.app.lifecycle import create_list
+from mailman.interfaces.address import (
+ AddressAlreadyLinkedError, AddressNotLinkedError)
+from mailman.interfaces.user import UnverifiedAddressError
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.layers import ConfigLayer
from mailman.utilities.datetime import now
+from zope.component import getUtility
@@ -74,3 +73,38 @@ class TestUser(unittest.TestCase):
self.assertEqual(len(emails), 2)
self.assertEqual(emails,
set(['anne@example.com', 'aperson@example.com']))
+
+ def test_uid_is_immutable(self):
+ with self.assertRaises(AttributeError):
+ self._anne.user_id = 'foo'
+
+ def test_addresses_may_only_be_linked_to_one_user(self):
+ user = getUtility(IUserManager).create_user()
+ # Anne's preferred address is already linked to her.
+ with self.assertRaises(AddressAlreadyLinkedError) as cm:
+ user.link(self._anne.preferred_address)
+ self.assertEqual(cm.exception.address, self._anne.preferred_address)
+
+ def test_unlink_from_address_not_linked_to(self):
+ # You cannot unlink an address from a user if that address is not
+ # already linked to the user.
+ user = getUtility(IUserManager).create_user()
+ with self.assertRaises(AddressNotLinkedError) as cm:
+ user.unlink(self._anne.preferred_address)
+ self.assertEqual(cm.exception.address, self._anne.preferred_address)
+
+ def test_unlink_address_which_is_not_linked(self):
+ # You cannot unlink an address which is not linked to any user.
+ address = getUtility(IUserManager).create_address('bart@example.com')
+ user = getUtility(IUserManager).create_user()
+ with self.assertRaises(AddressNotLinkedError) as cm:
+ user.unlink(address)
+ self.assertEqual(cm.exception.address, address)
+
+ def test_set_unverified_preferred_address(self):
+ # A user's preferred address cannot be set to an unverified address.
+ new_preferred = getUtility(IUserManager).create_address(
+ 'anne.person@example.com')
+ with self.assertRaises(UnverifiedAddressError) as cm:
+ self._anne.preferred_address = new_preferred
+ self.assertEqual(cm.exception.address, new_preferred)
diff --git a/src/mailman/model/uid.py b/src/mailman/model/uid.py
index 72ddd7b5a..94a4f1a17 100644
--- a/src/mailman/model/uid.py
+++ b/src/mailman/model/uid.py
@@ -17,20 +17,16 @@
"""Unique IDs."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'UID',
]
-from sqlalchemy import Column, Integer
-
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
from mailman.database.types import UUID
+from sqlalchemy import Column, Integer
diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py
index ab581fdc8..a85ef0d00 100644
--- a/src/mailman/model/user.py
+++ b/src/mailman/model/user.py
@@ -17,18 +17,10 @@
"""Model for users."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'User',
]
-from sqlalchemy import (
- Column, DateTime, ForeignKey, Integer, LargeBinary, Unicode)
-from sqlalchemy.orm import relationship, backref
-from zope.event import notify
-from zope.interface import implementer
from mailman.database.model import Model
from mailman.database.transaction import dbconnection
@@ -42,6 +34,10 @@ from mailman.model.preferences import Preferences
from mailman.model.roster import Memberships
from mailman.utilities.datetime import factory as date_factory
from mailman.utilities.uid import UniqueIDFactory
+from sqlalchemy import Column, DateTime, ForeignKey, Integer, Unicode
+from sqlalchemy.orm import relationship, backref
+from zope.event import notify
+from zope.interface import implementer
uid_factory = UniqueIDFactory(context='users')
@@ -56,7 +52,7 @@ class User(Model):
id = Column(Integer, primary_key=True)
display_name = Column(Unicode)
- _password = Column('password', LargeBinary)
+ _password = Column('password', Unicode)
_user_id = Column(UUID, index=True)
_created_on = Column(DateTime)
@@ -122,7 +118,7 @@ class User(Model):
def unlink(self, address):
"""See `IUser`."""
- if address.user is None:
+ if address.user is None or address.user is not self:
raise AddressNotLinkedError(address)
address.user = None
diff --git a/src/mailman/model/usermanager.py b/src/mailman/model/usermanager.py
index 726aa6120..374352033 100644
--- a/src/mailman/model/usermanager.py
+++ b/src/mailman/model/usermanager.py
@@ -17,16 +17,11 @@
"""A user manager."""
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
__all__ = [
'UserManager',
]
-from zope.interface import implementer
-
from mailman.database.transaction import dbconnection
from mailman.interfaces.address import ExistingAddressError
from mailman.interfaces.usermanager import IUserManager
@@ -34,6 +29,7 @@ from mailman.model.address import Address
from mailman.model.member import Member
from mailman.model.preferences import Preferences
from mailman.model.user import User
+from zope.interface import implementer