summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mailman/app/membership.py96
-rw-r--r--mailman/app/moderator.py17
-rw-r--r--mailman/app/notifications.py134
-rw-r--r--mailman/app/registrar.py49
-rw-r--r--mailman/bin/set_members.py6
-rw-r--r--mailman/bin/testall.py3
-rw-r--r--mailman/commands/__init__.py2
-rw-r--r--mailman/commands/cmd_join.py20
-rw-r--r--mailman/commands/cmd_subscribe.py133
-rw-r--r--mailman/commands/docs/echo.txt31
-rw-r--r--mailman/commands/docs/end.txt38
-rw-r--r--mailman/commands/docs/join.txt172
-rw-r--r--mailman/commands/echo.py4
-rw-r--r--mailman/commands/end.py (renamed from mailman/commands/cmd_end.py)37
-rw-r--r--mailman/commands/join.py127
-rw-r--r--mailman/docs/registration.txt59
-rw-r--r--mailman/initialize.py34
-rw-r--r--mailman/interfaces/command.py12
-rw-r--r--mailman/interfaces/registrar.py2
-rw-r--r--mailman/queue/command.py9
-rw-r--r--mailman/queue/docs/command.txt65
-rw-r--r--mailman/queue/docs/outgoing.txt9
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.