diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mailman/database/mailman.sql | 421 | ||||
| -rw-r--r-- | src/mailman/interfaces/mailinglist.py | 14 | ||||
| -rw-r--r-- | src/mailman/model/docs/mailinglist.txt | 51 | ||||
| -rw-r--r-- | src/mailman/model/mailinglist.py | 34 | ||||
| -rw-r--r-- | src/mailman/model/member.py | 29 |
5 files changed, 324 insertions, 225 deletions
diff --git a/src/mailman/database/mailman.sql b/src/mailman/database/mailman.sql index 64dc21cde..5af8656c2 100644 --- a/src/mailman/database/mailman.sql +++ b/src/mailman/database/mailman.sql @@ -1,13 +1,13 @@ CREATE TABLE _request ( - id INTEGER NOT NULL, - "key" TEXT, - request_type TEXT, - data_hash TEXT, - mailing_list_id INTEGER, - PRIMARY KEY (id), - CONSTRAINT _request_mailing_list_id_fk - FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id) -); + id INTEGER NOT NULL, + "key" TEXT, + request_type TEXT, + data_hash TEXT, + mailing_list_id INTEGER, + PRIMARY KEY (id), + CONSTRAINT _request_mailing_list_id_fk + FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id) + ); CREATE TABLE acceptablealias ( id INTEGER NOT NULL, @@ -23,33 +23,33 @@ CREATE INDEX ix_acceptablealias_alias ON acceptablealias ("alias"); CREATE TABLE address ( - id INTEGER NOT NULL, - email TEXT, - _original TEXT, - real_name TEXT, - verified_on TIMESTAMP, - registered_on TIMESTAMP, - user_id INTEGER, - preferences_id INTEGER, - PRIMARY KEY (id), - CONSTRAINT address_user_id_fk - FOREIGN KEY (user_id) REFERENCES user (id), - CONSTRAINT address_preferences_id_fk - FOREIGN KEY (preferences_id) REFERENCES preferences (id) -); + id INTEGER NOT NULL, + email TEXT, + _original TEXT, + real_name TEXT, + verified_on TIMESTAMP, + registered_on TIMESTAMP, + user_id INTEGER, + preferences_id INTEGER, + PRIMARY KEY (id), + CONSTRAINT address_user_id_fk + FOREIGN KEY (user_id) REFERENCES user (id), + CONSTRAINT address_preferences_id_fk + FOREIGN KEY (preferences_id) REFERENCES preferences (id) + ); CREATE TABLE autoresponserecord ( - id INTEGER NOT NULL, - address_id INTEGER, - mailing_list_id INTEGER, - response_type INTEGER, - date_sent TIMESTAMP, - PRIMARY KEY (id), - CONSTRAINT autoresponserecord_address_id_fk - FOREIGN KEY (address_id) REFERENCES address (id), - CONSTRAINT autoresponserecord_mailing_list_id - FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id) - ); + id INTEGER NOT NULL, + address_id INTEGER, + mailing_list_id INTEGER, + response_type INTEGER, + date_sent TIMESTAMP, + PRIMARY KEY (id), + CONSTRAINT autoresponserecord_address_id_fk + FOREIGN KEY (address_id) REFERENCES address (id), + CONSTRAINT autoresponserecord_mailing_list_id + FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id) + ); CREATE INDEX ix_autoresponserecord_address_id ON autoresponserecord (address_id); CREATE INDEX ix_autoresponserecord_mailing_list_id @@ -83,137 +83,143 @@ CREATE TABLE language ( ); CREATE TABLE mailinglist ( - id INTEGER NOT NULL, - -- List identity - list_name TEXT, - host_name TEXT, - list_id TEXT, - include_list_post_header BOOLEAN, - include_rfc2369_headers BOOLEAN, - -- Attributes not directly modifiable via the web u/i - created_at TIMESTAMP, - admin_member_chunksize INTEGER, - next_request_id INTEGER, - next_digest_number INTEGER, - digest_last_sent_at TIMESTAMP, - volume INTEGER, - last_post_at TIMESTAMP, - accept_these_nonmembers BLOB, - acceptable_aliases_id INTEGER, - admin_immed_notify BOOLEAN, - admin_notify_mchanges BOOLEAN, - administrivia BOOLEAN, - advertised BOOLEAN, - anonymous_list BOOLEAN, - archive BOOLEAN, - archive_private BOOLEAN, - archive_volume_frequency INTEGER, - -- Automatic responses. - autorespond_owner INTEGER, - autoresponse_owner_text TEXT, - autorespond_postings INTEGER, - autoresponse_postings_text TEXT, - autorespond_requests INTEGER, - autoresponse_request_text TEXT, - autoresponse_grace_period TEXT, - -- Bounces. - bounce_info_stale_after TEXT, - bounce_matching_headers TEXT, - bounce_notify_owner_on_disable BOOLEAN, - bounce_notify_owner_on_removal BOOLEAN, - bounce_processing BOOLEAN, - bounce_score_threshold INTEGER, - bounce_unrecognized_goes_to_list_owner BOOLEAN, - bounce_you_are_disabled_warnings INTEGER, - bounce_you_are_disabled_warnings_interval TEXT, - -- Content filtering. - filter_content BOOLEAN, - collapse_alternatives BOOLEAN, - convert_html_to_plaintext BOOLEAN, - default_member_action INTEGER, - default_nonmember_action INTEGER, - description TEXT, - digest_footer TEXT, - digest_header TEXT, - digest_is_default BOOLEAN, - digest_send_periodic BOOLEAN, - digest_size_threshold INTEGER, - digest_volume_frequency TEXT, - digestable BOOLEAN, - discard_these_nonmembers BLOB, - emergency BOOLEAN, - encode_ascii_prefixes BOOLEAN, - first_strip_reply_to BOOLEAN, - forward_auto_discards BOOLEAN, - gateway_to_mail BOOLEAN, - gateway_to_news BOOLEAN, - generic_nonmember_action INTEGER, - goodbye_msg TEXT, - header_matches BLOB, - hold_these_nonmembers BLOB, - info TEXT, - linked_newsgroup TEXT, - max_days_to_hold INTEGER, - max_message_size INTEGER, - max_num_recipients INTEGER, - member_moderation_notice TEXT, - mime_is_default_digest BOOLEAN, - moderator_password TEXT, - msg_footer TEXT, - msg_header TEXT, - new_member_options INTEGER, - news_moderation TEXT, - news_prefix_subject_too BOOLEAN, - nntp_host TEXT, - nondigestable BOOLEAN, - nonmember_rejection_notice TEXT, - obscure_addresses BOOLEAN, - personalize TEXT, - pipeline TEXT, - post_id INTEGER, - preferred_language TEXT, - private_roster BOOLEAN, - real_name TEXT, - reject_these_nonmembers BLOB, - reply_goes_to_list TEXT, - reply_to_address TEXT, - require_explicit_destination BOOLEAN, - respond_to_post_requests BOOLEAN, - scrub_nondigest BOOLEAN, - send_goodbye_msg BOOLEAN, - send_reminders BOOLEAN, - send_welcome_msg BOOLEAN, - start_chain TEXT, - subject_prefix TEXT, - subscribe_auto_approval BLOB, - subscribe_policy INTEGER, - topics BLOB, - topics_bodylines_limit INTEGER, - topics_enabled BOOLEAN, - unsubscribe_policy INTEGER, - welcome_msg TEXT, - PRIMARY KEY (id) -); + id INTEGER NOT NULL, + -- List identity + list_name TEXT, + host_name TEXT, + list_id TEXT, + include_list_post_header BOOLEAN, + include_rfc2369_headers BOOLEAN, + -- Attributes not directly modifiable via the web u/i + created_at TIMESTAMP, + admin_member_chunksize INTEGER, + next_request_id INTEGER, + next_digest_number INTEGER, + digest_last_sent_at TIMESTAMP, + volume INTEGER, + last_post_at TIMESTAMP, + accept_these_nonmembers BLOB, + acceptable_aliases_id INTEGER, + admin_immed_notify BOOLEAN, + admin_notify_mchanges BOOLEAN, + administrivia BOOLEAN, + advertised BOOLEAN, + anonymous_list BOOLEAN, + archive BOOLEAN, + archive_private BOOLEAN, + archive_volume_frequency INTEGER, + -- Automatic responses. + autorespond_owner INTEGER, + autoresponse_owner_text TEXT, + autorespond_postings INTEGER, + autoresponse_postings_text TEXT, + autorespond_requests INTEGER, + autoresponse_request_text TEXT, + autoresponse_grace_period TEXT, + -- Bounces. + bounce_info_stale_after TEXT, + bounce_matching_headers TEXT, + bounce_notify_owner_on_disable BOOLEAN, + bounce_notify_owner_on_removal BOOLEAN, + bounce_processing BOOLEAN, + bounce_score_threshold INTEGER, + bounce_unrecognized_goes_to_list_owner BOOLEAN, + bounce_you_are_disabled_warnings INTEGER, + bounce_you_are_disabled_warnings_interval TEXT, + -- Content filtering. + filter_content BOOLEAN, + collapse_alternatives BOOLEAN, + convert_html_to_plaintext BOOLEAN, + default_member_action INTEGER, + default_nonmember_action INTEGER, + description TEXT, + digest_footer TEXT, + digest_header TEXT, + digest_is_default BOOLEAN, + digest_send_periodic BOOLEAN, + digest_size_threshold INTEGER, + digest_volume_frequency TEXT, + digestable BOOLEAN, + discard_these_nonmembers BLOB, + emergency BOOLEAN, + encode_ascii_prefixes BOOLEAN, + first_strip_reply_to BOOLEAN, + forward_auto_discards BOOLEAN, + gateway_to_mail BOOLEAN, + gateway_to_news BOOLEAN, + generic_nonmember_action INTEGER, + goodbye_msg TEXT, + header_matches BLOB, + hold_these_nonmembers BLOB, + info TEXT, + linked_newsgroup TEXT, + max_days_to_hold INTEGER, + max_message_size INTEGER, + max_num_recipients INTEGER, + member_moderation_notice TEXT, + mime_is_default_digest BOOLEAN, + moderator_password TEXT, + msg_footer TEXT, + msg_header TEXT, + new_member_options INTEGER, + news_moderation TEXT, + news_prefix_subject_too BOOLEAN, + nntp_host TEXT, + nondigestable BOOLEAN, + nonmember_rejection_notice TEXT, + obscure_addresses BOOLEAN, + personalize TEXT, + pipeline TEXT, + post_id INTEGER, + preferred_language TEXT, + private_roster BOOLEAN, + real_name TEXT, + reject_these_nonmembers BLOB, + reply_goes_to_list TEXT, + reply_to_address TEXT, + require_explicit_destination BOOLEAN, + respond_to_post_requests BOOLEAN, + scrub_nondigest BOOLEAN, + send_goodbye_msg BOOLEAN, + send_reminders BOOLEAN, + send_welcome_msg BOOLEAN, + start_chain TEXT, + subject_prefix TEXT, + subscribe_auto_approval BLOB, + subscribe_policy INTEGER, + topics BLOB, + topics_bodylines_limit INTEGER, + topics_enabled BOOLEAN, + unsubscribe_policy INTEGER, + welcome_msg TEXT, + PRIMARY KEY (id) + ); + CREATE TABLE member ( - id INTEGER NOT NULL, - role TEXT, - mailing_list TEXT, - moderation_action INTEGER, - address_id INTEGER, - preferences_id INTEGER, - PRIMARY KEY (id), - CONSTRAINT member_address_id_fk - FOREIGN KEY (address_id) REFERENCES address (id), - CONSTRAINT member_preferences_id_fk - FOREIGN KEY (preferences_id) REFERENCES preferences (id) -); + id INTEGER NOT NULL, + role TEXT, + mailing_list TEXT, + moderation_action INTEGER, + address_id INTEGER, + preferences_id INTEGER, + user_id INTEGER, + PRIMARY KEY (id), + CONSTRAINT member_address_id_fk + FOREIGN KEY (address_id) REFERENCES address (id), + CONSTRAINT member_preferences_id_fk + FOREIGN KEY (preferences_id) REFERENCES preferences (id) + CONSTRAINT member_user_id_fk + FOREIGN KEY (user_id) REFERENCES user (id) + ); + CREATE TABLE message ( - id INTEGER NOT NULL, - message_id_hash TEXT, - path TEXT, - message_id TEXT, - PRIMARY KEY (id) -); + id INTEGER NOT NULL, + message_id_hash TEXT, + path TEXT, + message_id TEXT, + PRIMARY KEY (id) + ); + CREATE TABLE onelastdigest ( id INTEGER NOT NULL, mailing_list_id INTEGER, @@ -225,54 +231,59 @@ CREATE TABLE onelastdigest ( CONSTRAINT onelastdigest_address_id_fk FOREIGN KEY (address_id) REFERENCES address(id) ); + CREATE TABLE pended ( - id INTEGER NOT NULL, - token TEXT, - expiration_date TIMESTAMP, - PRIMARY KEY (id) -); + id INTEGER NOT NULL, + token TEXT, + expiration_date TIMESTAMP, + PRIMARY KEY (id) + ); + CREATE TABLE pendedkeyvalue ( - id INTEGER NOT NULL, - "key" TEXT, - value TEXT, - pended_id INTEGER, - PRIMARY KEY (id), - CONSTRAINT pendedkeyvalue_pended_id_fk - FOREIGN KEY (pended_id) REFERENCES pended (id) -); + id INTEGER NOT NULL, + "key" TEXT, + value TEXT, + pended_id INTEGER, + PRIMARY KEY (id), + CONSTRAINT pendedkeyvalue_pended_id_fk + FOREIGN KEY (pended_id) REFERENCES pended (id) + ); + CREATE TABLE preferences ( - id INTEGER NOT NULL, - acknowledge_posts BOOLEAN, - hide_address BOOLEAN, - preferred_language TEXT, - receive_list_copy BOOLEAN, - receive_own_postings BOOLEAN, - delivery_mode TEXT, - delivery_status TEXT, - PRIMARY KEY (id) -); + id INTEGER NOT NULL, + acknowledge_posts BOOLEAN, + hide_address BOOLEAN, + preferred_language TEXT, + receive_list_copy BOOLEAN, + receive_own_postings BOOLEAN, + delivery_mode TEXT, + delivery_status TEXT, + PRIMARY KEY (id) + ); + CREATE TABLE user ( - id INTEGER NOT NULL, - real_name TEXT, - password BINARY, - _user_id TEXT, - _created_on TIMESTAMP, - _preferred_address_id INTEGER, - preferences_id INTEGER, - PRIMARY KEY (id), - CONSTRAINT user_preferences_id_fk - FOREIGN KEY (preferences_id) REFERENCES preferences (id), - CONSTRAINT _preferred_address_id_fk - FOREIGN KEY (_preferred_address_id) REFERENCES address (id) -); + id INTEGER NOT NULL, + real_name TEXT, + password BINARY, + _user_id TEXT, + _created_on TIMESTAMP, + _preferred_address_id INTEGER, + preferences_id INTEGER, + PRIMARY KEY (id), + CONSTRAINT user_preferences_id_fk + FOREIGN KEY (preferences_id) REFERENCES preferences (id), + CONSTRAINT _preferred_address_id_fk + FOREIGN KEY (_preferred_address_id) REFERENCES address (id) + ); CREATE INDEX ix_user_user_id ON user (_user_id); CREATE TABLE version ( - id INTEGER NOT NULL, - component TEXT, - version INTEGER, - PRIMARY KEY (id) -); + id INTEGER NOT NULL, + component TEXT, + version INTEGER, + PRIMARY KEY (id) + ); + CREATE INDEX ix__request_mailing_list_id ON _request (mailing_list_id); CREATE INDEX ix_address_preferences_id ON address (preferences_id); CREATE INDEX ix_address_user_id ON address (user_id); @@ -286,4 +297,4 @@ CREATE TABLE ban ( email TEXT, mailing_list TEXT, PRIMARY KEY (id) -); + ); diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py index 32f21a792..23d21cd34 100644 --- a/src/mailman/interfaces/mailinglist.py +++ b/src/mailman/interfaces/mailinglist.py @@ -232,18 +232,20 @@ class IMailingList(Interface): :rtype: Roster """ - def subscribe(address, role): - """Subscribe the given address to the mailing list. + def subscribe(subscriber, role): + """Subscribe the given address or user to the mailing list. - :param address: The address to subscribe. - :type address: `IAddress` + :param subscriber: The address or user to subscribe to the mailing + list. The user's preferred address receives deliveries, if she + has one, otherwise no address for the user appears in the rosters. + :type subscriber: `IUser` or `IAddress` :param role: The role being subscribed to (e.g. a member, owner, or moderator of a mailing list. :type role: `MemberRole` :return: The member object representing the subscription. :rtype: `IMember` - :raises AlreadySubscribedError: If the address is already subscribed - to the mailing list with the given role. + :raises AlreadySubscribedError: If the address or user is already + subscribed to the mailing list with the given role. """ # Posting history. diff --git a/src/mailman/model/docs/mailinglist.txt b/src/mailman/model/docs/mailinglist.txt index cc95c077c..ec9e37ee0 100644 --- a/src/mailman/model/docs/mailinglist.txt +++ b/src/mailman/model/docs/mailinglist.txt @@ -101,3 +101,54 @@ All rosters can also be accessed indirectly. ... print member <Member: cperson@example.com on aardvark@example.com as MemberRole.moderator> + + +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. +:: + + >>> from mailman.utilities.datetime import now + >>> user = user_manager.create_user('dperson@example.com', 'Dave Person') + >>> address = list(user.addresses)[0] + >>> address.verified_on = now() + >>> user.preferred_address = address + + >>> mlist.subscribe(user) + <Member: Dave Person <dperson@example.com> on aardvark@example.com + as MemberRole.member> + >>> for member in mlist.members.members: + ... print member + <Member: aperson@example.com on aardvark@example.com as MemberRole.member> + <Member: bperson@example.com on aardvark@example.com as MemberRole.member> + <Member: Dave Person <dperson@example.com> on aardvark@example.com + as MemberRole.member> + + >>> new_address = user.register('dave.person@example.com') + >>> new_address.verified_on = now() + >>> user.preferred_address = new_address + + >>> for member in mlist.members.members: + ... print member + <Member: aperson@example.com on aardvark@example.com as MemberRole.member> + <Member: bperson@example.com on aardvark@example.com as MemberRole.member> + <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. + + >>> mlist.subscribe(user.preferred_address) + <Member: dave.person@example.com + on aardvark@example.com as MemberRole.member> diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py index 895ff1f0e..2b205775e 100644 --- a/src/mailman/model/mailinglist.py +++ b/src/mailman/model/mailinglist.py @@ -38,12 +38,14 @@ from zope.interface import implements from mailman.config import config from mailman.database.model import Model from mailman.database.types import Enum +from mailman.interfaces.address import IAddress from mailman.interfaces.domain import IDomainManager from mailman.interfaces.languages import ILanguageManager from mailman.interfaces.mailinglist import ( IAcceptableAlias, IAcceptableAliasSet, IMailingList, Personalization) from mailman.interfaces.member import AlreadySubscribedError, MemberRole from mailman.interfaces.mime import FilterType +from mailman.interfaces.user import IUser from mailman.model import roster from mailman.model.digests import OneLastDigest from mailman.model.member import Member @@ -443,20 +445,32 @@ class MailingList(Model): raise TypeError( 'Undefined MemberRole: {0}'.format(role)) - def subscribe(self, address, role=MemberRole.member): + def subscribe(self, subscriber, role=MemberRole.member): """See `IMailingList`.""" store = Store.of(self) - member = store.find( - Member, - Member.role == role, - Member.mailing_list == self.fqdn_listname, - Member.address == address).one() - if member: - raise AlreadySubscribedError( - self.fqdn_listname, address.email, role) + if IAddress.providedBy(subscriber): + member = store.find( + Member, + Member.role == role, + Member.mailing_list == self.fqdn_listname, + Member._address == subscriber).one() + if member: + raise AlreadySubscribedError( + self.fqdn_listname, subscriber.email, role) + elif IUser.providedBy(subscriber): + member = store.find( + Member, + Member.role == role, + Member.mailing_list == self.fqdn_listname, + Member._user == subscriber).one() + if member: + raise AlreadySubscribedError( + self.fqdn_listname, subscriber, role) + else: + raise ValueError('subscriber must be an address or user') member = Member(role=role, mailing_list=self.fqdn_listname, - address=address) + subscriber=subscriber) member.preferences = Preferences() store.add(member) return member diff --git a/src/mailman/model/member.py b/src/mailman/model/member.py index d32c586d9..50ae921c3 100644 --- a/src/mailman/model/member.py +++ b/src/mailman/model/member.py @@ -33,8 +33,10 @@ from mailman.core.constants import system_preferences from mailman.database.model import Model from mailman.database.types import Enum from mailman.interfaces.action import Action +from mailman.interfaces.address import IAddress from mailman.interfaces.listmanager import IListManager from mailman.interfaces.member import IMember, MemberRole +from mailman.interfaces.user import IUser from mailman.interfaces.usermanager import IUserManager @@ -48,14 +50,25 @@ class Member(Model): moderation_action = Enum() address_id = Int() - address = Reference(address_id, 'Address.id') + _address = Reference(address_id, 'Address.id') preferences_id = Int() preferences = Reference(preferences_id, 'Preferences.id') + user_id = Int() + _user = Reference(user_id, 'User.id') - def __init__(self, role, mailing_list, address): + def __init__(self, role, mailing_list, subscriber): self.role = role self.mailing_list = mailing_list - self.address = address + if IAddress.providedBy(subscriber): + self._address = subscriber + # Look this up dynamically. + self._user = None + elif IUser.providedBy(subscriber): + self._user = subscriber + # Look this up dynamically. + self._address = None + else: + raise ValueError('subscriber must be a user or address') if role in (MemberRole.owner, MemberRole.moderator): self.moderation_action = Action.accept elif role is MemberRole.member: @@ -72,8 +85,16 @@ class Member(Model): self.address, self.mailing_list, self.role) @property + def address(self): + return (self._user.preferred_address + if self._address is None + else self._address) + + @property def user(self): - return getUtility(IUserManager).get_user(self.address.email) + return (self._user + if self._address is None + else getUtility(IUserManager).get_user(self._address.email)) def _lookup(self, preference): pref = getattr(self.preferences, preference) |
