diff options
Diffstat (limited to 'src/mailman/model')
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 |
