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/app/registrar.py | 49 | ||||
| -rw-r--r-- | mailman/bin/set_members.py | 6 | ||||
| -rw-r--r-- | mailman/bin/testall.py | 3 | ||||
| -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 | 172 | ||||
| -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 | 127 | ||||
| -rw-r--r-- | mailman/docs/registration.txt | 59 | ||||
| -rw-r--r-- | mailman/initialize.py | 34 | ||||
| -rw-r--r-- | mailman/interfaces/command.py | 12 | ||||
| -rw-r--r-- | mailman/interfaces/registrar.py | 2 | ||||
| -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 |
22 files changed, 746 insertions, 313 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/app/registrar.py b/mailman/app/registrar.py index 60fbd5f07..692e25748 100644 --- a/mailman/app/registrar.py +++ b/mailman/app/registrar.py @@ -20,6 +20,7 @@ __metaclass__ = type __all__ = [ 'Registrar', + 'adapt_domain_to_registrar', ] @@ -27,13 +28,13 @@ import datetime import pkg_resources from zope.interface import implements -from zope.interface.interface import adapter_hooks from mailman.Message import UserNotification from mailman.Utils import ValidateEmail from mailman.configuration import config from mailman.i18n import _ from mailman.interfaces import IDomain, IPendable, IRegistrar +from mailman.interfaces.member import MemberRole @@ -49,29 +50,18 @@ class Registrar: def __init__(self, context): self._context = context - def register(self, address, real_name=None): + def register(self, address, real_name=None, mlist=None): """See `IUserRegistrar`.""" # First, do validation on the email address. If the address is # invalid, it will raise an exception, otherwise it just returns. ValidateEmail(address) - # Check to see if there is already a verified IAddress in the database - # matching this address. If so, there's nothing to do. - usermgr = config.db.user_manager - addr = usermgr.get_address(address) - if addr and addr.verified_on: - # Before returning, see if this address is linked to a user. If - # not, create one and link it now since no future verification - # will be done. - user = usermgr.get_user(address) - if user is None: - user = usermgr.create_user() - user.real_name = (real_name if real_name else addr.real_name) - user.link(addr) - return None - # Calculate the token for this confirmation record. - pendable = PendableRegistration(type=PendableRegistration.PEND_KEY, - address=address, - real_name=real_name) + # Create a pendable for the registration. + pendable = PendableRegistration( + type=PendableRegistration.PEND_KEY, + address=address, + real_name=real_name) + if mlist is not None: + pendable['list_name'] = mlist.fqdn_listname token = config.db.pendings.add(pendable) # Set up some local variables for translation interpolation. domain = IDomain(self._context) @@ -100,8 +90,8 @@ class Registrar: missing = object() address = pendable.get('address', missing) real_name = pendable.get('real_name', missing) - if (pendable.get('type') <> PendableRegistration.PEND_KEY or - address is missing or real_name is missing): + list_name = pendable.get('list_name', missing) + if pendable.get('type') != PendableRegistration.PEND_KEY: # It seems like it would be very difficult to accurately guess # tokens, or brute force an attack on the SHA1 hash, so we'll just # throw the pendable away in that case. It's possible we'll need @@ -112,8 +102,10 @@ class Registrar: # and an IUser linked to this IAddress. See if any of these objects # currently exist in our database. usermgr = config.db.user_manager - addr = usermgr.get_address(address) - user = usermgr.get_user(address) + addr = (usermgr.get_address(address) + if address is not missing else None) + user = (usermgr.get_user(address) + if address is not missing else None) # If there is neither an address nor a user matching the confirmed # record, then create the user, which will in turn create the address # and link the two together @@ -138,6 +130,13 @@ class Registrar: # do is verify the address. pass addr.verified_on = datetime.datetime.now() + # If this registration is tied to a mailing list, subscribe the person + # to the list right now. + list_name = pendable.get('list_name') + if list_name is not None: + mlist = config.db.list_manager.get(list_name) + if mlist: + addr.subscribe(mlist, MemberRole.member) return True def discard(self, token): @@ -159,5 +158,3 @@ def adapt_domain_to_registrar(iface, obj): return (Registrar(obj) if IDomain.providedBy(obj) and iface is IRegistrar else None) - -adapter_hooks.append(adapt_domain_to_registrar) 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/bin/testall.py b/mailman/bin/testall.py index ed90980ae..8ecc6aedf 100644 --- a/mailman/bin/testall.py +++ b/mailman/bin/testall.py @@ -34,7 +34,7 @@ import pkg_resources from mailman import Defaults from mailman.configuration import config from mailman.i18n import _ -from mailman.initialize import initialize_1, initialize_2 +from mailman.initialize import initialize_1, initialize_2, initialize_3 from mailman.testing.helpers import SMTPServer from mailman.version import MAILMAN_VERSION @@ -261,6 +261,7 @@ def main(): # With -vvv, turn on engine debugging. initialize_2(parser.options.verbosity > 3) + initialize_3() # Run the tests. XXX I'm not sure if basedir can be converted to # pkg_resources. 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..492297787 --- /dev/null +++ b/mailman/commands/docs/join.txt @@ -0,0 +1,172 @@ +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'alpha@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) + The results of your email command are provided below. + <BLANKLINE> + Confirmation email sent to Anne Person <anne@example.com> + <BLANKLINE> + +Anne is not yet a member because she must confirm her subscription request +first. + + >>> print config.db.user_manager.get_user(u'anne@example.com') + None + +Mailman has sent her the confirmation message. + + >>> from mailman.queue import Switchboard + >>> virginq = Switchboard(config.VIRGINQUEUE_DIR) + >>> qmsg, qdata = virginq.dequeue(virginq.files[0]) + >>> print qmsg.as_string() + MIME-Version: 1.0 + ... + Subject: confirm ... + From: confirm-...@example.com + To: anne@example.com + ... + <BLANKLINE> + Email Address Registration Confirmation + <BLANKLINE> + Hello, this is the GNU Mailman server at example.com. + <BLANKLINE> + We have received a registration request for the email address + <BLANKLINE> + anne@example.com + <BLANKLINE> + Before you can start using GNU Mailman at this site, you must first + confirm that this is your email address. You can do this by replying to + this message, keeping the Subject header intact. Or you can visit this + web page + <BLANKLINE> + http://www.example.com/confirm/... + <BLANKLINE> + If you do not wish to register this email address simply disregard this + message. If you think you are being maliciously subscribed to the list, or + have any other questions, you may contact + <BLANKLINE> + postmaster@example.com + <BLANKLINE> + +Once Anne confirms her registration, she will be made a member of the mailing +list. + + >>> token = str(qmsg['subject']).split()[1].strip() + >>> from mailman.interfaces.registrar import IRegistrar + >>> registrar = IRegistrar(config.domains['example.com']) + >>> registrar.confirm(token) + True + + >>> user = config.db.user_manager.get_user(u'anne@example.com') + >>> print user.real_name + Anne Person + >>> list(user.addresses) + [<Address: Anne Person <anne@example.com> [verified] at ...>] + +Anne is also now a member of the mailing list. + + >>> mlist.members.get_member(u'anne@example.com') + <Member: Anne Person <anne@example.com> + on alpha@example.com as MemberRole.member> + + +Joining a second list +--------------------- + + >>> mlist_2 = create_list(u'baker@example.com') + >>> msg = message_from_string("""\ + ... From: Anne Person <anne@example.com> + ... + ... """) + >>> print command.process(mlist_2, msg, {}, (), Results()) + ContinueProcessing.yes + +Anne of course, is still registered. + + >>> print config.db.user_manager.get_user(u'anne@example.com') + <User "Anne Person" at ...> + +But she is not a member of the mailing list. + + >>> print mlist_2.members.get_member(u'anne@example.com') + None + +One Anne confirms this subscription, she becomes a member of the mailing list. + + >>> qmsg, qdata = virginq.dequeue(virginq.files[0]) + >>> token = str(qmsg['subject']).split()[1].strip() + >>> registrar.confirm(token) + True + + >>> print mlist_2.members.get_member(u'anne@example.com') + <Member: Anne Person <anne@example.com> + on baker@example.com as MemberRole.member> 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..639a74a99 --- /dev/null +++ b/mailman/commands/join.py @@ -0,0 +1,127 @@ +# 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 formataddr, 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) +from mailman.interfaces.registrar import IRegistrar + + + +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_mode = self._parse_arguments(arguments) + if address is None: + real_name, 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 + domain = config.domains[mlist.host_name] + registrar = IRegistrar(domain) + registrar.register(address, real_name, mlist) + person = formataddr((real_name, address)) + print >> results, _('Confirmation email sent to $person') + 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 + + + +class Subscribe(Join): + """The email 'subscribe' command (an alias for 'join').""" + + name = 'subscribe' diff --git a/mailman/docs/registration.txt b/mailman/docs/registration.txt index 2e3ef23e5..73fb149e6 100644 --- a/mailman/docs/registration.txt +++ b/mailman/docs/registration.txt @@ -242,27 +242,12 @@ confirmation step is completed. >>> usermgr.get_address(u'cperson@example.com') <Address: cperson@example.com [verified] at ...> -If an address being registered has already been verified, linked or not to a -user, then registration sends no confirmation. +Even if the address being registered has already been verified, the +registration sends a confirmation. - >>> print registrar.register(u'cperson@example.com') - None - >>> len(switchboard.files) - 0 - -But if the already verified address is not linked to a user, then a user is -created now and they are linked, with no confirmation necessary. - - >>> address = usermgr.create_address(u'dperson@example.com', u'Dave Person') - >>> address.verified_on = datetime.now() - >>> print usermgr.get_user(u'dperson@example.com') - None - >>> print registrar.register(u'dperson@example.com') - None - >>> len(switchboard.files) - 0 - >>> usermgr.get_user(u'dperson@example.com') - <User "Dave Person" at ...> + >>> token = registrar.register(u'cperson@example.com') + >>> token is not None + True Discarding @@ -290,9 +275,12 @@ When a new address for an existing user is registered, there isn't too much different except that the new address will still need to be verified before it can be used. - >>> dperson = usermgr.get_user(u'dperson@example.com') + >>> dperson = usermgr.create_user(u'dperson@example.com', u'Dave Person') >>> dperson <User "Dave Person" at ...> + >>> address = usermgr.get_address(u'dperson@example.com') + >>> address.verified_on = datetime.now() + >>> from operator import attrgetter >>> sorted((addr for addr in dperson.addresses), key=attrgetter('address')) [<Address: Dave Person <dperson@example.com> [verified] at ...>] @@ -339,12 +327,27 @@ pending even matched with that token will still be removed. >>> print pendingdb.confirm(token) None -If somehow the pending registration event doesn't have an address in its -record, you will also get None back, and the record will be removed. - >>> pendable = SimplePendable(type='registration', foo='bar') - >>> token = pendingdb.add(pendable) - >>> registrar.confirm(token) - False - >>> print pendingdb.confirm(token) +Registration and subscription +----------------------------- + +Fred registers with Mailman at the same time that he subscribes to a mailing +list. + + >>> from mailman.app.lifecycle import create_list + >>> mlist = create_list(u'alpha@example.com') + >>> token = registrar.register( + ... u'fred.person@example.com', 'Fred Person', mlist) + +Before confirmation, Fred is not a member of the mailing list. + + >>> print mlist.members.get_member(u'fred.person@example.com') None + +But after confirmation, he is. + + >>> registrar.confirm(token) + True + >>> print mlist.members.get_member(u'fred.person@example.com') + <Member: Fred Person <fred.person@example.com> + on alpha@example.com as MemberRole.member> diff --git a/mailman/initialize.py b/mailman/initialize.py index d8dc0d69d..3f098b67f 100644 --- a/mailman/initialize.py +++ b/mailman/initialize.py @@ -26,6 +26,7 @@ by the command line arguments. import os +from zope.interface.interface import adapter_hooks from zope.interface.verify import verifyObject import mailman.configuration @@ -42,6 +43,17 @@ from mailman.interfaces import IDatabase # code will just call initialize(). def initialize_1(config_path, propagate_logs): + """First initialization step. + + * The configuration system + * Run-time directories + * The logging subsystem + + :param config_path: The path to the configuration file. + :type config_path: string + :param propagate_logs: Should the log output propagate to stderr? + :type propagate_logs: boolean + """ # By default, set the umask so that only owner and group can read and # write our files. Specifically we must have g+rw and we probably want # o-rwx although I think in most cases it doesn't hurt if other can read @@ -56,6 +68,17 @@ def initialize_1(config_path, propagate_logs): def initialize_2(debug=False): + """Second initialization step. + + * Archivers + * Rules + * Chains + * Pipelines + * Commands + + :param debug: Should the database layer be put in debug mode? + :type debug: boolean + """ database_plugin = get_plugin('mailman.database') # Instantiate the database plugin, ensure that it's of the right type, and # initialize it. Then stash the object on our configuration object. @@ -77,6 +100,17 @@ def initialize_2(debug=False): initialize_commands() +def initialize_3(): + """Third initialization step. + + * Adapters + """ + from mailman.app.registrar import adapt_domain_to_registrar + adapter_hooks.append(adapt_domain_to_registrar) + + + def initialize(config_path=None, propagate_logs=False): initialize_1(config_path, propagate_logs) initialize_2() + initialize_3() 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/interfaces/registrar.py b/mailman/interfaces/registrar.py index bfa02e04d..b7807dac6 100644 --- a/mailman/interfaces/registrar.py +++ b/mailman/interfaces/registrar.py @@ -34,7 +34,7 @@ class IRegistrar(Interface): syntax checking, or confirmation, while this interface does. """ - def register(address, real_name=None): + def register(address, real_name=None, mlist=None): """Register the email address, requesting verification. No IAddress or IUser is created during this step, but after successful 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. |
