diff options
| author | Barry Warsaw | 2007-09-16 12:08:24 -0400 |
|---|---|---|
| committer | Barry Warsaw | 2007-09-16 12:08:24 -0400 |
| commit | 0d623d7684e2ad8309219da934fc956a498e3a71 (patch) | |
| tree | 8e0e5430aae3b02078cf1a921905c420623980d7 | |
| parent | 0a3e802568a0bfaf9ceade4614a52db9a90b905c (diff) | |
| download | mailman-0d623d7684e2ad8309219da934fc956a498e3a71.tar.gz mailman-0d623d7684e2ad8309219da934fc956a498e3a71.tar.zst mailman-0d623d7684e2ad8309219da934fc956a498e3a71.zip | |
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.
| -rw-r--r-- | Mailman/Deliverer.py | 47 | ||||
| -rw-r--r-- | Mailman/MailList.py | 31 | ||||
| -rw-r--r-- | Mailman/app/membership.py | 40 | ||||
| -rw-r--r-- | Mailman/app/moderator.py | 124 | ||||
| -rw-r--r-- | Mailman/docs/requests.txt | 183 | ||||
| -rw-r--r-- | Mailman/templates/en/adminunsubscribeack.txt | 1 | ||||
| -rw-r--r-- | Mailman/templates/en/unsubauth.txt | 4 |
7 files changed, 262 insertions, 168 deletions
diff --git a/Mailman/Deliverer.py b/Mailman/Deliverer.py index c9b246400..c9b044286 100644 --- a/Mailman/Deliverer.py +++ b/Mailman/Deliverer.py @@ -37,53 +37,6 @@ mlog = logging.getLogger('mailman.mischief') 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() diff --git a/Mailman/MailList.py b/Mailman/MailList.py index cbcaab275..80d946634 100644 --- a/Mailman/MailList.py +++ b/Mailman/MailList.py @@ -597,37 +597,6 @@ class MailList(object, HTMLFormatter, Deliverer, 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: diff --git a/Mailman/app/membership.py b/Mailman/app/membership.py index abd2778f6..695366b91 100644 --- a/Mailman/app/membership.py +++ b/Mailman/app/membership.py @@ -151,3 +151,43 @@ def send_welcome_message(mlist, address, language, delivery_mode, text=''): 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) diff --git a/Mailman/app/moderator.py b/Mailman/app/moderator.py index 54e8956f0..823832184 100644 --- a/Mailman/app/moderator.py +++ b/Mailman/app/moderator.py @@ -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 Message 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 @@ def hold_subscription(mlist, address, realname, password, mode, language): 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 @@ def handle_subscription(mlist, id, action, comment=None): 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}) + msg = Message.UserNotification( + mlist.owner_address, mlist.owner_address, + subject, text, mlist.preferred_language) + msg.send(mlist, tomoderators=True) + return request_id -def _handleunsubscription(self, record, value, comment): - addr = record - if value == config.DEFER: - return DEFER - elif value == config.DISCARD: + + +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 @@ def _refuse(mlist, request, recip, comment, origmsg=None, lang=None): 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] diff --git a/Mailman/docs/requests.txt b/Mailman/docs/requests.txt index 05be06aac..242d57b9c 100644 --- a/Mailman/docs/requests.txt +++ b/Mailman/docs/requests.txt @@ -548,8 +548,8 @@ subscriber. ('recips', ['cperson@example.org']), ('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 @@ unsubscription holds can send the list's moderators an immediate notification. >>> mlist.admin_immed_notify = False >>> flush() >>> from Mailman.constants import MemberRole - >>> user_1 = config.db.user_manager.create_user('dperson@example.com') + >>> user_1 = config.db.user_manager.create_user('gperson@example.com') >>> flush() >>> address_1 = list(user_1.addresses)[0] >>> address_1.subscribe(mlist, MemberRole.member) - <Member: dperson@example.com on alist@example.com as MemberRole.member> - >>> user_2 = config.db.user_manager.create_user('eperson@example.com') + <Member: gperson@example.com on alist@example.com as MemberRole.member> + >>> user_2 = config.db.user_manager.create_user('hperson@example.com') >>> flush() >>> address_2 = list(user_2.addresses)[0] >>> address_2.subscribe(mlist, MemberRole.member) - <Member: eperson@example.com on alist@example.com as MemberRole.member> + <Member: hperson@example.com on alist@example.com as MemberRole.member> >>> flush() - >>> id_5 = moderator.hold_unsubscription(mlist, 'dperson@example.com') + >>> id_5 = moderator.hold_unsubscription(mlist, 'gperson@example.com') >>> flush() - >>> requests.get_request(id_5) is not None) + >>> requests.get_request(id_5) is not None True >>> virginq.files [] >>> mlist.admin_immed_notify = True - >>> id_6 = moderator.hold_unsubscription(mlist, 'eperson@example.com') + >>> id_6 = moderator.hold_unsubscription(mlist, 'hperson@example.com') >>> flush() >>> qmsg, qdata = dequeue() >>> print qmsg.as_string() - XXX + 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 + <BLANKLINE> + Your authorization is required for a mailing list unsubscription + request approval: + <BLANKLINE> + By: hperson@example.com + From: alist@example.com + <BLANKLINE> + At your convenience, visit: + <BLANKLINE> + http://www.example.com/admindb/alist@example.com + <BLANKLINE> + to process the request. + <BLANKLINE> >>> sorted(qdata.items()) - XXX + [('_parsemsg', False), + ('listname', 'alist@example.com'), ('nodecorate', True), + ('received_time', ...), + ('recips', ['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) + >>> 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('gperson@example.com') + <Member: gperson@example.com on alist@example.com 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: alist-bounces@example.com + To: hperson@example.com + Message-ID: ... + Date: ... + Precedence: bulk + <BLANKLINE> + Your request to the alist@example.com 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> + alist-owner@example.com + <BLANKLINE> + >>> sorted(qdata.items()) + [('_parsemsg', False), + ('listname', 'alist@example.com'), + ('nodecorate', True), ('received_time', ...), + ('recips', ['hperson@example.com']), + ('reduced_list_headers', True), ('version', 3)] + >>> mlist.members.get_member('hperson@example.com') + <Member: hperson@example.com on alist@example.com 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, 'gperson@example.com') + >>> flush() + >>> moderator.handle_unsubscription(mlist, id_7, Action.accept) + >>> flush() + >>> 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['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: alist-bounces@example.com + To: gperson@example.com + Message-ID: ... + Date: ... + Precedence: bulk + <BLANKLINE> + So long! + <BLANKLINE> + >>> sorted(goodbye_qdata.items()) + [('_parsemsg', False), + ('listname', 'alist@example.com'), + ('nodecorate', True), ('received_time', ...), + ('recips', ['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 + <BLANKLINE> + gperson@example.com has been removed from A Test List. + <BLANKLINE> + >>> sorted(admin_qdata.items()) + [('_parsemsg', False), ('envsender', 'changeme@example.com'), + ('listname', 'alist@example.com'), + ('nodecorate', True), ('received_time', ...), + ('recips', []), ('reduced_list_headers', True), ('version', 3)] diff --git a/Mailman/templates/en/adminunsubscribeack.txt b/Mailman/templates/en/adminunsubscribeack.txt index 2ebcfeb70..fb87ac87d 100644 --- a/Mailman/templates/en/adminunsubscribeack.txt +++ b/Mailman/templates/en/adminunsubscribeack.txt @@ -1,2 +1 @@ %(member)s has been removed from %(listname)s. - diff --git a/Mailman/templates/en/unsubauth.txt b/Mailman/templates/en/unsubauth.txt index 920f6c1b6..40bc3d5e6 100644 --- a/Mailman/templates/en/unsubauth.txt +++ b/Mailman/templates/en/unsubauth.txt @@ -1,8 +1,8 @@ Your authorization is required for a mailing list unsubscription request approval: - By: %(username)s - From: %(listname)s@%(hostname)s + By: %(address)s + From: %(listname)s At your convenience, visit: |
