1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
|
# Copyright (C) 2006 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.
"""Mailman LMTP runner (server).
Most mail servers can be configured to deliver local messages via 'LMTP'[1].
This module is actually an LMTP server rather than a standard queue runner.
Once it enters its main asyncore loop, it does not respond to mailmanctl
signals the same way as other runners do. All signals will kill this process,
but the normal mailmanctl watchdog will restart it upon exit.
The LMTP runner opens a local TCP port and waits for the mail server to
connect to it. The messages it receives over LMTP are very minimally parsed
for sanity and if they look okay, they are accepted and injected into
Mailman's incoming queue for processing through the normal pipeline. If they
don't look good, or are destined for a bogus sub-queue address, they are
rejected right away, hopefully so that the peer mail server can provide better
diagnostics.
[1] RFC 2033 Local Mail Transport Protocol
http://www.faqs.org/rfcs/rfc2033.html
See the variable USE_LMTP in Defaults.py.in for enabling this delivery
mechanism.
"""
# NOTE: LMTP delivery is experimental in Mailman 2.2.
import os
import email
import smtpd
import logging
import asyncore
from email.utils import parseaddr
from Mailman import Utils
from Mailman.Message import Message
from Mailman.Queue.Runner import Runner
from Mailman.Queue.sbcache import get_switchboard
from Mailman.configuration import config
elog = logging.getLogger('mailman.error')
qlog = logging.getLogger('mailman.qrunner')
# We only care about the listname and the subq as in listname@ or
# listname-request@
subqnames = (
'bounces', 'confirm', 'join', ' leave',
'owner', 'request', 'subscribe', 'unsubscribe',
)
DASH = '-'
CRLF = '\r\n'
ERR_451 = '451 Requested action aborted: error in processing'
ERR_502 = '502 Error: command HELO not implemented'
ERR_550 = config.LMTP_ERR_550
# XXX Blech
smtpd.__version__ = 'Python LMTP queue runner 1.0'
def getlistq(address):
localpart, domain = address.split('@', 1)
localpart = localpart.split(config.VERP_DELIMITER, 1)[0]
l = localpart.split(DASH)
if l[-1] in subqnames:
listname = DASH.join(l[:-1])
subq = l[-1]
else:
listname = localpart
subq = None
return listname, subq, domain
class SMTPChannel(smtpd.SMTPChannel):
# Override smtpd.SMTPChannel don't can't change the class name so that we
# don't have to reverse engineer Python's name mangling scheme.
#
# LMTP greeting is LHLO and no HELO/EHLO
def smtp_LHLO(self, arg):
smtpd.SMTPChannel.smtp_HELO(self, arg)
def smtp_HELO(self, arg):
self.push(ERR_502)
class LMTPRunner(Runner, smtpd.SMTPServer):
# Only __init__ is called on startup. Asyncore is responsible for later
# connections from MTA. slice and numslices are ignored and are
# necessary only to satisfy the API.
def __init__(self, slice=None, numslices=1):
localaddr = config.LMTP_HOST, config.LMTP_PORT
# Do not call Runner's constructor because there's no QDIR to create
smtpd.SMTPServer.__init__(self, localaddr, remoteaddr=None)
def handle_accept(self):
conn, addr = self.accept()
channel = SMTPChannel(self, conn, addr)
def process_message(self, peer, mailfrom, rcpttos, data):
try:
# Refresh the list of list names every time we process a message
# since the set of mailing lists could have changed. However, on
# a big site this could be fairly expensive, so we may need to
# cache this in some way.
listnames = Utils.list_names()
# Parse the message data. XXX Should we reject the message
# immediately if it has defects? Usually only spam has defects.
msg = email.message_from_string(data, Message)
msg['X-MailFrom'] = mailfrom
except Exception, e:
elog.error('%s', e)
return CRLF.join([ERR_451 for to in rcpttos])
# RFC 2033 requires us to return a status code for every recipient.
status = []
# Now for each address in the recipients, parse the address to first
# see if it's destined for a valid mailing list. If so, then queue
# the message to the appropriate place and record a 250 status for
# that recipient. If not, record a failure status for that recipient.
for to in rcpttos:
try:
to = parseaddr(to)[1].lower()
listname, subq, domain = getlistq(to)
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-queue, and if so, enqueue it.
msgdata = dict(listname=listname)
if subq in ('bounces', 'admin'):
queue = get_switchboard(config.BOUNCEQUEUE_DIR)
elif subq == 'confirm':
msgdata['toconfirm'] = True
queue = get_switchboard(config.CMDQUEUE_DIR)
elif subq in ('join', 'subscribe'):
msgdata['tojoin'] = True
queue = get_switchboard(config.CMDQUEUE_DIR)
elif subq in ('leave', 'unsubscribe'):
msgdata['toleave'] = True
queue = get_switchboard(config.CMDQUEUE_DIR)
elif subq == 'owner':
msgdata.update({
'toowner' : True,
'envsender' : config.SITE_OWNER_ADDRESS,
'pipeline' : config.OWNER_PIPELINE,
})
queue = get_switchboard(config.INQUEUE_DIR)
elif subq is None:
msgdata['tolist'] = True
queue = get_switchboard(config.INQUEUE_DIR)
elif subq == 'request':
msgdata['torequest'] = True
queue = get_switchboard(config.CMDQUEUE_DIR)
else:
elog.error('Unknown sub-queue: %s', subq)
status.append(ERR_550)
continue
queue.enqueue(msg, msgdata)
status.append('250 Ok')
except Exception, e:
elog.error('%s', e)
status.append(ERR_550)
# All done; returning this big status string should give the expected
# response to the LMTP client.
return CRLF.join(status)
def _cleanup(self):
pass
server = LMTPRunner()
qlog.info('LMTPRunner qrunner started.')
asyncore.loop()
# We'll never get here, but just in case...
qlog.info('LMTPRunner qrunner exiting.')
|