summaryrefslogtreecommitdiff
path: root/src/mailman/app
diff options
context:
space:
mode:
authorAbhilash Raj2015-04-20 15:16:15 +0530
committerAbhilash Raj2015-04-20 15:16:15 +0530
commit58ea970fa0f9064ae052d2b9ae1371ef00bd23e6 (patch)
tree4d21000f8ad772377a655ff332288b1c753f5be1 /src/mailman/app
parentec053e7682b14181147d0b7bedb1e5b19a46b56b (diff)
parent3eb81bf5078868b0fc44f991b0b4536a2a3f4b47 (diff)
downloadmailman-58ea970fa0f9064ae052d2b9ae1371ef00bd23e6.tar.gz
mailman-58ea970fa0f9064ae052d2b9ae1371ef00bd23e6.tar.zst
mailman-58ea970fa0f9064ae052d2b9ae1371ef00bd23e6.zip
merge trunk and fix merge conflicts
Diffstat (limited to 'src/mailman/app')
-rw-r--r--src/mailman/app/docs/chains.rst341
-rw-r--r--src/mailman/app/docs/subscriptions.rst262
-rw-r--r--src/mailman/app/moderator.py22
-rw-r--r--src/mailman/app/registrar.py4
-rw-r--r--src/mailman/app/subscriptions.py110
-rw-r--r--src/mailman/app/tests/test_registrar.py91
-rw-r--r--src/mailman/app/tests/test_subscriptions.py159
7 files changed, 358 insertions, 631 deletions
diff --git a/src/mailman/app/docs/chains.rst b/src/mailman/app/docs/chains.rst
deleted file mode 100644
index 328d0b624..000000000
--- a/src/mailman/app/docs/chains.rst
+++ /dev/null
@@ -1,341 +0,0 @@
-======
-Chains
-======
-
-When a new message is posted to a mailing list, Mailman uses a set of rule
-chains to decide whether the message gets accepted for posting, rejected,
-discarded, or held for moderator approval.
-
-There are a number of built-in chains available that act as end-points in the
-processing of messages.
-
-
-The Discard chain
-=================
-
-The `discard` chain simply throws the message away.
-::
-
- >>> chain = config.chains['discard']
- >>> print(chain.name)
- discard
- >>> print(chain.description)
- Discard a message and stop processing.
-
- >>> mlist = create_list('test@example.com')
- >>> msg = message_from_string("""\
- ... From: aperson@example.com
- ... To: test@example.com
- ... Subject: My first post
- ... Message-ID: <first>
- ...
- ... An important message.
- ... """)
-
- >>> def print_msgid(event):
- ... print('{0}: {1}'.format(
- ... event.chain.name.upper(), event.msg.get('message-id', 'n/a')))
-
- >>> from mailman.core.chains import process
- >>> from mailman.testing.helpers import event_subscribers
-
- >>> with event_subscribers(print_msgid):
- ... process(mlist, msg, {}, 'discard')
- DISCARD: <first>
-
-
-The Reject chain
-================
-
-The `reject` chain bounces the message back to the original sender, and logs
-this action.
-::
-
- >>> chain = config.chains['reject']
- >>> print(chain.name)
- reject
- >>> print(chain.description)
- Reject/bounce a message and stop processing.
-
- >>> with event_subscribers(print_msgid):
- ... process(mlist, msg, {}, 'reject')
- REJECT: <first>
-
-The bounce message is now sitting in the `virgin` queue.
-
- >>> from mailman.testing.helpers import get_queue_messages
- >>> qfiles = get_queue_messages('virgin')
- >>> len(qfiles)
- 1
- >>> print(qfiles[0].msg.as_string())
- Subject: My first post
- From: test-owner@example.com
- To: aperson@example.com
- ...
- [No bounce details are available]
- ...
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
- From: aperson@example.com
- To: test@example.com
- Subject: My first post
- Message-ID: <first>
- <BLANKLINE>
- An important message.
- <BLANKLINE>
- ...
-
-
-The Hold Chain
-==============
-
-The `hold` chain places the message into the administrative request database
-and depending on the list's settings, sends a notification to both the
-original sender and the list moderators. ::
-
- >>> chain = config.chains['hold']
- >>> print(chain.name)
- hold
- >>> print(chain.description)
- Hold a message and stop processing.
-
- >>> with event_subscribers(print_msgid):
- ... process(mlist, msg, {}, 'hold')
- HOLD: <first>
-
-There are now two messages in the virgin queue, one to the list moderators and
-one to the original author.
-
- >>> qfiles = get_queue_messages('virgin', sort_on='to')
- >>> len(qfiles)
- 2
-
-One of the message is addressed to the mailing list moderators, and the other
-is addressed to the original sender.
-
- >>> from operator import itemgetter
- >>> messages = sorted((item.msg for item in qfiles),
- ... key=itemgetter('to'), reverse=True)
-
-This one is addressed to the list moderators.
-
- >>> print(messages[0].as_string())
- Subject: test@example.com post from aperson@example.com requires approval
- From: test-owner@example.com
- To: test-owner@example.com
- MIME-Version: 1.0
- ...
- As list administrator, your authorization is requested for the
- following mailing list posting:
- <BLANKLINE>
- List: test@example.com
- From: aperson@example.com
- Subject: My first post
- Reason: N/A
- <BLANKLINE>
- At your convenience, visit your dashboard to approve or deny the
- request.
- <BLANKLINE>
- ...
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
- From: aperson@example.com
- To: test@example.com
- Subject: My first post
- Message-ID: <first>
- X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
- <BLANKLINE>
- An important message.
- <BLANKLINE>
- ...
- Content-Type: message/rfc822
- MIME-Version: 1.0
- <BLANKLINE>
- Content-Type: text/plain; charset="us-ascii"
- MIME-Version: 1.0
- Content-Transfer-Encoding: 7bit
- Subject: confirm ...
- From: test-request@example.com
- ...
- <BLANKLINE>
- If you reply to this message, keeping the Subject: header intact,
- Mailman will discard the held message. Do this if the message is
- spam. If you reply to this message and include an Approved: header
- with the list password in it, the message will be approved for posting
- to the list. The Approved: header can also appear in the first line
- of the body of the reply.
- ...
-
-This message is addressed to the sender of the message.
-
- >>> print(messages[1].as_string())
- MIME-Version: 1.0
- Content-Type: text/plain; charset="us-ascii"
- Content-Transfer-Encoding: 7bit
- Subject: Your message to test@example.com awaits moderator approval
- From: test-bounces@example.com
- To: aperson@example.com
- ...
- Your mail to 'test@example.com' with the subject
- <BLANKLINE>
- My first post
- <BLANKLINE>
- Is being held until the list moderator can review it for approval.
- <BLANKLINE>
- The reason it is being held:
- <BLANKLINE>
- N/A
- <BLANKLINE>
- Either the message will get posted to the list, or you will receive
- notification of the moderator's decision. If you would like to cancel
- this posting, please visit the following URL:
- <BLANKLINE>
- http://lists.example.com/confirm/test@example.com/...
- <BLANKLINE>
- <BLANKLINE>
-
-In addition, the pending database is holding the original messages, waiting
-for them to be disposed of by the original author or the list moderators. The
-database is essentially a dictionary, with the keys being the randomly
-selected tokens included in the urls and the values being a 2-tuple where the
-first item is a type code and the second item is a message id.
-::
-
- >>> import re
- >>> cookie = None
- >>> for line in messages[1].get_payload().splitlines():
- ... mo = re.search('confirm/[^/]+/(?P<cookie>.*)$', line)
- ... if mo:
- ... cookie = mo.group('cookie')
- ... break
- >>> assert cookie is not None, 'No confirmation token found'
-
- >>> from mailman.interfaces.pending import IPendings
- >>> from zope.component import getUtility
-
- >>> data = getUtility(IPendings).confirm(cookie)
- >>> dump_msgdata(data)
- id : 1
- type: held message
-
-The message itself is held in the message store.
-::
-
- >>> from mailman.interfaces.requests import IListRequests
- >>> list_requests = IListRequests(mlist)
- >>> rkey, rdata = list_requests.get_request(data['id'])
-
- >>> from mailman.interfaces.messages import IMessageStore
- >>> from zope.component import getUtility
- >>> msg = getUtility(IMessageStore).get_message_by_id(
- ... rdata['_mod_message_id'])
-
- >>> print(msg.as_string())
- From: aperson@example.com
- To: test@example.com
- Subject: My first post
- Message-ID: <first>
- X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
- <BLANKLINE>
- An important message.
- <BLANKLINE>
-
-
-The Accept chain
-================
-
-The `accept` chain sends the message on the `pipeline` queue, where it will be
-processed and sent on to the list membership.
-::
-
- >>> chain = config.chains['accept']
- >>> print(chain.name)
- accept
- >>> print(chain.description)
- Accept a message.
-
- >>> with event_subscribers(print_msgid):
- ... process(mlist, msg, {}, 'accept')
- ACCEPT: <first>
-
- >>> qfiles = get_queue_messages('pipeline')
- >>> len(qfiles)
- 1
- >>> print(qfiles[0].msg.as_string())
- From: aperson@example.com
- To: test@example.com
- Subject: My first post
- Message-ID: <first>
- X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
- <BLANKLINE>
- An important message.
- <BLANKLINE>
-
-
-Run-time chains
-===============
-
-We can also define chains at run time, and these chains can be mutated.
-Run-time chains are made up of links where each link associates both a rule
-and a `jump`. The rule is really a rule name, which is looked up when
-needed. The jump names a chain which is jumped to if the rule matches.
-
-There is one built-in posting chain. This is the default chain to use when no
-other input chain is defined for a mailing list. It runs through the default
-rules.
-
- >>> chain = config.chains['default-posting-chain']
- >>> print(chain.name)
- default-posting-chain
- >>> print(chain.description)
- The built-in moderation chain.
-
-Once the sender is a member of the mailing list, the previously created
-message is innocuous enough that it should pass through all default rules.
-This message will end up in the `pipeline` queue.
-::
-
- >>> from mailman.testing.helpers import subscribe
- >>> subscribe(mlist, 'Anne')
- <Member: aperson@example.com on test@example.com as MemberRole.member>
-
- >>> with event_subscribers(print_msgid):
- ... process(mlist, msg, {})
- ACCEPT: <first>
-
- >>> qfiles = get_queue_messages('pipeline')
- >>> len(qfiles)
- 1
- >>> print(qfiles[0].msg.as_string())
- From: aperson@example.com
- To: test@example.com
- Subject: My first post
- Message-ID: <first>
- X-Message-ID-Hash: RXJU4JL6N2OUN3OYMXXPPSCR7P7JE2BW
- X-Mailman-Rule-Misses: approved; emergency; loop; member-moderation;
- administrivia; implicit-dest; max-recipients; max-size;
- news-moderation; no-subject; suspicious-header; nonmember-moderation
- <BLANKLINE>
- An important message.
- <BLANKLINE>
-
-In addition, the message metadata now contains lists of all rules that have
-hit and all rules that have missed.
-
- >>> dump_list(qfiles[0].msgdata['rule_hits'])
- *Empty*
- >>> dump_list(qfiles[0].msgdata['rule_misses'])
- administrivia
- approved
- emergency
- implicit-dest
- loop
- max-recipients
- max-size
- member-moderation
- news-moderation
- no-subject
- nonmember-moderation
- suspicious-header
diff --git a/src/mailman/app/docs/subscriptions.rst b/src/mailman/app/docs/subscriptions.rst
index eaccdc3cc..2fc59d9a7 100644
--- a/src/mailman/app/docs/subscriptions.rst
+++ b/src/mailman/app/docs/subscriptions.rst
@@ -2,9 +2,10 @@
Subscription services
=====================
-The `ISubscriptionService` utility provides higher level convenience methods
-useful for searching, retrieving, iterating, adding, and removing
-memberships.
+The ``ISubscriptionService`` utility provides higher level convenience methods
+useful for searching, retrieving, iterating, and removing memberships across
+all mailing lists on th esystem. Adding new users is handled by the
+``IRegistrar`` interface.
>>> from mailman.interfaces.subscriptions import ISubscriptionService
>>> from zope.component import getUtility
@@ -22,183 +23,154 @@ membership role. At first, there are no memberships.
None
-Adding new members
-==================
-
-The service can be used to subscribe new members, by default with the `member`
-role. At a minimum, a mailing list and an address for the new user is
-required.
-
- >>> mlist = create_list('test@example.com')
- >>> anne = service.join('test.example.com', 'anne@example.com')
- >>> anne
- <Member: anne <anne@example.com> on test@example.com as MemberRole.member>
+Listing members
+===============
-The real name of the new member can be given.
+When there are some members, of any role on any mailing list, they can be
+retrieved through the subscription service.
- >>> bart = service.join('test.example.com', 'bart@example.com',
- ... 'Bart Person')
- >>> bart
- <Member: Bart Person <bart@example.com>
- on test@example.com as MemberRole.member>
+ >>> from mailman.app.lifecycle import create_list
+ >>> ant = create_list('ant@example.com')
+ >>> bee = create_list('bee@example.com')
+ >>> cat = create_list('cat@example.com')
-Other roles can also be subscribed.
+Some people become members.
>>> from mailman.interfaces.member import MemberRole
- >>> anne_owner = service.join('test.example.com', 'anne@example.com',
- ... role=MemberRole.owner)
- >>> anne_owner
- <Member: anne <anne@example.com> on test@example.com as MemberRole.owner>
+ >>> from mailman.testing.helpers import subscribe
+ >>> anne_1 = subscribe(ant, 'Anne')
+ >>> anne_2 = subscribe(ant, 'Anne', MemberRole.owner)
+ >>> bart_1 = subscribe(ant, 'Bart', MemberRole.moderator)
+ >>> bart_2 = subscribe(bee, 'Bart', MemberRole.owner)
+ >>> anne_3 = subscribe(cat, 'Anne', email='anne@example.com')
+ >>> cris_1 = subscribe(cat, 'Cris')
-And all the subscribed members can now be displayed.
+The service can be used to iterate over them.
- >>> service.get_members()
- [<Member: anne <anne@example.com> on test@example.com as MemberRole.owner>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.member>,
- <Member: Bart Person <bart@example.com> on test@example.com
- as MemberRole.member>]
- >>> sum(1 for member in service)
- 3
- >>> print(service.get_member(UUID(int=3)))
- <Member: anne <anne@example.com> on test@example.com as MemberRole.owner>
+ >>> for member in service.get_members():
+ ... print(member)
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.owner>
+ <Member: Bart Person <bperson@example.com>
+ on ant@example.com as MemberRole.moderator>
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.member>
+ <Member: Bart Person <bperson@example.com>
+ on bee@example.com as MemberRole.owner>
+ <Member: Anne Person <anne@example.com>
+ on cat@example.com as MemberRole.member>
+ <Member: Cris Person <cperson@example.com>
+ on cat@example.com as MemberRole.member>
-New members can also be added by providing an existing user id instead of an
-email address. However, the user must have a preferred email address.
-::
+The service can also be used to get the information about a single member.
- >>> from mailman.utilities.datetime import now
- >>> address = list(bart.user.addresses)[0]
- >>> address.verified_on = now()
- >>> bart.user.preferred_address = address
- >>> service.join('test.example.com', bart.user.user_id,
- ... role=MemberRole.owner)
- <Member: Bart Person <bart@example.com>
- on test@example.com as MemberRole.owner>
+ >>> print(service.get_member(bart_2.member_id))
+ <Member: Bart Person <bperson@example.com>
+ on bee@example.com as MemberRole.owner>
+There is an iteration shorthand for getting all the members.
-Removing members
-================
-
-Regular members can also be removed.
-
- >>> cris = service.join('test.example.com', 'cris@example.com')
- >>> service.get_members()
- [<Member: anne <anne@example.com> on test@example.com
- as MemberRole.owner>,
- <Member: Bart Person <bart@example.com> on test@example.com
- as MemberRole.owner>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.member>,
- <Member: Bart Person <bart@example.com> on test@example.com
- as MemberRole.member>,
- <Member: cris <cris@example.com> on test@example.com
- as MemberRole.member>]
- >>> sum(1 for member in service)
- 5
- >>> service.leave('test.example.com', 'cris@example.com')
- >>> service.get_members()
- [<Member: anne <anne@example.com> on test@example.com
- as MemberRole.owner>,
- <Member: Bart Person <bart@example.com> on test@example.com
- as MemberRole.owner>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.member>,
- <Member: Bart Person <bart@example.com> on test@example.com
- as MemberRole.member>]
- >>> sum(1 for member in service)
- 4
+ >>> for member in service:
+ ... print(member)
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.owner>
+ <Member: Bart Person <bperson@example.com>
+ on ant@example.com as MemberRole.moderator>
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.member>
+ <Member: Bart Person <bperson@example.com>
+ on bee@example.com as MemberRole.owner>
+ <Member: Anne Person <anne@example.com>
+ on cat@example.com as MemberRole.member>
+ <Member: Cris Person <cperson@example.com>
+ on cat@example.com as MemberRole.member>
Finding members
===============
-If you know the member id for a specific member, you can get that member.
-
- >>> service.get_member(UUID(int=3))
- <Member: anne <anne@example.com> on test@example.com as MemberRole.owner>
-
-If you know the member's address, you can find all their memberships, based on
-specific search criteria. We start by subscribing Anne to a couple of new
-mailing lists.
+The subscription service can be used to find memberships based on specific
+search criteria. For example, we can find all the mailing lists that Anne is
+a member of with her ``aperson@example.com`` address.
- >>> mlist2 = create_list('foo@example.com')
- >>> mlist3 = create_list('bar@example.com')
- >>> address = list(anne.user.addresses)[0]
- >>> address.verified_on = now()
- >>> anne.user.preferred_address = address
- >>> mlist.subscribe(anne.user, MemberRole.moderator)
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.moderator>
- >>> mlist2.subscribe(anne.user, MemberRole.member)
- <Member: anne <anne@example.com> on foo@example.com as MemberRole.member>
- >>> mlist3.subscribe(anne.user, MemberRole.owner)
- <Member: anne <anne@example.com> on bar@example.com as MemberRole.owner>
-
-And now we can find all of Anne's memberships.
-
- >>> service.find_members('anne@example.com')
- [<Member: anne <anne@example.com> on bar@example.com as MemberRole.owner>,
- <Member: anne <anne@example.com> on foo@example.com as MemberRole.member>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.member>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.owner>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.moderator>]
+ >>> for member in service.find_members('aperson@example.com'):
+ ... print(member)
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.member>
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.owner>
There may be no matching memberships.
- >>> service.find_members('cris@example.com')
+ >>> service.find_members('dave@example.com')
[]
Memberships can also be searched for by user id.
- >>> service.find_members(UUID(int=1))
- [<Member: anne <anne@example.com> on bar@example.com as MemberRole.owner>,
- <Member: anne <anne@example.com> on foo@example.com as MemberRole.member>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.member>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.owner>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.moderator>]
+ >>> for member in service.find_members(anne_1.user.user_id):
+ ... print(member)
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.member>
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.owner>
You can find all the memberships for a specific mailing list.
- >>> service.find_members(list_id='test.example.com')
- [<Member: anne <anne@example.com> on test@example.com
- as MemberRole.member>,
- <Member: anne <anne@example.com> on test@example.com as MemberRole.owner>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.moderator>,
- <Member: Bart Person <bart@example.com> on test@example.com
- as MemberRole.member>,
- <Member: Bart Person <bart@example.com> on test@example.com
- as MemberRole.owner>]
+ >>> for member in service.find_members(list_id='ant.example.com'):
+ ... print(member)
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.member>
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.owner>
+ <Member: Bart Person <bperson@example.com>
+ on ant@example.com as MemberRole.moderator>
You can find all the memberships for an address on a specific mailing list,
but you have to give it the list id, not the fqdn listname since the former is
stable but the latter could change if the list is moved.
- >>> service.find_members('anne@example.com', 'test.example.com')
- [<Member: anne <anne@example.com> on test@example.com
- as MemberRole.member>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.owner>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.moderator>]
+ >>> for member in service.find_members(
+ ... 'bperson@example.com', 'ant.example.com'):
+ ... print(member)
+ <Member: Bart Person <bperson@example.com>
+ on ant@example.com as MemberRole.moderator>
You can find all the memberships for an address with a specific role.
- >>> service.find_members('anne@example.com', role=MemberRole.owner)
- [<Member: anne <anne@example.com> on bar@example.com as MemberRole.owner>,
- <Member: anne <anne@example.com> on test@example.com
- as MemberRole.owner>]
+ >>> for member in service.find_members(
+ ... list_id='ant.example.com', role=MemberRole.owner):
+ ... print(member)
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.owner>
You can also find a specific membership by all three criteria.
- >>> service.find_members('anne@example.com', 'test.example.com',
- ... MemberRole.owner)
- [<Member: anne <anne@example.com> on test@example.com
- as MemberRole.owner>]
+ >>> for member in service.find_members(
+ ... 'bperson@example.com', 'bee.example.com', MemberRole.owner):
+ ... print(member)
+ <Member: Bart Person <bperson@example.com>
+ on bee@example.com as MemberRole.owner>
+
+
+Removing members
+================
+
+Members can be removed via this service.
+
+ >>> len(service.get_members())
+ 6
+ >>> service.leave('cat.example.com', 'cperson@example.com')
+ >>> len(service.get_members())
+ 5
+ >>> for member in service:
+ ... print(member)
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.owner>
+ <Member: Bart Person <bperson@example.com>
+ on ant@example.com as MemberRole.moderator>
+ <Member: Anne Person <aperson@example.com>
+ on ant@example.com as MemberRole.member>
+ <Member: Bart Person <bperson@example.com>
+ on bee@example.com as MemberRole.owner>
+ <Member: Anne Person <anne@example.com>
+ on cat@example.com as MemberRole.member>
diff --git a/src/mailman/app/moderator.py b/src/mailman/app/moderator.py
index 0e4d59479..eb848ea08 100644
--- a/src/mailman/app/moderator.py
+++ b/src/mailman/app/moderator.py
@@ -25,6 +25,7 @@ __all__ = [
'hold_message',
'hold_subscription',
'hold_unsubscription',
+ 'send_rejection',
]
@@ -125,8 +126,9 @@ def handle_message(mlist, id, action,
language = member.preferred_language
else:
language = None
- _refuse(mlist, _('Posting of your message titled "$subject"'),
- sender, comment or _('[No reason given]'), language)
+ send_rejection(
+ mlist, _('Posting of your message titled "$subject"'),
+ sender, comment or _('[No reason given]'), language)
elif action is Action.accept:
# Start by getting the message from the message store.
msg = message_store.get_message_by_id(message_id)
@@ -236,10 +238,11 @@ def handle_subscription(mlist, id, action, comment=None):
pass
elif action is Action.reject:
key, data = requestdb.get_request(id)
- _refuse(mlist, _('Subscription request'),
- data['email'],
- comment or _('[No reason given]'),
- lang=getUtility(ILanguageManager)[data['language']])
+ send_rejection(
+ mlist, _('Subscription request'),
+ data['email'],
+ comment or _('[No reason given]'),
+ lang=getUtility(ILanguageManager)[data['language']])
elif action is Action.accept:
key, data = requestdb.get_request(id)
delivery_mode = DeliveryMode[data['delivery_mode']]
@@ -307,8 +310,9 @@ def handle_unsubscription(mlist, id, action, comment=None):
pass
elif action is Action.reject:
key, data = requestdb.get_request(id)
- _refuse(mlist, _('Unsubscription request'), email,
- comment or _('[No reason given]'))
+ send_rejection(
+ mlist, _('Unsubscription request'), email,
+ comment or _('[No reason given]'))
elif action is Action.accept:
key, data = requestdb.get_request(id)
try:
@@ -324,7 +328,7 @@ def handle_unsubscription(mlist, id, action, comment=None):
-def _refuse(mlist, request, recip, comment, origmsg=None, lang=None):
+def send_rejection(mlist, request, recip, comment, origmsg=None, lang=None):
# As this message is going to the requester, try to set the language to
# his/her language choice, if they are a member. Otherwise use the list's
# preferred language.
diff --git a/src/mailman/app/registrar.py b/src/mailman/app/registrar.py
index ae4322d22..240742bc0 100644
--- a/src/mailman/app/registrar.py
+++ b/src/mailman/app/registrar.py
@@ -63,7 +63,7 @@ class Registrar:
pre_confirmed=pre_confirmed,
pre_approved=pre_approved)
list(workflow)
- return workflow.token
+ return workflow.token, workflow.token_owner, workflow.member
def confirm(self, token):
"""See `IRegistrar`."""
@@ -71,7 +71,7 @@ class Registrar:
workflow.token = token
workflow.restore()
list(workflow)
- return workflow.token
+ return workflow.token, workflow.token_owner, workflow.member
def discard(self, token):
"""See `IRegistrar`."""
diff --git a/src/mailman/app/subscriptions.py b/src/mailman/app/subscriptions.py
index 3138c513b..1593b4d58 100644
--- a/src/mailman/app/subscriptions.py
+++ b/src/mailman/app/subscriptions.py
@@ -24,16 +24,14 @@ __all__ = [
]
-
import uuid
import logging
from email.utils import formataddr
from enum import Enum
from datetime import timedelta
-from mailman.app.membership import add_member, delete_member
+from mailman.app.membership import delete_member
from mailman.app.workflow import Workflow
-from mailman.core.constants import system_preferences
from mailman.core.i18n import _
from mailman.database.transaction import dbconnection
from mailman.email.message import UserNotification
@@ -42,12 +40,10 @@ from mailman.interfaces.bans import IBanManager
from mailman.interfaces.listmanager import (
IListManager, ListDeletingEvent, NoSuchListError)
from mailman.interfaces.mailinglist import SubscriptionPolicy
-from mailman.interfaces.member import (
- DeliveryMode, MemberRole, MembershipIsBannedError)
+from mailman.interfaces.member import MembershipIsBannedError
from mailman.interfaces.pending import IPendable, IPendings
from mailman.interfaces.registrar import ConfirmationNeededEvent
-from mailman.interfaces.subscriptions import (
- ISubscriptionService, MissingUserError, RequestRecord)
+from mailman.interfaces.subscriptions import ISubscriptionService, TokenOwner
from mailman.interfaces.user import IUser
from mailman.interfaces.usermanager import IUserManager
from mailman.interfaces.workflow import IWorkflowStateManager
@@ -56,7 +52,6 @@ from mailman.utilities.datetime import now
from mailman.utilities.i18n import make
from operator import attrgetter
from sqlalchemy import and_, or_
-from uuid import UUID
from zope.component import getUtility
from zope.event import notify
from zope.interface import implementer
@@ -97,6 +92,7 @@ class SubscriptionWorkflow(Workflow):
'address_key',
'subscriber_key',
'user_key',
+ 'token_owner_key',
)
def __init__(self, mlist, subscriber=None, *,
@@ -106,6 +102,8 @@ class SubscriptionWorkflow(Workflow):
self.address = None
self.user = None
self.which = None
+ self.member = None
+ self._set_token(TokenOwner.no_one)
# The subscriber must be either an IUser or IAddress.
if IAddress.providedBy(subscriber):
self.address = subscriber
@@ -151,6 +149,36 @@ class SubscriptionWorkflow(Workflow):
def subscriber_key(self, key):
self.which = WhichSubscriber(key)
+ @property
+ def token_owner_key(self):
+ return self.token_owner.value
+
+ @token_owner_key.setter
+ def token_owner_key(self, value):
+ self.token_owner = TokenOwner(value)
+
+ def _set_token(self, token_owner):
+ assert isinstance(token_owner, TokenOwner)
+ pendings = getUtility(IPendings)
+ # Clear out the previous pending token if there is one.
+ if self.token is not None:
+ pendings.confirm(self.token)
+ # Create a new token to prevent replay attacks. It seems like this
+ # would produce the same token, but it won't because the pending adds a
+ # bit of randomization.
+ self.token_owner = token_owner
+ if token_owner is TokenOwner.no_one:
+ self.token = None
+ return
+ pendable = Pendable(
+ list_id=self.mlist.list_id,
+ email=self.address.email,
+ display_name=self.address.display_name,
+ when=now().replace(microsecond=0).isoformat(),
+ token_owner=token_owner.name,
+ )
+ self.token = pendings.add(pendable, timedelta(days=3650))
+
def _step_sanity_checks(self):
# Ensure that we have both an address and a user, even if the address
# is not verified. We can't set the preferred address until it is
@@ -174,13 +202,7 @@ class SubscriptionWorkflow(Workflow):
# Is this email address banned?
if IBanManager(self.mlist).is_banned(self.address.email):
raise MembershipIsBannedError(self.mlist, self.address.email)
- # Create a pending record. This will give us the hash token we can use
- # to uniquely name this workflow.
- pendable = Pendable(
- list_id=self.mlist.list_id,
- address=self.address.email,
- )
- self.token = getUtility(IPendings).add(pendable, timedelta(days=3650))
+ # Start out with the subscriber being the token owner.
self.push('verification_checks')
def _step_verification_checks(self):
@@ -207,10 +229,16 @@ class SubscriptionWorkflow(Workflow):
if self.mlist.subscription_policy is SubscriptionPolicy.moderate:
self.push('moderation_checks')
return
- # If the subscription has been pre-confirmed, then we can skip to the
- # moderation checks.
+ # If the subscription has been pre-confirmed, then we can skip the
+ # confirmation check can be skipped. If moderator approval is
+ # required we need to check that, otherwise we can go straight to
+ # subscription.
if self.pre_confirmed:
- self.push('moderation_checks')
+ next_step = ('moderation_checks'
+ if self.mlist.subscription_policy is
+ SubscriptionPolicy.confirm_then_moderate
+ else 'do_subscription')
+ self.push(next_step)
return
# The user must confirm their subscription.
self.push('send_confirmation')
@@ -219,7 +247,8 @@ class SubscriptionWorkflow(Workflow):
# Does the moderator need to approve the subscription request?
assert self.mlist.subscription_policy in (
SubscriptionPolicy.moderate,
- SubscriptionPolicy.confirm_then_moderate)
+ SubscriptionPolicy.confirm_then_moderate,
+ ), self.mlist.subscription_policy
if self.pre_approved:
self.push('do_subscription')
else:
@@ -229,6 +258,7 @@ class SubscriptionWorkflow(Workflow):
# Here's the next step in the workflow, assuming the moderator
# approves of the subscription. If they don't, the workflow and
# subscription request will just be thrown away.
+ self._set_token(TokenOwner.moderator)
self.push('subscribe_from_restored')
self.save()
log.info('{}: held subscription request from {}'.format(
@@ -255,6 +285,8 @@ class SubscriptionWorkflow(Workflow):
raise StopIteration
def _step_subscribe_from_restored(self):
+ # Prevent replay attacks.
+ self._set_token(TokenOwner.no_one)
# Restore a little extra state that can't be stored in the database
# (because the order of setattr() on restore is indeterminate), then
# subscribe the user.
@@ -267,12 +299,12 @@ class SubscriptionWorkflow(Workflow):
def _step_do_subscription(self):
# We can immediately subscribe the user to the mailing list.
- self.mlist.subscribe(self.subscriber)
+ self.member = self.mlist.subscribe(self.subscriber)
# This workflow is done so throw away any associated state.
getUtility(IWorkflowStateManager).restore(self.name, self.token)
- self.token = None
def _step_send_confirmation(self):
+ self._set_token(TokenOwner.subscriber)
self.push('do_confirm_verify')
self.save()
# Triggering this event causes the confirmation message to be sent.
@@ -290,14 +322,8 @@ class SubscriptionWorkflow(Workflow):
else:
assert self.which is WhichSubscriber.user
self.subscriber = self.user
- # Create a new token to prevent replay attacks. It seems like this
- # should produce the same token, but it won't because the pending adds
- # a bit of randomization.
- pendable = Pendable(
- list_id=self.mlist.list_id,
- address=self.address.email,
- )
- self.token = getUtility(IPendings).add(pendable, timedelta(days=3650))
+ # Reset the token so it can't be used in a replay attack.
+ self._set_token(TokenOwner.no_one)
# The user has confirmed their subscription request, and also verified
# their email address if necessary. This latter needs to be set on the
# IAddress, but there's nothing more to do about the confirmation step.
@@ -314,6 +340,7 @@ class SubscriptionWorkflow(Workflow):
self.push(next_step)
+
@implementer(ISubscriptionService)
class SubscriptionService:
"""Subscription services for the REST API."""
@@ -396,31 +423,6 @@ class SubscriptionService:
for member in self.get_members():
yield member
- def join(self, list_id, subscriber,
- display_name=None,
- delivery_mode=DeliveryMode.regular,
- role=MemberRole.member):
- """See `ISubscriptionService`."""
- mlist = getUtility(IListManager).get_by_list_id(list_id)
- if mlist is None:
- raise NoSuchListError(list_id)
- # Is the subscriber an email address or user id?
- if isinstance(subscriber, str):
- if display_name is None:
- display_name, at, domain = subscriber.partition('@')
- return add_member(
- mlist,
- RequestRecord(subscriber, display_name, delivery_mode,
- system_preferences.preferred_language),
- role)
- else:
- # We have to assume it's a UUID.
- assert isinstance(subscriber, UUID), 'Not a UUID'
- user = getUtility(IUserManager).get_user_by_id(subscriber)
- if user is None:
- raise MissingUserError(subscriber)
- return mlist.subscribe(user, role)
-
def leave(self, list_id, email):
"""See `ISubscriptionService`."""
mlist = getUtility(IListManager).get_by_list_id(list_id)
diff --git a/src/mailman/app/tests/test_registrar.py b/src/mailman/app/tests/test_registrar.py
index 4f5e1e3f9..e76009454 100644
--- a/src/mailman/app/tests/test_registrar.py
+++ b/src/mailman/app/tests/test_registrar.py
@@ -28,6 +28,7 @@ from mailman.app.lifecycle import create_list
from mailman.interfaces.mailinglist import SubscriptionPolicy
from mailman.interfaces.pending import IPendings
from mailman.interfaces.registrar import IRegistrar
+from mailman.interfaces.subscriptions import TokenOwner
from mailman.interfaces.usermanager import IUserManager
from mailman.testing.layers import ConfigLayer
from mailman.utilities.datetime import now
@@ -47,37 +48,34 @@ class TestRegistrar(unittest.TestCase):
self._anne = getUtility(IUserManager).create_address(
'anne@example.com')
- def test_unique_token(self):
+ def test_initial_conditions(self):
# Registering a subscription request provides a unique token associated
- # with a pendable.
+ # with a pendable, and the owner of the token.
self.assertEqual(self._pendings.count, 0)
- token = self._registrar.register(self._anne)
+ token, token_owner, member = self._registrar.register(self._anne)
self.assertIsNotNone(token)
+ self.assertEqual(token_owner, TokenOwner.subscriber)
+ self.assertIsNone(member)
self.assertEqual(self._pendings.count, 1)
record = self._pendings.confirm(token, expunge=False)
self.assertEqual(record['list_id'], self._mlist.list_id)
- self.assertEqual(record['address'], 'anne@example.com')
+ self.assertEqual(record['email'], 'anne@example.com')
- def test_no_token(self):
+ def test_subscribe(self):
# Registering a subscription request where no confirmation or
- # moderation steps are needed, leaves us with no token, since there's
- # nothing more to do.
+ # moderation steps are needed, leaves us with no token or owner, since
+ # there's nothing more to do.
self._mlist.subscription_policy = SubscriptionPolicy.open
self._anne.verified_on = now()
- token = self._registrar.register(self._anne)
+ token, token_owner, rmember = self._registrar.register(self._anne)
self.assertIsNone(token)
- record = self._pendings.confirm(token, expunge=False)
- self.assertIsNone(record)
-
- def test_is_subscribed(self):
- # Where no confirmation or moderation steps are needed, registration
- # happens immediately.
- self._mlist.subscription_policy = SubscriptionPolicy.open
- self._anne.verified_on = now()
- status = self._registrar.register(self._anne)
- self.assertIsNone(status)
+ self.assertEqual(token_owner, TokenOwner.no_one)
member = self._mlist.regular_members.get_member('anne@example.com')
+ self.assertEqual(rmember, member)
self.assertEqual(member.address, self._anne)
+ # There's nothing to confirm.
+ record = self._pendings.confirm(token, expunge=False)
+ self.assertIsNone(record)
def test_no_such_token(self):
# Given a token which is not in the database, a LookupError is raised.
@@ -90,13 +88,18 @@ class TestRegistrar(unittest.TestCase):
# to approve. Running the workflow gives us a token. Confirming the
# token subscribes the user.
self._mlist.subscription_policy = SubscriptionPolicy.open
- token = self._registrar.register(self._anne)
+ token, token_owner, rmember = self._registrar.register(self._anne)
self.assertIsNotNone(token)
+ self.assertEqual(token_owner, TokenOwner.subscriber)
+ self.assertIsNone(rmember)
member = self._mlist.regular_members.get_member('anne@example.com')
self.assertIsNone(member)
# Now confirm the subscription.
- self._registrar.confirm(token)
+ token, token_owner, rmember = self._registrar.confirm(token)
+ self.assertIsNone(token)
+ self.assertEqual(token_owner, TokenOwner.no_one)
member = self._mlist.regular_members.get_member('anne@example.com')
+ self.assertEqual(rmember, member)
self.assertEqual(member.address, self._anne)
def test_confirm_because_confirm(self):
@@ -106,13 +109,18 @@ class TestRegistrar(unittest.TestCase):
# user.
self._mlist.subscription_policy = SubscriptionPolicy.confirm
self._anne.verified_on = now()
- token = self._registrar.register(self._anne)
+ token, token_owner, rmember = self._registrar.register(self._anne)
self.assertIsNotNone(token)
+ self.assertEqual(token_owner, TokenOwner.subscriber)
+ self.assertIsNone(rmember)
member = self._mlist.regular_members.get_member('anne@example.com')
self.assertIsNone(member)
# Now confirm the subscription.
- self._registrar.confirm(token)
+ token, token_owner, rmember = self._registrar.confirm(token)
+ self.assertIsNone(token)
+ self.assertEqual(token_owner, TokenOwner.no_one)
member = self._mlist.regular_members.get_member('anne@example.com')
+ self.assertEqual(rmember, member)
self.assertEqual(member.address, self._anne)
def test_confirm_because_moderation(self):
@@ -121,13 +129,18 @@ class TestRegistrar(unittest.TestCase):
# token subscribes the user.
self._mlist.subscription_policy = SubscriptionPolicy.moderate
self._anne.verified_on = now()
- token = self._registrar.register(self._anne)
+ token, token_owner, rmember = self._registrar.register(self._anne)
self.assertIsNotNone(token)
+ self.assertEqual(token_owner, TokenOwner.moderator)
+ self.assertIsNone(rmember)
member = self._mlist.regular_members.get_member('anne@example.com')
self.assertIsNone(member)
# Now confirm the subscription.
- self._registrar.confirm(token)
+ token, token_owner, rmember = self._registrar.confirm(token)
+ self.assertIsNone(token)
+ self.assertEqual(token_owner, TokenOwner.no_one)
member = self._mlist.regular_members.get_member('anne@example.com')
+ self.assertEqual(rmember, member)
self.assertEqual(member.address, self._anne)
def test_confirm_because_confirm_then_moderation(self):
@@ -140,22 +153,30 @@ class TestRegistrar(unittest.TestCase):
SubscriptionPolicy.confirm_then_moderate
self._anne.verified_on = now()
# Runs until subscription confirmation.
- token = self._registrar.register(self._anne)
+ token, token_owner, rmember = self._registrar.register(self._anne)
self.assertIsNotNone(token)
+ self.assertEqual(token_owner, TokenOwner.subscriber)
+ self.assertIsNone(rmember)
member = self._mlist.regular_members.get_member('anne@example.com')
self.assertIsNone(member)
# Now confirm the subscription, and wait for the moderator to approve
# the subscription. She is still not subscribed.
- new_token = self._registrar.confirm(token)
+ new_token, token_owner, rmember = self._registrar.confirm(token)
# The new token, used for the moderator to approve the message, is not
# the same as the old token.
self.assertNotEqual(new_token, token)
+ self.assertIsNotNone(new_token)
+ self.assertEqual(token_owner, TokenOwner.moderator)
+ self.assertIsNone(rmember)
member = self._mlist.regular_members.get_member('anne@example.com')
self.assertIsNone(member)
# Confirm once more, this time as the moderator approving the
# subscription. Now she's a member.
- self._registrar.confirm(new_token)
+ token, token_owner, rmember = self._registrar.confirm(new_token)
+ self.assertIsNone(token)
+ self.assertEqual(token_owner, TokenOwner.no_one)
member = self._mlist.regular_members.get_member('anne@example.com')
+ self.assertEqual(rmember, member)
self.assertEqual(member.address, self._anne)
def test_confirm_then_moderate_with_different_tokens(self):
@@ -167,16 +188,20 @@ class TestRegistrar(unittest.TestCase):
SubscriptionPolicy.confirm_then_moderate
self._anne.verified_on = now()
# Runs until subscription confirmation.
- token = self._registrar.register(self._anne)
+ token, token_owner, rmember = self._registrar.register(self._anne)
self.assertIsNotNone(token)
+ self.assertEqual(token_owner, TokenOwner.subscriber)
+ self.assertIsNone(rmember)
member = self._mlist.regular_members.get_member('anne@example.com')
self.assertIsNone(member)
# Now confirm the subscription, and wait for the moderator to approve
# the subscription. She is still not subscribed.
- new_token = self._registrar.confirm(token)
+ new_token, token_owner, rmember = self._registrar.confirm(token)
# The status is not true because the user has not yet been subscribed
# to the mailing list.
self.assertIsNotNone(new_token)
+ self.assertEqual(token_owner, TokenOwner.moderator)
+ self.assertIsNone(rmember)
member = self._mlist.regular_members.get_member('anne@example.com')
self.assertIsNone(member)
# The new token is different than the old token.
@@ -185,10 +210,12 @@ class TestRegistrar(unittest.TestCase):
self.assertRaises(LookupError, self._registrar.confirm, token)
# Confirm once more, this time with the new token, as the moderator
# approving the subscription. Now she's a member.
- done_token = self._registrar.confirm(new_token)
+ done_token, token_owner, rmember = self._registrar.confirm(new_token)
# The token is None, signifying that the member has been subscribed.
self.assertIsNone(done_token)
+ self.assertEqual(token_owner, TokenOwner.no_one)
member = self._mlist.regular_members.get_member('anne@example.com')
+ self.assertEqual(rmember, member)
self.assertEqual(member.address, self._anne)
def test_discard_waiting_for_confirmation(self):
@@ -197,8 +224,10 @@ class TestRegistrar(unittest.TestCase):
self._mlist.subscription_policy = SubscriptionPolicy.confirm
self._anne.verified_on = now()
# Runs until subscription confirmation.
- token = self._registrar.register(self._anne)
+ token, token_owner, rmember = self._registrar.register(self._anne)
self.assertIsNotNone(token)
+ self.assertEqual(token_owner, TokenOwner.subscriber)
+ self.assertIsNone(rmember)
member = self._mlist.regular_members.get_member('anne@example.com')
self.assertIsNone(member)
# Now discard the subscription request.
diff --git a/src/mailman/app/tests/test_subscriptions.py b/src/mailman/app/tests/test_subscriptions.py
index a4de3fbb3..2d5a3733b 100644
--- a/src/mailman/app/tests/test_subscriptions.py
+++ b/src/mailman/app/tests/test_subscriptions.py
@@ -18,23 +18,18 @@
"""Tests for the subscription service."""
__all__ = [
- 'TestJoin',
'TestSubscriptionWorkflow',
]
-import uuid
import unittest
from mailman.app.lifecycle import create_list
from mailman.app.subscriptions import SubscriptionWorkflow
-from mailman.interfaces.address import InvalidEmailAddressError
from mailman.interfaces.bans import IBanManager
-from mailman.interfaces.member import (
- MemberRole, MembershipIsBannedError, MissingPreferredAddressError)
+from mailman.interfaces.member import MembershipIsBannedError
from mailman.interfaces.pending import IPendings
-from mailman.interfaces.subscriptions import (
- MissingUserError, ISubscriptionService)
+from mailman.interfaces.subscriptions import TokenOwner
from mailman.testing.helpers import LogFileMark, get_queue_messages
from mailman.testing.layers import ConfigLayer
from mailman.interfaces.mailinglist import SubscriptionPolicy
@@ -45,39 +40,6 @@ from zope.component import getUtility
-class TestJoin(unittest.TestCase):
- layer = ConfigLayer
-
- def setUp(self):
- self._mlist = create_list('test@example.com')
- self._service = getUtility(ISubscriptionService)
-
- def test_join_user_with_bogus_id(self):
- # When `subscriber` is a missing user id, an exception is raised.
- with self.assertRaises(MissingUserError) as cm:
- self._service.join('test.example.com', uuid.UUID(int=99))
- self.assertEqual(cm.exception.user_id, uuid.UUID(int=99))
-
- def test_join_user_with_invalid_email_address(self):
- # When `subscriber` is a string that is not an email address, an
- # exception is raised.
- with self.assertRaises(InvalidEmailAddressError) as cm:
- self._service.join('test.example.com', 'bogus')
- self.assertEqual(cm.exception.email, 'bogus')
-
- def test_missing_preferred_address(self):
- # A user cannot join a mailing list if they have no preferred address.
- anne = self._service.join(
- 'test.example.com', 'anne@example.com', 'Anne Person')
- # Try to join Anne as a user with a different role. Her user has no
- # preferred address, so this will fail.
- self.assertRaises(MissingPreferredAddressError,
- self._service.join,
- 'test.example.com', anne.user.user_id,
- role=MemberRole.owner)
-
-
-
class TestSubscriptionWorkflow(unittest.TestCase):
layer = ConfigLayer
maxDiff = None
@@ -88,6 +50,30 @@ class TestSubscriptionWorkflow(unittest.TestCase):
self._anne = 'anne@example.com'
self._user_manager = getUtility(IUserManager)
+ def test_start_state(self):
+ # The workflow starts with no tokens or member.
+ workflow = SubscriptionWorkflow(self._mlist)
+ self.assertIsNone(workflow.token)
+ self.assertEqual(workflow.token_owner, TokenOwner.no_one)
+ self.assertIsNone(workflow.member)
+
+ def test_pended_data(self):
+ # There is a Pendable associated with the held request, and it has
+ # some data associated with it.
+ anne = self._user_manager.create_address(self._anne)
+ workflow = SubscriptionWorkflow(self._mlist, anne)
+ try:
+ workflow.run_thru('send_confirmation')
+ except StopIteration:
+ pass
+ self.assertIsNotNone(workflow.token)
+ pendable = getUtility(IPendings).confirm(workflow.token, expunge=False)
+ self.assertEqual(pendable['list_id'], 'test.example.com')
+ self.assertEqual(pendable['email'], 'anne@example.com')
+ self.assertEqual(pendable['display_name'], '')
+ self.assertEqual(pendable['when'], '2005-08-01T07:49:23')
+ self.assertEqual(pendable['token_owner'], 'subscriber')
+
def test_user_or_address_required(self):
# The `subscriber` attribute must be a user or address.
workflow = SubscriptionWorkflow(self._mlist)
@@ -229,13 +215,29 @@ class TestSubscriptionWorkflow(unittest.TestCase):
def test_confirmation_checks_confirm_pre_confirmed(self):
# The subscription policy requires user confirmation, but their
- # subscription is pre-confirmed.
+ # subscription is pre-confirmed. Since moderation is not required,
+ # the user will be immediately subscribed.
self._mlist.subscription_policy = SubscriptionPolicy.confirm
anne = self._user_manager.create_address(self._anne)
workflow = SubscriptionWorkflow(self._mlist, anne,
pre_verified=True,
pre_confirmed=True)
workflow.run_thru('confirmation_checks')
+ with patch.object(workflow, '_step_do_subscription') as step:
+ next(workflow)
+ step.assert_called_once_with()
+
+ def test_confirmation_checks_confirm_then_moderate_pre_confirmed(self):
+ # The subscription policy requires user confirmation, but their
+ # subscription is pre-confirmed. Since moderation is required, that
+ # check will be performed.
+ self._mlist.subscription_policy = \
+ SubscriptionPolicy.confirm_then_moderate
+ anne = self._user_manager.create_address(self._anne)
+ workflow = SubscriptionWorkflow(self._mlist, anne,
+ pre_verified=True,
+ pre_confirmed=True)
+ workflow.run_thru('confirmation_checks')
with patch.object(workflow, '_step_moderation_checks') as step:
next(workflow)
step.assert_called_once_with()
@@ -311,6 +313,10 @@ class TestSubscriptionWorkflow(unittest.TestCase):
# Anne is now a member of the mailing list.
member = self._mlist.regular_members.get_member(self._anne)
self.assertEqual(member.address, anne)
+ self.assertEqual(workflow.member, member)
+ # No further token is needed.
+ self.assertIsNone(workflow.token)
+ self.assertEqual(workflow.token_owner, TokenOwner.no_one)
def test_do_subscription_pre_approved(self):
# An moderation-requiring subscription policy plus a pre-verified and
@@ -326,6 +332,10 @@ class TestSubscriptionWorkflow(unittest.TestCase):
# Anne is now a member of the mailing list.
member = self._mlist.regular_members.get_member(self._anne)
self.assertEqual(member.address, anne)
+ self.assertEqual(workflow.member, member)
+ # No further token is needed.
+ self.assertIsNone(workflow.token)
+ self.assertEqual(workflow.token_owner, TokenOwner.no_one)
def test_do_subscription_pre_approved_pre_confirmed(self):
# An moderation-requiring subscription policy plus a pre-verified and
@@ -343,6 +353,10 @@ class TestSubscriptionWorkflow(unittest.TestCase):
# Anne is now a member of the mailing list.
member = self._mlist.regular_members.get_member(self._anne)
self.assertEqual(member.address, anne)
+ self.assertEqual(workflow.member, member)
+ # No further token is needed.
+ self.assertIsNone(workflow.token)
+ self.assertEqual(workflow.token_owner, TokenOwner.no_one)
def test_do_subscription_cleanups(self):
# Once the user is subscribed, the token, and its associated pending
@@ -360,8 +374,10 @@ class TestSubscriptionWorkflow(unittest.TestCase):
# Anne is now a member of the mailing list.
member = self._mlist.regular_members.get_member(self._anne)
self.assertEqual(member.address, anne)
+ self.assertEqual(workflow.member, member)
# The workflow is done, so it has no token.
self.assertIsNone(workflow.token)
+ self.assertEqual(workflow.token_owner, TokenOwner.no_one)
# The pendable associated with the token has been evicted.
self.assertIsNone(getUtility(IPendings).confirm(token, expunge=False))
# There is no saved workflow associated with the token. This shows up
@@ -384,6 +400,10 @@ class TestSubscriptionWorkflow(unittest.TestCase):
# The user is not currently subscribed to the mailing list.
member = self._mlist.regular_members.get_member(self._anne)
self.assertIsNone(member)
+ self.assertIsNone(workflow.member)
+ # The token is owned by the moderator.
+ self.assertIsNotNone(workflow.token)
+ self.assertEqual(workflow.token_owner, TokenOwner.moderator)
# Create a new workflow with the previous workflow's save token, and
# restore its state. This models an approved subscription and should
# result in the user getting subscribed.
@@ -394,6 +414,10 @@ class TestSubscriptionWorkflow(unittest.TestCase):
# Now the user is subscribed to the mailing list.
member = self._mlist.regular_members.get_member(self._anne)
self.assertEqual(member.address, anne)
+ self.assertEqual(approved_workflow.member, member)
+ # No further token is needed.
+ self.assertIsNone(approved_workflow.token)
+ self.assertEqual(approved_workflow.token_owner, TokenOwner.no_one)
def test_get_moderator_approval_log_on_hold(self):
# When the subscription is held for moderator approval, a message is
@@ -406,11 +430,10 @@ class TestSubscriptionWorkflow(unittest.TestCase):
pre_confirmed=True)
# Consume the entire state machine.
list(workflow)
- line = mark.readline()
- self.assertEqual(
- line[28:-1],
- 'test@example.com: held subscription request from anne@example.com'
- )
+ self.assertIn(
+ 'test@example.com: held subscription request from anne@example.com',
+ mark.readline()
+ )
def test_get_moderator_approval_notifies_moderators(self):
# When the subscription is held for moderator approval, and the list
@@ -529,14 +552,26 @@ approval:
self.assertIsNone(anne.verified_on)
workflow = SubscriptionWorkflow(self._mlist, anne)
list(workflow)
- self.assertIsNone(self._mlist.regular_members.get_member(self._anne))
+ # Anne is not yet a member.
+ member = self._mlist.regular_members.get_member(self._anne)
+ self.assertIsNone(member)
+ self.assertIsNone(workflow.member)
+ # The token is owned by the subscriber.
+ self.assertIsNotNone(workflow.token)
+ self.assertEqual(workflow.token_owner, TokenOwner.subscriber)
+ # Confirm.
confirm_workflow = SubscriptionWorkflow(self._mlist)
confirm_workflow.token = workflow.token
confirm_workflow.restore()
list(confirm_workflow)
self.assertIsNotNone(anne.verified_on)
- self.assertEqual(
- self._mlist.regular_members.get_member(self._anne).address, anne)
+ # Anne is now a member.
+ member = self._mlist.regular_members.get_member(self._anne)
+ self.assertEqual(member.address, anne)
+ self.assertEqual(confirm_workflow.member, member)
+ # No further token is needed.
+ self.assertIsNone(confirm_workflow.token)
+ self.assertEqual(confirm_workflow.token_owner, TokenOwner.no_one)
def test_prevent_confirmation_replay_attacks(self):
# Ensure that if the workflow requires two confirmations, e.g. first
@@ -553,11 +588,18 @@ approval:
# Anne is not yet a member of the mailing list.
member = self._mlist.regular_members.get_member(self._anne)
self.assertIsNone(member)
+ self.assertIsNone(workflow.member)
+ # The token is owned by the subscriber.
+ self.assertIsNotNone(workflow.token)
+ self.assertEqual(workflow.token_owner, TokenOwner.subscriber)
# The old token will not work for moderator approval.
moderator_workflow = SubscriptionWorkflow(self._mlist)
moderator_workflow.token = token
moderator_workflow.restore()
list(moderator_workflow)
+ # The token is owned by the moderator.
+ self.assertIsNotNone(moderator_workflow.token)
+ self.assertEqual(moderator_workflow.token_owner, TokenOwner.moderator)
# While we wait for the moderator to approve the subscription, note
# that there's a new token for the next steps.
self.assertNotEqual(token, moderator_workflow.token)
@@ -570,6 +612,7 @@ approval:
# Anne is still not subscribed.
member = self._mlist.regular_members.get_member(self._anne)
self.assertIsNone(member)
+ self.assertIsNone(final_workflow.member)
# However, if we use the new token, her subscription request will be
# approved by the moderator.
final_workflow.token = moderator_workflow.token
@@ -578,3 +621,21 @@ approval:
# And now Anne is a member.
member = self._mlist.regular_members.get_member(self._anne)
self.assertEqual(member.address.email, self._anne)
+ self.assertEqual(final_workflow.member, member)
+ # No further token is needed.
+ self.assertIsNone(final_workflow.token)
+ self.assertEqual(final_workflow.token_owner, TokenOwner.no_one)
+
+ def test_confirmation_needed_and_pre_confirmed(self):
+ # The subscription policy is 'confirm' but the subscription is
+ # pre-confirmed so the moderation checks can be skipped.
+ self._mlist.subscription_policy = SubscriptionPolicy.confirm
+ anne = self._user_manager.create_address(self._anne)
+ workflow = SubscriptionWorkflow(
+ self._mlist, anne,
+ pre_verified=True, pre_confirmed=True, pre_approved=True)
+ list(workflow)
+ # Anne was subscribed.
+ self.assertIsNone(workflow.token)
+ self.assertEqual(workflow.token_owner, TokenOwner.no_one)
+ self.assertEqual(workflow.member.address, anne)