summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mailman/commands/docs/control.txt4
-rw-r--r--src/mailman/commands/eml_confirm.py47
-rw-r--r--src/mailman/commands/eml_join.py28
-rw-r--r--src/mailman/queue/command.py7
-rw-r--r--src/mailman/queue/docs/command.txt99
-rw-r--r--src/mailman/queue/docs/lmtp.txt206
-rw-r--r--src/mailman/queue/lmtp.py82
7 files changed, 435 insertions, 38 deletions
diff --git a/src/mailman/commands/docs/control.txt b/src/mailman/commands/docs/control.txt
index bd1e09324..7bfaf9265 100644
--- a/src/mailman/commands/docs/control.txt
+++ b/src/mailman/commands/docs/control.txt
@@ -63,7 +63,7 @@ Starting the daemons prints a useful message and starts the master qrunner
watcher process in the background.
>>> start.process(args)
- Starting Mailman's master qrunner
+ Starting Mailman's master queue runner
>>> import errno, os, time
>>> from datetime import timedelta, datetime
@@ -104,7 +104,7 @@ stops all the child processes too.
>>> from mailman.commands.cli_control import Stop
>>> stop = Stop()
>>> stop.process(args)
- Shutting down Mailman's master qrunner
+ Shutting down Mailman's master queue runner
>>> def bury_master():
... until = timedelta(seconds=10) + datetime.now()
diff --git a/src/mailman/commands/eml_confirm.py b/src/mailman/commands/eml_confirm.py
new file mode 100644
index 000000000..0c957b295
--- /dev/null
+++ b/src/mailman/commands/eml_confirm.py
@@ -0,0 +1,47 @@
+# Copyright (C) 2009 by the Free Software Foundation, Inc.
+#
+# This file is part of GNU Mailman.
+#
+# GNU Mailman 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 3 of the License, or (at your option)
+# any later version.
+#
+# GNU Mailman 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
+# GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
+
+"""Module stuff."""
+
+from __future__ import absolute_import, unicode_literals
+
+__metaclass__ = type
+__all__ = [
+ 'Confirm',
+ ]
+
+
+from zope.interface import implements
+
+from mailman.core.i18n import _
+from mailman.interfaces.command import ContinueProcessing, IEmailCommand
+
+
+
+class Confirm:
+ """The email 'confirm' command."""
+
+ implements(IEmailCommand)
+
+ name = 'confirm'
+ argument_description = ''
+ description = ''
+
+ def process(self, mlist, msg, msgdata, arguments, results):
+ """See `IEmailCommand`."""
+ print >> results, _('Confirmed')
+ return ContinueProcessing.yes
diff --git a/src/mailman/commands/eml_join.py b/src/mailman/commands/eml_join.py
index f29684b93..4e3f6edb9 100644
--- a/src/mailman/commands/eml_join.py
+++ b/src/mailman/commands/eml_join.py
@@ -17,10 +17,14 @@
"""The email commands 'join' and 'subscribe'."""
+from __future__ import absolute_import, unicode_literals
+
__metaclass__ = type
__all__ = [
'Join',
'Subscribe',
+ 'Leave',
+ 'Unsubscribe',
]
@@ -38,6 +42,7 @@ from mailman.interfaces.registrar import IRegistrar
class Join:
"""The email 'join' command."""
+
implements(IEmailCommand)
name = 'join'
@@ -125,3 +130,26 @@ class Subscribe(Join):
"""The email 'subscribe' command (an alias for 'join')."""
name = 'subscribe'
+
+
+
+class Leave:
+ """The email 'leave' command."""
+
+ implements(IEmailCommand)
+
+ name = 'leave'
+ argument_description = ''
+ description = ''
+
+ def process(self, mlist, msg, msgdata, arguments, results):
+ """See `IEmailCommand`."""
+ person = msg['from']
+ print >> results, _('$person left $mlist.fqdn_listname')
+ return ContinueProcessing.yes
+
+
+class Unsubscribe(Leave):
+ """The email 'unsubscribe' command (an alias for 'leave')."""
+
+ name = 'unsubscribe'
diff --git a/src/mailman/queue/command.py b/src/mailman/queue/command.py
index 178d2ba0b..f36f9a31d 100644
--- a/src/mailman/queue/command.py
+++ b/src/mailman/queue/command.py
@@ -60,11 +60,12 @@ class CommandFinder:
# commands. For example, if this was sent to the -join or -leave
# addresses, it's the same as if 'join' or 'leave' commands were sent
# to the -request address.
- if msgdata.get('tojoin'):
+ subaddress = msgdata.get('subaddress')
+ if subaddress == 'join':
self.command_lines.append('join')
- elif msgdata.get('toleave'):
+ elif subaddress == 'leave':
self.command_lines.append('leave')
- elif msgdata.get('toconfirm'):
+ elif subaddress == 'confirm':
mo = re.match(config.mta.verp_confirm_regexp, msg.get('to', ''))
if mo:
self.command_lines.append('confirm ' + mo.group('cookie'))
diff --git a/src/mailman/queue/docs/command.txt b/src/mailman/queue/docs/command.txt
index 88e4387d4..fd0236a8c 100644
--- a/src/mailman/queue/docs/command.txt
+++ b/src/mailman/queue/docs/command.txt
@@ -108,6 +108,105 @@ message is plain text.
<BLANKLINE>
+Implicit commands
+=================
+
+For some commands, specifically for joining and leaving a mailing list, there
+are email aliases that act like commands, even when there's nothing else in
+the Subject or body. For example, to join a mailing list, a user need only
+email the -join address or -subscribe address (the latter is deprecated).
+
+Because Dirk has never registered with Mailman before, he gets two responses.
+The first is a confirmation message so that Dirk can validate his email
+address, and the other is the results of his email command.
+
+ >>> msg = message_from_string("""\
+ ... From: Dirk Person <dperson@example.com>
+ ... To: test-join@example.com
+ ...
+ ... """)
+
+ >>> inject_message(mlist, msg, switchboard='command', subaddress='join')
+ >>> command.run()
+ >>> len(virgin_queue.files)
+ 2
+
+ >>> def sortkey(item):
+ ... return item.msg['subject']
+ >>> messages = sorted(get_queue_messages('virgin'), key=sortkey)
+ >>> for item in messages:
+ ... print 'Subject:', item.msg['subject']
+ Subject: confirm ...
+ Subject: The results of your email commands
+
+Similarly, to leave a mailing list, the user need only email the -leave or
+-unsubscribe address (the latter is deprecated).
+
+ >>> msg = message_from_string("""\
+ ... From: dperson@example.com
+ ... To: test-leave@example.com
+ ...
+ ... """)
+
+ >>> inject_message(mlist, msg, switchboard='command', subaddress='leave')
+ >>> command.run()
+ >>> len(virgin_queue.files)
+ 1
+ >>> item = get_queue_messages('virgin')[0]
+ >>> print item.msg.as_string()
+ Subject: The results of your email commands
+ From: test-bounces@example.com
+ To: dperson@example.com
+ ...
+ <BLANKLINE>
+ The results of your email command are provided below.
+ <BLANKLINE>
+ - Original message details:
+ From: dperson@example.com
+ Subject: n/a
+ Date: ...
+ Message-ID: ...
+ <BLANKLINE>
+ - Results:
+ dperson@example.com left test@example.com
+ <BLANKLINE>
+ - Done.
+ <BLANKLINE>
+
+The -confirm address is also available as an implicit command.
+
+ >>> msg = message_from_string("""\
+ ... From: dperson@example.com
+ ... To: test-confirm+123@example.com
+ ...
+ ... """)
+
+ >>> inject_message(mlist, msg, switchboard='command', subaddress='confirm')
+ >>> command.run()
+ >>> len(virgin_queue.files)
+ 1
+ >>> item = get_queue_messages('virgin')[0]
+ >>> print item.msg.as_string()
+ Subject: The results of your email commands
+ From: test-bounces@example.com
+ To: dperson@example.com
+ ...
+ <BLANKLINE>
+ The results of your email command are provided below.
+ <BLANKLINE>
+ - Original message details:
+ From: dperson@example.com
+ Subject: n/a
+ Date: ...
+ Message-ID: ...
+ <BLANKLINE>
+ - Results:
+ Confirmed
+ <BLANKLINE>
+ - Done.
+ <BLANKLINE>
+
+
Stopping command processing
===========================
diff --git a/src/mailman/queue/docs/lmtp.txt b/src/mailman/queue/docs/lmtp.txt
index 5961bea50..549161b08 100644
--- a/src/mailman/queue/docs/lmtp.txt
+++ b/src/mailman/queue/docs/lmtp.txt
@@ -61,6 +61,25 @@ Once the mailing list is created, the posting address is valid.
... """)
{}
+ >>> from mailman.testing.helpers import get_queue_messages
+ >>> messages = get_queue_messages('in')
+ >>> len(messages)
+ 1
+ >>> print messages[0].msg.as_string()
+ From: anne.person@example.com
+ To: mylist@example.com
+ Subject: An interesting message
+ Message-ID: <badger>
+ X-MailFrom: anne.person@example.com
+ <BLANKLINE>
+ This is an interesting message.
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ listname : mylist@example.com
+ original_size: ...
+ to_list : True
+ version : ...
+
Sub-addresses
=============
@@ -98,6 +117,193 @@ But the message is accepted if posted to a valid sub-address.
{}
+Request subaddress
+------------------
+
+Depending on the subaddress, there is a message in the appropriate queue for
+later processing. For example, all -request messages are put into the command
+queue for processing.
+
+ >>> messages = get_queue_messages('command')
+ >>> len(messages)
+ 1
+ >>> print messages[0].msg.as_string()
+ From: anne.person@example.com
+ To: mylist-request@example.com
+ Subject: Help
+ Message-ID: <dog>
+ X-MailFrom: anne.person@example.com
+ <BLANKLINE>
+ Please help me.
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ listname : mylist@example.com
+ original_size: ...
+ subaddress : request
+ version : ...
+
+
+Bounce processor
+----------------
+
+A message to the -bounces address goes to the bounce processor.
+
+ >>> lmtp.sendmail(
+ ... 'mail-daemon@example.com',
+ ... ['mylist-bounces@example.com'], """\
+ ... From: mail-daemon@example.com
+ ... To: mylist-bounces@example.com
+ ... Subject: A bounce
+ ... Message-ID: <elephant>
+ ...
+ ... Bouncy bouncy.
+ ... """)
+ {}
+ >>> messages = get_queue_messages('bounces')
+ >>> len(messages)
+ 1
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ listname : mylist@example.com
+ original_size: ...
+ subaddress : bounces
+ version : ...
+
+
+Command processor
+-----------------
+
+Confirmation messages go to the command processor...
+
+ >>> lmtp.sendmail(
+ ... 'anne.person@example.com',
+ ... ['mylist-confirm@example.com'], """\
+ ... From: anne.person@example.com
+ ... To: mylist-confirm@example.com
+ ... Subject: A bounce
+ ... Message-ID: <falcon>
+ ...
+ ... confirm 123
+ ... """)
+ {}
+ >>> messages = get_queue_messages('command')
+ >>> len(messages)
+ 1
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ listname : mylist@example.com
+ original_size: ...
+ subaddress : confirm
+ version : ...
+
+...as do join messages...
+
+ >>> lmtp.sendmail(
+ ... 'anne.person@example.com',
+ ... ['mylist-join@example.com'], """\
+ ... From: anne.person@example.com
+ ... To: mylist-join@example.com
+ ... Message-ID: <giraffe>
+ ...
+ ... """)
+ {}
+ >>> messages = get_queue_messages('command')
+ >>> len(messages)
+ 1
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ listname : mylist@example.com
+ original_size: ...
+ subaddress : join
+ version : ...
+
+ >>> lmtp.sendmail(
+ ... 'anne.person@example.com',
+ ... ['mylist-subscribe@example.com'], """\
+ ... From: anne.person@example.com
+ ... To: mylist-subscribe@example.com
+ ... Message-ID: <hippopotamus>
+ ...
+ ... """)
+ {}
+ >>> messages = get_queue_messages('command')
+ >>> len(messages)
+ 1
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ listname : mylist@example.com
+ original_size: ...
+ subaddress : join
+ version : ...
+
+...and leave messages.
+
+ >>> lmtp.sendmail(
+ ... 'anne.person@example.com',
+ ... ['mylist-leave@example.com'], """\
+ ... From: anne.person@example.com
+ ... To: mylist-leave@example.com
+ ... Message-ID: <iguana>
+ ...
+ ... """)
+ {}
+ >>> messages = get_queue_messages('command')
+ >>> len(messages)
+ 1
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ listname : mylist@example.com
+ original_size: ...
+ subaddress : leave
+ version : ...
+
+ >>> lmtp.sendmail(
+ ... 'anne.person@example.com',
+ ... ['mylist-unsubscribe@example.com'], """\
+ ... From: anne.person@example.com
+ ... To: mylist-unsubscribe@example.com
+ ... Message-ID: <jackal>
+ ...
+ ... """)
+ {}
+ >>> messages = get_queue_messages('command')
+ >>> len(messages)
+ 1
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ listname : mylist@example.com
+ original_size: ...
+ subaddress : leave
+ version : ...
+
+
+Incoming processor
+------------------
+
+Messages to the -owner address go to the incoming processor.
+
+ >>> lmtp.sendmail(
+ ... 'anne.person@example.com',
+ ... ['mylist-owner@example.com'], """\
+ ... From: anne.person@example.com
+ ... To: mylist-owner@example.com
+ ... Message-ID: <kangaroo>
+ ...
+ ... """)
+ {}
+ >>> messages = get_queue_messages('in')
+ >>> len(messages)
+ 1
+ >>> dump_msgdata(messages[0].msgdata)
+ _parsemsg : False
+ envsender : changeme@example.com
+ listname : mylist@example.com
+ original_size: ...
+ subaddress : owner
+ to_owner : True
+ version : ...
+
+
Clean up
========
diff --git a/src/mailman/queue/lmtp.py b/src/mailman/queue/lmtp.py
index c07594c0e..e93fa6624 100644
--- a/src/mailman/queue/lmtp.py
+++ b/src/mailman/queue/lmtp.py
@@ -50,10 +50,29 @@ qlog = logging.getLogger('mailman.qrunner')
# We only care about the listname and the sub-addresses as in listname@ or
-# listname-request@
-SUBADDRESS_NAMES = (
- 'bounces', 'confirm', 'join', ' leave',
- 'owner', 'request', 'subscribe', 'unsubscribe',
+# listname-request@. This maps user visible subaddress names (which may
+# include aliases) to the internal canonical subaddress name.
+SUBADDRESS_NAMES = dict(
+ admin='bounces',
+ bounces='bounces',
+ confirm='confirm',
+ join='join',
+ leave='leave',
+ owner='owner',
+ request='request',
+ subscribe='join',
+ unsubscribe='leave',
+ )
+
+# This maps subaddress canonical name to the destination queue that handles
+# messages sent to that subaddress.
+SUBADDRESS_QUEUES = dict(
+ bounces='bounces',
+ confirm='command',
+ join='command',
+ leave='command',
+ owner='in',
+ request='command',
)
DASH = '-'
@@ -136,7 +155,6 @@ class LMTPRunner(Runner, smtpd.SMTPServer):
# Refresh the list of list names every time we process a message
# since the set of mailing lists could have changed.
listnames = set(getUtility(IListManager).names)
- qlog.debug('listnames: %s', listnames)
# Parse the message data. If there are any defects in the
# message, reject it right away; it's probably spam.
msg = email.message_from_string(data, Message)
@@ -144,6 +162,7 @@ class LMTPRunner(Runner, smtpd.SMTPServer):
if msg.defects:
return ERR_501
msg['X-MailFrom'] = mailfrom
+ message_id = msg['message-id']
except Exception, e:
elog.exception('LMTP message parsing')
config.db.abort()
@@ -158,48 +177,45 @@ class LMTPRunner(Runner, smtpd.SMTPServer):
try:
to = parseaddr(to)[1].lower()
listname, subaddress, domain = split_recipient(to)
- qlog.debug('to: %s, list: %s, sub: %s, dom: %s',
- to, listname, subaddress, domain)
+ qlog.debug('%s to: %s, list: %s, sub: %s, dom: %s',
+ message_id, to, listname, subaddress, domain)
listname += '@' + domain
if listname not in listnames:
status.append(ERR_550)
continue
- # The recipient is a valid mailing list; see if it's a valid
- # sub-address, and if so, enqueue it.
+ # The recipient is a valid mailing list. Find the subaddress
+ # if there is one, and set things up to enqueue to the proper
+ # queue runner.
queue = None
msgdata = dict(listname=listname,
original_size=msg.original_size)
- if subaddress in ('bounces', 'admin'):
- queue = 'bounce'
- elif subaddress == 'confirm':
- msgdata['to_confirm'] = True
- queue = 'command'
- elif subaddress in ('join', 'subscribe'):
- msgdata['to_join'] = True
- queue = 'command'
- elif subaddress in ('leave', 'unsubscribe'):
- msgdata['to_leave'] = True
- queue = 'command'
- elif subaddress == 'owner':
- msgdata.update(dict(
- to_owner=True,
- envsender=config.mailman.site_owner,
- ))
- queue = 'in'
- elif subaddress is None:
+ canonical_subaddress = SUBADDRESS_NAMES.get(subaddress)
+ queue = SUBADDRESS_QUEUES.get(canonical_subaddress)
+ if subaddress is None:
+ # The message is destined for the mailing list.
msgdata['to_list'] = True
queue = 'in'
- elif subaddress == 'request':
- msgdata['to_request'] = True
- queue = 'command'
- else:
- elog.error('Unknown sub-address: %s', subaddress)
+ elif canonical_subaddress is None:
+ # The subaddress was bogus.
+ elog.error('%s unknown sub-address: %s',
+ message_id, subaddress)
status.append(ERR_550)
continue
- # If we found a valid subaddress, enqueue the message and add
+ else:
+ # A valid subaddress.
+ msgdata['subaddress'] = canonical_subaddress
+ if canonical_subaddress == 'owner':
+ msgdata.update(dict(
+ to_owner=True,
+ envsender=config.mailman.site_owner,
+ ))
+ queue = 'in'
+ # If we found a valid destination, enqueue the message and add
# a success status for this recipient.
if queue is not None:
config.switchboards[queue].enqueue(msg, msgdata)
+ qlog.debug('%s subaddress: %s, queue: %s',
+ message_id, canonical_subaddress, queue)
status.append('250 Ok')
except Exception, e:
elog.exception('Queue detection: %s', msg['message-id'])