------------------------------------------------------------ revno: 6551 committer: Barry Warsaw <[EMAIL PROTECTED]> branch nick: 3.0 timestamp: Sun 2007-09-16 12:08:24 -0400 message: Finish up the request hold conversion. ListAdmin can go away though it hasn't yet. SendSubscribeAck(), SendUnsubscribeAck(), and ApprovedDeleteMember() are all removed, though the latter is not yet completely eradicated. modified: Mailman/Deliverer.py Mailman/MailList.py Mailman/app/membership.py Mailman/app/moderator.py Mailman/docs/requests.txt Mailman/templates/en/adminunsubscribeack.txt Mailman/templates/en/unsubauth.txt
=== modified file 'Mailman/Deliverer.py' --- a/Mailman/Deliverer.py 2007-08-01 20:11:08 +0000 +++ b/Mailman/Deliverer.py 2007-09-16 16:08:24 +0000 @@ -37,53 +37,6 @@ class Deliverer: - def SendSubscribeAck(self, name, password, digest, text=''): - pluser = self.getMemberLanguage(name) - if self.welcome_msg: - welcome = Utils.wrap(self.welcome_msg) + '\n' - else: - welcome = '' - if self.umbrella_list: - addr = self.GetMemberAdminEmail(name) - umbrella = Utils.wrap(_('''\ -Note: Since this is a list of mailing lists, administrative -notices like the password reminder will be sent to -your membership administrative address, %(addr)s.''')) - else: - umbrella = '' - # get the text from the template - text += Utils.maketext( - 'subscribeack.txt', - {'real_name' : self.real_name, - 'host_name' : self.host_name, - 'welcome' : welcome, - 'umbrella' : umbrella, - 'emailaddr' : self.GetListEmail(), - 'listinfo_url': self.GetScriptURL('listinfo', absolute=True), - 'optionsurl' : self.GetOptionsURL(name, absolute=True), - 'password' : password, - 'user' : self.getMemberCPAddress(name), - }, lang=pluser, mlist=self) - if digest: - digmode = _(' (Digest mode)') - else: - digmode = '' - realname = self.real_name - msg = Message.UserNotification( - self.GetMemberAdminEmail(name), self.GetRequestEmail(), - _('Welcome to the "%(realname)s" mailing list%(digmode)s'), - text, pluser) - msg['X-No-Archive'] = 'yes' - msg.send(self, verp=config.VERP_PERSONALIZED_DELIVERIES) - - def SendUnsubscribeAck(self, addr, lang): - realname = self.real_name - msg = Message.UserNotification( - self.GetMemberAdminEmail(addr), self.GetBouncesEmail(), - _('You have been unsubscribed from the %(realname)s mailing list'), - Utils.wrap(self.goodbye_msg), lang) - msg.send(self, verp=config.VERP_PERSONALIZED_DELIVERIES) - def MailUserPassword(self, user): listfullname = self.fqdn_listname requestaddr = self.GetRequestEmail() === modified file 'Mailman/MailList.py' --- a/Mailman/MailList.py 2007-09-16 11:15:53 +0000 +++ b/Mailman/MailList.py 2007-09-16 16:08:24 +0000 @@ -597,37 +597,6 @@ raise Errors.MMNeedApproval, _( 'unsubscriptions require moderator approval') - def ApprovedDeleteMember(self, name, whence=None, - admin_notif=None, userack=None): - if userack is None: - userack = self.send_goodbye_msg - if admin_notif is None: - admin_notif = self.admin_notify_mchanges - # Delete a member, for which we know the approval has been made - fullname, emailaddr = parseaddr(name) - userlang = self.getMemberLanguage(emailaddr) - # Remove the member - self.removeMember(emailaddr) - # And send an acknowledgement to the user... - if userack: - self.SendUnsubscribeAck(emailaddr, userlang) - # ...and to the administrator - if admin_notif: - realname = self.real_name - subject = _('%(realname)s unsubscribe notification') - text = Utils.maketext( - 'adminunsubscribeack.txt', - {'member' : name, - 'listname': self.real_name, - }, mlist=self) - msg = Message.OwnerNotification(self, subject, text) - msg.send(self) - if whence: - whence = "; %s" % whence - else: - whence = "" - slog.info('%s: deleted %s%s', self.internal_name(), name, whence) - def ChangeMemberName(self, addr, name, globally): self.setMemberName(addr, name) if not globally: === modified file 'Mailman/app/membership.py' --- a/Mailman/app/membership.py 2007-09-16 11:15:53 +0000 +++ b/Mailman/app/membership.py 2007-09-16 16:08:24 +0000 @@ -151,3 +151,43 @@ text, language) msg['X-No-Archive'] = 'yes' msg.send(mlist, verp=config.VERP_PERSONALIZED_DELIVERIES) + + + +def delete_member(mlist, address, admin_notif=None, userack=None): + if userack is None: + userack = mlist.send_goodbye_msg + if admin_notif is None: + admin_notif = mlist.admin_notify_mchanges + # Delete a member, for which we know the approval has been made + member = mlist.members.get_member(address) + language = member.preferred_language + member.unsubscribe() + # And send an acknowledgement to the user... + if userack: + send_goodbye_message(mlist, address, language) + # ...and to the administrator. + if admin_notif: + user = config.db.user_manager.get_user(address) + realname = user.real_name + subject = _('$mlist.real_name unsubscription notification') + text = Utils.maketext( + 'adminunsubscribeack.txt', + {'listname': mlist.real_name, + 'member' : formataddr((realname, address)), + }, mlist=mlist) + msg = Message.OwnerNotification(mlist, subject, text) + msg.send(mlist) + + + +def send_goodbye_message(mlist, address, language): + if mlist.goodbye_msg: + goodbye = Utils.wrap(mlist.goodbye_msg) + '\n' + else: + goodbye = '' + msg = Message.UserNotification( + address, mlist.bounces_address, + _('You have been unsubscribed from the $mlist.real_name mailing list'), + goodbye, language) + msg.send(mlist, verp=config.VERP_PERSONALIZED_DELIVERIES) === modified file 'Mailman/app/moderator.py' --- a/Mailman/app/moderator.py 2007-09-16 11:15:53 +0000 +++ b/Mailman/app/moderator.py 2007-09-16 16:08:24 +0000 @@ -17,10 +17,13 @@ """Application support for moderators.""" -from __future__ import with_statement - __all__ = [ + 'handle_message', + 'handle_subscription', + 'handle_unsubscription', 'hold_message', + 'hold_subscription', + 'hold_unsubscription', ] import logging @@ -33,7 +36,7 @@ from Mailman import Utils from Mailman import i18n from Mailman.Queue.sbcache import get_switchboard -from Mailman.app.membership import add_member +from Mailman.app.membership import add_member, delete_member from Mailman.configuration import config from Mailman.constants import Action, DeliveryMode from Mailman.interfaces import RequestType @@ -188,9 +191,8 @@ mlist.fqdn_listname, address) # Possibly notify the administrator in default list language if mlist.admin_immed_notify: - realname = mlist.real_name subject = _( - 'New subscription request to list $realname from $address') + 'New subscription request to list $mlist.real_name from $address') text = Utils.maketext( 'subauth.txt', {'username' : address, @@ -241,54 +243,62 @@ raise AssertionError('Unexpected action: %s' % action) # Delete the request from the database. requestdb.delete_request(id) - return -def HoldUnsubscription(self, addr): - # Assure the database is open for writing - self._opendb() - # Get the next unique id - id = self._next_id - # All we need to do is save the unsubscribing address - self._db[id] = (UNSUBSCRIPTION, addr) +def hold_unsubscription(mlist, address): + data = dict(address=address) + requestsdb = config.db.requests.get_list_requests(mlist) + request_id = requestsdb.hold_request( + RequestType.unsubscription, address, data) vlog.info('%s: held unsubscription request from %s', - self.internal_name(), addr) + mlist.fqdn_listname, address) # Possibly notify the administrator of the hold - if self.admin_immed_notify: - realname = self.real_name + if mlist.admin_immed_notify: subject = _( - 'New unsubscription request from %(realname)s by %(addr)s') + 'New unsubscription request from $mlist.real_name by $address') text = Utils.maketext( 'unsubauth.txt', - {'username' : addr, - 'listname' : self.internal_name(), - 'hostname' : self.host_name, - 'admindb_url': self.GetScriptURL('admindb', absolute=1), - }, mlist=self) + {'address' : address, + 'listname' : mlist.fqdn_listname, + 'admindb_url': mlist.script_url('admindb'), + }, mlist=mlist) # This message should appear to come from the <list>-owner so as # to avoid any useless bounce processing. - owneraddr = self.GetOwnerEmail() - msg = Message.UserNotification(owneraddr, owneraddr, subject, text, - self.preferred_language) - msg.send(self, **{'tomoderators': 1}) - -def _handleunsubscription(self, record, value, comment): - addr = record - if value == config.DEFER: - return DEFER - elif value == config.DISCARD: + msg = Message.UserNotification( + mlist.owner_address, mlist.owner_address, + subject, text, mlist.preferred_language) + msg.send(mlist, tomoderators=True) + return request_id + + + +def handle_unsubscription(mlist, id, action, comment=None): + requestdb = config.db.requests.get_list_requests(mlist) + key, data = requestdb.get_request(id) + address = data['address'] + if action is Action.defer: + # Nothing to do. + return + elif action is Action.discard: + # Nothing to do except delete the request from the database. pass - elif value == config.REJECT: - self._refuse(_('Unsubscription request'), addr, comment) - else: - assert value == config.UNSUBSCRIBE + elif action is Action.reject: + key, data = requestdb.get_request(id) + _refuse(mlist, _('Unsubscription request'), address, + comment or _('[No reason given]')) + elif action is Action.accept: + key, data = requestdb.get_request(id) try: - self.ApprovedDeleteMember(addr) + delete_member(mlist, address) except Errors.NotAMemberError: - # User has already been unsubscribed + # User has already been unsubscribed. pass - return REMOVE + slog.info('%s: deleted %s', mlist.fqdn_listname, address) + else: + raise AssertionError('Unexpected action: %s' % action) + # Delete the request from the database. + requestdb.delete_request(id) @@ -324,41 +334,3 @@ msg = Message.UserNotification(recip, mlist.bounces_address, subject, text, lang) msg.send(mlist) - - - -def readMessage(path): - # For backwards compatibility, we must be able to read either a flat text - # file or a pickle. - ext = os.path.splitext(path)[1] - with open(path) as fp: - if ext == '.txt': - msg = email.message_from_file(fp, Message.Message) - else: - assert ext == '.pck' - msg = cPickle.load(fp) - return msg - - - -def handle_request(mlist, id, value, - comment=None, preserve=None, forward=None, addr=None): - requestsdb = config.db.get_list_requests(mlist) - key, data = requestsdb.get_record(id) - - self._opendb() - rtype, data = self._db[id] - if rtype == HELDMSG: - status = self._handlepost(data, value, comment, preserve, - forward, addr) - elif rtype == UNSUBSCRIPTION: - status = self._handleunsubscription(data, value, comment) - else: - assert rtype == SUBSCRIPTION - status = self._handlesubscription(data, value, comment) - if status <> DEFER: - # BAW: Held message ids are linked to Pending cookies, allowing - # the user to cancel their post before the moderator has approved - # it. We should probably remove the cookie associated with this - # id, but we have no way currently of correlating them. :( - del self._db[id] === modified file 'Mailman/docs/requests.txt' --- a/Mailman/docs/requests.txt 2007-09-16 11:15:53 +0000 +++ b/Mailman/docs/requests.txt 2007-09-16 16:08:24 +0000 @@ -548,8 +548,8 @@ ('recips', ['[EMAIL PROTECTED]']), ('reduced_list_headers', True), ('version', 3)] -Or the subscription can be approved/accepted. This subscribes the member to -the mailing list. +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, @@ -711,28 +711,189 @@ >>> mlist.admin_immed_notify = False >>> flush() >>> from Mailman.constants import MemberRole - >>> user_1 = config.db.user_manager.create_user('[EMAIL PROTECTED]') + >>> user_1 = config.db.user_manager.create_user('[EMAIL PROTECTED]') >>> flush() >>> address_1 = list(user_1.addresses)[0] >>> address_1.subscribe(mlist, MemberRole.member) - <Member: [EMAIL PROTECTED] on [EMAIL PROTECTED] as MemberRole.member> - >>> user_2 = config.db.user_manager.create_user('[EMAIL PROTECTED]') + <Member: [EMAIL PROTECTED] on [EMAIL PROTECTED] as MemberRole.member> + >>> user_2 = config.db.user_manager.create_user('[EMAIL PROTECTED]') >>> flush() >>> address_2 = list(user_2.addresses)[0] >>> address_2.subscribe(mlist, MemberRole.member) - <Member: [EMAIL PROTECTED] on [EMAIL PROTECTED] as MemberRole.member> - >>> flush() - >>> id_5 = moderator.hold_unsubscription(mlist, '[EMAIL PROTECTED]') - >>> flush() - >>> requests.get_request(id_5) is not None) + <Member: [EMAIL PROTECTED] on [EMAIL PROTECTED] as MemberRole.member> + >>> flush() + >>> id_5 = moderator.hold_unsubscription(mlist, '[EMAIL PROTECTED]') + >>> flush() + >>> requests.get_request(id_5) is not None True >>> virginq.files [] >>> mlist.admin_immed_notify = True - >>> id_6 = moderator.hold_unsubscription(mlist, '[EMAIL PROTECTED]') - >>> flush() - >>> qmsg, qdata = dequeue() - >>> print qmsg.as_string() - XXX - >>> sorted(qdata.items()) - XXX + >>> id_6 = moderator.hold_unsubscription(mlist, '[EMAIL PROTECTED]') + >>> flush() + >>> 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 [EMAIL PROTECTED] + From: [EMAIL PROTECTED] + To: [EMAIL PROTECTED] + Message-ID: ... + Date: ... + Precedence: bulk + <BLANKLINE> + Your authorization is required for a mailing list unsubscription + request approval: + <BLANKLINE> + By: [EMAIL PROTECTED] + From: [EMAIL PROTECTED] + <BLANKLINE> + At your convenience, visit: + <BLANKLINE> + http://www.example.com/admindb/[EMAIL PROTECTED] + <BLANKLINE> + to process the request. + <BLANKLINE> + >>> sorted(qdata.items()) + [('_parsemsg', False), + ('listname', '[EMAIL PROTECTED]'), ('nodecorate', True), + ('received_time', ...), + ('recips', ['[EMAIL PROTECTED]']), + ('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) + >>> flush() + >>> 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) + >>> flush() + >>> print requests.get_request(id_5) + None + >>> mlist.members.get_member('[EMAIL PROTECTED]') + <Member: [EMAIL PROTECTED] on [EMAIL PROTECTED] as MemberRole.member> + +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.') + >>> flush() + >>> 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: [EMAIL PROTECTED] + To: [EMAIL PROTECTED] + Message-ID: ... + Date: ... + Precedence: bulk + <BLANKLINE> + Your request to the [EMAIL PROTECTED] mailing list + <BLANKLINE> + Unsubscription request + <BLANKLINE> + has been rejected by the list moderator. The moderator gave the + following reason for rejecting your request: + <BLANKLINE> + "This list is a prison." + <BLANKLINE> + Any questions or comments should be directed to the list administrator + at: + <BLANKLINE> + [EMAIL PROTECTED] + <BLANKLINE> + >>> sorted(qdata.items()) + [('_parsemsg', False), + ('listname', '[EMAIL PROTECTED]'), + ('nodecorate', True), ('received_time', ...), + ('recips', ['[EMAIL PROTECTED]']), + ('reduced_list_headers', True), ('version', 3)] + >>> mlist.members.get_member('[EMAIL PROTECTED]') + <Member: [EMAIL PROTECTED] on [EMAIL PROTECTED] as MemberRole.member> + +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 + >>> flush() + >>> id_7 = moderator.hold_unsubscription(mlist, '[EMAIL PROTECTED]') + >>> flush() + >>> moderator.handle_unsubscription(mlist, id_7, Action.accept) + >>> flush() + >>> print mlist.members.get_member('[EMAIL PROTECTED]') + 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 '[EMAIL PROTECTED]' in qdata_1['recips']: + ... # 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: [EMAIL PROTECTED] + To: [EMAIL PROTECTED] + Message-ID: ... + Date: ... + Precedence: bulk + <BLANKLINE> + So long! + <BLANKLINE> + >>> sorted(goodbye_qdata.items()) + [('_parsemsg', False), + ('listname', '[EMAIL PROTECTED]'), + ('nodecorate', True), ('received_time', ...), + ('recips', ['[EMAIL PROTECTED]']), + ('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: [EMAIL PROTECTED] + To: [EMAIL PROTECTED] + Message-ID: ... + Date: ... + Precedence: bulk + <BLANKLINE> + [EMAIL PROTECTED] has been removed from A Test List. + <BLANKLINE> + >>> sorted(admin_qdata.items()) + [('_parsemsg', False), ('envsender', '[EMAIL PROTECTED]'), + ('listname', '[EMAIL PROTECTED]'), + ('nodecorate', True), ('received_time', ...), + ('recips', []), ('reduced_list_headers', True), ('version', 3)] === modified file 'Mailman/templates/en/adminunsubscribeack.txt' --- a/Mailman/templates/en/adminunsubscribeack.txt 2001-05-18 21:28:54 +0000 +++ b/Mailman/templates/en/adminunsubscribeack.txt 2007-09-16 16:08:24 +0000 @@ -1,2 +1,1 @@ %(member)s has been removed from %(listname)s. - === modified file 'Mailman/templates/en/unsubauth.txt' --- a/Mailman/templates/en/unsubauth.txt 2001-10-21 06:41:57 +0000 +++ b/Mailman/templates/en/unsubauth.txt 2007-09-16 16:08:24 +0000 @@ -1,8 +1,8 @@ Your authorization is required for a mailing list unsubscription request approval: - By: %(username)s - From: %(listname)[EMAIL PROTECTED](hostname)s + By: %(address)s + From: %(listname)s At your convenience, visit: -- https://code.launchpad.net/~mailman-coders/mailman/3.0 You are receiving this branch notification because you are subscribed to it. To unsubscribe from this branch go to https://code.launchpad.net/~mailman-coders/mailman/3.0/+subscription/mailman-checkins. _______________________________________________ Mailman-checkins mailing list Mailman-checkins@python.org Unsubscribe: http://mail.python.org/mailman/options/mailman-checkins/archive%40jab.org