From 35e4ca77838ac7bbeca3f8da6ea64a93a6e501d5 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 22 Aug 2011 22:45:49 -0400 Subject: Rename IDomain.email_host to .mail_host for consistency. Similarly in the REST API. (LP: #831660) --- src/mailman/model/docs/domains.rst | 123 +++++ src/mailman/model/docs/domains.txt | 123 ----- src/mailman/model/docs/requests.rst | 903 ++++++++++++++++++++++++++++++++++++ src/mailman/model/docs/requests.txt | 903 ------------------------------------ 4 files changed, 1026 insertions(+), 1026 deletions(-) create mode 100644 src/mailman/model/docs/domains.rst delete mode 100644 src/mailman/model/docs/domains.txt create mode 100644 src/mailman/model/docs/requests.rst delete mode 100644 src/mailman/model/docs/requests.txt (limited to 'src/mailman/model/docs') diff --git a/src/mailman/model/docs/domains.rst b/src/mailman/model/docs/domains.rst new file mode 100644 index 000000000..4fa39aa2f --- /dev/null +++ b/src/mailman/model/docs/domains.rst @@ -0,0 +1,123 @@ +======= +Domains +======= + +.. # The test framework starts out with an example domain, so let's delete + # that first. + >>> from mailman.interfaces.domain import IDomainManager + >>> from zope.component import getUtility + >>> manager = getUtility(IDomainManager) + >>> manager.remove('example.com') + + +Domains are how Mailman interacts with email host names and web host names. +:: + + >>> from operator import attrgetter + >>> def show_domains(): + ... if len(manager) == 0: + ... print 'no domains' + ... return + ... for domain in sorted(manager, key=attrgetter('mail_host')): + ... print domain + + >>> show_domains() + no domains + +Adding a domain requires some basic information, of which the email host name +is the only required piece. The other parts are inferred from that. + + >>> manager.add('example.org') + + >>> show_domains() + + +We can remove domains too. + + >>> manager.remove('example.org') + + >>> show_domains() + no domains + +Sometimes the email host name is different than the base url for hitting the +web interface for the domain. + + >>> manager.add('example.com', base_url='https://mail.example.com') + + >>> show_domains() + + +Domains can have explicit descriptions and contact addresses. +:: + + >>> manager.add( + ... 'example.net', + ... base_url='http://lists.example.net', + ... contact_address='postmaster@example.com', + ... description='The example domain') + + + >>> show_domains() + + + +In the global domain manager, domains are indexed by their email host name. +:: + + >>> for domain in sorted(manager, key=attrgetter('mail_host')): + ... print domain.mail_host + example.com + example.net + + >>> print manager['example.net'] + + + >>> print manager['doesnotexist.com'] + Traceback (most recent call last): + ... + KeyError: u'doesnotexist.com' + +As with a dictionary, you can also get the domain. If the domain does not +exist, ``None`` or a default is returned. +:: + + >>> print manager.get('example.net') + + + >>> print manager.get('doesnotexist.com') + None + + >>> print manager.get('doesnotexist.com', 'blahdeblah') + blahdeblah + +Non-existent domains cannot be removed. + + >>> manager.remove('doesnotexist.com') + Traceback (most recent call last): + ... + KeyError: u'doesnotexist.com' + + +Confirmation tokens +=================== + +Confirmation tokens can be added to the domain's url to generate the URL to a +page users can use to confirm their subscriptions. + + >>> domain = manager['example.net'] + >>> print domain.confirm_url('abc') + http://lists.example.net/confirm/abc diff --git a/src/mailman/model/docs/domains.txt b/src/mailman/model/docs/domains.txt deleted file mode 100644 index 9fe43a5f1..000000000 --- a/src/mailman/model/docs/domains.txt +++ /dev/null @@ -1,123 +0,0 @@ -======= -Domains -======= - -.. # The test framework starts out with an example domain, so let's delete - # that first. - >>> from mailman.interfaces.domain import IDomainManager - >>> from zope.component import getUtility - >>> manager = getUtility(IDomainManager) - >>> manager.remove('example.com') - - -Domains are how Mailman interacts with email host names and web host names. -:: - - >>> from operator import attrgetter - >>> def show_domains(): - ... if len(manager) == 0: - ... print 'no domains' - ... return - ... for domain in sorted(manager, key=attrgetter('email_host')): - ... print domain - - >>> show_domains() - no domains - -Adding a domain requires some basic information, of which the email host name -is the only required piece. The other parts are inferred from that. - - >>> manager.add('example.org') - - >>> show_domains() - - -We can remove domains too. - - >>> manager.remove('example.org') - - >>> show_domains() - no domains - -Sometimes the email host name is different than the base url for hitting the -web interface for the domain. - - >>> manager.add('example.com', base_url='https://mail.example.com') - - >>> show_domains() - - -Domains can have explicit descriptions and contact addresses. -:: - - >>> manager.add( - ... 'example.net', - ... base_url='http://lists.example.net', - ... contact_address='postmaster@example.com', - ... description='The example domain') - - - >>> show_domains() - - - -In the global domain manager, domains are indexed by their email host name. -:: - - >>> for domain in sorted(manager, key=attrgetter('email_host')): - ... print domain.email_host - example.com - example.net - - >>> print manager['example.net'] - - - >>> print manager['doesnotexist.com'] - Traceback (most recent call last): - ... - KeyError: u'doesnotexist.com' - -As with a dictionary, you can also get the domain. If the domain does not -exist, ``None`` or a default is returned. -:: - - >>> print manager.get('example.net') - - - >>> print manager.get('doesnotexist.com') - None - - >>> print manager.get('doesnotexist.com', 'blahdeblah') - blahdeblah - -Non-existent domains cannot be removed. - - >>> manager.remove('doesnotexist.com') - Traceback (most recent call last): - ... - KeyError: u'doesnotexist.com' - - -Confirmation tokens -=================== - -Confirmation tokens can be added to the domain's url to generate the URL to a -page users can use to confirm their subscriptions. - - >>> domain = manager['example.net'] - >>> print domain.confirm_url('abc') - http://lists.example.net/confirm/abc diff --git a/src/mailman/model/docs/requests.rst b/src/mailman/model/docs/requests.rst new file mode 100644 index 000000000..e01544490 --- /dev/null +++ b/src/mailman/model/docs/requests.rst @@ -0,0 +1,903 @@ +================== +Moderator requests +================== + +Various actions will be held for moderator approval, such as subscriptions to +closed lists, or postings by non-members. The requests database is the low +level interface to these actions requiring approval. + +Here is a helper function for printing out held requests. + + >>> def show_holds(requests): + ... for request in requests.held_requests: + ... key, data = requests.get_request(request.id) + ... print request.id, str(request.request_type), key + ... if data is not None: + ... for key in sorted(data): + ... print ' {0}: {1}'.format(key, data[key]) + +And another helper for displaying messages in the virgin queue. + + >>> virginq = config.switchboards['virgin'] + >>> def dequeue(whichq=None, expected_count=1): + ... if whichq is None: + ... whichq = virginq + ... assert len(whichq.files) == expected_count, ( + ... 'Unexpected file count: %d' % len(whichq.files)) + ... filebase = whichq.files[0] + ... qmsg, qdata = whichq.dequeue(filebase) + ... whichq.finish(filebase) + ... return qmsg, qdata + + +Mailing list centric +==================== + +A set of requests are always related to a particular mailing list, so given a +mailing list you need to get its requests object. +:: + + >>> from mailman.interfaces.requests import IListRequests, IRequests + >>> from zope.component import getUtility + >>> from zope.interface.verify import verifyObject + + >>> mlist = create_list('test@example.com') + >>> requests = getUtility(IRequests).get_list_requests(mlist) + >>> verifyObject(IListRequests, requests) + True + >>> requests.mailing_list + + + +Holding requests +================ + +The list's requests database starts out empty. + + >>> requests.count + 0 + >>> dump_list(requests.held_requests) + *Empty* + +At the lowest level, the requests database is very simple. Holding a request +requires a request type (as an enum value), a key, and an optional dictionary +of associated data. The request database assigns no semantics to the held +data, except for the request type. Here we hold some simple bits of data. + + >>> from mailman.interfaces.requests import RequestType + >>> id_1 = requests.hold_request(RequestType.held_message, 'hold_1') + >>> id_2 = requests.hold_request(RequestType.subscription, 'hold_2') + >>> id_3 = requests.hold_request(RequestType.unsubscription, 'hold_3') + >>> id_4 = requests.hold_request(RequestType.held_message, 'hold_4') + >>> id_1, id_2, id_3, id_4 + (1, 2, 3, 4) + +And of course, now we can see that there are four requests being held. + + >>> requests.count + 4 + >>> requests.count_of(RequestType.held_message) + 2 + >>> requests.count_of(RequestType.subscription) + 1 + >>> requests.count_of(RequestType.unsubscription) + 1 + >>> show_holds(requests) + 1 RequestType.held_message hold_1 + 2 RequestType.subscription hold_2 + 3 RequestType.unsubscription hold_3 + 4 RequestType.held_message hold_4 + +If we try to hold a request with a bogus type, we get an exception. + + >>> requests.hold_request(5, 'foo') + Traceback (most recent call last): + ... + TypeError: 5 + +We can hold requests with additional data. + + >>> data = dict(foo='yes', bar='no') + >>> id_5 = requests.hold_request(RequestType.held_message, 'hold_5', data) + >>> id_5 + 5 + >>> requests.count + 5 + >>> show_holds(requests) + 1 RequestType.held_message hold_1 + 2 RequestType.subscription hold_2 + 3 RequestType.unsubscription hold_3 + 4 RequestType.held_message hold_4 + 5 RequestType.held_message hold_5 + bar: no + foo: yes + + +Getting requests +================ + +We can ask the requests database for a specific request, by providing the id +of the request data we want. This returns a 2-tuple of the key and data we +originally held. + + >>> key, data = requests.get_request(2) + >>> print key + hold_2 + +Because we did not store additional data with request 2, it comes back as None +now. + + >>> print data + None + +However, if we ask for a request that had data, we'd get it back now. + + >>> key, data = requests.get_request(5) + >>> print key + hold_5 + >>> dump_msgdata(data) + bar: no + foo: yes + +If we ask for a request that is not in the database, we get None back. + + >>> print requests.get_request(801) + None + + +Iterating over requests +======================= + +To make it easier to find specific requests, the list requests can be iterated +over by type. + + >>> requests.count_of(RequestType.held_message) + 3 + >>> for request in requests.of_type(RequestType.held_message): + ... assert request.request_type is RequestType.held_message + ... key, data = requests.get_request(request.id) + ... print request.id, key + ... if data is not None: + ... for key in sorted(data): + ... print ' {0}: {1}'.format(key, data[key]) + 1 hold_1 + 4 hold_4 + 5 hold_5 + bar: no + foo: yes + + +Deleting requests +================= + +Once a specific request has been handled, it will be deleted from the requests +database. + + >>> requests.delete_request(2) + >>> requests.count + 4 + >>> show_holds(requests) + 1 RequestType.held_message hold_1 + 3 RequestType.unsubscription hold_3 + 4 RequestType.held_message hold_4 + 5 RequestType.held_message hold_5 + bar: no + foo: yes + >>> print requests.get_request(2) + None + +We get an exception if we ask to delete a request that isn't in the database. + + >>> requests.delete_request(801) + Traceback (most recent call last): + ... + KeyError: 801 + +For the next section, we first clean up all the current requests. + + >>> for request in requests.held_requests: + ... requests.delete_request(request.id) + >>> requests.count + 0 + + +Application support +=================== + +There are several higher level interfaces available in the ``mailman.app`` +package which can be used to hold messages, subscription, and unsubscriptions. +There are also interfaces for disposing of these requests in an application +specific and consistent way. + + >>> from mailman.app import moderator + + +Holding messages +================ + +For this section, we need a mailing list and at least one message. + + >>> mlist = create_list('alist@example.com') + >>> mlist.preferred_language = 'en' + >>> mlist.real_name = 'A Test List' + >>> msg = message_from_string("""\ + ... From: aperson@example.org + ... To: alist@example.com + ... Subject: Something important + ... + ... Here's something important about our mailing list. + ... """) + +Holding a message means keeping a copy of it that a moderator must approve +before the message is posted to the mailing list. To hold the message, you +must supply the message, message metadata, and a text reason for the hold. In +this case, we won't include any additional metadata. + + >>> id_1 = moderator.hold_message(mlist, msg, {}, 'Needs approval') + >>> requests.get_request(id_1) is not None + True + +We can also hold a message with some additional metadata. +:: + + # Delete the Message-ID from the previous hold so we don't try to store + # collisions in the message storage. + >>> del msg['message-id'] + >>> msgdata = dict(sender='aperson@example.com', + ... approved=True, + ... received_time=123.45) + >>> id_2 = moderator.hold_message(mlist, msg, msgdata, 'Feeling ornery') + >>> requests.get_request(id_2) is not None + True + +Once held, the moderator can select one of several dispositions. The most +trivial is to simply defer a decision for now. + + >>> from mailman.interfaces.action import Action + >>> moderator.handle_message(mlist, id_1, Action.defer) + >>> requests.get_request(id_1) is not None + True + +The moderator can also discard the message. This is often done with spam. +Bye bye message! + + >>> moderator.handle_message(mlist, id_1, Action.discard) + >>> print requests.get_request(id_1) + None + >>> virginq.files + [] + +The message can be rejected, meaning it is bounced back to the sender. + + >>> moderator.handle_message(mlist, id_2, Action.reject, 'Off topic') + >>> print requests.get_request(id_2) + None + >>> qmsg, qdata = dequeue() + >>> print qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: Request to mailing list "A Test List" rejected + From: alist-bounces@example.com + To: aperson@example.org + Message-ID: ... + Date: ... + Precedence: bulk + + Your request to the alist@example.com mailing list + + Posting of your message titled "Something important" + + has been rejected by the list moderator. The moderator gave the + following reason for rejecting your request: + + "Off topic" + + Any questions or comments should be directed to the list administrator + at: + + alist-owner@example.com + + >>> dump_msgdata(qdata) + _parsemsg : False + listname : alist@example.com + nodecorate : True + recipients : set([u'aperson@example.org']) + reduced_list_headers: True + version : 3 + +Or the message can be approved. This actually places the message back into +the incoming queue for further processing, however the message metadata +indicates that the message has been approved. + + >>> id_3 = moderator.hold_message(mlist, msg, msgdata, 'Needs approval') + >>> moderator.handle_message(mlist, id_3, Action.accept) + >>> inq = config.switchboards['pipeline'] + >>> qmsg, qdata = dequeue(inq) + >>> print qmsg.as_string() + From: aperson@example.org + To: alist@example.com + Subject: Something important + Message-ID: ... + X-Message-ID-Hash: ... + X-Mailman-Approved-At: ... + + Here's something important about our mailing list. + + >>> dump_msgdata(qdata) + _parsemsg : False + approved : True + moderator_approved: True + sender : aperson@example.com + version : 3 + +In addition to any of the above dispositions, the message can also be +preserved for further study. Ordinarily the message is removed from the +global message store after its disposition (though approved messages may be +re-added to the message store). When handling a message, we can tell the +moderator interface to also preserve a copy, essentially telling it not to +delete the message from the storage. First, without the switch, the message +is deleted. +:: + + >>> msg = message_from_string("""\ + ... From: aperson@example.org + ... To: alist@example.com + ... Subject: Something important + ... Message-ID: <12345> + ... + ... Here's something important about our mailing list. + ... """) + >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval') + >>> moderator.handle_message(mlist, id_4, Action.discard) + + >>> from mailman.interfaces.messages import IMessageStore + >>> from zope.component import getUtility + >>> message_store = getUtility(IMessageStore) + + >>> print message_store.get_message_by_id('<12345>') + None + +But if we ask to preserve the message when we discard it, it will be held in +the message store after disposition. + + >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval') + >>> moderator.handle_message(mlist, id_4, Action.discard, preserve=True) + >>> stored_msg = message_store.get_message_by_id('<12345>') + >>> print stored_msg.as_string() + From: aperson@example.org + To: alist@example.com + Subject: Something important + Message-ID: <12345> + X-Message-ID-Hash: 4CF7EAU3SIXBPXBB5S6PEUMO62MWGQN6 + + Here's something important about our mailing list. + + +Orthogonal to preservation, the message can also be forwarded to another +address. This is helpful for getting the message into the inbox of one of the +moderators. +:: + + # Set a new Message-ID from the previous hold so we don't try to store + # collisions in the message storage. + >>> del msg['message-id'] + >>> msg['Message-ID'] = '' + >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval') + >>> moderator.handle_message(mlist, id_4, Action.discard, + ... forward=['zperson@example.com']) + >>> qmsg, qdata = dequeue() + >>> print qmsg.as_string() + Subject: Forward of moderated message + From: alist-bounces@example.com + To: zperson@example.com + MIME-Version: 1.0 + Content-Type: message/rfc822 + Message-ID: ... + Date: ... + Precedence: bulk + + From: aperson@example.org + To: alist@example.com + Subject: Something important + Message-ID: + X-Message-ID-Hash: EN2R5UQFMOUTCL44FLNNPLSXBIZW62ER + + Here's something important about our mailing list. + + >>> dump_msgdata(qdata) + _parsemsg : False + listname : alist@example.com + nodecorate : True + recipients : [u'zperson@example.com'] + reduced_list_headers: True + version : 3 + + +Holding subscription requests +============================= + +For closed lists, subscription requests will also be held for moderator +approval. In this case, several pieces of information related to the +subscription must be provided, including the subscriber's address and real +name, their password (possibly hashed), what kind of delivery option they are +choosing and their preferred language. + + >>> from mailman.interfaces.member import DeliveryMode + >>> mlist.admin_immed_notify = False + >>> id_3 = moderator.hold_subscription(mlist, + ... 'bperson@example.org', 'Ben Person', + ... '{NONE}abcxyz', DeliveryMode.regular, 'en') + >>> requests.get_request(id_3) is not None + True + +In the above case the mailing list was not configured to send the list +moderators a notice about the hold, so no email message is in the virgin +queue. + + >>> virginq.files + [] + +But if we set the list up to notify the list moderators immediately when a +message is held for approval, there will be a message placed in the virgin +queue when the message is held. + + >>> mlist.admin_immed_notify = True + >>> # XXX This will almost certainly change once we've worked out the web + >>> # space layout for mailing lists now. + >>> id_4 = moderator.hold_subscription(mlist, + ... 'cperson@example.org', 'Claire Person', + ... '{NONE}zyxcba', DeliveryMode.regular, 'en') + >>> requests.get_request(id_4) is not None + True + >>> qmsg, qdata = dequeue() + >>> print qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: New subscription request to list A Test List from + cperson@example.org + From: alist-owner@example.com + To: alist-owner@example.com + Message-ID: ... + Date: ... + Precedence: bulk + + Your authorization is required for a mailing list subscription request + approval: + + For: cperson@example.org + List: alist@example.com + + At your convenience, visit: + + http://lists.example.com/admindb/alist@example.com + + to process the request. + + >>> dump_msgdata(qdata) + _parsemsg : False + listname : alist@example.com + nodecorate : True + recipients : set([u'alist-owner@example.com']) + reduced_list_headers: True + tomoderators : True + version : 3 + +Once held, the moderator can select one of several dispositions. The most +trivial is to simply defer a decision for now. + + >>> moderator.handle_subscription(mlist, id_3, Action.defer) + >>> requests.get_request(id_3) is not None + True + +The held subscription can also be discarded. + + >>> moderator.handle_subscription(mlist, id_3, Action.discard) + >>> print requests.get_request(id_3) + None + +The request can be rejected, in which case a message is sent to the +subscriber. + + >>> moderator.handle_subscription(mlist, id_4, Action.reject, + ... 'This is a closed list') + >>> print requests.get_request(id_4) + None + >>> qmsg, qdata = dequeue() + >>> print qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: Request to mailing list "A Test List" rejected + From: alist-bounces@example.com + To: cperson@example.org + Message-ID: ... + Date: ... + Precedence: bulk + + Your request to the alist@example.com mailing list + + Subscription request + + has been rejected by the list moderator. The moderator gave the + following reason for rejecting your request: + + "This is a closed list" + + Any questions or comments should be directed to the list administrator + at: + + alist-owner@example.com + + >>> dump_msgdata(qdata) + _parsemsg : False + listname : alist@example.com + nodecorate : True + recipients : set([u'cperson@example.org']) + reduced_list_headers: True + version : 3 + +The subscription can also be accepted. This subscribes the address to the +mailing list. + + >>> mlist.send_welcome_msg = True + >>> id_4 = moderator.hold_subscription(mlist, + ... 'fperson@example.org', 'Frank Person', + ... '{NONE}abcxyz', DeliveryMode.regular, 'en') + +A message will be sent to the moderators telling them about the held +subscription and the fact that they may need to approve it. + + >>> qmsg, qdata = dequeue() + >>> print qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: New subscription request to list A Test List from + fperson@example.org + From: alist-owner@example.com + To: alist-owner@example.com + Message-ID: ... + Date: ... + Precedence: bulk + + Your authorization is required for a mailing list subscription request + approval: + + For: fperson@example.org + List: alist@example.com + + At your convenience, visit: + + http://lists.example.com/admindb/alist@example.com + + to process the request. + + >>> dump_msgdata(qdata) + _parsemsg : False + listname : alist@example.com + nodecorate : True + recipients : set([u'alist-owner@example.com']) + reduced_list_headers: True + tomoderators : True + version : 3 + +Accept the subscription request. + + >>> mlist.admin_notify_mchanges = True + >>> moderator.handle_subscription(mlist, id_4, Action.accept) + +There are now two messages in the virgin queue. One is a welcome message +being sent to the user and the other is a subscription notification that is +sent to the moderators. The only good way to tell which is which is to look +at the recipient list. + + >>> qmsg_1, qdata_1 = dequeue(expected_count=2) + >>> qmsg_2, qdata_2 = dequeue() + >>> if 'fperson@example.org' in qdata_1['recipients']: + ... # The first message is the welcome message + ... welcome_qmsg = qmsg_1 + ... welcome_qdata = qdata_1 + ... admin_qmsg = qmsg_2 + ... admin_qdata = qdata_2 + ... else: + ... welcome_qmsg = qmsg_2 + ... welcome_qdata = qdata_2 + ... admin_qmsg = qmsg_1 + ... admin_qdata = qdata_1 + +The welcome message is sent to the person who just subscribed. + + >>> print welcome_qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: Welcome to the "A Test List" mailing list + From: alist-request@example.com + To: fperson@example.org + X-No-Archive: yes + Message-ID: ... + Date: ... + Precedence: bulk + + Welcome to the "A Test List" mailing list! + + To post to this list, send your email to: + + alist@example.com + + General information about the mailing list is at: + + http://lists.example.com/listinfo/alist@example.com + + If you ever want to unsubscribe or change your options (eg, switch to + or from digest mode, change your password, etc.), visit your + subscription page at: + + http://example.com/fperson@example.org + + You can also make such adjustments via email by sending a message to: + + alist-request@example.com + + with the word 'help' in the subject or body (don't include the + quotes), and you will get back a message with instructions. You will + need your password to change your options, but for security purposes, + this email is not included here. There is also a button on your + options page that will send your current password to you. + + >>> dump_msgdata(welcome_qdata) + _parsemsg : False + listname : alist@example.com + nodecorate : True + recipients : set([u'fperson@example.org']) + reduced_list_headers: True + verp : False + version : 3 + +The admin message is sent to the moderators. + + >>> print admin_qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: A Test List subscription notification + From: changeme@example.com + To: alist-owner@example.com + Message-ID: ... + Date: ... + Precedence: bulk + + Frank Person has been successfully subscribed to + A Test List. + + >>> dump_msgdata(admin_qdata) + _parsemsg : False + envsender : changeme@example.com + listname : alist@example.com + nodecorate : True + recipients : set([]) + reduced_list_headers: True + version : 3 + +Frank Person is now a member of the mailing list. +:: + + >>> member = mlist.members.get_member('fperson@example.org') + >>> member + + on alist@example.com as MemberRole.member> + >>> member.preferred_language + + >>> print member.delivery_mode + DeliveryMode.regular + >>> print member.user.real_name + Frank Person + >>> print member.user.password + {NONE}abcxyz + + +Holding unsubscription requests +=============================== + +Some lists, though it is rare, require moderator approval for unsubscriptions. +In this case, only the unsubscribing address is required. Like subscriptions, +unsubscription holds can send the list's moderators an immediate +notification. +:: + + + >>> from mailman.interfaces.usermanager import IUserManager + >>> from zope.component import getUtility + >>> user_manager = getUtility(IUserManager) + + >>> mlist.admin_immed_notify = False + >>> from mailman.interfaces.member import MemberRole + >>> user_1 = user_manager.create_user('gperson@example.com') + >>> address_1 = list(user_1.addresses)[0] + >>> mlist.subscribe(address_1, MemberRole.member) + + + >>> user_2 = user_manager.create_user('hperson@example.com') + >>> address_2 = list(user_2.addresses)[0] + >>> mlist.subscribe(address_2, MemberRole.member) + + + >>> id_5 = moderator.hold_unsubscription(mlist, 'gperson@example.com') + >>> requests.get_request(id_5) is not None + True + >>> virginq.files + [] + >>> mlist.admin_immed_notify = True + >>> id_6 = moderator.hold_unsubscription(mlist, 'hperson@example.com') + >>> qmsg, qdata = dequeue() + >>> print qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: New unsubscription request from A Test List by hperson@example.com + From: alist-owner@example.com + To: alist-owner@example.com + Message-ID: ... + Date: ... + Precedence: bulk + + Your authorization is required for a mailing list unsubscription + request approval: + + By: hperson@example.com + From: alist@example.com + + At your convenience, visit: + + http://lists.example.com/admindb/alist@example.com + + to process the request. + + >>> dump_msgdata(qdata) + _parsemsg : False + listname : alist@example.com + nodecorate : True + recipients : set([u'alist-owner@example.com']) + reduced_list_headers: True + tomoderators : True + version : 3 + +There are now two addresses with held unsubscription requests. As above, one +of the actions we can take is to defer to the decision. + + >>> moderator.handle_unsubscription(mlist, id_5, Action.defer) + >>> requests.get_request(id_5) is not None + True + +The held unsubscription can also be discarded, and the member will remain +subscribed. + + >>> moderator.handle_unsubscription(mlist, id_5, Action.discard) + >>> print requests.get_request(id_5) + None + >>> mlist.members.get_member('gperson@example.com') + + +The request can be rejected, in which case a message is sent to the member, +and the person remains a member of the mailing list. +:: + + >>> moderator.handle_unsubscription(mlist, id_6, Action.reject, + ... 'This list is a prison.') + >>> print requests.get_request(id_6) + None + >>> qmsg, qdata = dequeue() + >>> print qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: Request to mailing list "A Test List" rejected + From: alist-bounces@example.com + To: hperson@example.com + Message-ID: ... + Date: ... + Precedence: bulk + + Your request to the alist@example.com mailing list + + Unsubscription request + + has been rejected by the list moderator. The moderator gave the + following reason for rejecting your request: + + "This list is a prison." + + Any questions or comments should be directed to the list administrator + at: + + alist-owner@example.com + + >>> dump_msgdata(qdata) + _parsemsg : False + listname : alist@example.com + nodecorate : True + recipients : set([u'hperson@example.com']) + reduced_list_headers: True + version : 3 + + >>> mlist.members.get_member('hperson@example.com') + + +The unsubscription request can also be accepted. This removes the member from +the mailing list. + + >>> mlist.send_goodbye_msg = True + >>> mlist.goodbye_msg = 'So long!' + >>> mlist.admin_immed_notify = False + >>> id_7 = moderator.hold_unsubscription(mlist, 'gperson@example.com') + >>> moderator.handle_unsubscription(mlist, id_7, Action.accept) + >>> print mlist.members.get_member('gperson@example.com') + None + +There are now two messages in the virgin queue, one to the member who was just +unsubscribed and another to the moderators informing them of this membership +change. + + >>> qmsg_1, qdata_1 = dequeue(expected_count=2) + >>> qmsg_2, qdata_2 = dequeue() + >>> if 'gperson@example.com' in qdata_1['recipients']: + ... # The first message is the goodbye message + ... goodbye_qmsg = qmsg_1 + ... goodbye_qdata = qdata_1 + ... admin_qmsg = qmsg_2 + ... admin_qdata = qdata_2 + ... else: + ... goodbye_qmsg = qmsg_2 + ... goodbye_qdata = qdata_2 + ... admin_qmsg = qmsg_1 + ... admin_qdata = qdata_1 + +The goodbye message... + + >>> print goodbye_qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: You have been unsubscribed from the A Test List mailing list + From: alist-bounces@example.com + To: gperson@example.com + Message-ID: ... + Date: ... + Precedence: bulk + + So long! + + >>> dump_msgdata(goodbye_qdata) + _parsemsg : False + listname : alist@example.com + nodecorate : True + recipients : set([u'gperson@example.com']) + reduced_list_headers: True + verp : False + version : 3 + +...and the admin message. + + >>> print admin_qmsg.as_string() + MIME-Version: 1.0 + Content-Type: text/plain; charset="us-ascii" + Content-Transfer-Encoding: 7bit + Subject: A Test List unsubscription notification + From: changeme@example.com + To: alist-owner@example.com + Message-ID: ... + Date: ... + Precedence: bulk + + gperson@example.com has been removed from A Test List. + + >>> dump_msgdata(admin_qdata) + _parsemsg : False + envsender : changeme@example.com + listname : alist@example.com + nodecorate : True + recipients : set([]) + reduced_list_headers: True + version : 3 diff --git a/src/mailman/model/docs/requests.txt b/src/mailman/model/docs/requests.txt deleted file mode 100644 index 812d25a43..000000000 --- a/src/mailman/model/docs/requests.txt +++ /dev/null @@ -1,903 +0,0 @@ -================== -Moderator requests -================== - -Various actions will be held for moderator approval, such as subscriptions to -closed lists, or postings by non-members. The requests database is the low -level interface to these actions requiring approval. - -Here is a helper function for printing out held requests. - - >>> def show_holds(requests): - ... for request in requests.held_requests: - ... key, data = requests.get_request(request.id) - ... print request.id, str(request.request_type), key - ... if data is not None: - ... for key in sorted(data): - ... print ' {0}: {1}'.format(key, data[key]) - -And another helper for displaying messages in the virgin queue. - - >>> virginq = config.switchboards['virgin'] - >>> def dequeue(whichq=None, expected_count=1): - ... if whichq is None: - ... whichq = virginq - ... assert len(whichq.files) == expected_count, ( - ... 'Unexpected file count: %d' % len(whichq.files)) - ... filebase = whichq.files[0] - ... qmsg, qdata = whichq.dequeue(filebase) - ... whichq.finish(filebase) - ... return qmsg, qdata - - -Mailing list centric -==================== - -A set of requests are always related to a particular mailing list, so given a -mailing list you need to get its requests object. -:: - - >>> from mailman.interfaces.requests import IListRequests, IRequests - >>> from zope.component import getUtility - >>> from zope.interface.verify import verifyObject - - >>> mlist = create_list('test@example.com') - >>> requests = getUtility(IRequests).get_list_requests(mlist) - >>> verifyObject(IListRequests, requests) - True - >>> requests.mailing_list - - - -Holding requests -================ - -The list's requests database starts out empty. - - >>> requests.count - 0 - >>> dump_list(requests.held_requests) - *Empty* - -At the lowest level, the requests database is very simple. Holding a request -requires a request type (as an enum value), a key, and an optional dictionary -of associated data. The request database assigns no semantics to the held -data, except for the request type. Here we hold some simple bits of data. - - >>> from mailman.interfaces.requests import RequestType - >>> id_1 = requests.hold_request(RequestType.held_message, 'hold_1') - >>> id_2 = requests.hold_request(RequestType.subscription, 'hold_2') - >>> id_3 = requests.hold_request(RequestType.unsubscription, 'hold_3') - >>> id_4 = requests.hold_request(RequestType.held_message, 'hold_4') - >>> id_1, id_2, id_3, id_4 - (1, 2, 3, 4) - -And of course, now we can see that there are four requests being held. - - >>> requests.count - 4 - >>> requests.count_of(RequestType.held_message) - 2 - >>> requests.count_of(RequestType.subscription) - 1 - >>> requests.count_of(RequestType.unsubscription) - 1 - >>> show_holds(requests) - 1 RequestType.held_message hold_1 - 2 RequestType.subscription hold_2 - 3 RequestType.unsubscription hold_3 - 4 RequestType.held_message hold_4 - -If we try to hold a request with a bogus type, we get an exception. - - >>> requests.hold_request(5, 'foo') - Traceback (most recent call last): - ... - TypeError: 5 - -We can hold requests with additional data. - - >>> data = dict(foo='yes', bar='no') - >>> id_5 = requests.hold_request(RequestType.held_message, 'hold_5', data) - >>> id_5 - 5 - >>> requests.count - 5 - >>> show_holds(requests) - 1 RequestType.held_message hold_1 - 2 RequestType.subscription hold_2 - 3 RequestType.unsubscription hold_3 - 4 RequestType.held_message hold_4 - 5 RequestType.held_message hold_5 - bar: no - foo: yes - - -Getting requests -================ - -We can ask the requests database for a specific request, by providing the id -of the request data we want. This returns a 2-tuple of the key and data we -originally held. - - >>> key, data = requests.get_request(2) - >>> print key - hold_2 - -Because we did not store additional data with request 2, it comes back as None -now. - - >>> print data - None - -However, if we ask for a request that had data, we'd get it back now. - - >>> key, data = requests.get_request(5) - >>> print key - hold_5 - >>> dump_msgdata(data) - bar: no - foo: yes - -If we ask for a request that is not in the database, we get None back. - - >>> print requests.get_request(801) - None - - -Iterating over requests -======================= - -To make it easier to find specific requests, the list requests can be iterated -over by type. - - >>> requests.count_of(RequestType.held_message) - 3 - >>> for request in requests.of_type(RequestType.held_message): - ... assert request.request_type is RequestType.held_message - ... key, data = requests.get_request(request.id) - ... print request.id, key - ... if data is not None: - ... for key in sorted(data): - ... print ' {0}: {1}'.format(key, data[key]) - 1 hold_1 - 4 hold_4 - 5 hold_5 - bar: no - foo: yes - - -Deleting requests -================= - -Once a specific request has been handled, it will be deleted from the requests -database. - - >>> requests.delete_request(2) - >>> requests.count - 4 - >>> show_holds(requests) - 1 RequestType.held_message hold_1 - 3 RequestType.unsubscription hold_3 - 4 RequestType.held_message hold_4 - 5 RequestType.held_message hold_5 - bar: no - foo: yes - >>> print requests.get_request(2) - None - -We get an exception if we ask to delete a request that isn't in the database. - - >>> requests.delete_request(801) - Traceback (most recent call last): - ... - KeyError: 801 - -For the next section, we first clean up all the current requests. - - >>> for request in requests.held_requests: - ... requests.delete_request(request.id) - >>> requests.count - 0 - - -Application support -=================== - -There are several higher level interfaces available in the ``mailman.app`` -package which can be used to hold messages, subscription, and unsubscriptions. -There are also interfaces for disposing of these requests in an application -specific and consistent way. - - >>> from mailman.app import moderator - - -Holding messages -================ - -For this section, we need a mailing list and at least one message. - - >>> mlist = create_list('alist@example.com') - >>> mlist.preferred_language = 'en' - >>> mlist.real_name = 'A Test List' - >>> msg = message_from_string("""\ - ... From: aperson@example.org - ... To: alist@example.com - ... Subject: Something important - ... - ... Here's something important about our mailing list. - ... """) - -Holding a message means keeping a copy of it that a moderator must approve -before the message is posted to the mailing list. To hold the message, you -must supply the message, message metadata, and a text reason for the hold. In -this case, we won't include any additional metadata. - - >>> id_1 = moderator.hold_message(mlist, msg, {}, 'Needs approval') - >>> requests.get_request(id_1) is not None - True - -We can also hold a message with some additional metadata. -:: - - # Delete the Message-ID from the previous hold so we don't try to store - # collisions in the message storage. - >>> del msg['message-id'] - >>> msgdata = dict(sender='aperson@example.com', - ... approved=True, - ... received_time=123.45) - >>> id_2 = moderator.hold_message(mlist, msg, msgdata, 'Feeling ornery') - >>> requests.get_request(id_2) is not None - True - -Once held, the moderator can select one of several dispositions. The most -trivial is to simply defer a decision for now. - - >>> from mailman.interfaces.action import Action - >>> moderator.handle_message(mlist, id_1, Action.defer) - >>> requests.get_request(id_1) is not None - True - -The moderator can also discard the message. This is often done with spam. -Bye bye message! - - >>> moderator.handle_message(mlist, id_1, Action.discard) - >>> print requests.get_request(id_1) - None - >>> virginq.files - [] - -The message can be rejected, meaning it is bounced back to the sender. - - >>> moderator.handle_message(mlist, id_2, Action.reject, 'Off topic') - >>> print requests.get_request(id_2) - None - >>> qmsg, qdata = dequeue() - >>> print qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: Request to mailing list "A Test List" rejected - From: alist-bounces@example.com - To: aperson@example.org - Message-ID: ... - Date: ... - Precedence: bulk - - Your request to the alist@example.com mailing list - - Posting of your message titled "Something important" - - has been rejected by the list moderator. The moderator gave the - following reason for rejecting your request: - - "Off topic" - - Any questions or comments should be directed to the list administrator - at: - - alist-owner@example.com - - >>> dump_msgdata(qdata) - _parsemsg : False - listname : alist@example.com - nodecorate : True - recipients : set([u'aperson@example.org']) - reduced_list_headers: True - version : 3 - -Or the message can be approved. This actually places the message back into -the incoming queue for further processing, however the message metadata -indicates that the message has been approved. - - >>> id_3 = moderator.hold_message(mlist, msg, msgdata, 'Needs approval') - >>> moderator.handle_message(mlist, id_3, Action.accept) - >>> inq = config.switchboards['in'] - >>> qmsg, qdata = dequeue(inq) - >>> print qmsg.as_string() - From: aperson@example.org - To: alist@example.com - Subject: Something important - Message-ID: ... - X-Message-ID-Hash: ... - X-Mailman-Approved-At: ... - - Here's something important about our mailing list. - - >>> dump_msgdata(qdata) - _parsemsg : False - approved : True - moderator_approved: True - sender : aperson@example.com - version : 3 - -In addition to any of the above dispositions, the message can also be -preserved for further study. Ordinarily the message is removed from the -global message store after its disposition (though approved messages may be -re-added to the message store). When handling a message, we can tell the -moderator interface to also preserve a copy, essentially telling it not to -delete the message from the storage. First, without the switch, the message -is deleted. -:: - - >>> msg = message_from_string("""\ - ... From: aperson@example.org - ... To: alist@example.com - ... Subject: Something important - ... Message-ID: <12345> - ... - ... Here's something important about our mailing list. - ... """) - >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval') - >>> moderator.handle_message(mlist, id_4, Action.discard) - - >>> from mailman.interfaces.messages import IMessageStore - >>> from zope.component import getUtility - >>> message_store = getUtility(IMessageStore) - - >>> print message_store.get_message_by_id('<12345>') - None - -But if we ask to preserve the message when we discard it, it will be held in -the message store after disposition. - - >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval') - >>> moderator.handle_message(mlist, id_4, Action.discard, preserve=True) - >>> stored_msg = message_store.get_message_by_id('<12345>') - >>> print stored_msg.as_string() - From: aperson@example.org - To: alist@example.com - Subject: Something important - Message-ID: <12345> - X-Message-ID-Hash: 4CF7EAU3SIXBPXBB5S6PEUMO62MWGQN6 - - Here's something important about our mailing list. - - -Orthogonal to preservation, the message can also be forwarded to another -address. This is helpful for getting the message into the inbox of one of the -moderators. -:: - - # Set a new Message-ID from the previous hold so we don't try to store - # collisions in the message storage. - >>> del msg['message-id'] - >>> msg['Message-ID'] = '' - >>> id_4 = moderator.hold_message(mlist, msg, {}, 'Needs approval') - >>> moderator.handle_message(mlist, id_4, Action.discard, - ... forward=['zperson@example.com']) - >>> qmsg, qdata = dequeue() - >>> print qmsg.as_string() - Subject: Forward of moderated message - From: alist-bounces@example.com - To: zperson@example.com - MIME-Version: 1.0 - Content-Type: message/rfc822 - Message-ID: ... - Date: ... - Precedence: bulk - - From: aperson@example.org - To: alist@example.com - Subject: Something important - Message-ID: - X-Message-ID-Hash: EN2R5UQFMOUTCL44FLNNPLSXBIZW62ER - - Here's something important about our mailing list. - - >>> dump_msgdata(qdata) - _parsemsg : False - listname : alist@example.com - nodecorate : True - recipients : [u'zperson@example.com'] - reduced_list_headers: True - version : 3 - - -Holding subscription requests -============================= - -For closed lists, subscription requests will also be held for moderator -approval. In this case, several pieces of information related to the -subscription must be provided, including the subscriber's address and real -name, their password (possibly hashed), what kind of delivery option they are -choosing and their preferred language. - - >>> from mailman.interfaces.member import DeliveryMode - >>> mlist.admin_immed_notify = False - >>> id_3 = moderator.hold_subscription(mlist, - ... 'bperson@example.org', 'Ben Person', - ... '{NONE}abcxyz', DeliveryMode.regular, 'en') - >>> requests.get_request(id_3) is not None - True - -In the above case the mailing list was not configured to send the list -moderators a notice about the hold, so no email message is in the virgin -queue. - - >>> virginq.files - [] - -But if we set the list up to notify the list moderators immediately when a -message is held for approval, there will be a message placed in the virgin -queue when the message is held. - - >>> mlist.admin_immed_notify = True - >>> # XXX This will almost certainly change once we've worked out the web - >>> # space layout for mailing lists now. - >>> id_4 = moderator.hold_subscription(mlist, - ... 'cperson@example.org', 'Claire Person', - ... '{NONE}zyxcba', DeliveryMode.regular, 'en') - >>> requests.get_request(id_4) is not None - True - >>> qmsg, qdata = dequeue() - >>> print qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: New subscription request to list A Test List from - cperson@example.org - From: alist-owner@example.com - To: alist-owner@example.com - Message-ID: ... - Date: ... - Precedence: bulk - - Your authorization is required for a mailing list subscription request - approval: - - For: cperson@example.org - List: alist@example.com - - At your convenience, visit: - - http://lists.example.com/admindb/alist@example.com - - to process the request. - - >>> dump_msgdata(qdata) - _parsemsg : False - listname : alist@example.com - nodecorate : True - recipients : set([u'alist-owner@example.com']) - reduced_list_headers: True - tomoderators : True - version : 3 - -Once held, the moderator can select one of several dispositions. The most -trivial is to simply defer a decision for now. - - >>> moderator.handle_subscription(mlist, id_3, Action.defer) - >>> requests.get_request(id_3) is not None - True - -The held subscription can also be discarded. - - >>> moderator.handle_subscription(mlist, id_3, Action.discard) - >>> print requests.get_request(id_3) - None - -The request can be rejected, in which case a message is sent to the -subscriber. - - >>> moderator.handle_subscription(mlist, id_4, Action.reject, - ... 'This is a closed list') - >>> print requests.get_request(id_4) - None - >>> qmsg, qdata = dequeue() - >>> print qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: Request to mailing list "A Test List" rejected - From: alist-bounces@example.com - To: cperson@example.org - Message-ID: ... - Date: ... - Precedence: bulk - - Your request to the alist@example.com mailing list - - Subscription request - - has been rejected by the list moderator. The moderator gave the - following reason for rejecting your request: - - "This is a closed list" - - Any questions or comments should be directed to the list administrator - at: - - alist-owner@example.com - - >>> dump_msgdata(qdata) - _parsemsg : False - listname : alist@example.com - nodecorate : True - recipients : set([u'cperson@example.org']) - reduced_list_headers: True - version : 3 - -The subscription can also be accepted. This subscribes the address to the -mailing list. - - >>> mlist.send_welcome_msg = True - >>> id_4 = moderator.hold_subscription(mlist, - ... 'fperson@example.org', 'Frank Person', - ... '{NONE}abcxyz', DeliveryMode.regular, 'en') - -A message will be sent to the moderators telling them about the held -subscription and the fact that they may need to approve it. - - >>> qmsg, qdata = dequeue() - >>> print qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: New subscription request to list A Test List from - fperson@example.org - From: alist-owner@example.com - To: alist-owner@example.com - Message-ID: ... - Date: ... - Precedence: bulk - - Your authorization is required for a mailing list subscription request - approval: - - For: fperson@example.org - List: alist@example.com - - At your convenience, visit: - - http://lists.example.com/admindb/alist@example.com - - to process the request. - - >>> dump_msgdata(qdata) - _parsemsg : False - listname : alist@example.com - nodecorate : True - recipients : set([u'alist-owner@example.com']) - reduced_list_headers: True - tomoderators : True - version : 3 - -Accept the subscription request. - - >>> mlist.admin_notify_mchanges = True - >>> moderator.handle_subscription(mlist, id_4, Action.accept) - -There are now two messages in the virgin queue. One is a welcome message -being sent to the user and the other is a subscription notification that is -sent to the moderators. The only good way to tell which is which is to look -at the recipient list. - - >>> qmsg_1, qdata_1 = dequeue(expected_count=2) - >>> qmsg_2, qdata_2 = dequeue() - >>> if 'fperson@example.org' in qdata_1['recipients']: - ... # The first message is the welcome message - ... welcome_qmsg = qmsg_1 - ... welcome_qdata = qdata_1 - ... admin_qmsg = qmsg_2 - ... admin_qdata = qdata_2 - ... else: - ... welcome_qmsg = qmsg_2 - ... welcome_qdata = qdata_2 - ... admin_qmsg = qmsg_1 - ... admin_qdata = qdata_1 - -The welcome message is sent to the person who just subscribed. - - >>> print welcome_qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: Welcome to the "A Test List" mailing list - From: alist-request@example.com - To: fperson@example.org - X-No-Archive: yes - Message-ID: ... - Date: ... - Precedence: bulk - - Welcome to the "A Test List" mailing list! - - To post to this list, send your email to: - - alist@example.com - - General information about the mailing list is at: - - http://lists.example.com/listinfo/alist@example.com - - If you ever want to unsubscribe or change your options (eg, switch to - or from digest mode, change your password, etc.), visit your - subscription page at: - - http://example.com/fperson@example.org - - You can also make such adjustments via email by sending a message to: - - alist-request@example.com - - with the word 'help' in the subject or body (don't include the - quotes), and you will get back a message with instructions. You will - need your password to change your options, but for security purposes, - this email is not included here. There is also a button on your - options page that will send your current password to you. - - >>> dump_msgdata(welcome_qdata) - _parsemsg : False - listname : alist@example.com - nodecorate : True - recipients : set([u'fperson@example.org']) - reduced_list_headers: True - verp : False - version : 3 - -The admin message is sent to the moderators. - - >>> print admin_qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: A Test List subscription notification - From: changeme@example.com - To: alist-owner@example.com - Message-ID: ... - Date: ... - Precedence: bulk - - Frank Person has been successfully subscribed to - A Test List. - - >>> dump_msgdata(admin_qdata) - _parsemsg : False - envsender : changeme@example.com - listname : alist@example.com - nodecorate : True - recipients : set([]) - reduced_list_headers: True - version : 3 - -Frank Person is now a member of the mailing list. -:: - - >>> member = mlist.members.get_member('fperson@example.org') - >>> member - - on alist@example.com as MemberRole.member> - >>> member.preferred_language - - >>> print member.delivery_mode - DeliveryMode.regular - >>> print member.user.real_name - Frank Person - >>> print member.user.password - {NONE}abcxyz - - -Holding unsubscription requests -=============================== - -Some lists, though it is rare, require moderator approval for unsubscriptions. -In this case, only the unsubscribing address is required. Like subscriptions, -unsubscription holds can send the list's moderators an immediate -notification. -:: - - - >>> from mailman.interfaces.usermanager import IUserManager - >>> from zope.component import getUtility - >>> user_manager = getUtility(IUserManager) - - >>> mlist.admin_immed_notify = False - >>> from mailman.interfaces.member import MemberRole - >>> user_1 = user_manager.create_user('gperson@example.com') - >>> address_1 = list(user_1.addresses)[0] - >>> mlist.subscribe(address_1, MemberRole.member) - - - >>> user_2 = user_manager.create_user('hperson@example.com') - >>> address_2 = list(user_2.addresses)[0] - >>> mlist.subscribe(address_2, MemberRole.member) - - - >>> id_5 = moderator.hold_unsubscription(mlist, 'gperson@example.com') - >>> requests.get_request(id_5) is not None - True - >>> virginq.files - [] - >>> mlist.admin_immed_notify = True - >>> id_6 = moderator.hold_unsubscription(mlist, 'hperson@example.com') - >>> qmsg, qdata = dequeue() - >>> print qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: New unsubscription request from A Test List by hperson@example.com - From: alist-owner@example.com - To: alist-owner@example.com - Message-ID: ... - Date: ... - Precedence: bulk - - Your authorization is required for a mailing list unsubscription - request approval: - - By: hperson@example.com - From: alist@example.com - - At your convenience, visit: - - http://lists.example.com/admindb/alist@example.com - - to process the request. - - >>> dump_msgdata(qdata) - _parsemsg : False - listname : alist@example.com - nodecorate : True - recipients : set([u'alist-owner@example.com']) - reduced_list_headers: True - tomoderators : True - version : 3 - -There are now two addresses with held unsubscription requests. As above, one -of the actions we can take is to defer to the decision. - - >>> moderator.handle_unsubscription(mlist, id_5, Action.defer) - >>> requests.get_request(id_5) is not None - True - -The held unsubscription can also be discarded, and the member will remain -subscribed. - - >>> moderator.handle_unsubscription(mlist, id_5, Action.discard) - >>> print requests.get_request(id_5) - None - >>> mlist.members.get_member('gperson@example.com') - - -The request can be rejected, in which case a message is sent to the member, -and the person remains a member of the mailing list. -:: - - >>> moderator.handle_unsubscription(mlist, id_6, Action.reject, - ... 'This list is a prison.') - >>> print requests.get_request(id_6) - None - >>> qmsg, qdata = dequeue() - >>> print qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: Request to mailing list "A Test List" rejected - From: alist-bounces@example.com - To: hperson@example.com - Message-ID: ... - Date: ... - Precedence: bulk - - Your request to the alist@example.com mailing list - - Unsubscription request - - has been rejected by the list moderator. The moderator gave the - following reason for rejecting your request: - - "This list is a prison." - - Any questions or comments should be directed to the list administrator - at: - - alist-owner@example.com - - >>> dump_msgdata(qdata) - _parsemsg : False - listname : alist@example.com - nodecorate : True - recipients : set([u'hperson@example.com']) - reduced_list_headers: True - version : 3 - - >>> mlist.members.get_member('hperson@example.com') - - -The unsubscription request can also be accepted. This removes the member from -the mailing list. - - >>> mlist.send_goodbye_msg = True - >>> mlist.goodbye_msg = 'So long!' - >>> mlist.admin_immed_notify = False - >>> id_7 = moderator.hold_unsubscription(mlist, 'gperson@example.com') - >>> moderator.handle_unsubscription(mlist, id_7, Action.accept) - >>> print mlist.members.get_member('gperson@example.com') - None - -There are now two messages in the virgin queue, one to the member who was just -unsubscribed and another to the moderators informing them of this membership -change. - - >>> qmsg_1, qdata_1 = dequeue(expected_count=2) - >>> qmsg_2, qdata_2 = dequeue() - >>> if 'gperson@example.com' in qdata_1['recipients']: - ... # The first message is the goodbye message - ... goodbye_qmsg = qmsg_1 - ... goodbye_qdata = qdata_1 - ... admin_qmsg = qmsg_2 - ... admin_qdata = qdata_2 - ... else: - ... goodbye_qmsg = qmsg_2 - ... goodbye_qdata = qdata_2 - ... admin_qmsg = qmsg_1 - ... admin_qdata = qdata_1 - -The goodbye message... - - >>> print goodbye_qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: You have been unsubscribed from the A Test List mailing list - From: alist-bounces@example.com - To: gperson@example.com - Message-ID: ... - Date: ... - Precedence: bulk - - So long! - - >>> dump_msgdata(goodbye_qdata) - _parsemsg : False - listname : alist@example.com - nodecorate : True - recipients : set([u'gperson@example.com']) - reduced_list_headers: True - verp : False - version : 3 - -...and the admin message. - - >>> print admin_qmsg.as_string() - MIME-Version: 1.0 - Content-Type: text/plain; charset="us-ascii" - Content-Transfer-Encoding: 7bit - Subject: A Test List unsubscription notification - From: changeme@example.com - To: alist-owner@example.com - Message-ID: ... - Date: ... - Precedence: bulk - - gperson@example.com has been removed from A Test List. - - >>> dump_msgdata(admin_qdata) - _parsemsg : False - envsender : changeme@example.com - listname : alist@example.com - nodecorate : True - recipients : set([]) - reduced_list_headers: True - version : 3 -- cgit v1.3.1