diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/app/membership.py | 12 | ||||
| -rw-r--r-- | src/mailman/commands/cli_lists.py | 4 | ||||
| -rw-r--r-- | src/mailman/core/errors.py | 41 | ||||
| -rw-r--r-- | src/mailman/docs/lifecycle.txt | 2 | ||||
| -rw-r--r-- | src/mailman/docs/listmanager.txt | 2 | ||||
| -rw-r--r-- | src/mailman/docs/registration.txt | 12 | ||||
| -rw-r--r-- | src/mailman/email/validate.py | 6 | ||||
| -rw-r--r-- | src/mailman/interfaces/address.py | 7 | ||||
| -rw-r--r-- | src/mailman/interfaces/listmanager.py | 13 | ||||
| -rw-r--r-- | src/mailman/interfaces/member.py | 26 | ||||
| -rw-r--r-- | src/mailman/interfaces/membership.py | 42 | ||||
| -rw-r--r-- | src/mailman/interfaces/registrar.py | 2 | ||||
| -rw-r--r-- | src/mailman/interfaces/user.py | 2 | ||||
| -rw-r--r-- | src/mailman/model/listmanager.py | 4 | ||||
| -rw-r--r-- | src/mailman/model/roster.py | 13 | ||||
| -rw-r--r-- | src/mailman/queue/docs/outgoing.txt | 6 | ||||
| -rw-r--r-- | src/mailman/rest/adapters.py | 36 | ||||
| -rw-r--r-- | src/mailman/rest/docs/membership.txt | 33 |
18 files changed, 208 insertions, 55 deletions
diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py index f8234f808..2adba814a 100644 --- a/src/mailman/app/membership.py +++ b/src/mailman/app/membership.py @@ -35,7 +35,8 @@ from mailman.core import errors from mailman.core.i18n import _ from mailman.email.message import OwnerNotification from mailman.email.validate import validate -from mailman.interfaces.member import AlreadySubscribedError, MemberRole +from mailman.interfaces.member import ( + AlreadySubscribedError, MemberRole, MembershipIsBannedError) from mailman.interfaces.usermanager import IUserManager @@ -58,6 +59,12 @@ def add_member(mlist, address, realname, password, delivery_mode, language): :type delivery_mode: DeliveryMode :param language: The language that the subscriber is going to use. :type language: string + :return: The just created member. + :rtype: `IMember` + :raises AlreadySubscribedError: if the user is already subscribed to + the mailing list. + :raises InvalidEmailAddressError: if the email address is not valid. + :raises MembershipIsBannedError: if the membership is not allowed. """ # Let's be extra cautious. validate(address) @@ -68,7 +75,7 @@ def add_member(mlist, address, realname, password, delivery_mode, language): # confirmations. pattern = Utils.get_pattern(address, mlist.ban_list) if pattern: - raise errors.MembershipIsBanned(pattern) + raise MembershipIsBannedError(mlist, address) # Do the actual addition. First, see if there's already a user linked # with the given address. user_manager = getUtility(IUserManager) @@ -110,6 +117,7 @@ def add_member(mlist, address, realname, password, delivery_mode, language): member.preferences.delivery_mode = delivery_mode ## mlist.setMemberOption(email, config.Moderate, ## mlist.default_member_moderation) + return member diff --git a/src/mailman/commands/cli_lists.py b/src/mailman/commands/cli_lists.py index 93749c45a..79341ca6c 100644 --- a/src/mailman/commands/cli_lists.py +++ b/src/mailman/commands/cli_lists.py @@ -34,9 +34,9 @@ from mailman.Utils import maketext from mailman.app.lifecycle import create_list, remove_list from mailman.config import config from mailman.core.constants import system_preferences -from mailman.core.errors import InvalidEmailAddress from mailman.core.i18n import _ from mailman.email.message import UserNotification +from mailman.interfaces.address import InvalidEmailAddressError from mailman.interfaces.command import ICLISubCommand from mailman.interfaces.domain import ( BadDomainSpecificationError, IDomainManager) @@ -188,7 +188,7 @@ class Create: domain_manager.add(domain) try: mlist = create_list(fqdn_listname, args.owners) - except InvalidEmailAddress: + except InvalidEmailAddressError: self.parser.error(_('Illegal list name: $fqdn_listname')) return except ListAlreadyExistsError: diff --git a/src/mailman/core/errors.py b/src/mailman/core/errors.py index 8037ea823..2d31863c3 100644 --- a/src/mailman/core/errors.py +++ b/src/mailman/core/errors.py @@ -15,7 +15,16 @@ # You should have received a copy of the GNU General Public License along with # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. -"""Mailman errors.""" +"""Legacy Mailman exceptions. + +This module is largely obsolete, though not all exceptions in use have been +migrated to their proper location. There are still a number of Mailman 2.1 +exceptions floating about in here too. + +The right place for exceptions is in the interface module for their related +interfaces. +""" + from __future__ import absolute_import, unicode_literals @@ -29,18 +38,14 @@ __all__ = [ 'EmailAddressError', 'HandlerError', 'HoldMessage', - 'HostileSubscriptionError', - 'InvalidEmailAddress', 'LostHeldMessage', 'MailmanError', 'MailmanException', 'MemberError', - 'MembershipIsBanned', 'MustDigestError', 'NotAMemberError', 'PasswordError', 'RejectMessage', - 'SubscriptionError', ] @@ -58,7 +63,6 @@ class AlreadyReceivingDigests(MemberError): pass class AlreadyReceivingRegularDeliveries(MemberError): pass class CantDigestError(MemberError): pass class MustDigestError(MemberError): pass -class MembershipIsBanned(MemberError): pass @@ -71,17 +75,6 @@ class MailmanError(MailmanException): -# Exception hierarchy for bad email address errors that can be raised from -# mailman.email.validate.validate() -class EmailAddressError(MailmanError): - """Base class for email address validation errors.""" - - -class InvalidEmailAddress(EmailAddressError): - """Email address is invalid.""" - - - # Exceptions for admin request database class LostHeldMessage(MailmanError): """Held message was lost.""" @@ -134,20 +127,6 @@ class RejectMessage(HandlerError): -# Subscription exceptions -class SubscriptionError(MailmanError): - """Subscription errors base class.""" - - -class HostileSubscriptionError(SubscriptionError): - """A cross-subscription attempt was made. - - This exception gets raised when an invitee attempts to use the - invitation to cross-subscribe to some other mailing list. - """ - - - class PasswordError(MailmanError): """A password related error.""" diff --git a/src/mailman/docs/lifecycle.txt b/src/mailman/docs/lifecycle.txt index ee4657f1d..a1cd50825 100644 --- a/src/mailman/docs/lifecycle.txt +++ b/src/mailman/docs/lifecycle.txt @@ -29,7 +29,7 @@ bogus posting address, you get an exception. >>> create_list('not a valid address') Traceback (most recent call last): ... - InvalidEmailAddress: u'not a valid address' + InvalidEmailAddressError: u'not a valid address' If the posting address is valid, but the domain has not been registered with Mailman yet, you get an exception. diff --git a/src/mailman/docs/listmanager.txt b/src/mailman/docs/listmanager.txt index e8af0d71d..e07659066 100644 --- a/src/mailman/docs/listmanager.txt +++ b/src/mailman/docs/listmanager.txt @@ -47,7 +47,7 @@ It is an error to create a mailing list that isn't a fully qualified list name >>> list_manager.create('foo') Traceback (most recent call last): ... - InvalidEmailAddress: foo + InvalidEmailAddressError: foo Deleting a mailing list diff --git a/src/mailman/docs/registration.txt b/src/mailman/docs/registration.txt index a3dfc7004..abc7f2c93 100644 --- a/src/mailman/docs/registration.txt +++ b/src/mailman/docs/registration.txt @@ -49,27 +49,27 @@ addresses are rejected outright. >>> registrar.register(mlist, '') Traceback (most recent call last): ... - InvalidEmailAddress: u'' + InvalidEmailAddressError: u'' >>> registrar.register(mlist, 'some name@example.com') Traceback (most recent call last): ... - InvalidEmailAddress: u'some name@example.com' + InvalidEmailAddressError: u'some name@example.com' >>> registrar.register(mlist, '<script>@example.com') Traceback (most recent call last): ... - InvalidEmailAddress: u'<script>@example.com' + InvalidEmailAddressError: u'<script>@example.com' >>> registrar.register(mlist, '\xa0@example.com') Traceback (most recent call last): ... - InvalidEmailAddress: u'\xa0@example.com' + InvalidEmailAddressError: u'\xa0@example.com' >>> registrar.register(mlist, 'noatsign') Traceback (most recent call last): ... - InvalidEmailAddress: u'noatsign' + InvalidEmailAddressError: u'noatsign' >>> registrar.register(mlist, 'nodom@ain') Traceback (most recent call last): ... - InvalidEmailAddress: u'nodom@ain' + InvalidEmailAddressError: u'nodom@ain' Register an email address diff --git a/src/mailman/email/validate.py b/src/mailman/email/validate.py index fc33b1509..9e6e62b39 100644 --- a/src/mailman/email/validate.py +++ b/src/mailman/email/validate.py @@ -28,8 +28,8 @@ __all__ = [ import re -from mailman.core.errors import InvalidEmailAddress from mailman.email.utils import split_email +from mailman.interfaces.address import InvalidEmailAddressError # What other characters should be disallowed? @@ -42,10 +42,10 @@ def validate(address): :param address: An email address. :type address: string - :raise `InvalidEmailAddress`: when the address is deemed invalid. + :raise InvalidEmailAddressError: when the address is deemed invalid. """ if not is_valid(address): - raise InvalidEmailAddress(repr(address)) + raise InvalidEmailAddressError(repr(address)) diff --git a/src/mailman/interfaces/address.py b/src/mailman/interfaces/address.py index 968ded3ee..4b82c13db 100644 --- a/src/mailman/interfaces/address.py +++ b/src/mailman/interfaces/address.py @@ -26,10 +26,13 @@ __all__ = [ 'AddressNotLinkedError', 'ExistingAddressError', 'IAddress', + 'InvalidEmailAddressError', ] +from lazr.restful.declarations import error_status from zope.interface import Interface, Attribute + from mailman.interfaces.errors import MailmanError @@ -50,6 +53,10 @@ class AddressNotLinkedError(AddressError): """The address is not linked to the user.""" +@error_status(400) +class InvalidEmailAddressError(AddressError): + """Email address is invalid.""" + class IAddress(Interface): diff --git a/src/mailman/interfaces/listmanager.py b/src/mailman/interfaces/listmanager.py index 8a058eaa5..b707df7e7 100644 --- a/src/mailman/interfaces/listmanager.py +++ b/src/mailman/interfaces/listmanager.py @@ -23,6 +23,7 @@ __metaclass__ = type __all__ = [ 'IListManager', 'ListAlreadyExistsError', + 'NoSuchListError', ] @@ -45,6 +46,18 @@ class ListAlreadyExistsError(MailmanError): """ +@error_status(400) +class NoSuchListError(MailmanError): + """Attempt to access a mailing list that does not exist.""" + + def __init__(self, fqdn_listname): + self.fqdn_listname = fqdn_listname + + def __str__(self): + return 'No such mailing list: {0.fqdn_listname}'.format(self) + + + class IListManager(Interface): """The interface of the global list manager. diff --git a/src/mailman/interfaces/member.py b/src/mailman/interfaces/member.py index e5a82060a..87ad19c83 100644 --- a/src/mailman/interfaces/member.py +++ b/src/mailman/interfaces/member.py @@ -26,15 +26,17 @@ __all__ = [ 'DeliveryStatus', 'IMember', 'MemberRole', + 'MembershipError', + 'MembershipIsBannedError', ] from lazr.restful.declarations import ( - export_as_webservice_entry, exported) + error_status, export_as_webservice_entry, exported) from munepy import Enum from zope.interface import Interface, Attribute -from mailman.core.errors import SubscriptionError +from mailman.core.errors import MailmanError @@ -71,7 +73,11 @@ class MemberRole(Enum): -class AlreadySubscribedError(SubscriptionError): +class MembershipError(MailmanError): + """Base exception for all membership errors.""" + + +class AlreadySubscribedError(MembershipError): """The member is already subscribed to the mailing list with this role.""" def __init__(self, fqdn_listname, address, role): @@ -85,6 +91,20 @@ class AlreadySubscribedError(SubscriptionError): self._address, self._role, self._fqdn_listname) +@error_status(400) +class MembershipIsBannedError(MembershipError): + """The address is not allowed to subscribe to the mailing list.""" + + def __init__(self, mlist, address): + super(MembershipIsBanned, self).__init__() + self._mlist = mlist + self._address = address + + def __str__(self): + return '{0} is not allowed to subscribe to {1.fqdn_listname}'.format( + self._address, self._mlist) + + class IMember(Interface): """A member of a mailing list.""" diff --git a/src/mailman/interfaces/membership.py b/src/mailman/interfaces/membership.py index 0a04ff630..51e36c6e5 100644 --- a/src/mailman/interfaces/membership.py +++ b/src/mailman/interfaces/membership.py @@ -27,8 +27,9 @@ __all__ = [ from lazr.restful.declarations import ( collection_default_content, export_as_webservice_collection, - export_factory_operation) + export_write_operation, operation_parameters) from zope.interface import Interface +from zope.schema import TextLine from mailman.core.i18n import _ from mailman.interfaces.member import IMember @@ -53,3 +54,42 @@ class ISubscriptionService(Interface): :return: The list of all members. :rtype: list of `IMember` """ + + @operation_parameters( + fqdn_listname=TextLine(), + address=TextLine(), + real_name=TextLine(), + delivery_mode=TextLine(), + ) + @export_write_operation() + def join(fqdn_listname, address, real_name=None, delivery_mode=None): + """Subscribe to a mailing list. + + A user for the address is created if it is not yet known to Mailman, + however newly registered addresses will not yet be validated. No + confirmation message will be sent to the address, and the approval of + the subscription request is still dependent on the policy of the + mailing list. + + :param fqdn_listname: The posting address of the mailing list to + subscribe the user to. + :type fqdn_listname: string + :param address: The address of the user getting subscribed. + :type address: string + :param real_name: The name of the user. This is only used if a new + user is created, and it defaults to the local part of the email + address if not given. + :type real_name: string + :param delivery_mode: The delivery mode for this subscription. This + can be one of the enum values of `DeliveryMode`. If not given, + regular delivery is assumed. + :type delivery_mode: string + :return: The just created member. + :rtype: `IMember` + :raises AlreadySubscribedError: if the user is already subscribed to + the mailing list. + :raises InvalidEmailAddressError: if the email address is not valid. + :raises MembershipIsBannedError: if the membership is not allowed. + :raises NoSuchListError: if the named mailing list does not exist. + :raises ValueError: when `delivery_mode` is invalid. + """ diff --git a/src/mailman/interfaces/registrar.py b/src/mailman/interfaces/registrar.py index dae4d53a8..d7eac1072 100644 --- a/src/mailman/interfaces/registrar.py +++ b/src/mailman/interfaces/registrar.py @@ -61,7 +61,7 @@ class IRegistrar(Interface): :type real_name: str :return: The confirmation token string. :rtype: str - :raises InvalidEmailAddress: if the address is not allowed. + :raises InvalidEmailAddressError: if the address is not allowed. """ def confirm(token): diff --git a/src/mailman/interfaces/user.py b/src/mailman/interfaces/user.py index 5c3ff58cd..2892b44d2 100644 --- a/src/mailman/interfaces/user.py +++ b/src/mailman/interfaces/user.py @@ -42,7 +42,7 @@ class IUser(Interface): """An iterator over all the IAddresses controlled by this user.""") memberships = Attribute( - """A roster of this user's membership.""") + """A roster of this user's memberships.""") def register(address, real_name=None): """Register the given email address and link it to this user. diff --git a/src/mailman/model/listmanager.py b/src/mailman/model/listmanager.py index d4fd49cd0..a89b771b0 100644 --- a/src/mailman/model/listmanager.py +++ b/src/mailman/model/listmanager.py @@ -30,7 +30,7 @@ import datetime from zope.interface import implements from mailman.config import config -from mailman.core.errors import InvalidEmailAddress +from mailman.interfaces.address import InvalidEmailAddressError from mailman.interfaces.listmanager import IListManager, ListAlreadyExistsError from mailman.interfaces.rest import IResolvePathNames from mailman.model.mailinglist import MailingList @@ -47,7 +47,7 @@ class ListManager: """See `IListManager`.""" listname, at, hostname = fqdn_listname.partition('@') if len(hostname) == 0: - raise InvalidEmailAddress(fqdn_listname) + raise InvalidEmailAddressError(fqdn_listname) mlist = config.db.store.find( MailingList, MailingList.list_name == listname, diff --git a/src/mailman/model/roster.py b/src/mailman/model/roster.py index 62e3ab5dc..094c2c21c 100644 --- a/src/mailman/model/roster.py +++ b/src/mailman/model/roster.py @@ -67,6 +67,7 @@ class AbstractRoster: @property def members(self): + """See `IRoster`.""" for member in config.db.store.find( Member, mailing_list=self._mlist.fqdn_listname, @@ -75,6 +76,7 @@ class AbstractRoster: @property def users(self): + """See `IRoster`.""" # Members are linked to addresses, which in turn are linked to users. # So while the 'members' attribute does most of the work, we have to # keep a set of unique users. It's possible for the same user to be @@ -86,12 +88,14 @@ class AbstractRoster: @property def addresses(self): + """See `IRoster`.""" # Every Member is linked to exactly one address so the 'members' # attribute does most of the work. for member in self.members: yield member.address def get_member(self, address): + """See `IRoster`.""" results = config.db.store.find( Member, Member.mailing_list == self._mlist.fqdn_listname, @@ -140,6 +144,7 @@ class AdministratorRoster(AbstractRoster): @property def members(self): + """See `IRoster`.""" # Administrators are defined as the union of the owners and the # moderators. members = config.db.store.find( @@ -151,6 +156,7 @@ class AdministratorRoster(AbstractRoster): yield member def get_member(self, address): + """See `IRoster`.""" results = config.db.store.find( Member, Member.mailing_list == self._mlist.fqdn_listname, @@ -195,6 +201,7 @@ class RegularMemberRoster(DeliveryMemberRoster): @property def members(self): + """See `IRoster`.""" for member in self._get_members(DeliveryMode.regular): yield member @@ -207,6 +214,7 @@ class DigestMemberRoster(DeliveryMemberRoster): @property def members(self): + """See `IRoster`.""" for member in self._get_members(DeliveryMode.plaintext_digests, DeliveryMode.mime_digests, DeliveryMode.summary_digests): @@ -221,6 +229,7 @@ class Subscribers(AbstractRoster): @property def members(self): + """See `IRoster`.""" for member in config.db.store.find( Member, mailing_list=self._mlist.fqdn_listname): @@ -240,6 +249,7 @@ class Memberships: @property def members(self): + """See `IRoster`.""" results = config.db.store.find( Member, Address.user_id == self._user.id, @@ -249,14 +259,17 @@ class Memberships: @property def users(self): + """See `IRoster`.""" yield self._user @property def addresses(self): + """See `IRoster`.""" for address in self._user.addresses: yield address def get_member(self, address): + """See `IRoster`.""" results = config.db.store.find( Member, Member.address_id == Address.id, diff --git a/src/mailman/queue/docs/outgoing.txt b/src/mailman/queue/docs/outgoing.txt index 6188ab35c..cdabdb795 100644 --- a/src/mailman/queue/docs/outgoing.txt +++ b/src/mailman/queue/docs/outgoing.txt @@ -18,10 +18,16 @@ move messages to the 'retry queue' for handling delivery failures. >>> from mailman.interfaces.member import DeliveryMode >>> add_member(mlist, 'aperson@example.com', 'Anne Person', ... 'password', DeliveryMode.regular, 'en') + <Member: Anne Person <aperson@example.com> + on test@example.com as MemberRole.member> >>> add_member(mlist, 'bperson@example.com', 'Bart Person', ... 'password', DeliveryMode.regular, 'en') + <Member: Bart Person <bperson@example.com> + on test@example.com as MemberRole.member> >>> add_member(mlist, 'cperson@example.com', 'Cris Person', ... 'password', DeliveryMode.regular, 'en') + <Member: Cris Person <cperson@example.com> + on test@example.com as MemberRole.member> Normally, messages would show up in the outgoing queue after the message has been processed by the rule set and pipeline. But we can simulate that here by diff --git a/src/mailman/rest/adapters.py b/src/mailman/rest/adapters.py index 720bdd7cb..0cc5e2f95 100644 --- a/src/mailman/rest/adapters.py +++ b/src/mailman/rest/adapters.py @@ -31,8 +31,12 @@ from zope.component import getUtility from zope.interface import implements from zope.publisher.interfaces import NotFound +from mailman.app.membership import add_member +from mailman.core.constants import system_preferences +from mailman.interfaces.address import InvalidEmailAddressError from mailman.interfaces.domain import IDomainCollection, IDomainManager -from mailman.interfaces.listmanager import IListManager +from mailman.interfaces.listmanager import IListManager, NoSuchListError +from mailman.interfaces.member import DeliveryMode from mailman.interfaces.membership import ISubscriptionService from mailman.interfaces.rest import IResolvePathNames @@ -91,3 +95,33 @@ class SubscriptionService: sorted((member for member in mailing_list.members.members), key=address_of_member)) return members + + def join(self, fqdn_listname, address, + real_name= None, delivery_mode=None): + """See `ISubscriptionService`.""" + mlist = getUtility(IListManager).get(fqdn_listname) + if mlist is None: + raise NoSuchListError(fqdn_listname) + # Convert from string to enum. Allow ValueError exceptions to be + # returned by the API. XXX 2009-12-30 Does lazr.restful intelligently + # handle ValueError? + mode = (DeliveryMode.regular + if delivery_mode is None + else DeliveryMode(delivery_mode)) + if real_name is None: + real_name, at, domain = address.partition('@') + if len(at) == 0: + # It can't possibly be a valid email address. + raise InvalidEmailAddressError(address) + # Because we want to keep the REST API simple, there is no password or + # language given to us. We'll use the system's default language for + # the user's default language. We'll set the password to None. XXX + # Is that a good idea? Maybe we should set it to something else, + # except that once we encode the password (as we must do to avoid + # cleartext passwords in the database) we'll never be able to retrieve + # it. + # + # Note that none of these are used unless the address is completely + # new to us. + return add_member(mlist, address, real_name, None, mode, + system_preferences.preferred_language) diff --git a/src/mailman/rest/docs/membership.txt b/src/mailman/rest/docs/membership.txt index e225184b5..0e333f3d1 100644 --- a/src/mailman/rest/docs/membership.txt +++ b/src/mailman/rest/docs/membership.txt @@ -169,3 +169,36 @@ test-one mailing list. resource_type_link: http://localhost:8001/3.0/#members start: 0 total_size: 7 + + +Joining a mailing list +====================== + +A user can be subscribed to a mailing list via the REST API. Actually, +addresses not users are subscribed to mailing lists, but addresses are always +tied to users. A subscribed user is called a member. + +Elly subscribes to the alpha mailing list. By default, get gets a regular +delivery. Since Elly's email address is not yet known to Mailman, a user is +created for her. + + >>> dump_json('http://localhost:8001/3.0/members', { + ... 'ws.op': 'join', + ... 'fqdn_listname': 'alpha@example.com', + ... 'address': 'eperson@example.com', + ... 'real_name': 'Elly Person', + ... }) + http_etag: "a0213c9ff485ef3d24a6e2cc8ee68ed147f05398" + resource_type_link: http://localhost:8001/3.0/#member + self_link: http://localhost:8001/3.0/lists/alpha@example.com/member/eperson@example.com + +Elly is now a member of the mailing list. + + >>> elly = user_manager.get_user('eperson@example.com') + >>> elly + <User "Elly Person" at ...> + + >>> member_of_lists = set(member.mailing_list + ... for member in elly.memberships.members) + >>> member_of_lists + set([u'alpha@example.com']) |
