summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBarry Warsaw2011-04-17 11:37:06 -0400
committerBarry Warsaw2011-04-17 11:37:06 -0400
commitc2130f46808b3cf3cc27311d6d6f6b608e76b267 (patch)
treef020d0d8034f0e21bc3f00787e5d3b7f447954e5
parentc894152772595a1acbd6dc4c1ac0e033888ea923 (diff)
parente2ed3ae7dfa138a8288b4c49cd2b671c35f6c703 (diff)
downloadmailman-c2130f46808b3cf3cc27311d6d6f6b608e76b267.tar.gz
mailman-c2130f46808b3cf3cc27311d6d6f6b608e76b267.tar.zst
mailman-c2130f46808b3cf3cc27311d6d6f6b608e76b267.zip
-rw-r--r--src/mailman/app/lifecycle.py8
-rw-r--r--src/mailman/app/membership.py5
-rw-r--r--src/mailman/app/registrar.py2
-rw-r--r--src/mailman/database/mailman.sql424
-rw-r--r--src/mailman/interfaces/address.py11
-rw-r--r--src/mailman/interfaces/mailinglist.py18
-rw-r--r--src/mailman/interfaces/user.py18
-rw-r--r--src/mailman/model/address.py23
-rw-r--r--src/mailman/model/docs/addresses.txt41
-rw-r--r--src/mailman/model/docs/mailinglist.txt150
-rw-r--r--src/mailman/model/docs/membership.txt12
-rw-r--r--src/mailman/model/docs/requests.txt4
-rw-r--r--src/mailman/model/docs/users.txt95
-rw-r--r--src/mailman/model/mailinglist.py36
-rw-r--r--src/mailman/model/member.py29
-rw-r--r--src/mailman/model/user.py28
-rw-r--r--src/mailman/mta/docs/decorating.txt6
-rw-r--r--src/mailman/pipeline/docs/acknowledge.txt4
-rw-r--r--src/mailman/pipeline/docs/avoid-duplicates.txt4
-rw-r--r--src/mailman/pipeline/docs/calc-recips.txt12
-rw-r--r--src/mailman/pipeline/docs/file-recips.txt2
-rw-r--r--src/mailman/queue/docs/digester.txt2
-rw-r--r--src/mailman/rules/moderation.py2
-rw-r--r--src/mailman/testing/helpers.py6
24 files changed, 583 insertions, 359 deletions
diff --git a/src/mailman/app/lifecycle.py b/src/mailman/app/lifecycle.py
index b30266f3b..689fab484 100644
--- a/src/mailman/app/lifecycle.py
+++ b/src/mailman/app/lifecycle.py
@@ -80,11 +80,11 @@ def create_list(fqdn_listname, owners=None):
# owners of the mailing list.
user_manager = getUtility(IUserManager)
for owner_address in owners:
- addr = user_manager.get_address(owner_address)
- if addr is None:
+ address = user_manager.get_address(owner_address)
+ if address is None:
user = user_manager.create_user(owner_address)
- addr = list(user.addresses)[0]
- addr.subscribe(mlist, MemberRole.owner)
+ address = list(user.addresses)[0]
+ mlist.subscribe(address, MemberRole.owner)
return mlist
diff --git a/src/mailman/app/membership.py b/src/mailman/app/membership.py
index aaf7f05df..be2382a7f 100644
--- a/src/mailman/app/membership.py
+++ b/src/mailman/app/membership.py
@@ -99,7 +99,7 @@ def add_member(mlist, email, realname, password, delivery_mode, language):
# scheme is recorded in the hashed password string.
user.password = encrypt_password(password)
user.preferences.preferred_language = language
- member = address.subscribe(mlist, MemberRole.member)
+ member = mlist.subscribe(address, MemberRole.member)
member.preferences.delivery_mode = delivery_mode
else:
# The user exists and is linked to the address.
@@ -110,8 +110,7 @@ def add_member(mlist, email, realname, password, delivery_mode, language):
raise AssertionError(
'User should have had linked address: {0}'.format(address))
# Create the member and set the appropriate preferences.
- # pylint: disable-msg=W0631
- member = address.subscribe(mlist, MemberRole.member)
+ member = mlist.subscribe(address, MemberRole.member)
member.preferences.preferred_language = language
member.preferences.delivery_mode = delivery_mode
return member
diff --git a/src/mailman/app/registrar.py b/src/mailman/app/registrar.py
index ec899237a..f6f2e8679 100644
--- a/src/mailman/app/registrar.py
+++ b/src/mailman/app/registrar.py
@@ -138,7 +138,7 @@ class Registrar:
if list_name is not None:
mlist = getUtility(IListManager).get(list_name)
if mlist:
- address.subscribe(mlist, MemberRole.member)
+ mlist.subscribe(address, MemberRole.member)
return True
def discard(self, token):
diff --git a/src/mailman/database/mailman.sql b/src/mailman/database/mailman.sql
index 7c09fb79f..5af8656c2 100644
--- a/src/mailman/database/mailman.sql
+++ b/src/mailman/database/mailman.sql
@@ -1,12 +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,
@@ -14,7 +15,7 @@ CREATE TABLE acceptablealias (
mailing_list_id INTEGER NOT NULL,
PRIMARY KEY (id),
CONSTRAINT acceptablealias_mailing_list_id_fk
- FOREIGN KEY(mailing_list_id) REFERENCES mailinglist (id)
+ FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id)
);
CREATE INDEX ix_acceptablealias_mailing_list_id
ON acceptablealias (mailing_list_id);
@@ -22,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
@@ -61,8 +62,7 @@ CREATE TABLE contentfilter (
filter_type INTEGER,
PRIMARY KEY (id),
CONSTRAINT contentfilter_mailing_list_id
- FOREIGN KEY (mailing_list_id)
- REFERENCES mailinglist (id)
+ FOREIGN KEY (mailing_list_id) REFERENCES mailinglist (id)
);
CREATE INDEX ix_contentfilter_mailing_list_id
ON contentfilter (mailing_list_id);
@@ -83,135 +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,
@@ -219,55 +227,63 @@ CREATE TABLE onelastdigest (
delivery_mode TEXT,
PRIMARY KEY (id),
CONSTRAINT onelastdigest_mailing_list_id_fk
- FOREIGN KEY(mailing_list_id) REFERENCES mailinglist(id),
+ FOREIGN KEY (mailing_list_id) REFERENCES mailinglist(id),
CONSTRAINT onelastdigest_address_id_fk
- FOREIGN KEY(address_id) REFERENCES address(id)
+ 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,
- preferences_id INTEGER,
- PRIMARY KEY (id),
- CONSTRAINT user_preferences_id_fk
- FOREIGN KEY(preferences_id)
- REFERENCES preferences (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);
@@ -281,4 +297,4 @@ CREATE TABLE ban (
email TEXT,
mailing_list TEXT,
PRIMARY KEY (id)
-);
+ );
diff --git a/src/mailman/interfaces/address.py b/src/mailman/interfaces/address.py
index c051c9b0c..2f6d05bba 100644
--- a/src/mailman/interfaces/address.py
+++ b/src/mailman/interfaces/address.py
@@ -41,6 +41,7 @@ class AddressError(MailmanError):
"""A general address-related error occurred."""
def __init__(self, address):
+ super(AddressError, self).__init__()
self.address = address
def __str__(self):
@@ -99,16 +100,6 @@ class IAddress(Interface):
None if the email address has not yet been validated. The specific
method of validation is not defined here.""")
- def subscribe(mailing_list, role):
- """Subscribe the address to the given mailing list with the given role.
-
- :param mailing_list: The IMailingList being subscribed to.
- :param role: A MemberRole enum value.
- :return: The IMember representing this subscription.
- :raises AlreadySubscribedError: If the address is already subscribed
- to the mailing list with the given role.
- """
-
preferences = Attribute(
"""This address's preferences.""")
diff --git a/src/mailman/interfaces/mailinglist.py b/src/mailman/interfaces/mailinglist.py
index d8c0ebb26..23d21cd34 100644
--- a/src/mailman/interfaces/mailinglist.py
+++ b/src/mailman/interfaces/mailinglist.py
@@ -182,7 +182,7 @@ class IMailingList(Interface):
def confirm_address(cookie=''):
"""The address used for various forms of email confirmation."""
- # Rosters.
+ # Rosters and subscriptions.
owners = Attribute(
"""The IUser owners of this mailing list.
@@ -232,6 +232,22 @@ class IMailingList(Interface):
:rtype: Roster
"""
+ def subscribe(subscriber, role):
+ """Subscribe the given address or user to the mailing list.
+
+ :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 or user is already
+ subscribed to the mailing list with the given role.
+ """
+
# Posting history.
last_post_at = Attribute(
diff --git a/src/mailman/interfaces/user.py b/src/mailman/interfaces/user.py
index 2c2652413..ceffec57f 100644
--- a/src/mailman/interfaces/user.py
+++ b/src/mailman/interfaces/user.py
@@ -22,11 +22,26 @@ from __future__ import absolute_import, unicode_literals
__metaclass__ = type
__all__ = [
'IUser',
+ 'UnverifiedAddressError',
]
from zope.interface import Interface, Attribute
+from mailman.interfaces.errors import MailmanError
+
+
+
+class UnverifiedAddressError(MailmanError):
+ """Unverified address cannot be used as a user's preferred address."""
+
+ def __init__(self, address):
+ super(UnverifiedAddressError, self).__init__()
+ self.address = address
+
+ def __str__(self):
+ return self.address
+
class IUser(Interface):
@@ -47,6 +62,9 @@ class IUser(Interface):
addresses = Attribute(
"""An iterator over all the `IAddresses` controlled by this user.""")
+ preferred_address = Attribute(
+ """The user's preferred `IAddress`. This must be validated.""")
+
memberships = Attribute(
"""A roster of this user's memberships.""")
diff --git a/src/mailman/model/address.py b/src/mailman/model/address.py
index 6fa310c48..92f7f8986 100644
--- a/src/mailman/model/address.py
+++ b/src/mailman/model/address.py
@@ -26,14 +26,11 @@ __all__ = [
from email.utils import formataddr
-from storm.locals import DateTime, Int, Reference, Store, Unicode
+from storm.locals import DateTime, Int, Reference, Unicode
from zope.interface import implements
from mailman.database.model import Model
-from mailman.interfaces.member import AlreadySubscribedError
from mailman.interfaces.address import IAddress
-from mailman.model.member import Member
-from mailman.model.preferences import Preferences
@@ -73,24 +70,6 @@ class Address(Model):
return '<Address: {0} [{1}] key: {2} at {3:#x}>'.format(
address_str, verified, self.email, id(self))
- def subscribe(self, mailing_list, role):
- # This member has no preferences by default.
- store = Store.of(self)
- member = store.find(
- Member,
- Member.role == role,
- Member.mailing_list == mailing_list.fqdn_listname,
- Member.address == self).one()
- if member:
- raise AlreadySubscribedError(
- mailing_list.fqdn_listname, self.email, role)
- member = Member(role=role,
- mailing_list=mailing_list.fqdn_listname,
- address=self)
- member.preferences = Preferences()
- store.add(member)
- return member
-
@property
def original_email(self):
return (self.email if self._original is None else self._original)
diff --git a/src/mailman/model/docs/addresses.txt b/src/mailman/model/docs/addresses.txt
index fdcc993b5..ffbb897ab 100644
--- a/src/mailman/model/docs/addresses.txt
+++ b/src/mailman/model/docs/addresses.txt
@@ -153,47 +153,6 @@ And of course, you can also set the validation date.
2007-05-13 22:54:01
-Subscriptions
-=============
-
-Addresses get subscribed to mailing lists, not users. When the address is
-subscribed, a role is specified.
-::
-
- >>> address_5 = user_manager.create_address(
- ... 'eperson@example.com', 'Elly Person')
- >>> mlist = create_list('test@example.com')
-
- >>> from mailman.interfaces.member import MemberRole
- >>> address_5.subscribe(mlist, MemberRole.owner)
- <Member: Elly Person <eperson@example.com> on
- test@example.com as MemberRole.owner>
- >>> address_5.subscribe(mlist, MemberRole.member)
- <Member: Elly Person <eperson@example.com> on
- test@example.com as MemberRole.member>
-
-Now Elly is both an owner and a member of the mailing list.
-
- >>> def memberkey(member):
- ... return member.mailing_list, member.address.email, int(member.role)
- >>> dump_list(mlist.owners.members, key=memberkey)
- <Member: Elly Person <eperson@example.com> on
- test@example.com as MemberRole.owner>
- >>> dump_list(mlist.moderators.members, key=memberkey)
- *Empty*
- >>> dump_list(mlist.administrators.members, key=memberkey)
- <Member: Elly Person <eperson@example.com> on
- test@example.com as MemberRole.owner>
- >>> dump_list(mlist.members.members, key=memberkey)
- <Member: Elly Person <eperson@example.com> on
- test@example.com as MemberRole.member>
- >>> dump_list(mlist.regular_members.members, key=memberkey)
- <Member: Elly Person <eperson@example.com> on
- test@example.com as MemberRole.member>
- >>> dump_list(mlist.digest_members.members, key=memberkey)
- *Empty*
-
-
Case-preserved addresses
========================
diff --git a/src/mailman/model/docs/mailinglist.txt b/src/mailman/model/docs/mailinglist.txt
index 33d681762..ec9e37ee0 100644
--- a/src/mailman/model/docs/mailinglist.txt
+++ b/src/mailman/model/docs/mailinglist.txt
@@ -9,16 +9,16 @@ the system by its posting address, i.e. the email address you would send a
message to in order to post a message to the mailing list. This must be fully
qualified.
- >>> alpha = create_list('alpha@example.com')
- >>> print alpha.fqdn_listname
- alpha@example.com
+ >>> mlist = create_list('aardvark@example.com')
+ >>> print mlist.fqdn_listname
+ aardvark@example.com
The mailing list also has convenient attributes for accessing the list's short
name (i.e. local part) and host name.
- >>> print alpha.list_name
- alpha
- >>> print alpha.host_name
+ >>> print mlist.list_name
+ aardvark
+ >>> print mlist.host_name
example.com
@@ -29,46 +29,126 @@ Mailing list membership is represented by `rosters`. Each mailing list has
several rosters of members, representing the subscribers to the mailing list,
the owners, the moderators, and so on. The rosters are defined by a
membership role.
+
+Addresses can be explicitly subscribed to a mailing list. By default, a
+subscription puts the address in the `member` role, meaning that address will
+receive a copy of any message sent to the mailing list.
::
+ >>> from mailman.interfaces.usermanager import IUserManager
+ >>> from zope.component import getUtility
+ >>> user_manager = getUtility(IUserManager)
+
+ >>> aperson = user_manager.create_address('aperson@example.com')
+ >>> bperson = user_manager.create_address('bperson@example.com')
+ >>> mlist.subscribe(aperson)
+ <Member: aperson@example.com on aardvark@example.com as MemberRole.member>
+ >>> mlist.subscribe(bperson)
+ <Member: bperson@example.com on aardvark@example.com as MemberRole.member>
+
+Both addresses appear on the roster of members.
+
+ >>> 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>
+
+By explicitly specifying the role of the subscription, an address can be added
+to the owner and moderator rosters.
+
>>> from mailman.interfaces.member import MemberRole
- >>> from mailman.testing.helpers import subscribe
+ >>> mlist.subscribe(aperson, MemberRole.owner)
+ <Member: aperson@example.com on aardvark@example.com as MemberRole.owner>
+ >>> cperson = user_manager.create_address('cperson@example.com')
+ >>> mlist.subscribe(cperson, MemberRole.owner)
+ <Member: cperson@example.com on aardvark@example.com as MemberRole.owner>
+ >>> mlist.subscribe(cperson, MemberRole.moderator)
+ <Member: cperson@example.com on aardvark@example.com
+ as MemberRole.moderator>
- >>> subscribe(alpha, 'Anne')
- >>> subscribe(alpha, 'Bart')
- >>> subscribe(alpha, 'Cris')
- >>> subscribe(alpha, 'Anne', MemberRole.owner)
- >>> subscribe(alpha, 'Dave', MemberRole.owner)
- >>> subscribe(alpha, 'Elle', MemberRole.moderator)
+A Person is now both a member and an owner of the mailing list. C Person is
+an owner and a moderator.
+::
+
+ >>> for member in mlist.owners.members:
+ ... print member
+ <Member: aperson@example.com on aardvark@example.com as MemberRole.owner>
+ <Member: cperson@example.com on aardvark@example.com as MemberRole.owner>
-We can retrieve a roster directly...
+ >>> for member in mlist.moderators.members:
+ ... print member
+ <Member: cperson@example.com on aardvark@example.com
+ as MemberRole.moderator>
- >>> for member in alpha.members.members:
- ... print member.address
- Anne Person <aperson@example.com>
- Bart Person <bperson@example.com>
- Cris Person <cperson@example.com>
-...or programmatically.
+All rosters can also be accessed indirectly.
+::
- >>> roster = alpha.get_roster(MemberRole.member)
+ >>> roster = mlist.get_roster(MemberRole.member)
>>> for member in roster.members:
- ... print member.address
- Anne Person <aperson@example.com>
- Bart Person <bperson@example.com>
- Cris Person <cperson@example.com>
+ ... print member
+ <Member: aperson@example.com on aardvark@example.com as MemberRole.member>
+ <Member: bperson@example.com on aardvark@example.com as MemberRole.member>
-This includes the roster of owners...
+ >>> roster = mlist.get_roster(MemberRole.owner)
+ >>> for member in roster.members:
+ ... print member
+ <Member: aperson@example.com on aardvark@example.com as MemberRole.owner>
+ <Member: cperson@example.com on aardvark@example.com as MemberRole.owner>
- >>> roster = alpha.get_roster(MemberRole.owner)
+ >>> roster = mlist.get_roster(MemberRole.moderator)
>>> for member in roster.members:
- ... print member.address
- Anne Person <aperson@example.com>
- Dave Person <dperson@example.com>
+ ... print member
+ <Member: cperson@example.com on aardvark@example.com
+ as MemberRole.moderator>
-...and moderators.
- >>> roster = alpha.get_roster(MemberRole.moderator)
- >>> for member in roster.members:
- ... print member.address
- Elle Person <eperson@example.com>
+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/docs/membership.txt b/src/mailman/model/docs/membership.txt
index 6f5b82622..8a3f0da16 100644
--- a/src/mailman/model/docs/membership.txt
+++ b/src/mailman/model/docs/membership.txt
@@ -51,7 +51,7 @@ her.
>>> from mailman.interfaces.member import MemberRole
>>> address_1 = list(user_1.addresses)[0]
- >>> address_1.subscribe(mlist, MemberRole.owner)
+ >>> mlist.subscribe(address_1, MemberRole.owner)
<Member: Anne Person <aperson@example.com> on
test@example.com as MemberRole.owner>
>>> dump_list(member.address for member in mlist.owners.members)
@@ -73,7 +73,7 @@ Bart becomes a moderator of the list.
>>> print user_2
<User "Bart Person" (...) at ...>
>>> address_2 = list(user_2.addresses)[0]
- >>> address_2.subscribe(mlist, MemberRole.moderator)
+ >>> mlist.subscribe(address_2, MemberRole.moderator)
<Member: Bart Person <bperson@example.com>
on test@example.com as MemberRole.moderator>
>>> dump_list(member.address for member in mlist.moderators.members)
@@ -102,7 +102,7 @@ role.
>>> user_3 = user_manager.create_user(
... 'cperson@example.com', 'Cris Person')
>>> address_3 = list(user_3.addresses)[0]
- >>> member = address_3.subscribe(mlist, MemberRole.member)
+ >>> member = mlist.subscribe(address_3, MemberRole.member)
>>> member
<Member: Cris Person <cperson@example.com>
on test@example.com as MemberRole.member>
@@ -125,7 +125,7 @@ It's easy to make the list administrators members of the mailing list too.
>>> members = []
>>> for address in mlist.administrators.addresses:
- ... member = address.subscribe(mlist, MemberRole.member)
+ ... member = mlist.subscribe(address, MemberRole.member)
... members.append(member)
>>> dump_list(members, key=attrgetter('address.email'))
<Member: Anne Person <aperson@example.com> on
@@ -157,7 +157,7 @@ role.
>>> user_6 = user_manager.create_user('fperson@example.com', 'Fred Person')
>>> address_6 = list(user_6.addresses)[0]
- >>> member_6 = address_6.subscribe(mlist, MemberRole.nonmember)
+ >>> member_6 = mlist.subscribe(address_6, MemberRole.nonmember)
>>> member_6
<Member: Fred Person <fperson@example.com> on test@example.com
as MemberRole.nonmember>
@@ -233,7 +233,7 @@ Double subscriptions
It is an error to subscribe someone to a list with the same role twice.
- >>> address_1.subscribe(mlist, MemberRole.owner)
+ >>> mlist.subscribe(address_1, MemberRole.owner)
Traceback (most recent call last):
...
AlreadySubscribedError: aperson@example.com is already a MemberRole.owner
diff --git a/src/mailman/model/docs/requests.txt b/src/mailman/model/docs/requests.txt
index 2ff173422..bebb61259 100644
--- a/src/mailman/model/docs/requests.txt
+++ b/src/mailman/model/docs/requests.txt
@@ -716,12 +716,12 @@ notification.
>>> from mailman.interfaces.member import MemberRole
>>> user_1 = user_manager.create_user('gperson@example.com')
>>> address_1 = list(user_1.addresses)[0]
- >>> address_1.subscribe(mlist, MemberRole.member)
+ >>> mlist.subscribe(address_1, MemberRole.member)
<Member: gperson@example.com on alist@example.com as MemberRole.member>
>>> user_2 = user_manager.create_user('hperson@example.com')
>>> address_2 = list(user_2.addresses)[0]
- >>> address_2.subscribe(mlist, MemberRole.member)
+ >>> mlist.subscribe(address_2, MemberRole.member)
<Member: hperson@example.com on alist@example.com as MemberRole.member>
>>> id_5 = moderator.hold_unsubscription(mlist, 'gperson@example.com')
diff --git a/src/mailman/model/docs/users.txt b/src/mailman/model/docs/users.txt
index c8244c506..1813ef636 100644
--- a/src/mailman/model/docs/users.txt
+++ b/src/mailman/model/docs/users.txt
@@ -4,7 +4,8 @@ Users
Users are entities that represent people. A user has a real name and a
optional encoded password. A user may also have an optional preferences and a
-set of addresses they control.
+set of addresses they control. They can even have a *preferred address*,
+i.e. one that they use by default.
See `usermanager.txt`_ for examples of how to create, delete, and find users.
@@ -142,6 +143,90 @@ But don't try to unlink the address from a user it's not linked to.
AddressNotLinkedError: zperson@example.net
+Preferred address
+=================
+
+Users can register a preferred address. When subscribing to a mailing list,
+unless some other address is explicitly specified, the user will be subscribed
+with their preferred address. This allows them to change their preferred
+address once, and have all their subscriptions automatically track this
+change.
+
+By default, a user has no preferred address.
+
+ >>> user_2 = user_manager.create_user()
+ >>> print user_2.preferred_address
+ None
+
+Even when a user registers an address, this address will not be set as the
+preferred address.
+
+ >>> anne = user_2.register('anne@example.com', 'Anne Person')
+ >>> 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.
+::
+
+ >>> from mailman.utilities.datetime import now
+ >>> anne.verified_on = now()
+ >>> anne
+ <Address: Anne Person <anne@example.com> [verified] at ...>
+ >>> user_2.controls(anne.email)
+ True
+ >>> user_2.preferred_address = anne
+ >>> user_2.preferred_address
+ <Address: Anne Person <anne@example.com> [verified] at ...>
+
+ >>> aperson = user_manager.create_address('aperson@example.com')
+ >>> user_2.controls(aperson.email)
+ False
+ >>> aperson.verified_on = now()
+ >>> user_2.preferred_address = aperson
+ >>> user_2.controls(aperson.email)
+ True
+
+ >>> zperson = list(user_1.addresses)[0]
+ >>> 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
+ <Address: aperson@example.com [verified] at ...>
+ >>> del user_2.preferred_address
+ >>> print user_2.preferred_address
+ None
+
+The preferred address always shows up in the set of addresses controlled by
+this user.
+
+ >>> for address in user_2.addresses:
+ ... print address.email
+ anne@example.com
+ aperson@example.com
+
+
Users and preferences
=====================
@@ -207,14 +292,14 @@ membership role.
>>> mlist_3 = create_list('xtest_3@example.com')
>>> from mailman.interfaces.member import MemberRole
- >>> com.subscribe(mlist_1, MemberRole.member)
+ >>> mlist_1.subscribe(com, MemberRole.member)
<Member: Zoe Person <zperson@example.com> on xtest_1@example.com as
MemberRole.member>
- >>> org.subscribe(mlist_2, MemberRole.member)
+ >>> mlist_2.subscribe(org, MemberRole.member)
<Member: zperson@example.org on xtest_2@example.com as MemberRole.member>
- >>> org.subscribe(mlist_2, MemberRole.owner)
+ >>> mlist_2.subscribe(org, MemberRole.owner)
<Member: zperson@example.org on xtest_2@example.com as MemberRole.owner>
- >>> net.subscribe(mlist_3, MemberRole.moderator)
+ >>> mlist_3.subscribe(net, MemberRole.moderator)
<Member: zperson@example.net on xtest_3@example.com as
MemberRole.moderator>
diff --git a/src/mailman/model/mailinglist.py b/src/mailman/model/mailinglist.py
index 488b6da3d..2b205775e 100644
--- a/src/mailman/model/mailinglist.py
+++ b/src/mailman/model/mailinglist.py
@@ -38,15 +38,19 @@ 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 MemberRole
+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
from mailman.model.mime import ContentFilter
+from mailman.model.preferences import Preferences
from mailman.utilities.filesystem import makedirs
from mailman.utilities.string import expand
@@ -441,6 +445,36 @@ class MailingList(Model):
raise TypeError(
'Undefined MemberRole: {0}'.format(role))
+ def subscribe(self, subscriber, role=MemberRole.member):
+ """See `IMailingList`."""
+ store = Store.of(self)
+ 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,
+ subscriber=subscriber)
+ member.preferences = Preferences()
+ store.add(member)
+ return member
+
class AcceptableAlias(Model):
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)
diff --git a/src/mailman/model/user.py b/src/mailman/model/user.py
index f0048c5f4..7c4c15d36 100644
--- a/src/mailman/model/user.py
+++ b/src/mailman/model/user.py
@@ -32,7 +32,7 @@ from mailman.config import config
from mailman.database.model import Model
from mailman.interfaces.address import (
AddressAlreadyLinkedError, AddressNotLinkedError)
-from mailman.interfaces.user import IUser
+from mailman.interfaces.user import IUser, UnverifiedAddressError
from mailman.model.address import Address
from mailman.model.preferences import Preferences
from mailman.model.roster import Memberships
@@ -53,6 +53,8 @@ class User(Model):
_created_on = DateTime()
addresses = ReferenceSet(id, 'Address.user_id')
+ _preferred_address_id = Int()
+ _preferred_address = Reference(_preferred_address_id, 'Address.id')
preferences_id = Int()
preferences = Reference(preferences_id, 'Preferences.id')
@@ -93,6 +95,30 @@ class User(Model):
raise AddressNotLinkedError(address)
address.user = None
+ @property
+ def preferred_address(self):
+ """See `IUser`."""
+ return self._preferred_address
+
+ @preferred_address.setter
+ def preferred_address(self, address):
+ """See `IUser`."""
+ if address.verified_on is None:
+ raise UnverifiedAddressError(address)
+ if self.controls(address.email):
+ # This user already controls the email address.
+ pass
+ elif address.user is None:
+ self.link(address)
+ elif address.user != self:
+ raise AddressAlreadyLinkedError(address)
+ self._preferred_address = address
+
+ @preferred_address.deleter
+ def preferred_address(self):
+ """See `IUser`."""
+ self._preferred_address = None
+
def controls(self, email):
"""See `IUser`."""
found = config.db.store.find(Address, email=email)
diff --git a/src/mailman/mta/docs/decorating.txt b/src/mailman/mta/docs/decorating.txt
index 0bc9649c8..05196eb78 100644
--- a/src/mailman/mta/docs/decorating.txt
+++ b/src/mailman/mta/docs/decorating.txt
@@ -69,17 +69,17 @@ list.
>>> anne = user_manager.create_user('aperson@example.com', 'Anne Person')
>>> anne.password = b'AAA'
- >>> list(anne.addresses)[0].subscribe(mlist, MemberRole.member)
+ >>> mlist.subscribe(list(anne.addresses)[0], MemberRole.member)
<Member: Anne Person <aperson@example.com> ...
>>> bart = user_manager.create_user('bperson@example.com', 'Bart Person')
>>> bart.password = b'BBB'
- >>> list(bart.addresses)[0].subscribe(mlist, MemberRole.member)
+ >>> mlist.subscribe(list(bart.addresses)[0], MemberRole.member)
<Member: Bart Person <bperson@example.com> ...
>>> cris = user_manager.create_user('cperson@example.com', 'Cris Person')
>>> cris.password = b'CCC'
- >>> list(cris.addresses)[0].subscribe(mlist, MemberRole.member)
+ >>> mlist.subscribe(list(cris.addresses)[0], MemberRole.member)
<Member: Cris Person <cperson@example.com> ...
The decorations happen when the message is delivered.
diff --git a/src/mailman/pipeline/docs/acknowledge.txt b/src/mailman/pipeline/docs/acknowledge.txt
index 5e4240626..b2ce11b7f 100644
--- a/src/mailman/pipeline/docs/acknowledge.txt
+++ b/src/mailman/pipeline/docs/acknowledge.txt
@@ -29,7 +29,7 @@ Subscribe a user to the mailing list.
>>> from mailman.interfaces.member import MemberRole
>>> user_1 = user_manager.create_user('aperson@example.com')
>>> address_1 = list(user_1.addresses)[0]
- >>> address_1.subscribe(mlist, MemberRole.member)
+ >>> mlist.subscribe(address_1, MemberRole.member)
<Member: aperson@example.com on _xtest@example.com as MemberRole.member>
@@ -82,7 +82,7 @@ will be sent.
>>> user_2 = user_manager.create_user('dperson@example.com')
>>> address_2 = list(user_2.addresses)[0]
- >>> address_2.subscribe(mlist, MemberRole.member)
+ >>> mlist.subscribe(address_2, MemberRole.member)
<Member: dperson@example.com on _xtest@example.com as MemberRole.member>
>>> handler.process(mlist, msg,
diff --git a/src/mailman/pipeline/docs/avoid-duplicates.txt b/src/mailman/pipeline/docs/avoid-duplicates.txt
index bd753f9e9..1e46793c2 100644
--- a/src/mailman/pipeline/docs/avoid-duplicates.txt
+++ b/src/mailman/pipeline/docs/avoid-duplicates.txt
@@ -19,8 +19,8 @@ Create some members we're going to use.
>>> address_b = user_manager.create_address('bperson@example.com')
>>> from mailman.interfaces.member import MemberRole
- >>> member_a = address_a.subscribe(mlist, MemberRole.member)
- >>> member_b = address_b.subscribe(mlist, MemberRole.member)
+ >>> member_a = mlist.subscribe(address_a, MemberRole.member)
+ >>> member_b = mlist.subscribe(address_b, MemberRole.member)
>>> # This is the message metadata dictionary as it would be produced by
>>> # the CalcRecips handler.
>>> recips = dict(
diff --git a/src/mailman/pipeline/docs/calc-recips.txt b/src/mailman/pipeline/docs/calc-recips.txt
index efa1bc9c7..6dca85816 100644
--- a/src/mailman/pipeline/docs/calc-recips.txt
+++ b/src/mailman/pipeline/docs/calc-recips.txt
@@ -26,12 +26,12 @@ start out with. First, create a bunch of addresses...
...then subscribe these addresses to the mailing list as members...
>>> from mailman.interfaces.member import MemberRole
- >>> member_a = address_a.subscribe(mlist, MemberRole.member)
- >>> member_b = address_b.subscribe(mlist, MemberRole.member)
- >>> member_c = address_c.subscribe(mlist, MemberRole.member)
- >>> member_d = address_d.subscribe(mlist, MemberRole.member)
- >>> member_e = address_e.subscribe(mlist, MemberRole.member)
- >>> member_f = address_f.subscribe(mlist, MemberRole.member)
+ >>> member_a = mlist.subscribe(address_a, MemberRole.member)
+ >>> member_b = mlist.subscribe(address_b, MemberRole.member)
+ >>> member_c = mlist.subscribe(address_c, MemberRole.member)
+ >>> member_d = mlist.subscribe(address_d, MemberRole.member)
+ >>> member_e = mlist.subscribe(address_e, MemberRole.member)
+ >>> member_f = mlist.subscribe(address_f, MemberRole.member)
...then make some of the members digest members.
diff --git a/src/mailman/pipeline/docs/file-recips.txt b/src/mailman/pipeline/docs/file-recips.txt
index c994f820e..7d157ccc5 100644
--- a/src/mailman/pipeline/docs/file-recips.txt
+++ b/src/mailman/pipeline/docs/file-recips.txt
@@ -93,7 +93,7 @@ in the recipients list.
... 'cperson@example.com')
>>> from mailman.interfaces.member import MemberRole
- >>> address_1.subscribe(mlist, MemberRole.member)
+ >>> mlist.subscribe(address_1, MemberRole.member)
<Member: cperson@example.com on _xtest@example.com as MemberRole.member>
>>> msg = message_from_string("""\
diff --git a/src/mailman/queue/docs/digester.txt b/src/mailman/queue/docs/digester.txt
index 56aaf17c5..285b2072a 100644
--- a/src/mailman/queue/docs/digester.txt
+++ b/src/mailman/queue/docs/digester.txt
@@ -496,7 +496,7 @@ digests, or MIME digests.
>>> from mailman.interfaces.member import DeliveryMode, MemberRole
>>> def subscribe(email, mode):
... address = user_manager.create_address(email)
- ... member = address.subscribe(mlist, MemberRole.member)
+ ... member = mlist.subscribe(address, MemberRole.member)
... member.preferences.delivery_mode = mode
... return member
diff --git a/src/mailman/rules/moderation.py b/src/mailman/rules/moderation.py
index 733edd70c..5377b14a2 100644
--- a/src/mailman/rules/moderation.py
+++ b/src/mailman/rules/moderation.py
@@ -86,7 +86,7 @@ class NonmemberModeration:
address = user_manager.get_address(sender)
assert address is not None, (
'Posting address is not registered: {0}'.format(sender))
- address.subscribe(mlist, MemberRole.nonmember)
+ mlist.subscribe(address, MemberRole.nonmember)
# Do nonmember moderation check.
for sender in msg.senders:
nonmember = mlist.nonmembers.get_member(sender)
diff --git a/src/mailman/testing/helpers.py b/src/mailman/testing/helpers.py
index 2ba778813..bd52d5d6e 100644
--- a/src/mailman/testing/helpers.py
+++ b/src/mailman/testing/helpers.py
@@ -272,12 +272,12 @@ def subscribe(mlist, first_name, role=MemberRole.member):
if address is None:
person = user_manager.create_user(email, full_name)
preferred_address = list(person.addresses)[0]
- preferred_address.subscribe(mlist, role)
+ mlist.subscribe(preferred_address, role)
else:
- address.subscribe(mlist, role)
+ mlist.subscribe(address, role)
else:
preferred_address = list(person.addresses)[0]
- preferred_address.subscribe(mlist, role)
+ mlist.subscribe(preferred_address, role)
config.db.commit()