blob: 778d6d628001c542d5f45163f995342be1cb1898 [file] [log] [blame]
Benjamin Peterson90f5ba52010-03-11 22:53:45 +00001#! /usr/bin/env python3
R David Murrayd1a30c92012-05-26 14:33:59 -04002"""An RFC 5321 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
R David Murrayd1a30c92012-05-26 14:33:59 -040023 --size limit
24 -s limit
25 Restrict the total size of the incoming message to "limit" number of
26 bytes via the RFC 1870 SIZE extension. Defaults to 33554432 bytes.
27
Barry Warsaw7e0d9562001-01-31 22:51:35 +000028 --debug
29 -d
30 Turn on debugging prints.
31
32 --help
33 -h
34 Print this message and exit.
35
36Version: %(__version__)s
37
Barry Warsaw0e8427e2001-10-04 16:27:04 +000038If localhost is not given then `localhost' is used, and if localport is not
39given then 8025 is used. If remotehost is not given then `localhost' is used,
40and if remoteport is not given, then 25 is used.
Barry Warsaw7e0d9562001-01-31 22:51:35 +000041"""
42
43# Overview:
44#
R David Murrayd1a30c92012-05-26 14:33:59 -040045# This file implements the minimal SMTP protocol as defined in RFC 5321. It
Barry Warsaw7e0d9562001-01-31 22:51:35 +000046# has a hierarchy of classes which implement the backend functionality for the
47# smtpd. A number of classes are provided:
48#
Guido van Rossumb8b45ea2001-04-15 13:06:04 +000049# SMTPServer - the base class for the backend. Raises NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +000050# if you try to use it.
51#
52# DebuggingServer - simply prints each message it receives on stdout.
53#
54# PureProxy - Proxies all messages to a real smtpd which does final
55# delivery. One known problem with this class is that it doesn't handle
56# SMTP errors from the backend server at all. This should be fixed
57# (contributions are welcome!).
58#
59# MailmanProxy - An experimental hack to work with GNU Mailman
60# <www.list.org>. Using this server as your real incoming smtpd, your
61# mailhost will automatically recognize and accept mail destined to Mailman
62# lists when those lists are created. Every message not destined for a list
63# gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
64# are not handled correctly yet.
65#
Barry Warsaw7e0d9562001-01-31 22:51:35 +000066#
Barry Warsawb1027642004-07-12 23:10:08 +000067# Author: Barry Warsaw <barry@python.org>
Barry Warsaw7e0d9562001-01-31 22:51:35 +000068#
69# TODO:
70#
71# - support mailbox delivery
72# - alias files
R David Murrayd1a30c92012-05-26 14:33:59 -040073# - Handle more ESMTP extensions
Barry Warsaw7e0d9562001-01-31 22:51:35 +000074# - handle error codes from the backend smtpd
75
76import sys
77import os
78import errno
79import getopt
80import time
81import socket
82import asyncore
83import asynchat
R David Murrayd1a30c92012-05-26 14:33:59 -040084import collections
Richard Jones803ef8a2010-07-24 09:51:40 +000085from warnings import warn
R David Murrayd1a30c92012-05-26 14:33:59 -040086from email._header_value_parser import get_addr_spec, get_angle_addr
Barry Warsaw7e0d9562001-01-31 22:51:35 +000087
Skip Montanaro0de65802001-02-15 22:15:14 +000088__all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
Barry Warsaw7e0d9562001-01-31 22:51:35 +000089
90program = sys.argv[0]
R David Murrayd1a30c92012-05-26 14:33:59 -040091__version__ = 'Python SMTP proxy version 0.3'
Barry Warsaw7e0d9562001-01-31 22:51:35 +000092
93
94class Devnull:
95 def write(self, msg): pass
96 def flush(self): pass
97
98
99DEBUGSTREAM = Devnull()
100NEWLINE = '\n'
101EMPTYSTRING = ''
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000102COMMASPACE = ', '
R David Murrayd1a30c92012-05-26 14:33:59 -0400103DATA_SIZE_DEFAULT = 33554432
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000104
105
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000106def usage(code, msg=''):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000107 print(__doc__ % globals(), file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000108 if msg:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000109 print(msg, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000110 sys.exit(code)
111
112
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000113class SMTPChannel(asynchat.async_chat):
114 COMMAND = 0
115 DATA = 1
116
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000117 command_size_limit = 512
R David Murrayd1a30c92012-05-26 14:33:59 -0400118 command_size_limits = collections.defaultdict(lambda x=command_size_limit: x)
119 command_size_limits.update({
120 'MAIL': command_size_limit + 26,
121 })
122 max_command_size_limit = max(command_size_limits.values())
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000123
R David Murrayd1a30c92012-05-26 14:33:59 -0400124 def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000125 asynchat.async_chat.__init__(self, conn)
Richard Jones803ef8a2010-07-24 09:51:40 +0000126 self.smtp_server = server
127 self.conn = conn
128 self.addr = addr
R David Murrayd1a30c92012-05-26 14:33:59 -0400129 self.data_size_limit = data_size_limit
Richard Jones803ef8a2010-07-24 09:51:40 +0000130 self.received_lines = []
131 self.smtp_state = self.COMMAND
132 self.seen_greeting = ''
133 self.mailfrom = None
134 self.rcpttos = []
135 self.received_data = ''
136 self.fqdn = socket.getfqdn()
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000137 self.num_bytes = 0
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000138 try:
139 self.peer = conn.getpeername()
140 except socket.error as err:
141 # a race condition may occur if the other end is closing
142 # before we can get the peername
143 self.close()
144 if err.args[0] != errno.ENOTCONN:
145 raise
146 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000147 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
148 self.push('220 %s %s' % (self.fqdn, __version__))
Josiah Carlsond74900e2008-07-07 04:15:08 +0000149 self.set_terminator(b'\r\n')
R David Murrayd1a30c92012-05-26 14:33:59 -0400150 self.extended_smtp = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000151
Richard Jones803ef8a2010-07-24 09:51:40 +0000152 # properties for backwards-compatibility
153 @property
154 def __server(self):
155 warn("Access to __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100156 "use 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000157 return self.smtp_server
158 @__server.setter
159 def __server(self, value):
160 warn("Setting __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100161 "set 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000162 self.smtp_server = value
163
164 @property
165 def __line(self):
166 warn("Access to __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100167 "use 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000168 return self.received_lines
169 @__line.setter
170 def __line(self, value):
171 warn("Setting __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100172 "set 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000173 self.received_lines = value
174
175 @property
176 def __state(self):
177 warn("Access to __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100178 "use 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000179 return self.smtp_state
180 @__state.setter
181 def __state(self, value):
182 warn("Setting __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100183 "set 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000184 self.smtp_state = value
185
186 @property
187 def __greeting(self):
188 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100189 "use 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000190 return self.seen_greeting
191 @__greeting.setter
192 def __greeting(self, value):
193 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100194 "set 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000195 self.seen_greeting = value
196
197 @property
198 def __mailfrom(self):
199 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100200 "use 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000201 return self.mailfrom
202 @__mailfrom.setter
203 def __mailfrom(self, value):
204 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100205 "set 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000206 self.mailfrom = value
207
208 @property
209 def __rcpttos(self):
210 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100211 "use 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000212 return self.rcpttos
213 @__rcpttos.setter
214 def __rcpttos(self, value):
215 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100216 "set 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000217 self.rcpttos = value
218
219 @property
220 def __data(self):
221 warn("Access to __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100222 "use 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000223 return self.received_data
224 @__data.setter
225 def __data(self, value):
226 warn("Setting __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100227 "set 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000228 self.received_data = value
229
230 @property
231 def __fqdn(self):
232 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100233 "use 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000234 return self.fqdn
235 @__fqdn.setter
236 def __fqdn(self, value):
237 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100238 "set 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000239 self.fqdn = value
240
241 @property
242 def __peer(self):
243 warn("Access to __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100244 "use 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000245 return self.peer
246 @__peer.setter
247 def __peer(self, value):
248 warn("Setting __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100249 "set 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000250 self.peer = value
251
252 @property
253 def __conn(self):
254 warn("Access to __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100255 "use 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000256 return self.conn
257 @__conn.setter
258 def __conn(self, value):
259 warn("Setting __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100260 "set 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000261 self.conn = value
262
263 @property
264 def __addr(self):
265 warn("Access to __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100266 "use 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000267 return self.addr
268 @__addr.setter
269 def __addr(self, value):
270 warn("Setting __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100271 "set 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000272 self.addr = value
273
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000274 # Overrides base class for convenience
275 def push(self, msg):
Josiah Carlsond74900e2008-07-07 04:15:08 +0000276 asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000277
278 # Implementation of base class abstract method
279 def collect_incoming_data(self, data):
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000280 limit = None
281 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400282 limit = self.max_command_size_limit
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000283 elif self.smtp_state == self.DATA:
284 limit = self.data_size_limit
285 if limit and self.num_bytes > limit:
286 return
287 elif limit:
288 self.num_bytes += len(data)
Marc-André Lemburg8f36af72011-02-25 15:42:01 +0000289 self.received_lines.append(str(data, "utf-8"))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000290
291 # Implementation of base class abstract method
292 def found_terminator(self):
Richard Jones803ef8a2010-07-24 09:51:40 +0000293 line = EMPTYSTRING.join(self.received_lines)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000294 print('Data:', repr(line), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000295 self.received_lines = []
296 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400297 sz, self.num_bytes = self.num_bytes, 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000298 if not line:
299 self.push('500 Error: bad syntax')
300 return
301 method = None
302 i = line.find(' ')
303 if i < 0:
304 command = line.upper()
305 arg = None
306 else:
307 command = line[:i].upper()
308 arg = line[i+1:].strip()
R David Murrayd1a30c92012-05-26 14:33:59 -0400309 max_sz = (self.command_size_limits[command]
310 if self.extended_smtp else self.command_size_limit)
311 if sz > max_sz:
312 self.push('500 Error: line too long')
313 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000314 method = getattr(self, 'smtp_' + command, None)
315 if not method:
R David Murrayd1a30c92012-05-26 14:33:59 -0400316 self.push('500 Error: command "%s" not recognized' % command)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000317 return
318 method(arg)
319 return
320 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000321 if self.smtp_state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000322 self.push('451 Internal confusion')
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000323 self.num_bytes = 0
324 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400325 if self.data_size_limit and self.num_bytes > self.data_size_limit:
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000326 self.push('552 Error: Too much mail data')
327 self.num_bytes = 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000328 return
329 # Remove extraneous carriage returns and de-transparency according
R David Murrayd1a30c92012-05-26 14:33:59 -0400330 # to RFC 5321, Section 4.5.2.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000331 data = []
332 for text in line.split('\r\n'):
333 if text and text[0] == '.':
334 data.append(text[1:])
335 else:
336 data.append(text)
Richard Jones803ef8a2010-07-24 09:51:40 +0000337 self.received_data = NEWLINE.join(data)
Georg Brandl17e3d692010-07-31 10:08:09 +0000338 status = self.smtp_server.process_message(self.peer,
339 self.mailfrom,
340 self.rcpttos,
341 self.received_data)
Richard Jones803ef8a2010-07-24 09:51:40 +0000342 self.rcpttos = []
343 self.mailfrom = None
344 self.smtp_state = self.COMMAND
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000345 self.num_bytes = 0
Josiah Carlsond74900e2008-07-07 04:15:08 +0000346 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000347 if not status:
R David Murrayd1a30c92012-05-26 14:33:59 -0400348 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000349 else:
350 self.push(status)
351
352 # SMTP and ESMTP commands
353 def smtp_HELO(self, arg):
354 if not arg:
355 self.push('501 Syntax: HELO hostname')
356 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000357 if self.seen_greeting:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000358 self.push('503 Duplicate HELO/EHLO')
359 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000360 self.seen_greeting = arg
R David Murrayd1a30c92012-05-26 14:33:59 -0400361 self.extended_smtp = False
Richard Jones803ef8a2010-07-24 09:51:40 +0000362 self.push('250 %s' % self.fqdn)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000363
R David Murrayd1a30c92012-05-26 14:33:59 -0400364 def smtp_EHLO(self, arg):
365 if not arg:
366 self.push('501 Syntax: EHLO hostname')
367 return
368 if self.seen_greeting:
369 self.push('503 Duplicate HELO/EHLO')
370 else:
371 self.seen_greeting = arg
372 self.extended_smtp = True
373 self.push('250-%s' % self.fqdn)
374 if self.data_size_limit:
375 self.push('250-SIZE %s' % self.data_size_limit)
376 self.push('250 HELP')
377
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000378 def smtp_NOOP(self, arg):
379 if arg:
380 self.push('501 Syntax: NOOP')
381 else:
R David Murrayd1a30c92012-05-26 14:33:59 -0400382 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000383
384 def smtp_QUIT(self, arg):
385 # args is ignored
386 self.push('221 Bye')
387 self.close_when_done()
388
R David Murrayd1a30c92012-05-26 14:33:59 -0400389 def _strip_command_keyword(self, keyword, arg):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000390 keylen = len(keyword)
391 if arg[:keylen].upper() == keyword:
R David Murrayd1a30c92012-05-26 14:33:59 -0400392 return arg[keylen:].strip()
393 return ''
394
395 def _getaddr(self, arg):
396 if not arg:
397 return '', ''
398 if arg.lstrip().startswith('<'):
399 address, rest = get_angle_addr(arg)
400 else:
401 address, rest = get_addr_spec(arg)
402 if not address:
403 return address, rest
404 return address.addr_spec, rest
405
406 def _getparams(self, params):
407 # Return any parameters that appear to be syntactically valid according
408 # to RFC 1869, ignore all others. (Postel rule: accept what we can.)
409 params = [param.split('=', 1) for param in params.split()
410 if '=' in param]
411 return {k: v for k, v in params if k.isalnum()}
412
413 def smtp_HELP(self, arg):
414 if arg:
415 extended = ' [SP <mail parameters]'
416 lc_arg = arg.upper()
417 if lc_arg == 'EHLO':
418 self.push('250 Syntax: EHLO hostname')
419 elif lc_arg == 'HELO':
420 self.push('250 Syntax: HELO hostname')
421 elif lc_arg == 'MAIL':
422 msg = '250 Syntax: MAIL FROM: <address>'
423 if self.extended_smtp:
424 msg += extended
425 self.push(msg)
426 elif lc_arg == 'RCPT':
427 msg = '250 Syntax: RCPT TO: <address>'
428 if self.extended_smtp:
429 msg += extended
430 self.push(msg)
431 elif lc_arg == 'DATA':
432 self.push('250 Syntax: DATA')
433 elif lc_arg == 'RSET':
434 self.push('250 Syntax: RSET')
435 elif lc_arg == 'NOOP':
436 self.push('250 Syntax: NOOP')
437 elif lc_arg == 'QUIT':
438 self.push('250 Syntax: QUIT')
439 elif lc_arg == 'VRFY':
440 self.push('250 Syntax: VRFY <address>')
441 else:
442 self.push('501 Supported commands: EHLO HELO MAIL RCPT '
443 'DATA RSET NOOP QUIT VRFY')
444 else:
445 self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '
446 'RSET NOOP QUIT VRFY')
447
448 def smtp_VRFY(self, arg):
449 if arg:
450 address, params = self._getaddr(arg)
451 if address:
452 self.push('252 Cannot VRFY user, but will accept message '
453 'and attempt delivery')
454 else:
455 self.push('502 Could not VRFY %s' % arg)
456 else:
457 self.push('501 Syntax: VRFY <address>')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000458
459 def smtp_MAIL(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400460 if not self.seen_greeting:
461 self.push('503 Error: send HELO first');
462 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000463 print('===> MAIL', arg, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400464 syntaxerr = '501 Syntax: MAIL FROM: <address>'
465 if self.extended_smtp:
466 syntaxerr += ' [SP <mail-parameters>]'
467 if arg is None:
468 self.push(syntaxerr)
469 return
470 arg = self._strip_command_keyword('FROM:', arg)
471 address, params = self._getaddr(arg)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000472 if not address:
R David Murrayd1a30c92012-05-26 14:33:59 -0400473 self.push(syntaxerr)
474 return
475 if not self.extended_smtp and params:
476 self.push(syntaxerr)
477 return
478 if not address:
479 self.push(syntaxerr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000480 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000481 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000482 self.push('503 Error: nested MAIL command')
483 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400484 params = self._getparams(params.upper())
485 if params is None:
486 self.push(syntaxerr)
487 return
488 size = params.pop('SIZE', None)
489 if size:
490 if not size.isdigit():
491 self.push(syntaxerr)
492 return
493 elif self.data_size_limit and int(size) > self.data_size_limit:
494 self.push('552 Error: message size exceeds fixed maximum message size')
495 return
496 if len(params.keys()) > 0:
497 self.push('555 MAIL FROM parameters not recognized or not implemented')
498 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000499 self.mailfrom = address
500 print('sender:', self.mailfrom, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400501 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000502
503 def smtp_RCPT(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400504 if not self.seen_greeting:
505 self.push('503 Error: send HELO first');
506 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000507 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000508 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000509 self.push('503 Error: need MAIL command')
510 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400511 syntaxerr = '501 Syntax: RCPT TO: <address>'
512 if self.extended_smtp:
513 syntaxerr += ' [SP <mail-parameters>]'
514 if arg is None:
515 self.push(syntaxerr)
516 return
517 arg = self._strip_command_keyword('TO:', arg)
518 address, params = self._getaddr(arg)
519 if not address:
520 self.push(syntaxerr)
521 return
522 if params:
523 if self.extended_smtp:
524 params = self._getparams(params.upper())
525 if params is None:
526 self.push(syntaxerr)
527 return
528 else:
529 self.push(syntaxerr)
530 return
531 if not address:
532 self.push(syntaxerr)
533 return
534 if params and len(params.keys()) > 0:
535 self.push('555 RCPT TO parameters not recognized or not implemented')
536 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000537 if not address:
538 self.push('501 Syntax: RCPT TO: <address>')
539 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000540 self.rcpttos.append(address)
541 print('recips:', self.rcpttos, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400542 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000543
544 def smtp_RSET(self, arg):
545 if arg:
546 self.push('501 Syntax: RSET')
547 return
548 # Resets the sender, recipients, and data, but not the greeting
Richard Jones803ef8a2010-07-24 09:51:40 +0000549 self.mailfrom = None
550 self.rcpttos = []
551 self.received_data = ''
552 self.smtp_state = self.COMMAND
R David Murrayd1a30c92012-05-26 14:33:59 -0400553 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000554
555 def smtp_DATA(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400556 if not self.seen_greeting:
557 self.push('503 Error: send HELO first');
558 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000559 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000560 self.push('503 Error: need RCPT command')
561 return
562 if arg:
563 self.push('501 Syntax: DATA')
564 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000565 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000566 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000567 self.push('354 End data with <CR><LF>.<CR><LF>')
568
R David Murrayd1a30c92012-05-26 14:33:59 -0400569 # Commands that have not been implemented
570 def smtp_EXPN(self, arg):
571 self.push('502 EXPN not implemented')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000572
R David Murrayd1a30c92012-05-26 14:33:59 -0400573
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000574class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000575 # SMTPChannel class to use for managing client connections
576 channel_class = SMTPChannel
577
R David Murrayd1a30c92012-05-26 14:33:59 -0400578 def __init__(self, localaddr, remoteaddr,
579 data_size_limit=DATA_SIZE_DEFAULT):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000580 self._localaddr = localaddr
581 self._remoteaddr = remoteaddr
R David Murrayd1a30c92012-05-26 14:33:59 -0400582 self.data_size_limit = data_size_limit
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000583 asyncore.dispatcher.__init__(self)
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000584 try:
585 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
586 # try to re-use a server port if possible
587 self.set_reuse_addr()
588 self.bind(localaddr)
589 self.listen(5)
590 except:
591 self.close()
592 raise
593 else:
594 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
595 self.__class__.__name__, time.ctime(time.time()),
596 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000597
Giampaolo Rodolà977c7072010-10-04 21:08:36 +0000598 def handle_accepted(self, conn, addr):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000599 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400600 channel = self.channel_class(self, conn, addr, self.data_size_limit)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000601
602 # API for "doing something useful with the message"
603 def process_message(self, peer, mailfrom, rcpttos, data):
604 """Override this abstract method to handle messages from the client.
605
606 peer is a tuple containing (ipaddr, port) of the client that made the
607 socket connection to our smtp port.
608
609 mailfrom is the raw address the client claims the message is coming
610 from.
611
612 rcpttos is a list of raw addresses the client wishes to deliver the
613 message to.
614
615 data is a string containing the entire full text of the message,
616 headers (if supplied) and all. It has been `de-transparencied'
617 according to RFC 821, Section 4.5.2. In other words, a line
618 containing a `.' followed by other text has had the leading dot
619 removed.
620
621 This function should return None, for a normal `250 Ok' response;
622 otherwise it returns the desired response string in RFC 821 format.
623
624 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000625 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000626
Tim Peters658cba62001-02-09 20:06:00 +0000627
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000628class DebuggingServer(SMTPServer):
629 # Do something with the gathered message
630 def process_message(self, peer, mailfrom, rcpttos, data):
631 inheaders = 1
632 lines = data.split('\n')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000633 print('---------- MESSAGE FOLLOWS ----------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000634 for line in lines:
635 # headers first
636 if inheaders and not line:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000637 print('X-Peer:', peer[0])
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000638 inheaders = 0
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000639 print(line)
640 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000641
642
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000643class PureProxy(SMTPServer):
644 def process_message(self, peer, mailfrom, rcpttos, data):
645 lines = data.split('\n')
646 # Look for the last header
647 i = 0
648 for line in lines:
649 if not line:
650 break
651 i += 1
652 lines.insert(i, 'X-Peer: %s' % peer[0])
653 data = NEWLINE.join(lines)
654 refused = self._deliver(mailfrom, rcpttos, data)
655 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000656 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000657
658 def _deliver(self, mailfrom, rcpttos, data):
659 import smtplib
660 refused = {}
661 try:
662 s = smtplib.SMTP()
663 s.connect(self._remoteaddr[0], self._remoteaddr[1])
664 try:
665 refused = s.sendmail(mailfrom, rcpttos, data)
666 finally:
667 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000668 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000669 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000670 refused = e.recipients
Guido van Rossumb940e112007-01-10 16:19:56 +0000671 except (socket.error, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000672 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000673 # All recipients were refused. If the exception had an associated
674 # error code, use it. Otherwise,fake it with a non-triggering
675 # exception code.
676 errcode = getattr(e, 'smtp_code', -1)
677 errmsg = getattr(e, 'smtp_error', 'ignore')
678 for r in rcpttos:
679 refused[r] = (errcode, errmsg)
680 return refused
681
682
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000683class MailmanProxy(PureProxy):
684 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000685 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000686 from Mailman import Utils
687 from Mailman import Message
688 from Mailman import MailList
689 # If the message is to a Mailman mailing list, then we'll invoke the
690 # Mailman script directly, without going through the real smtpd.
691 # Otherwise we'll forward it to the local proxy for disposition.
692 listnames = []
693 for rcpt in rcpttos:
694 local = rcpt.lower().split('@')[0]
695 # We allow the following variations on the theme
696 # listname
697 # listname-admin
698 # listname-owner
699 # listname-request
700 # listname-join
701 # listname-leave
702 parts = local.split('-')
703 if len(parts) > 2:
704 continue
705 listname = parts[0]
706 if len(parts) == 2:
707 command = parts[1]
708 else:
709 command = ''
710 if not Utils.list_exists(listname) or command not in (
711 '', 'admin', 'owner', 'request', 'join', 'leave'):
712 continue
713 listnames.append((rcpt, listname, command))
714 # Remove all list recipients from rcpttos and forward what we're not
715 # going to take care of ourselves. Linear removal should be fine
716 # since we don't expect a large number of recipients.
717 for rcpt, listname, command in listnames:
718 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000719 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000720 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000721 if rcpttos:
722 refused = self._deliver(mailfrom, rcpttos, data)
723 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000724 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000725 # Now deliver directly to the list commands
726 mlists = {}
727 s = StringIO(data)
728 msg = Message.Message(s)
729 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000730 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000731 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000732 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000733 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000734 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000735 msg['Date'] = time.ctime(time.time())
736 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000737 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000738 mlist = mlists.get(listname)
739 if not mlist:
740 mlist = MailList.MailList(listname, lock=0)
741 mlists[listname] = mlist
742 # dispatch on the type of command
743 if command == '':
744 # post
745 msg.Enqueue(mlist, tolist=1)
746 elif command == 'admin':
747 msg.Enqueue(mlist, toadmin=1)
748 elif command == 'owner':
749 msg.Enqueue(mlist, toowner=1)
750 elif command == 'request':
751 msg.Enqueue(mlist, torequest=1)
752 elif command in ('join', 'leave'):
753 # TBD: this is a hack!
754 if command == 'join':
755 msg['Subject'] = 'subscribe'
756 else:
757 msg['Subject'] = 'unsubscribe'
758 msg.Enqueue(mlist, torequest=1)
759
760
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000761class Options:
762 setuid = 1
763 classname = 'PureProxy'
R David Murrayd1a30c92012-05-26 14:33:59 -0400764 size_limit = None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000765
766
767def parseargs():
768 global DEBUGSTREAM
769 try:
770 opts, args = getopt.getopt(
R David Murrayd1a30c92012-05-26 14:33:59 -0400771 sys.argv[1:], 'nVhc:s:d',
772 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000773 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000774 usage(1, e)
775
776 options = Options()
777 for opt, arg in opts:
778 if opt in ('-h', '--help'):
779 usage(0)
780 elif opt in ('-V', '--version'):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000781 print(__version__, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000782 sys.exit(0)
783 elif opt in ('-n', '--nosetuid'):
784 options.setuid = 0
785 elif opt in ('-c', '--class'):
786 options.classname = arg
787 elif opt in ('-d', '--debug'):
788 DEBUGSTREAM = sys.stderr
R David Murrayd1a30c92012-05-26 14:33:59 -0400789 elif opt in ('-s', '--size'):
790 try:
791 int_size = int(arg)
792 options.size_limit = int_size
793 except:
794 print('Invalid size: ' + arg, file=sys.stderr)
795 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000796
797 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000798 if len(args) < 1:
799 localspec = 'localhost:8025'
800 remotespec = 'localhost:25'
801 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000802 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000803 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000804 elif len(args) < 3:
805 localspec = args[0]
806 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000807 else:
808 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
809
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000810 # split into host/port pairs
811 i = localspec.find(':')
812 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000813 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000814 options.localhost = localspec[:i]
815 try:
816 options.localport = int(localspec[i+1:])
817 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000818 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000819 i = remotespec.find(':')
820 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000821 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000822 options.remotehost = remotespec[:i]
823 try:
824 options.remoteport = int(remotespec[i+1:])
825 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000826 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000827 return options
828
829
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000830if __name__ == '__main__':
831 options = parseargs()
832 # Become nobody
Florent Xicluna711f87c2011-10-20 23:03:43 +0200833 classname = options.classname
834 if "." in classname:
835 lastdot = classname.rfind(".")
836 mod = __import__(classname[:lastdot], globals(), locals(), [""])
837 classname = classname[lastdot+1:]
838 else:
839 import __main__ as mod
840 class_ = getattr(mod, classname)
841 proxy = class_((options.localhost, options.localport),
R David Murrayd1a30c92012-05-26 14:33:59 -0400842 (options.remotehost, options.remoteport),
843 options.size_limit)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000844 if options.setuid:
845 try:
846 import pwd
847 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000848 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000849 sys.exit(1)
850 nobody = pwd.getpwnam('nobody')[2]
851 try:
852 os.setuid(nobody)
Guido van Rossumb940e112007-01-10 16:19:56 +0000853 except OSError as e:
Guido van Rossum4ba3d652001-03-02 06:42:34 +0000854 if e.errno != errno.EPERM: raise
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000855 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000856 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000857 try:
858 asyncore.loop()
859 except KeyboardInterrupt:
860 pass