Use my lazr.config megamerge branch for now, even though it's still under
[mailman.git] / mailman / queue / lmtp.py
blob18f430e555edcfe0fe9a7ca75e7e4bcaf73b94cc
1 # Copyright (C) 2006-2008 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Mailman LMTP runner (server).
20 Most mail servers can be configured to deliver local messages via 'LMTP'[1].
21 This module is actually an LMTP server rather than a standard queue runner.
23 The LMTP runner opens a local TCP port and waits for the mail server to
24 connect to it. The messages it receives over LMTP are very minimally parsed
25 for sanity and if they look okay, they are accepted and injected into
26 Mailman's incoming queue for normal processing. If they don't look good, or
27 are destined for a bogus sub-address, they are rejected right away, hopefully
28 so that the peer mail server can provide better diagnostics.
30 [1] RFC 2033 Local Mail Transport Protocol
31 http://www.faqs.org/rfcs/rfc2033.html
33 See the variable USE_LMTP in Defaults.py.in for enabling this delivery
34 mechanism.
35 """
37 import os
38 import email
39 import smtpd
40 import logging
41 import asyncore
43 from email.utils import parseaddr
45 from mailman.Message import Message
46 from mailman.config import config
47 from mailman.database.transaction import txn
48 from mailman.queue import Runner, Switchboard
50 elog = logging.getLogger('mailman.error')
51 qlog = logging.getLogger('mailman.qrunner')
54 # We only care about the listname and the sub-addresses as in listname@ or
55 # listname-request@
56 SUBADDRESS_NAMES = (
57 'bounces', 'confirm', 'join', ' leave',
58 'owner', 'request', 'subscribe', 'unsubscribe',
61 DASH = '-'
62 CRLF = '\r\n'
63 ERR_451 = '451 Requested action aborted: error in processing'
64 ERR_501 = '501 Message has defects'
65 ERR_502 = '502 Error: command HELO not implemented'
66 ERR_550 = config.LMTP_ERR_550
68 # XXX Blech
69 smtpd.__version__ = 'Python LMTP queue runner 1.0'
73 def split_recipient(address):
74 """Split an address into listname, subaddress and domain parts.
76 For example:
78 >>> split_recipient('mylist@example.com')
79 ('mylist', None, 'example.com')
81 >>> split_recipient('mylist-request@example.com')
82 ('mylist', 'request', 'example.com')
84 :param address: The destination address.
85 :return: A 3-tuple of the form (list-shortname, subaddress, domain).
86 subaddress may be None if this is the list's posting address.
87 """
88 localpart, domain = address.split('@', 1)
89 localpart = localpart.split(config.VERP_DELIMITER, 1)[0]
90 parts = localpart.split(DASH)
91 if parts[-1] in SUBADDRESS_NAMES:
92 listname = DASH.join(parts[:-1])
93 subaddress = parts[-1]
94 else:
95 listname = localpart
96 subaddress = None
97 return listname, subaddress, domain
101 class Channel(smtpd.SMTPChannel):
102 """An LMTP channel."""
104 def __init__(self, server, conn, addr):
105 smtpd.SMTPChannel.__init__(self, server, conn, addr)
106 # Stash this here since the subclass uses private attributes. :(
107 self._server = server
109 def smtp_LHLO(self, arg):
110 """The LMTP greeting, used instead of HELO/EHLO."""
111 smtpd.SMTPChannel.smtp_HELO(self, arg)
113 def smtp_HELO(self, arg):
114 """HELO is not a valid LMTP command."""
115 self.push(ERR_502)
119 class LMTPRunner(Runner, smtpd.SMTPServer):
120 # Only __init__ is called on startup. Asyncore is responsible for later
121 # connections from the MTA. slice and numslices are ignored and are
122 # necessary only to satisfy the API.
123 def __init__(self, slice=None, numslices=1):
124 localaddr = config.LMTP_HOST, config.LMTP_PORT
125 # Do not call Runner's constructor because there's no QDIR to create
126 smtpd.SMTPServer.__init__(self, localaddr, remoteaddr=None)
127 qlog.debug('LMTP server listening on %s:%s',
128 config.LMTP_HOST, config.LMTP_PORT)
130 def handle_accept(self):
131 conn, addr = self.accept()
132 channel = Channel(self, conn, addr)
133 qlog.debug('LMTP accept from %s', addr)
135 @txn
136 def process_message(self, peer, mailfrom, rcpttos, data):
137 try:
138 # Refresh the list of list names every time we process a message
139 # since the set of mailing lists could have changed.
140 listnames = set(config.db.list_manager.names)
141 # Parse the message data. If there are any defects in the
142 # message, reject it right away; it's probably spam.
143 msg = email.message_from_string(data, Message)
144 if msg.defects:
145 return ERR_501
146 msg['X-MailFrom'] = mailfrom
147 except Exception, e:
148 elog.exception('LMTP message parsing')
149 config.db.abort()
150 return CRLF.join([ERR_451 for to in rcpttos])
151 # RFC 2033 requires us to return a status code for every recipient.
152 status = []
153 # Now for each address in the recipients, parse the address to first
154 # see if it's destined for a valid mailing list. If so, then queue
155 # the message to the appropriate place and record a 250 status for
156 # that recipient. If not, record a failure status for that recipient.
157 for to in rcpttos:
158 try:
159 to = parseaddr(to)[1].lower()
160 listname, subaddress, domain = split_recipient(to)
161 qlog.debug('to: %s, list: %s, sub: %s, dom: %s',
162 to, listname, subaddress, domain)
163 listname += '@' + domain
164 if listname not in listnames:
165 status.append(ERR_550)
166 continue
167 # The recipient is a valid mailing list; see if it's a valid
168 # sub-address, and if so, enqueue it.
169 queue = None
170 msgdata = dict(listname=listname)
171 if subaddress in ('bounces', 'admin'):
172 queue = Switchboard(config.BOUNCEQUEUE_DIR)
173 elif subaddress == 'confirm':
174 msgdata['toconfirm'] = True
175 queue = Switchboard(config.CMDQUEUE_DIR)
176 elif subaddress in ('join', 'subscribe'):
177 msgdata['tojoin'] = True
178 queue = Switchboard(config.CMDQUEUE_DIR)
179 elif subaddress in ('leave', 'unsubscribe'):
180 msgdata['toleave'] = True
181 queue = Switchboard(config.CMDQUEUE_DIR)
182 elif subaddress == 'owner':
183 msgdata.update(dict(
184 toowner=True,
185 envsender=config.SITE_OWNER_ADDRESS,
186 pipeline=config.OWNER_PIPELINE,
188 queue = Switchboard(config.INQUEUE_DIR)
189 elif subaddress is None:
190 msgdata['tolist'] = True
191 queue = Switchboard(config.INQUEUE_DIR)
192 elif subaddress == 'request':
193 msgdata['torequest'] = True
194 queue = Switchboard(config.CMDQUEUE_DIR)
195 else:
196 elog.error('Unknown sub-address: %s', subaddress)
197 status.append(ERR_550)
198 continue
199 # If we found a valid subaddress, enqueue the message and add
200 # a success status for this recipient.
201 if queue is not None:
202 queue.enqueue(msg, msgdata)
203 status.append('250 Ok')
204 except Exception, e:
205 elog.exception('Queue detection: %s', msg['message-id'])
206 config.db.abort()
207 status.append(ERR_550)
208 # All done; returning this big status string should give the expected
209 # response to the LMTP client.
210 return CRLF.join(status)
212 def run(self):
213 """See `IRunner`."""
214 asyncore.loop()
216 def stop(self):
217 """See `IRunner`."""
218 asyncore.socket_map.clear()
219 asyncore.close_all()
220 self.close()