diff options
| -rw-r--r-- | mailman/app/membership.py | 96 | ||||
| -rw-r--r-- | mailman/app/moderator.py | 17 | ||||
| -rw-r--r-- | mailman/app/notifications.py | 134 | ||||
| -rw-r--r-- | mailman/bin/set_members.py | 6 | ||||
| -rw-r--r-- | mailman/commands/__init__.py | 2 | ||||
| -rw-r--r-- | mailman/commands/cmd_join.py | 20 | ||||
| -rw-r--r-- | mailman/commands/cmd_subscribe.py | 133 | ||||
| -rw-r--r-- | mailman/commands/docs/echo.txt | 31 | ||||
| -rw-r--r-- | mailman/commands/docs/end.txt | 38 | ||||
| -rw-r--r-- | mailman/commands/docs/join.txt | 80 | ||||
| -rw-r--r-- | mailman/commands/echo.py | 4 | ||||
| -rw-r--r-- | mailman/commands/end.py (renamed from mailman/commands/cmd_end.py) | 37 | ||||
| -rw-r--r-- | mailman/commands/join.py | 193 | ||||
| -rw-r--r-- | mailman/interfaces/command.py | 12 | ||||
| -rw-r--r-- | mailman/queue/command.py | 9 | ||||
| -rw-r--r-- | mailman/queue/docs/command.txt | 65 | ||||
| -rw-r--r-- | mailman/queue/docs/outgoing.txt | 9 |
17 files changed, 629 insertions, 257 deletions
diff --git a/mailman/app/membership.py b/mailman/app/membership.py index 8a6e571a1..227a85003 100644 --- a/mailman/app/membership.py +++ b/mailman/app/membership.py @@ -23,10 +23,8 @@ __metaclass__ = type __all__ = [ 'add_member', 'delete_member', - 'send_goodbye_message', - 'send_welcome_message', ] - + from email.utils import formataddr @@ -34,6 +32,7 @@ from mailman import Errors from mailman import Message from mailman import Utils from mailman import i18n +from mailman.app.notifications import send_goodbye_message from mailman.configuration import config from mailman.interfaces import AlreadySubscribedError, DeliveryMode, MemberRole @@ -41,31 +40,30 @@ _ = i18n._ -def add_member(mlist, address, realname, password, delivery_mode, language, - ack=None, admin_notif=None, text=''): +def add_member(mlist, address, realname, password, delivery_mode, language): """Add a member right now. The member's subscription must be approved by whatever policy the list enforces. - ack is a flag that specifies whether the user should get an - acknowledgement of their being subscribed. Default is to use the - list's default flag value. - - admin_notif is a flag that specifies whether the list owner should get - an acknowledgement of this subscription. Default is to use the list's - default flag value. + :param mlist: the mailing list to add the member to + :type mlist: IMailingList + :param address: the address to subscribe + :type address: string + :param realname: the subscriber's full name + :type realname: string + :param password: the subscriber's password + :type password: string + :param delivery_mode: the delivery mode the subscriber has chosen + :type delivery_mode: DeliveryMode + :param language: the language that the subscriber is going to use + :type language: string """ - # Set up default flag values - if ack is None: - ack = mlist.send_welcome_msg - if admin_notif is None: - admin_notif = mlist.admin_notify_mchanges # Let's be extra cautious. Utils.ValidateEmail(address) if mlist.members.get_member(address) is not None: - raise AlreadySubscribedError(mlist.fqdn_listname, address, - MemberRole.member) + raise AlreadySubscribedError( + mlist.fqdn_listname, address, MemberRole.member) # Check for banned address here too for admin mass subscribes and # confirmations. pattern = Utils.get_pattern(address, mlist.ban_list) @@ -110,53 +108,6 @@ def add_member(mlist, address, realname, password, delivery_mode, language, member.preferences.delivery_mode = delivery_mode ## mlist.setMemberOption(email, config.Moderate, ## mlist.default_member_moderation) - # Send notifications. - if ack: - send_welcome_message(mlist, address, language, delivery_mode, text) - if admin_notif: - with i18n.using_language(mlist.preferred_language): - subject = _('$mlist.real_name subscription notification') - if isinstance(realname, unicode): - realname = realname.encode(Utils.GetCharSet(language), 'replace') - text = Utils.maketext( - 'adminsubscribeack.txt', - {'listname' : mlist.real_name, - 'member' : formataddr((realname, address)), - }, mlist=mlist) - msg = Message.OwnerNotification(mlist, subject, text) - msg.send(mlist) - - - -def send_welcome_message(mlist, address, language, delivery_mode, text=''): - if mlist.welcome_msg: - welcome = Utils.wrap(mlist.welcome_msg) + '\n' - else: - welcome = '' - # Find the IMember object which is subscribed to the mailing list, because - # from there, we can get the member's options url. - member = mlist.members.get_member(address) - options_url = member.options_url - # Get the text from the template. - text += Utils.maketext( - 'subscribeack.txt', { - 'real_name' : mlist.real_name, - 'posting_address' : mlist.fqdn_listname, - 'listinfo_url' : mlist.script_url('listinfo'), - 'optionsurl' : options_url, - 'request_address' : mlist.request_address, - 'welcome' : welcome, - }, lang=language, mlist=mlist) - if delivery_mode is not DeliveryMode.regular: - digmode = _(' (Digest mode)') - else: - digmode = '' - msg = Message.UserNotification( - address, mlist.request_address, - _('Welcome to the "$mlist.real_name" mailing list${digmode}'), - text, language) - msg['X-No-Archive'] = 'yes' - msg.send(mlist, verp=config.VERP_PERSONALIZED_DELIVERIES) @@ -184,16 +135,3 @@ def delete_member(mlist, address, admin_notif=None, userack=None): }, 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 8b2db9ef7..6cd32c562 100644 --- a/mailman/app/moderator.py +++ b/mailman/app/moderator.py @@ -38,8 +38,11 @@ from mailman import Message from mailman import Utils from mailman import i18n from mailman.app.membership import add_member, delete_member +from mailman.app.notifications import ( + send_admin_subscription_notice, send_welcome_message) from mailman.configuration import config from mailman.interfaces import Action, DeliveryMode, RequestType +from mailman.interfaces.member import AlreadySubscribedError from mailman.queue import Switchboard _ = i18n._ @@ -237,13 +240,21 @@ def handle_subscription(mlist, id, action, comment=None): delivery_mode = DeliveryMode(enum_value) address = data['address'] realname = data['realname'] + language = data['language'] + password = data['password'] try: - add_member(mlist, address, realname, data['password'], - delivery_mode, data['language']) - except Errors.AlreadySubscribedError: + add_member(mlist, address, realname, password, + delivery_mode, language) + except AlreadySubscribedError: # The address got subscribed in some other way after the original # request was made and accepted. pass + else: + if mlist.send_welcome_msg: + send_welcome_message(mlist, address, language, delivery_mode) + if mlist.admin_notify_mchanges: + send_admin_subscription_notice( + mlist, address, realname, language) slog.info('%s: new %s, %s %s', mlist.fqdn_listname, delivery_mode, formataddr((realname, address)), 'via admin approval') diff --git a/mailman/app/notifications.py b/mailman/app/notifications.py new file mode 100644 index 000000000..74068c0c2 --- /dev/null +++ b/mailman/app/notifications.py @@ -0,0 +1,134 @@ +# Copyright (C) 2007-2008 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, +# USA. + +"""Sending notifications.""" + +from __future__ import with_statement + +__metaclass__ = type +__all__ = [ + 'send_admin_subscription_notice', + 'send_goodbye_message', + 'send_welcome_message', + ] + + +from email.utils import formataddr + +from mailman import Message +from mailman import Utils +from mailman import i18n +from mailman.configuration import config +from mailman.interfaces.member import DeliveryMode + +_ = i18n._ + + + +def send_welcome_message(mlist, address, language, delivery_mode, text=''): + """Send a welcome message to a subscriber. + + Prepending to the standard welcome message template is the mailing list's + welcome message, if there is one. + + :param mlist: the mailing list + :type mlist: IMailingList + :param address: The address to respond to + :type address: string + :param language: the language of the response + :type language: string + :param delivery_mode: the type of delivery the subscriber is getting + :type delivery_mode: DeliveryMode + """ + if mlist.welcome_msg: + welcome = Utils.wrap(mlist.welcome_msg) + '\n' + else: + welcome = '' + # Find the IMember object which is subscribed to the mailing list, because + # from there, we can get the member's options url. + member = mlist.members.get_member(address) + options_url = member.options_url + # Get the text from the template. + text += Utils.maketext( + 'subscribeack.txt', { + 'real_name' : mlist.real_name, + 'posting_address' : mlist.fqdn_listname, + 'listinfo_url' : mlist.script_url('listinfo'), + 'optionsurl' : options_url, + 'request_address' : mlist.request_address, + 'welcome' : welcome, + }, lang=language, mlist=mlist) + if delivery_mode is not DeliveryMode.regular: + digmode = _(' (Digest mode)') + else: + digmode = '' + msg = Message.UserNotification( + address, mlist.request_address, + _('Welcome to the "$mlist.real_name" mailing list${digmode}'), + text, language) + msg['X-No-Archive'] = 'yes' + msg.send(mlist, verp=config.VERP_PERSONALIZED_DELIVERIES) + + + +def send_goodbye_message(mlist, address, language): + """Send a goodbye message to a subscriber. + + Prepending to the standard goodbye message template is the mailing list's + goodbye message, if there is one. + + :param mlist: the mailing list + :type mlist: IMailingList + :param address: The address to respond to + :type address: string + :param language: the language of the response + :type language: string + """ + 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) + + + +def send_admin_subscription_notice(mlist, address, full_name, language): + """Send the list administrators a subscription notice. + + :param mlist: the mailing list + :type mlist: IMailingList + :param address: the address being subscribed + :type address: string + :param full_name: the name of the subscriber + :type full_name: string + :param language: the language of the address's realname + :type language: string + """ + with i18n.using_language(mlist.preferred_language): + subject = _('$mlist.real_name subscription notification') + full_name = full_name.encode(Utils.GetCharSet(language), 'replace') + text = Utils.maketext( + 'adminsubscribeack.txt', + {'listname' : mlist.real_name, + 'member' : formataddr((full_name, address)), + }, mlist=mlist) + msg = Message.OwnerNotification(mlist, subject, text) + msg.send(mlist) diff --git a/mailman/bin/set_members.py b/mailman/bin/set_members.py index a97b13df8..4071627a7 100644 --- a/mailman/bin/set_members.py +++ b/mailman/bin/set_members.py @@ -25,6 +25,8 @@ from mailman import Utils from mailman import i18n from mailman import passwords from mailman.app.membership import add_member +from mailman.app.notifications import ( + send_admin_subscription_notice, send_welcome_message) from mailman.configuration import config from mailman.initialize import initialize from mailman.interfaces import DeliveryMode @@ -176,6 +178,10 @@ def main(): add_member(mlist, address, real_name, password, delivery_mode, mlist.preferred_language, send_welcome_msg, admin_notify) + if send_welcome_msg: + send_welcome_message(mlist, address, language, delivery_mode) + if admin_notify: + send_admin_subscription_notice(mlist, address, real_name) config.db.flush() diff --git a/mailman/commands/__init__.py b/mailman/commands/__init__.py index 81035e44a..044ffd6a5 100644 --- a/mailman/commands/__init__.py +++ b/mailman/commands/__init__.py @@ -17,4 +17,6 @@ __all__ = [ 'echo', + 'end', + 'join', ] diff --git a/mailman/commands/cmd_join.py b/mailman/commands/cmd_join.py deleted file mode 100644 index 7a80cd72b..000000000 --- a/mailman/commands/cmd_join.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (C) 2002-2008 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -"""The `join' command is synonymous with `subscribe'. -""" - -from mailman.Commands.cmd_subscribe import process diff --git a/mailman/commands/cmd_subscribe.py b/mailman/commands/cmd_subscribe.py deleted file mode 100644 index e1f7e6721..000000000 --- a/mailman/commands/cmd_subscribe.py +++ /dev/null @@ -1,133 +0,0 @@ -# Copyright (C) 2002-2008 by the Free Software Foundation, Inc. -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - -""" - subscribe [password] [digest|nodigest] [address=<address>] - Subscribe to this mailing list. Your password must be given to - unsubscribe or change your options, but if you omit the password, one - will be generated for you. You may be periodically reminded of your - password. - - The next argument may be either: `nodigest' or `digest' (no quotes!). - If you wish to subscribe an address other than the address you sent - this request from, you may specify `address=<address>' (no brackets - around the email address, and no quotes!) -""" - -from email.Utils import parseaddr -from email.Header import decode_header, make_header - -from mailman import Utils -from mailman import Errors -from mailman.UserDesc import UserDesc -from mailman.i18n import _ - -STOP = 1 - - - -def gethelp(mlist): - return _(__doc__) - - - -def process(res, args): - mlist = res.mlist - digest = None - password = None - address = None - realname = None - # Parse the args - argnum = 0 - for arg in args: - if arg.startswith('address='): - address = arg[8:] - elif argnum == 0: - password = arg - elif argnum == 1: - if arg.lower() not in ('digest', 'nodigest'): - res.results.append(_('Bad digest specifier: %(arg)s')) - return STOP - if arg.lower() == 'digest': - digest = 1 - else: - digest = 0 - else: - res.results.append(_('Usage:')) - res.results.append(gethelp(mlist)) - return STOP - argnum += 1 - # Fill in empty defaults - if digest is None: - digest = mlist.digest_is_default - if password is None: - password = Utils.MakeRandomPassword() - if address is None: - realname, address = parseaddr(res.msg['from']) - if not address: - # Fall back to the sender address - address = res.msg.get_sender() - if not address: - res.results.append(_('No valid address found to subscribe')) - return STOP - # Watch for encoded names - try: - h = make_header(decode_header(realname)) - # BAW: in Python 2.2, use just unicode(h) - realname = h.__unicode__() - except UnicodeError: - realname = u'' - # Coerce to byte string if uh contains only ascii - try: - realname = realname.encode('us-ascii') - except UnicodeError: - pass - # Create the UserDesc record and do a non-approved subscription - listowner = mlist.GetOwnerEmail() - userdesc = UserDesc(address, realname, password, digest) - remote = res.msg.get_sender() - try: - mlist.AddMember(userdesc, remote) - except Errors.MembershipIsBanned: - res.results.append(_("""\ -The email address you supplied is banned from this mailing list. -If you think this restriction is erroneous, please contact the list -owners at %(listowner)s.""")) - return STOP - except Errors.InvalidEmailAddress: - res.results.append(_("""\ -Mailman won't accept the given email address as a valid address.""")) - return STOP - except Errors.MMAlreadyAMember: - res.results.append(_('You are already subscribed!')) - return STOP - except Errors.MMCantDigestError: - res.results.append( - _('No one can subscribe to the digest of this list!')) - return STOP - except Errors.MMMustDigestError: - res.results.append(_('This list only supports digest subscriptions!')) - return STOP - except Errors.MMSubscribeNeedsConfirmation: - # We don't need to respond /and/ send a confirmation message. - res.respond = 0 - except Errors.MMNeedApproval: - res.results.append(_("""\ -Your subscription request has been forwarded to the list administrator -at %(listowner)s for review.""")) - else: - # Everything is a-ok - res.results.append(_('Subscription request succeeded.')) diff --git a/mailman/commands/docs/echo.txt b/mailman/commands/docs/echo.txt new file mode 100644 index 000000000..d2781d330 --- /dev/null +++ b/mailman/commands/docs/echo.txt @@ -0,0 +1,31 @@ +The 'echo' command +================== + +The mail command 'echo' simply replies with the original command and arguments +to the sender. + + >>> from mailman.configuration import config + >>> command = config.commands['echo'] + >>> command.name + 'echo' + >>> command.argument_description + '[args]' + >>> command.description + u'Echo an acknowledgement. Arguments are return unchanged.' + +The original message is ignored, but the results receive the echoed command. + + >>> from mailman.app.lifecycle import create_list + >>> mlist = create_list(u'test@example.com') + + >>> from mailman.queue.command import Results + >>> results = Results() + + >>> from mailman.Message import Message + >>> print command.process(mlist, Message(), {}, ('foo', 'bar'), results) + ContinueProcessing.yes + >>> print unicode(results) + The results of your email command are provided below. + <BLANKLINE> + echo foo bar + <BLANKLINE> diff --git a/mailman/commands/docs/end.txt b/mailman/commands/docs/end.txt new file mode 100644 index 000000000..bd632de48 --- /dev/null +++ b/mailman/commands/docs/end.txt @@ -0,0 +1,38 @@ +The 'end' command +================= + +The mail command processor recognized an 'end' command which tells it to stop +processing email messages. + + >>> from mailman.configuration import config + >>> command = config.commands['end'] + >>> command.name + 'end' + >>> command.description + u'Stop processing commands.' + +The 'end' command takes no arguments. + + >>> command.argument_description + '' + +The command itself is fairly simple; it just stops command processing, and the +message isn't even looked at. + + >>> from mailman.app.lifecycle import create_list + >>> mlist = create_list(u'test@example.com') + >>> from mailman.Message import Message + >>> print command.process(mlist, Message(), {}, (), None) + ContinueProcessing.no + +The 'stop' command is a synonym for 'end'. + + >>> command = config.commands['stop'] + >>> command.name + 'stop' + >>> command.description + u'Stop processing commands.' + >>> command.argument_description + '' + >>> print command.process(mlist, Message(), {}, (), None) + ContinueProcessing.no diff --git a/mailman/commands/docs/join.txt b/mailman/commands/docs/join.txt new file mode 100644 index 000000000..d9163fbb6 --- /dev/null +++ b/mailman/commands/docs/join.txt @@ -0,0 +1,80 @@ +The 'join' command +================== + +The mail command 'join' subscribes an email address to the mailing list. +'subscribe' is an alias for 'join'. + + >>> from mailman.configuration import config + >>> command = config.commands['join'] + >>> print command.name + join + >>> print command.description + Join this mailing list. You will be asked to confirm your subscription + request and you may be issued a provisional password. + <BLANKLINE> + By using the 'digest' option, you can specify whether you want digest + delivery or not. If not specified, the mailing list's default will be + used. You can also subscribe an alternative address by using the + 'address' option. For example: + <BLANKLINE> + join address=myotheraddress@example.com + <BLANKLINE> + >>> print command.argument_description + [digest=<yes|no>] [address=<address>] + + +No address to join +------------------ + + >>> from mailman.Message import Message + >>> from mailman.app.lifecycle import create_list + >>> from mailman.queue.command import Results + >>> mlist = create_list(u'test@example.com') + +When no address argument is given, the message's From address will be used. +If that's missing though, then an error is returned. + + >>> results = Results() + >>> print command.process(mlist, Message(), {}, (), results) + ContinueProcessing.no + >>> print unicode(results) + The results of your email command are provided below. + <BLANKLINE> + join: No valid address found to subscribe + <BLANKLINE> + +The 'subscribe' command is an alias. + + >>> subscribe = config.commands['subscribe'] + >>> print subscribe.name + subscribe + >>> results = Results() + >>> print subscribe.process(mlist, Message(), {}, (), results) + ContinueProcessing.no + >>> print unicode(results) + The results of your email command are provided below. + <BLANKLINE> + subscribe: No valid address found to subscribe + <BLANKLINE> + + +Joining the sender +------------------ + +When the message has a From field, that address will be subscribed. + + >>> msg = message_from_string("""\ + ... From: Anne Person <anne@example.com> + ... + ... """) + >>> results = Results() + >>> print command.process(mlist, msg, {}, (), results) + ContinueProcessing.yes + >>> print unicode(results) + XXX + +Anne is not yet a member because she must confirm her subscription request +first. Mailman has sent her the confirmation message. + + >>> for message in smtpd.messages: + ... print message.as_string() diff --git a/mailman/commands/echo.py b/mailman/commands/echo.py index d95e72aa1..547f6a9b2 100644 --- a/mailman/commands/echo.py +++ b/mailman/commands/echo.py @@ -25,7 +25,7 @@ __all__ = [ from zope.interface import implements from mailman.i18n import _ -from mailman.interfaces import IEmailCommand +from mailman.interfaces import ContinueProcessing, IEmailCommand SPACE = ' ' @@ -44,4 +44,4 @@ class Echo: def process(self, mlist, msg, msgdata, arguments, results): """See `IEmailCommand`.""" print >> results, 'echo', SPACE.join(arguments) - return True + return ContinueProcessing.yes diff --git a/mailman/commands/cmd_end.py b/mailman/commands/end.py index 81cb15a4a..6e76e1eb2 100644 --- a/mailman/commands/cmd_end.py +++ b/mailman/commands/end.py @@ -14,20 +14,37 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -""" - end - Stop processing commands. Use this if your mail program automatically - adds a signature file. -""" +"""The email commands 'end' and 'stop'.""" + +__metaclass__ = type +__all__ = [ + 'End', + 'Stop', + ] + + +from zope.interface import implements from mailman.i18n import _ +from mailman.interfaces import ContinueProcessing, IEmailCommand -def gethelp(mlist): - return _(__doc__) +class End: + """The email 'end' command.""" + implements(IEmailCommand) + name = 'end' + argument_description = '' + description = _('Stop processing commands.') - -def process(res, args): - return 1 # STOP + def process(self, mlist, msg, msgdata, arguments, results): + """See `IEmailCommand`.""" + # Ignore all arguments. + return ContinueProcessing.no + + +class Stop(End): + """The email 'stop' command (an alias for 'end').""" + + name = 'stop' diff --git a/mailman/commands/join.py b/mailman/commands/join.py new file mode 100644 index 000000000..bfc3435e7 --- /dev/null +++ b/mailman/commands/join.py @@ -0,0 +1,193 @@ +# Copyright (C) 2002-2008 by the Free Software Foundation, Inc. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +"""The email commands 'join' and 'subscribe'.""" + +__metaclass__ = type +__all__ = [ + 'Join', + 'Subscribe', + ] + + +from email.header import decode_header, make_header +from email.utils import parseaddr +from zope.interface import implements + +from mailman.Utils import MakeRandomPassword +from mailman.configuration import config +from mailman.i18n import _ +from mailman.interfaces import ( + ContinueProcessing, DeliveryMode, IEmailCommand) + + + +class Join: + """The email 'join' command.""" + implements(IEmailCommand) + + name = 'join' + argument_description = '[digest=<yes|no>] [address=<address>]' + description = _("""\ +Join this mailing list. You will be asked to confirm your subscription +request and you may be issued a provisional password. + +By using the 'digest' option, you can specify whether you want digest delivery +or not. If not specified, the mailing list's default will be used. You can +also subscribe an alternative address by using the 'address' option. For +example: + + join address=myotheraddress@example.com +""") + + def process(self, mlist, msg, msgdata, arguments, results): + """See `IEmailCommand`.""" + # Parse the arguments. + address, delivery_mmode = self._parse_arguments(arguments) + if address is None: + realname, address = parseaddr(msg['from']) + # Address could be None or the empty string. + if not address: + address = msg.get_sender() + if not address: + print >> results, _( + '$self.name: No valid address found to subscribe') + return ContinueProcessing.no + password = MakeRandomPassword() + try: + validate_subscription(mlist, address) + confirm_subscription(mlist, address, realname, password, + delivery_mode, mlist_preferred_language) + except XXX: + pass + print >> results, self.name, address, \ + (_('digest delivery') if digest else _('regular delivery')) + return ContinueProcessing.yes + + def _parse_arguments(self, arguments): + """Parse command arguments. + + :param arguments: The sequences of arguments as given to the + `process()` method. + :return: address, delivery_mode + """ + address = None + delivery_mode = None + for argument in arguments: + parts = argument.split('=', 1) + if parts[0].lower() == 'digest': + if digest is not None: + print >> results, self.name, \ + _('duplicate argument: $argument') + return ContinueProcessing.no + if len(parts) == 0: + # We treat just plain 'digest' as 'digest=yes'. We don't + # yet support the other types of digest delivery. + delivery_mode = DeliveryMode.mime_digests + else: + if parts[1].lower() == 'yes': + delivery_mode = DeliveryMode.mime_digests + elif parts[1].lower() == 'no': + delivery_mode = DeliveryMode.regular + else: + print >> results, self.name, \ + _('bad argument: $argument') + return ContinueProcessing.no + elif parts[0].lower() == 'address': + if address is not None: + print >> results, self.name, \ + _('duplicate argument $argument') + return ContinueProcessing.no + if len(parts) == 0: + print >> results, self.name, \ + _('missing argument value: $argument') + return ContinueProcessing.no + if len(parts) > 1: + print >> results, self.name, \ + _('too many argument values: $argument') + return ContinueProcessing.no + address = parts[1] + return address, delivery_mode + +def ignore(): + # Fill in empty defaults + if digest is None: + digest = mlist.digest_is_default + if password is None: + password = Utils.MakeRandomPassword() + if address is None: + realname, address = parseaddr(res.msg['from']) + if not address: + # Fall back to the sender address + address = res.msg.get_sender() + if not address: + res.results.append(_('No valid address found to subscribe')) + return STOP + # Watch for encoded names + try: + h = make_header(decode_header(realname)) + # BAW: in Python 2.2, use just unicode(h) + realname = h.__unicode__() + except UnicodeError: + realname = u'' + # Coerce to byte string if uh contains only ascii + try: + realname = realname.encode('us-ascii') + except UnicodeError: + pass + # Create the UserDesc record and do a non-approved subscription + listowner = mlist.GetOwnerEmail() + userdesc = UserDesc(address, realname, password, digest) + remote = res.msg.get_sender() + try: + mlist.AddMember(userdesc, remote) + except Errors.MembershipIsBanned: + res.results.append(_("""\ +The email address you supplied is banned from this mailing list. +If you think this restriction is erroneous, please contact the list +owners at %(listowner)s.""")) + return STOP + except Errors.InvalidEmailAddress: + res.results.append(_("""\ +Mailman won't accept the given email address as a valid address.""")) + return STOP + except Errors.MMAlreadyAMember: + res.results.append(_('You are already subscribed!')) + return STOP + except Errors.MMCantDigestError: + res.results.append( + _('No one can subscribe to the digest of this list!')) + return STOP + except Errors.MMMustDigestError: + res.results.append(_('This list only supports digest subscriptions!')) + return STOP + except Errors.MMSubscribeNeedsConfirmation: + # We don't need to respond /and/ send a confirmation message. + res.respond = 0 + except Errors.MMNeedApproval: + res.results.append(_("""\ +Your subscription request has been forwarded to the list administrator +at %(listowner)s for review.""")) + else: + # Everything is a-ok + res.results.append(_('Subscription request succeeded.')) + + + +class Subscribe(Join): + """The email 'subscribe' command (an alias for 'join').""" + + name = 'subscribe' diff --git a/mailman/interfaces/command.py b/mailman/interfaces/command.py index 553dcb0e3..5e294fe08 100644 --- a/mailman/interfaces/command.py +++ b/mailman/interfaces/command.py @@ -17,10 +17,18 @@ """Interfaces defining email commands.""" +from munepy import Enum from zope.interface import Interface, Attribute +class ContinueProcessing(Enum): + """Should `IEmailCommand.process()` continue or not.""" + no = 0 + yes = 1 + + + class IEmailResults(Interface): """The email command results object.""" @@ -45,6 +53,6 @@ class IEmailCommand(Interface): :param msgdata: The message metadata. :param arguments: The command arguments tuple. :param results: An IEmailResults object for these commands. - :return: True if further processing should be taken of the email - commands in this message. + :return: A `ContinueProcessing` enum specifying whether to continue + processing or not. """ diff --git a/mailman/queue/command.py b/mailman/queue/command.py index e9009809e..9c547184c 100644 --- a/mailman/queue/command.py +++ b/mailman/queue/command.py @@ -44,7 +44,7 @@ from mailman import Utils from mailman.app.replybot import autorespond_to_sender from mailman.configuration import config from mailman.i18n import _ -from mailman.interfaces import IEmailResults +from mailman.interfaces import ContinueProcessing, IEmailResults from mailman.queue import Runner NL = '\n' @@ -179,7 +179,12 @@ class CommandRunner(Runner): if command is None: print >> results, _('No such command: $command_name') else: - command.process(mlist, msg, msgdata, arguments, results) + status = command.process( + mlist, msg, msgdata, arguments, results) + assert status in ContinueProcessing, ( + 'Invalid status: %s' % status) + if status == ContinueProcessing.no: + break # All done, send the response. if len(finder.command_lines) > 0: print >> results, _('\n- Unprocessed:') diff --git a/mailman/queue/docs/command.txt b/mailman/queue/docs/command.txt index c18e7a34c..470a632b7 100644 --- a/mailman/queue/docs/command.txt +++ b/mailman/queue/docs/command.txt @@ -104,3 +104,68 @@ message is plain text. <BLANKLINE> - Done. <BLANKLINE> + + +Stopping command processing +--------------------------- + +The 'end' command stops email processing, so that nothing following is looked +at by the command queue. + + >>> msg = message_from_string("""\ + ... From: cperson@example.com + ... To: test-request@example.com + ... Message-ID: <caribou> + ... + ... echo foo bar + ... end ignored + ... echo baz qux + ... """) + + >>> inject_message(mlist, msg, qdir=config.CMDQUEUE_DIR) + >>> command.run() + >>> len(virgin_queue.files) + 1 + >>> item = get_queue_messages(virgin_queue)[0] + >>> print item.msg.as_string() + Subject: The results of your email commands + ... + <BLANKLINE> + - Results: + echo foo bar + <BLANKLINE> + - Unprocessed: + echo baz qux + <BLANKLINE> + - Done. + <BLANKLINE> + +The 'stop' command is an alias for 'end'. + + >>> msg = message_from_string("""\ + ... From: cperson@example.com + ... To: test-request@example.com + ... Message-ID: <caribou> + ... + ... echo foo bar + ... stop ignored + ... echo baz qux + ... """) + + >>> inject_message(mlist, msg, qdir=config.CMDQUEUE_DIR) + >>> command.run() + >>> len(virgin_queue.files) + 1 + >>> item = get_queue_messages(virgin_queue)[0] + >>> print item.msg.as_string() + Subject: The results of your email commands + ... + <BLANKLINE> + - Results: + echo foo bar + <BLANKLINE> + - Unprocessed: + echo baz qux + <BLANKLINE> + - Done. + <BLANKLINE> diff --git a/mailman/queue/docs/outgoing.txt b/mailman/queue/docs/outgoing.txt index cfb6c6988..edc806d47 100644 --- a/mailman/queue/docs/outgoing.txt +++ b/mailman/queue/docs/outgoing.txt @@ -17,14 +17,11 @@ move messages to the 'retry queue' for handling delivery failures. >>> from mailman.app.membership import add_member >>> from mailman.interfaces import DeliveryMode >>> add_member(mlist, u'aperson@example.com', u'Anne Person', - ... u'password', DeliveryMode.regular, u'en', - ... ack=False, admin_notif=False) + ... u'password', DeliveryMode.regular, u'en') >>> add_member(mlist, u'bperson@example.com', u'Bart Person', - ... u'password', DeliveryMode.regular, u'en', - ... ack=False, admin_notif=False) + ... u'password', DeliveryMode.regular, u'en') >>> add_member(mlist, u'cperson@example.com', u'Cris Person', - ... u'password', DeliveryMode.regular, u'en', - ... ack=False, admin_notif=False) + ... u'password', DeliveryMode.regular, u'en') By setting the mailing list to personalize messages, each recipient will get a unique copy of the message, with certain headers tailored for that recipient. |
