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)
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
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
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
57 'bounces', 'confirm', 'join', ' leave',
58 'owner', 'request', 'subscribe', 'unsubscribe',
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
69 smtpd
.__version
__ = 'Python LMTP queue runner 1.0'
73 def split_recipient(address
):
74 """Split an address into listname, subaddress and domain parts.
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.
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]
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."""
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
)
136 def process_message(self
, peer
, mailfrom
, rcpttos
, data
):
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
)
146 msg
['X-MailFrom'] = mailfrom
148 elog
.exception('LMTP message parsing')
150 return CRLF
.join([ERR_451
for to
in rcpttos
])
151 # RFC 2033 requires us to return a status code for every recipient.
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.
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
)
167 # The recipient is a valid mailing list; see if it's a valid
168 # sub-address, and if so, enqueue it.
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':
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
)
196 elog
.error('Unknown sub-address: %s', subaddress
)
197 status
.append(ERR_550
)
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')
205 elog
.exception('Queue detection: %s', msg
['message-id'])
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
)
218 asyncore
.socket_map
.clear()