blob: 179a1b9d63a8e7bfedcf85ae2aa976f198ec9b50 [file] [log] [blame]
Benjamin Peterson90f5ba52010-03-11 22:53:45 +00001#! /usr/bin/env python3
Barry Warsaw406d46e2001-08-13 21:18:01 +00002"""An RFC 2821 smtp proxy.
Barry Warsaw7e0d9562001-01-31 22:51:35 +00003
Barry Warsaw0e8427e2001-10-04 16:27:04 +00004Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
Barry Warsaw7e0d9562001-01-31 22:51:35 +00005
6Options:
7
8 --nosetuid
9 -n
10 This program generally tries to setuid `nobody', unless this flag is
11 set. The setuid call will fail if this program is not run as root (in
12 which case, use this flag).
13
14 --version
15 -V
16 Print the version number and exit.
17
18 --class classname
19 -c classname
Barry Warsawf267b622004-10-09 21:44:13 +000020 Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by
Barry Warsaw7e0d9562001-01-31 22:51:35 +000021 default.
22
23 --debug
24 -d
25 Turn on debugging prints.
26
27 --help
28 -h
29 Print this message and exit.
30
31Version: %(__version__)s
32
Barry Warsaw0e8427e2001-10-04 16:27:04 +000033If localhost is not given then `localhost' is used, and if localport is not
34given then 8025 is used. If remotehost is not given then `localhost' is used,
35and if remoteport is not given, then 25 is used.
Barry Warsaw7e0d9562001-01-31 22:51:35 +000036"""
37
Barry Warsaw0e8427e2001-10-04 16:27:04 +000038
Barry Warsaw7e0d9562001-01-31 22:51:35 +000039# Overview:
40#
41# This file implements the minimal SMTP protocol as defined in RFC 821. It
42# has a hierarchy of classes which implement the backend functionality for the
43# smtpd. A number of classes are provided:
44#
Guido van Rossumb8b45ea2001-04-15 13:06:04 +000045# SMTPServer - the base class for the backend. Raises NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +000046# if you try to use it.
47#
48# DebuggingServer - simply prints each message it receives on stdout.
49#
50# PureProxy - Proxies all messages to a real smtpd which does final
51# delivery. One known problem with this class is that it doesn't handle
52# SMTP errors from the backend server at all. This should be fixed
53# (contributions are welcome!).
54#
55# MailmanProxy - An experimental hack to work with GNU Mailman
56# <www.list.org>. Using this server as your real incoming smtpd, your
57# mailhost will automatically recognize and accept mail destined to Mailman
58# lists when those lists are created. Every message not destined for a list
59# gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
60# are not handled correctly yet.
61#
Barry Warsaw7e0d9562001-01-31 22:51:35 +000062#
Barry Warsawb1027642004-07-12 23:10:08 +000063# Author: Barry Warsaw <barry@python.org>
Barry Warsaw7e0d9562001-01-31 22:51:35 +000064#
65# TODO:
66#
67# - support mailbox delivery
68# - alias files
69# - ESMTP
70# - handle error codes from the backend smtpd
71
72import sys
73import os
74import errno
75import getopt
76import time
77import socket
78import asyncore
79import asynchat
Richard Jones803ef8a2010-07-24 09:51:40 +000080from warnings import warn
Barry Warsaw7e0d9562001-01-31 22:51:35 +000081
Skip Montanaro0de65802001-02-15 22:15:14 +000082__all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
Barry Warsaw7e0d9562001-01-31 22:51:35 +000083
84program = sys.argv[0]
85__version__ = 'Python SMTP proxy version 0.2'
86
87
88class Devnull:
89 def write(self, msg): pass
90 def flush(self): pass
91
92
93DEBUGSTREAM = Devnull()
94NEWLINE = '\n'
95EMPTYSTRING = ''
Barry Warsaw0e8427e2001-10-04 16:27:04 +000096COMMASPACE = ', '
Barry Warsaw7e0d9562001-01-31 22:51:35 +000097
98
Barry Warsaw0e8427e2001-10-04 16:27:04 +000099
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000100def usage(code, msg=''):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000101 print(__doc__ % globals(), file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000102 if msg:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000103 print(msg, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000104 sys.exit(code)
105
106
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000107
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000108class SMTPChannel(asynchat.async_chat):
109 COMMAND = 0
110 DATA = 1
111
112 def __init__(self, server, conn, addr):
113 asynchat.async_chat.__init__(self, conn)
Richard Jones803ef8a2010-07-24 09:51:40 +0000114 self.smtp_server = server
115 self.conn = conn
116 self.addr = addr
117 self.received_lines = []
118 self.smtp_state = self.COMMAND
119 self.seen_greeting = ''
120 self.mailfrom = None
121 self.rcpttos = []
122 self.received_data = ''
123 self.fqdn = socket.getfqdn()
Giampaolo RodolĂ 9cf5ef42010-08-23 22:28:13 +0000124 try:
125 self.peer = conn.getpeername()
126 except socket.error as err:
127 # a race condition may occur if the other end is closing
128 # before we can get the peername
129 self.close()
130 if err.args[0] != errno.ENOTCONN:
131 raise
132 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000133 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
134 self.push('220 %s %s' % (self.fqdn, __version__))
Josiah Carlsond74900e2008-07-07 04:15:08 +0000135 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000136
Richard Jones803ef8a2010-07-24 09:51:40 +0000137 # properties for backwards-compatibility
138 @property
139 def __server(self):
140 warn("Access to __server attribute on SMTPChannel is deprecated, "
141 "use 'smtp_server' instead", PendingDeprecationWarning, 2)
142 return self.smtp_server
143 @__server.setter
144 def __server(self, value):
145 warn("Setting __server attribute on SMTPChannel is deprecated, "
146 "set 'smtp_server' instead", PendingDeprecationWarning, 2)
147 self.smtp_server = value
148
149 @property
150 def __line(self):
151 warn("Access to __line attribute on SMTPChannel is deprecated, "
152 "use 'received_lines' instead", PendingDeprecationWarning, 2)
153 return self.received_lines
154 @__line.setter
155 def __line(self, value):
156 warn("Setting __line attribute on SMTPChannel is deprecated, "
157 "set 'received_lines' instead", PendingDeprecationWarning, 2)
158 self.received_lines = value
159
160 @property
161 def __state(self):
162 warn("Access to __state attribute on SMTPChannel is deprecated, "
163 "use 'smtp_state' instead", PendingDeprecationWarning, 2)
164 return self.smtp_state
165 @__state.setter
166 def __state(self, value):
167 warn("Setting __state attribute on SMTPChannel is deprecated, "
168 "set 'smtp_state' instead", PendingDeprecationWarning, 2)
169 self.smtp_state = value
170
171 @property
172 def __greeting(self):
173 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
174 "use 'seen_greeting' instead", PendingDeprecationWarning, 2)
175 return self.seen_greeting
176 @__greeting.setter
177 def __greeting(self, value):
178 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
179 "set 'seen_greeting' instead", PendingDeprecationWarning, 2)
180 self.seen_greeting = value
181
182 @property
183 def __mailfrom(self):
184 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
185 "use 'mailfrom' instead", PendingDeprecationWarning, 2)
186 return self.mailfrom
187 @__mailfrom.setter
188 def __mailfrom(self, value):
189 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
190 "set 'mailfrom' instead", PendingDeprecationWarning, 2)
191 self.mailfrom = value
192
193 @property
194 def __rcpttos(self):
195 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
196 "use 'rcpttos' instead", PendingDeprecationWarning, 2)
197 return self.rcpttos
198 @__rcpttos.setter
199 def __rcpttos(self, value):
200 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
201 "set 'rcpttos' instead", PendingDeprecationWarning, 2)
202 self.rcpttos = value
203
204 @property
205 def __data(self):
206 warn("Access to __data attribute on SMTPChannel is deprecated, "
207 "use 'received_data' instead", PendingDeprecationWarning, 2)
208 return self.received_data
209 @__data.setter
210 def __data(self, value):
211 warn("Setting __data attribute on SMTPChannel is deprecated, "
212 "set 'received_data' instead", PendingDeprecationWarning, 2)
213 self.received_data = value
214
215 @property
216 def __fqdn(self):
217 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
218 "use 'fqdn' instead", PendingDeprecationWarning, 2)
219 return self.fqdn
220 @__fqdn.setter
221 def __fqdn(self, value):
222 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
223 "set 'fqdn' instead", PendingDeprecationWarning, 2)
224 self.fqdn = value
225
226 @property
227 def __peer(self):
228 warn("Access to __peer attribute on SMTPChannel is deprecated, "
229 "use 'peer' instead", PendingDeprecationWarning, 2)
230 return self.peer
231 @__peer.setter
232 def __peer(self, value):
233 warn("Setting __peer attribute on SMTPChannel is deprecated, "
234 "set 'peer' instead", PendingDeprecationWarning, 2)
235 self.peer = value
236
237 @property
238 def __conn(self):
239 warn("Access to __conn attribute on SMTPChannel is deprecated, "
240 "use 'conn' instead", PendingDeprecationWarning, 2)
241 return self.conn
242 @__conn.setter
243 def __conn(self, value):
244 warn("Setting __conn attribute on SMTPChannel is deprecated, "
245 "set 'conn' instead", PendingDeprecationWarning, 2)
246 self.conn = value
247
248 @property
249 def __addr(self):
250 warn("Access to __addr attribute on SMTPChannel is deprecated, "
251 "use 'addr' instead", PendingDeprecationWarning, 2)
252 return self.addr
253 @__addr.setter
254 def __addr(self, value):
255 warn("Setting __addr attribute on SMTPChannel is deprecated, "
256 "set 'addr' instead", PendingDeprecationWarning, 2)
257 self.addr = value
258
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000259 # Overrides base class for convenience
260 def push(self, msg):
Josiah Carlsond74900e2008-07-07 04:15:08 +0000261 asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000262
263 # Implementation of base class abstract method
264 def collect_incoming_data(self, data):
Richard Jones803ef8a2010-07-24 09:51:40 +0000265 self.received_lines.append(str(data, "utf8"))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000266
267 # Implementation of base class abstract method
268 def found_terminator(self):
Richard Jones803ef8a2010-07-24 09:51:40 +0000269 line = EMPTYSTRING.join(self.received_lines)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000270 print('Data:', repr(line), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000271 self.received_lines = []
272 if self.smtp_state == self.COMMAND:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000273 if not line:
274 self.push('500 Error: bad syntax')
275 return
276 method = None
277 i = line.find(' ')
278 if i < 0:
279 command = line.upper()
280 arg = None
281 else:
282 command = line[:i].upper()
283 arg = line[i+1:].strip()
284 method = getattr(self, 'smtp_' + command, None)
285 if not method:
286 self.push('502 Error: command "%s" not implemented' % command)
287 return
288 method(arg)
289 return
290 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000291 if self.smtp_state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000292 self.push('451 Internal confusion')
293 return
294 # Remove extraneous carriage returns and de-transparency according
295 # to RFC 821, Section 4.5.2.
296 data = []
297 for text in line.split('\r\n'):
298 if text and text[0] == '.':
299 data.append(text[1:])
300 else:
301 data.append(text)
Richard Jones803ef8a2010-07-24 09:51:40 +0000302 self.received_data = NEWLINE.join(data)
Georg Brandl17e3d692010-07-31 10:08:09 +0000303 status = self.smtp_server.process_message(self.peer,
304 self.mailfrom,
305 self.rcpttos,
306 self.received_data)
Richard Jones803ef8a2010-07-24 09:51:40 +0000307 self.rcpttos = []
308 self.mailfrom = None
309 self.smtp_state = self.COMMAND
Josiah Carlsond74900e2008-07-07 04:15:08 +0000310 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000311 if not status:
312 self.push('250 Ok')
313 else:
314 self.push(status)
315
316 # SMTP and ESMTP commands
317 def smtp_HELO(self, arg):
318 if not arg:
319 self.push('501 Syntax: HELO hostname')
320 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000321 if self.seen_greeting:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000322 self.push('503 Duplicate HELO/EHLO')
323 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000324 self.seen_greeting = arg
325 self.push('250 %s' % self.fqdn)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000326
327 def smtp_NOOP(self, arg):
328 if arg:
329 self.push('501 Syntax: NOOP')
330 else:
331 self.push('250 Ok')
332
333 def smtp_QUIT(self, arg):
334 # args is ignored
335 self.push('221 Bye')
336 self.close_when_done()
337
338 # factored
339 def __getaddr(self, keyword, arg):
340 address = None
341 keylen = len(keyword)
342 if arg[:keylen].upper() == keyword:
343 address = arg[keylen:].strip()
Barry Warsawebf54272001-11-04 03:04:25 +0000344 if not address:
345 pass
346 elif address[0] == '<' and address[-1] == '>' and address != '<>':
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000347 # Addresses can be in the form <person@dom.com> but watch out
348 # for null address, e.g. <>
349 address = address[1:-1]
350 return address
351
352 def smtp_MAIL(self, arg):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000353 print('===> MAIL', arg, file=DEBUGSTREAM)
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000354 address = self.__getaddr('FROM:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000355 if not address:
356 self.push('501 Syntax: MAIL FROM:<address>')
357 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000358 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000359 self.push('503 Error: nested MAIL command')
360 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000361 self.mailfrom = address
362 print('sender:', self.mailfrom, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000363 self.push('250 Ok')
364
365 def smtp_RCPT(self, arg):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000366 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000367 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000368 self.push('503 Error: need MAIL command')
369 return
Guido van Rossum8ce8a782007-11-01 19:42:39 +0000370 address = self.__getaddr('TO:', arg) if arg else None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000371 if not address:
372 self.push('501 Syntax: RCPT TO: <address>')
373 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000374 self.rcpttos.append(address)
375 print('recips:', self.rcpttos, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000376 self.push('250 Ok')
377
378 def smtp_RSET(self, arg):
379 if arg:
380 self.push('501 Syntax: RSET')
381 return
382 # Resets the sender, recipients, and data, but not the greeting
Richard Jones803ef8a2010-07-24 09:51:40 +0000383 self.mailfrom = None
384 self.rcpttos = []
385 self.received_data = ''
386 self.smtp_state = self.COMMAND
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000387 self.push('250 Ok')
388
389 def smtp_DATA(self, arg):
Richard Jones803ef8a2010-07-24 09:51:40 +0000390 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000391 self.push('503 Error: need RCPT command')
392 return
393 if arg:
394 self.push('501 Syntax: DATA')
395 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000396 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000397 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000398 self.push('354 End data with <CR><LF>.<CR><LF>')
399
400
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000401
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000402class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000403 # SMTPChannel class to use for managing client connections
404 channel_class = SMTPChannel
405
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000406 def __init__(self, localaddr, remoteaddr):
407 self._localaddr = localaddr
408 self._remoteaddr = remoteaddr
409 asyncore.dispatcher.__init__(self)
Giampaolo RodolĂ 610aa4f2010-06-30 17:47:39 +0000410 try:
411 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
412 # try to re-use a server port if possible
413 self.set_reuse_addr()
414 self.bind(localaddr)
415 self.listen(5)
416 except:
417 self.close()
418 raise
419 else:
420 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
421 self.__class__.__name__, time.ctime(time.time()),
422 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000423
Giampaolo RodolĂ 522180a2010-08-21 18:58:21 +0000424 def handle_accept(self):
Giampaolo RodolĂ 9cf5ef42010-08-23 22:28:13 +0000425 try:
426 conn, addr = self.accept()
427 except TypeError:
428 # sometimes accept() might return None
429 return
430 except socket.error as err:
431 # ECONNABORTED might be thrown
432 if err.args[0] != errno.ECONNABORTED:
433 raise
434 return
435 else:
436 # sometimes addr == None instead of (ip, port)
437 if addr == None:
438 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000439 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000440 channel = self.channel_class(self, conn, addr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000441
442 # API for "doing something useful with the message"
443 def process_message(self, peer, mailfrom, rcpttos, data):
444 """Override this abstract method to handle messages from the client.
445
446 peer is a tuple containing (ipaddr, port) of the client that made the
447 socket connection to our smtp port.
448
449 mailfrom is the raw address the client claims the message is coming
450 from.
451
452 rcpttos is a list of raw addresses the client wishes to deliver the
453 message to.
454
455 data is a string containing the entire full text of the message,
456 headers (if supplied) and all. It has been `de-transparencied'
457 according to RFC 821, Section 4.5.2. In other words, a line
458 containing a `.' followed by other text has had the leading dot
459 removed.
460
461 This function should return None, for a normal `250 Ok' response;
462 otherwise it returns the desired response string in RFC 821 format.
463
464 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000465 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000466
Tim Peters658cba62001-02-09 20:06:00 +0000467
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000468
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000469class DebuggingServer(SMTPServer):
470 # Do something with the gathered message
471 def process_message(self, peer, mailfrom, rcpttos, data):
472 inheaders = 1
473 lines = data.split('\n')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000474 print('---------- MESSAGE FOLLOWS ----------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000475 for line in lines:
476 # headers first
477 if inheaders and not line:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000478 print('X-Peer:', peer[0])
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000479 inheaders = 0
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000480 print(line)
481 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000482
483
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000484
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000485class PureProxy(SMTPServer):
486 def process_message(self, peer, mailfrom, rcpttos, data):
487 lines = data.split('\n')
488 # Look for the last header
489 i = 0
490 for line in lines:
491 if not line:
492 break
493 i += 1
494 lines.insert(i, 'X-Peer: %s' % peer[0])
495 data = NEWLINE.join(lines)
496 refused = self._deliver(mailfrom, rcpttos, data)
497 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000498 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000499
500 def _deliver(self, mailfrom, rcpttos, data):
501 import smtplib
502 refused = {}
503 try:
504 s = smtplib.SMTP()
505 s.connect(self._remoteaddr[0], self._remoteaddr[1])
506 try:
507 refused = s.sendmail(mailfrom, rcpttos, data)
508 finally:
509 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000510 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000511 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000512 refused = e.recipients
Guido van Rossumb940e112007-01-10 16:19:56 +0000513 except (socket.error, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000514 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000515 # All recipients were refused. If the exception had an associated
516 # error code, use it. Otherwise,fake it with a non-triggering
517 # exception code.
518 errcode = getattr(e, 'smtp_code', -1)
519 errmsg = getattr(e, 'smtp_error', 'ignore')
520 for r in rcpttos:
521 refused[r] = (errcode, errmsg)
522 return refused
523
524
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000525
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000526class MailmanProxy(PureProxy):
527 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000528 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000529 from Mailman import Utils
530 from Mailman import Message
531 from Mailman import MailList
532 # If the message is to a Mailman mailing list, then we'll invoke the
533 # Mailman script directly, without going through the real smtpd.
534 # Otherwise we'll forward it to the local proxy for disposition.
535 listnames = []
536 for rcpt in rcpttos:
537 local = rcpt.lower().split('@')[0]
538 # We allow the following variations on the theme
539 # listname
540 # listname-admin
541 # listname-owner
542 # listname-request
543 # listname-join
544 # listname-leave
545 parts = local.split('-')
546 if len(parts) > 2:
547 continue
548 listname = parts[0]
549 if len(parts) == 2:
550 command = parts[1]
551 else:
552 command = ''
553 if not Utils.list_exists(listname) or command not in (
554 '', 'admin', 'owner', 'request', 'join', 'leave'):
555 continue
556 listnames.append((rcpt, listname, command))
557 # Remove all list recipients from rcpttos and forward what we're not
558 # going to take care of ourselves. Linear removal should be fine
559 # since we don't expect a large number of recipients.
560 for rcpt, listname, command in listnames:
561 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000562 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000563 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000564 if rcpttos:
565 refused = self._deliver(mailfrom, rcpttos, data)
566 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000567 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000568 # Now deliver directly to the list commands
569 mlists = {}
570 s = StringIO(data)
571 msg = Message.Message(s)
572 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000573 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000574 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000575 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000576 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000577 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000578 msg['Date'] = time.ctime(time.time())
579 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000580 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000581 mlist = mlists.get(listname)
582 if not mlist:
583 mlist = MailList.MailList(listname, lock=0)
584 mlists[listname] = mlist
585 # dispatch on the type of command
586 if command == '':
587 # post
588 msg.Enqueue(mlist, tolist=1)
589 elif command == 'admin':
590 msg.Enqueue(mlist, toadmin=1)
591 elif command == 'owner':
592 msg.Enqueue(mlist, toowner=1)
593 elif command == 'request':
594 msg.Enqueue(mlist, torequest=1)
595 elif command in ('join', 'leave'):
596 # TBD: this is a hack!
597 if command == 'join':
598 msg['Subject'] = 'subscribe'
599 else:
600 msg['Subject'] = 'unsubscribe'
601 msg.Enqueue(mlist, torequest=1)
602
603
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000604
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000605class Options:
606 setuid = 1
607 classname = 'PureProxy'
608
609
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000610
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000611def parseargs():
612 global DEBUGSTREAM
613 try:
614 opts, args = getopt.getopt(
615 sys.argv[1:], 'nVhc:d',
616 ['class=', 'nosetuid', 'version', 'help', 'debug'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000617 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000618 usage(1, e)
619
620 options = Options()
621 for opt, arg in opts:
622 if opt in ('-h', '--help'):
623 usage(0)
624 elif opt in ('-V', '--version'):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000625 print(__version__, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000626 sys.exit(0)
627 elif opt in ('-n', '--nosetuid'):
628 options.setuid = 0
629 elif opt in ('-c', '--class'):
630 options.classname = arg
631 elif opt in ('-d', '--debug'):
632 DEBUGSTREAM = sys.stderr
633
634 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000635 if len(args) < 1:
636 localspec = 'localhost:8025'
637 remotespec = 'localhost:25'
638 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000639 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000640 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000641 elif len(args) < 3:
642 localspec = args[0]
643 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000644 else:
645 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
646
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000647 # split into host/port pairs
648 i = localspec.find(':')
649 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000650 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000651 options.localhost = localspec[:i]
652 try:
653 options.localport = int(localspec[i+1:])
654 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000655 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000656 i = remotespec.find(':')
657 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000658 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000659 options.remotehost = remotespec[:i]
660 try:
661 options.remoteport = int(remotespec[i+1:])
662 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000663 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000664 return options
665
666
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000667
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000668if __name__ == '__main__':
669 options = parseargs()
670 # Become nobody
671 if options.setuid:
672 try:
673 import pwd
674 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000675 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000676 sys.exit(1)
677 nobody = pwd.getpwnam('nobody')[2]
678 try:
679 os.setuid(nobody)
Guido van Rossumb940e112007-01-10 16:19:56 +0000680 except OSError as e:
Guido van Rossum4ba3d652001-03-02 06:42:34 +0000681 if e.errno != errno.EPERM: raise
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000682 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000683 sys.exit(1)
Skip Montanaro90e01532004-06-26 19:18:49 +0000684 classname = options.classname
685 if "." in classname:
686 lastdot = classname.rfind(".")
687 mod = __import__(classname[:lastdot], globals(), locals(), [""])
688 classname = classname[lastdot+1:]
689 else:
690 import __main__ as mod
Skip Montanaro90e01532004-06-26 19:18:49 +0000691 class_ = getattr(mod, classname)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000692 proxy = class_((options.localhost, options.localport),
693 (options.remotehost, options.remoteport))
694 try:
695 asyncore.loop()
696 except KeyboardInterrupt:
697 pass