blob: 1fa157ade7ccf6b88e20f745704264a4feb2783e [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
Vinay Sajip30298b42013-06-07 15:21:41 +0100124 def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT,
125 map=None):
126 asynchat.async_chat.__init__(self, conn, map=map)
Richard Jones803ef8a2010-07-24 09:51:40 +0000127 self.smtp_server = server
128 self.conn = conn
129 self.addr = addr
R David Murrayd1a30c92012-05-26 14:33:59 -0400130 self.data_size_limit = data_size_limit
Richard Jones803ef8a2010-07-24 09:51:40 +0000131 self.received_lines = []
132 self.smtp_state = self.COMMAND
133 self.seen_greeting = ''
134 self.mailfrom = None
135 self.rcpttos = []
136 self.received_data = ''
137 self.fqdn = socket.getfqdn()
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000138 self.num_bytes = 0
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000139 try:
140 self.peer = conn.getpeername()
Andrew Svetlov0832af62012-12-18 23:10:48 +0200141 except OSError as err:
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000142 # a race condition may occur if the other end is closing
143 # before we can get the peername
144 self.close()
145 if err.args[0] != errno.ENOTCONN:
146 raise
147 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000148 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
149 self.push('220 %s %s' % (self.fqdn, __version__))
Josiah Carlsond74900e2008-07-07 04:15:08 +0000150 self.set_terminator(b'\r\n')
R David Murrayd1a30c92012-05-26 14:33:59 -0400151 self.extended_smtp = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000152
Richard Jones803ef8a2010-07-24 09:51:40 +0000153 # properties for backwards-compatibility
154 @property
155 def __server(self):
156 warn("Access to __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100157 "use 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000158 return self.smtp_server
159 @__server.setter
160 def __server(self, value):
161 warn("Setting __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100162 "set 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000163 self.smtp_server = value
164
165 @property
166 def __line(self):
167 warn("Access to __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100168 "use 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000169 return self.received_lines
170 @__line.setter
171 def __line(self, value):
172 warn("Setting __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100173 "set 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000174 self.received_lines = value
175
176 @property
177 def __state(self):
178 warn("Access to __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100179 "use 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000180 return self.smtp_state
181 @__state.setter
182 def __state(self, value):
183 warn("Setting __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100184 "set 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000185 self.smtp_state = value
186
187 @property
188 def __greeting(self):
189 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100190 "use 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000191 return self.seen_greeting
192 @__greeting.setter
193 def __greeting(self, value):
194 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100195 "set 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000196 self.seen_greeting = value
197
198 @property
199 def __mailfrom(self):
200 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100201 "use 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000202 return self.mailfrom
203 @__mailfrom.setter
204 def __mailfrom(self, value):
205 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100206 "set 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000207 self.mailfrom = value
208
209 @property
210 def __rcpttos(self):
211 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100212 "use 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000213 return self.rcpttos
214 @__rcpttos.setter
215 def __rcpttos(self, value):
216 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100217 "set 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000218 self.rcpttos = value
219
220 @property
221 def __data(self):
222 warn("Access to __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100223 "use 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000224 return self.received_data
225 @__data.setter
226 def __data(self, value):
227 warn("Setting __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100228 "set 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000229 self.received_data = value
230
231 @property
232 def __fqdn(self):
233 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100234 "use 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000235 return self.fqdn
236 @__fqdn.setter
237 def __fqdn(self, value):
238 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100239 "set 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000240 self.fqdn = value
241
242 @property
243 def __peer(self):
244 warn("Access to __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100245 "use 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000246 return self.peer
247 @__peer.setter
248 def __peer(self, value):
249 warn("Setting __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100250 "set 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000251 self.peer = value
252
253 @property
254 def __conn(self):
255 warn("Access to __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100256 "use 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000257 return self.conn
258 @__conn.setter
259 def __conn(self, value):
260 warn("Setting __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100261 "set 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000262 self.conn = value
263
264 @property
265 def __addr(self):
266 warn("Access to __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100267 "use 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000268 return self.addr
269 @__addr.setter
270 def __addr(self, value):
271 warn("Setting __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100272 "set 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000273 self.addr = value
274
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000275 # Overrides base class for convenience
276 def push(self, msg):
Josiah Carlsond74900e2008-07-07 04:15:08 +0000277 asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000278
279 # Implementation of base class abstract method
280 def collect_incoming_data(self, data):
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000281 limit = None
282 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400283 limit = self.max_command_size_limit
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000284 elif self.smtp_state == self.DATA:
285 limit = self.data_size_limit
286 if limit and self.num_bytes > limit:
287 return
288 elif limit:
289 self.num_bytes += len(data)
Marc-André Lemburg8f36af72011-02-25 15:42:01 +0000290 self.received_lines.append(str(data, "utf-8"))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000291
292 # Implementation of base class abstract method
293 def found_terminator(self):
Richard Jones803ef8a2010-07-24 09:51:40 +0000294 line = EMPTYSTRING.join(self.received_lines)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000295 print('Data:', repr(line), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000296 self.received_lines = []
297 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400298 sz, self.num_bytes = self.num_bytes, 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000299 if not line:
300 self.push('500 Error: bad syntax')
301 return
302 method = None
303 i = line.find(' ')
304 if i < 0:
305 command = line.upper()
306 arg = None
307 else:
308 command = line[:i].upper()
309 arg = line[i+1:].strip()
R David Murrayd1a30c92012-05-26 14:33:59 -0400310 max_sz = (self.command_size_limits[command]
311 if self.extended_smtp else self.command_size_limit)
312 if sz > max_sz:
313 self.push('500 Error: line too long')
314 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000315 method = getattr(self, 'smtp_' + command, None)
316 if not method:
R David Murrayd1a30c92012-05-26 14:33:59 -0400317 self.push('500 Error: command "%s" not recognized' % command)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000318 return
319 method(arg)
320 return
321 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000322 if self.smtp_state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000323 self.push('451 Internal confusion')
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000324 self.num_bytes = 0
325 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400326 if self.data_size_limit and self.num_bytes > self.data_size_limit:
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000327 self.push('552 Error: Too much mail data')
328 self.num_bytes = 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000329 return
330 # Remove extraneous carriage returns and de-transparency according
R David Murrayd1a30c92012-05-26 14:33:59 -0400331 # to RFC 5321, Section 4.5.2.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000332 data = []
333 for text in line.split('\r\n'):
334 if text and text[0] == '.':
335 data.append(text[1:])
336 else:
337 data.append(text)
Richard Jones803ef8a2010-07-24 09:51:40 +0000338 self.received_data = NEWLINE.join(data)
Georg Brandl17e3d692010-07-31 10:08:09 +0000339 status = self.smtp_server.process_message(self.peer,
340 self.mailfrom,
341 self.rcpttos,
342 self.received_data)
Richard Jones803ef8a2010-07-24 09:51:40 +0000343 self.rcpttos = []
344 self.mailfrom = None
345 self.smtp_state = self.COMMAND
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000346 self.num_bytes = 0
Josiah Carlsond74900e2008-07-07 04:15:08 +0000347 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000348 if not status:
R David Murrayd1a30c92012-05-26 14:33:59 -0400349 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000350 else:
351 self.push(status)
352
353 # SMTP and ESMTP commands
354 def smtp_HELO(self, arg):
355 if not arg:
356 self.push('501 Syntax: HELO hostname')
357 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000358 if self.seen_greeting:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000359 self.push('503 Duplicate HELO/EHLO')
360 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000361 self.seen_greeting = arg
R David Murrayd1a30c92012-05-26 14:33:59 -0400362 self.extended_smtp = False
Richard Jones803ef8a2010-07-24 09:51:40 +0000363 self.push('250 %s' % self.fqdn)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000364
R David Murrayd1a30c92012-05-26 14:33:59 -0400365 def smtp_EHLO(self, arg):
366 if not arg:
367 self.push('501 Syntax: EHLO hostname')
368 return
369 if self.seen_greeting:
370 self.push('503 Duplicate HELO/EHLO')
371 else:
372 self.seen_greeting = arg
373 self.extended_smtp = True
374 self.push('250-%s' % self.fqdn)
375 if self.data_size_limit:
376 self.push('250-SIZE %s' % self.data_size_limit)
377 self.push('250 HELP')
378
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000379 def smtp_NOOP(self, arg):
380 if arg:
381 self.push('501 Syntax: NOOP')
382 else:
R David Murrayd1a30c92012-05-26 14:33:59 -0400383 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000384
385 def smtp_QUIT(self, arg):
386 # args is ignored
387 self.push('221 Bye')
388 self.close_when_done()
389
R David Murrayd1a30c92012-05-26 14:33:59 -0400390 def _strip_command_keyword(self, keyword, arg):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000391 keylen = len(keyword)
392 if arg[:keylen].upper() == keyword:
R David Murrayd1a30c92012-05-26 14:33:59 -0400393 return arg[keylen:].strip()
394 return ''
395
396 def _getaddr(self, arg):
397 if not arg:
398 return '', ''
399 if arg.lstrip().startswith('<'):
400 address, rest = get_angle_addr(arg)
401 else:
402 address, rest = get_addr_spec(arg)
403 if not address:
404 return address, rest
405 return address.addr_spec, rest
406
407 def _getparams(self, params):
408 # Return any parameters that appear to be syntactically valid according
409 # to RFC 1869, ignore all others. (Postel rule: accept what we can.)
410 params = [param.split('=', 1) for param in params.split()
411 if '=' in param]
412 return {k: v for k, v in params if k.isalnum()}
413
414 def smtp_HELP(self, arg):
415 if arg:
416 extended = ' [SP <mail parameters]'
417 lc_arg = arg.upper()
418 if lc_arg == 'EHLO':
419 self.push('250 Syntax: EHLO hostname')
420 elif lc_arg == 'HELO':
421 self.push('250 Syntax: HELO hostname')
422 elif lc_arg == 'MAIL':
423 msg = '250 Syntax: MAIL FROM: <address>'
424 if self.extended_smtp:
425 msg += extended
426 self.push(msg)
427 elif lc_arg == 'RCPT':
428 msg = '250 Syntax: RCPT TO: <address>'
429 if self.extended_smtp:
430 msg += extended
431 self.push(msg)
432 elif lc_arg == 'DATA':
433 self.push('250 Syntax: DATA')
434 elif lc_arg == 'RSET':
435 self.push('250 Syntax: RSET')
436 elif lc_arg == 'NOOP':
437 self.push('250 Syntax: NOOP')
438 elif lc_arg == 'QUIT':
439 self.push('250 Syntax: QUIT')
440 elif lc_arg == 'VRFY':
441 self.push('250 Syntax: VRFY <address>')
442 else:
443 self.push('501 Supported commands: EHLO HELO MAIL RCPT '
444 'DATA RSET NOOP QUIT VRFY')
445 else:
446 self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '
447 'RSET NOOP QUIT VRFY')
448
449 def smtp_VRFY(self, arg):
450 if arg:
451 address, params = self._getaddr(arg)
452 if address:
453 self.push('252 Cannot VRFY user, but will accept message '
454 'and attempt delivery')
455 else:
456 self.push('502 Could not VRFY %s' % arg)
457 else:
458 self.push('501 Syntax: VRFY <address>')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000459
460 def smtp_MAIL(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400461 if not self.seen_greeting:
462 self.push('503 Error: send HELO first');
463 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000464 print('===> MAIL', arg, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400465 syntaxerr = '501 Syntax: MAIL FROM: <address>'
466 if self.extended_smtp:
467 syntaxerr += ' [SP <mail-parameters>]'
468 if arg is None:
469 self.push(syntaxerr)
470 return
471 arg = self._strip_command_keyword('FROM:', arg)
472 address, params = self._getaddr(arg)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000473 if not address:
R David Murrayd1a30c92012-05-26 14:33:59 -0400474 self.push(syntaxerr)
475 return
476 if not self.extended_smtp and params:
477 self.push(syntaxerr)
478 return
479 if not address:
480 self.push(syntaxerr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000481 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000482 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000483 self.push('503 Error: nested MAIL command')
484 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400485 params = self._getparams(params.upper())
486 if params is None:
487 self.push(syntaxerr)
488 return
489 size = params.pop('SIZE', None)
490 if size:
491 if not size.isdigit():
492 self.push(syntaxerr)
493 return
494 elif self.data_size_limit and int(size) > self.data_size_limit:
495 self.push('552 Error: message size exceeds fixed maximum message size')
496 return
497 if len(params.keys()) > 0:
498 self.push('555 MAIL FROM parameters not recognized or not implemented')
499 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000500 self.mailfrom = address
501 print('sender:', self.mailfrom, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400502 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000503
504 def smtp_RCPT(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400505 if not self.seen_greeting:
506 self.push('503 Error: send HELO first');
507 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000508 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000509 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000510 self.push('503 Error: need MAIL command')
511 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400512 syntaxerr = '501 Syntax: RCPT TO: <address>'
513 if self.extended_smtp:
514 syntaxerr += ' [SP <mail-parameters>]'
515 if arg is None:
516 self.push(syntaxerr)
517 return
518 arg = self._strip_command_keyword('TO:', arg)
519 address, params = self._getaddr(arg)
520 if not address:
521 self.push(syntaxerr)
522 return
523 if params:
524 if self.extended_smtp:
525 params = self._getparams(params.upper())
526 if params is None:
527 self.push(syntaxerr)
528 return
529 else:
530 self.push(syntaxerr)
531 return
532 if not address:
533 self.push(syntaxerr)
534 return
535 if params and len(params.keys()) > 0:
536 self.push('555 RCPT TO parameters not recognized or not implemented')
537 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000538 if not address:
539 self.push('501 Syntax: RCPT TO: <address>')
540 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000541 self.rcpttos.append(address)
542 print('recips:', self.rcpttos, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400543 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000544
545 def smtp_RSET(self, arg):
546 if arg:
547 self.push('501 Syntax: RSET')
548 return
549 # Resets the sender, recipients, and data, but not the greeting
Richard Jones803ef8a2010-07-24 09:51:40 +0000550 self.mailfrom = None
551 self.rcpttos = []
552 self.received_data = ''
553 self.smtp_state = self.COMMAND
R David Murrayd1a30c92012-05-26 14:33:59 -0400554 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000555
556 def smtp_DATA(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400557 if not self.seen_greeting:
558 self.push('503 Error: send HELO first');
559 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000560 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000561 self.push('503 Error: need RCPT command')
562 return
563 if arg:
564 self.push('501 Syntax: DATA')
565 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000566 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000567 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000568 self.push('354 End data with <CR><LF>.<CR><LF>')
569
R David Murrayd1a30c92012-05-26 14:33:59 -0400570 # Commands that have not been implemented
571 def smtp_EXPN(self, arg):
572 self.push('502 EXPN not implemented')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000573
R David Murrayd1a30c92012-05-26 14:33:59 -0400574
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000575class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000576 # SMTPChannel class to use for managing client connections
577 channel_class = SMTPChannel
578
R David Murrayd1a30c92012-05-26 14:33:59 -0400579 def __init__(self, localaddr, remoteaddr,
Vinay Sajip30298b42013-06-07 15:21:41 +0100580 data_size_limit=DATA_SIZE_DEFAULT, map=None):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000581 self._localaddr = localaddr
582 self._remoteaddr = remoteaddr
R David Murrayd1a30c92012-05-26 14:33:59 -0400583 self.data_size_limit = data_size_limit
Vinay Sajip30298b42013-06-07 15:21:41 +0100584 asyncore.dispatcher.__init__(self, map=map)
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000585 try:
586 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
587 # try to re-use a server port if possible
588 self.set_reuse_addr()
589 self.bind(localaddr)
590 self.listen(5)
591 except:
592 self.close()
593 raise
594 else:
595 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
596 self.__class__.__name__, time.ctime(time.time()),
597 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000598
Giampaolo Rodolà977c7072010-10-04 21:08:36 +0000599 def handle_accepted(self, conn, addr):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000600 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
Vinay Sajip30298b42013-06-07 15:21:41 +0100601 channel = self.channel_class(self, conn, addr, self.data_size_limit,
602 self._map)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000603
604 # API for "doing something useful with the message"
605 def process_message(self, peer, mailfrom, rcpttos, data):
606 """Override this abstract method to handle messages from the client.
607
608 peer is a tuple containing (ipaddr, port) of the client that made the
609 socket connection to our smtp port.
610
611 mailfrom is the raw address the client claims the message is coming
612 from.
613
614 rcpttos is a list of raw addresses the client wishes to deliver the
615 message to.
616
617 data is a string containing the entire full text of the message,
618 headers (if supplied) and all. It has been `de-transparencied'
619 according to RFC 821, Section 4.5.2. In other words, a line
620 containing a `.' followed by other text has had the leading dot
621 removed.
622
623 This function should return None, for a normal `250 Ok' response;
624 otherwise it returns the desired response string in RFC 821 format.
625
626 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000627 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000628
Tim Peters658cba62001-02-09 20:06:00 +0000629
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000630class DebuggingServer(SMTPServer):
631 # Do something with the gathered message
632 def process_message(self, peer, mailfrom, rcpttos, data):
633 inheaders = 1
634 lines = data.split('\n')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000635 print('---------- MESSAGE FOLLOWS ----------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000636 for line in lines:
637 # headers first
638 if inheaders and not line:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000639 print('X-Peer:', peer[0])
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000640 inheaders = 0
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000641 print(line)
642 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000643
644
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000645class PureProxy(SMTPServer):
646 def process_message(self, peer, mailfrom, rcpttos, data):
647 lines = data.split('\n')
648 # Look for the last header
649 i = 0
650 for line in lines:
651 if not line:
652 break
653 i += 1
654 lines.insert(i, 'X-Peer: %s' % peer[0])
655 data = NEWLINE.join(lines)
656 refused = self._deliver(mailfrom, rcpttos, data)
657 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000658 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000659
660 def _deliver(self, mailfrom, rcpttos, data):
661 import smtplib
662 refused = {}
663 try:
664 s = smtplib.SMTP()
665 s.connect(self._remoteaddr[0], self._remoteaddr[1])
666 try:
667 refused = s.sendmail(mailfrom, rcpttos, data)
668 finally:
669 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000670 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000671 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000672 refused = e.recipients
Andrew Svetlov0832af62012-12-18 23:10:48 +0200673 except (OSError, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000674 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000675 # All recipients were refused. If the exception had an associated
676 # error code, use it. Otherwise,fake it with a non-triggering
677 # exception code.
678 errcode = getattr(e, 'smtp_code', -1)
679 errmsg = getattr(e, 'smtp_error', 'ignore')
680 for r in rcpttos:
681 refused[r] = (errcode, errmsg)
682 return refused
683
684
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000685class MailmanProxy(PureProxy):
686 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000687 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000688 from Mailman import Utils
689 from Mailman import Message
690 from Mailman import MailList
691 # If the message is to a Mailman mailing list, then we'll invoke the
692 # Mailman script directly, without going through the real smtpd.
693 # Otherwise we'll forward it to the local proxy for disposition.
694 listnames = []
695 for rcpt in rcpttos:
696 local = rcpt.lower().split('@')[0]
697 # We allow the following variations on the theme
698 # listname
699 # listname-admin
700 # listname-owner
701 # listname-request
702 # listname-join
703 # listname-leave
704 parts = local.split('-')
705 if len(parts) > 2:
706 continue
707 listname = parts[0]
708 if len(parts) == 2:
709 command = parts[1]
710 else:
711 command = ''
712 if not Utils.list_exists(listname) or command not in (
713 '', 'admin', 'owner', 'request', 'join', 'leave'):
714 continue
715 listnames.append((rcpt, listname, command))
716 # Remove all list recipients from rcpttos and forward what we're not
717 # going to take care of ourselves. Linear removal should be fine
718 # since we don't expect a large number of recipients.
719 for rcpt, listname, command in listnames:
720 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000721 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000722 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000723 if rcpttos:
724 refused = self._deliver(mailfrom, rcpttos, data)
725 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000726 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000727 # Now deliver directly to the list commands
728 mlists = {}
729 s = StringIO(data)
730 msg = Message.Message(s)
731 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000732 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000733 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000734 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000735 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000736 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000737 msg['Date'] = time.ctime(time.time())
738 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000739 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000740 mlist = mlists.get(listname)
741 if not mlist:
742 mlist = MailList.MailList(listname, lock=0)
743 mlists[listname] = mlist
744 # dispatch on the type of command
745 if command == '':
746 # post
747 msg.Enqueue(mlist, tolist=1)
748 elif command == 'admin':
749 msg.Enqueue(mlist, toadmin=1)
750 elif command == 'owner':
751 msg.Enqueue(mlist, toowner=1)
752 elif command == 'request':
753 msg.Enqueue(mlist, torequest=1)
754 elif command in ('join', 'leave'):
755 # TBD: this is a hack!
756 if command == 'join':
757 msg['Subject'] = 'subscribe'
758 else:
759 msg['Subject'] = 'unsubscribe'
760 msg.Enqueue(mlist, torequest=1)
761
762
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000763class Options:
764 setuid = 1
765 classname = 'PureProxy'
R David Murrayd1a30c92012-05-26 14:33:59 -0400766 size_limit = None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000767
768
769def parseargs():
770 global DEBUGSTREAM
771 try:
772 opts, args = getopt.getopt(
R David Murrayd1a30c92012-05-26 14:33:59 -0400773 sys.argv[1:], 'nVhc:s:d',
774 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000775 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000776 usage(1, e)
777
778 options = Options()
779 for opt, arg in opts:
780 if opt in ('-h', '--help'):
781 usage(0)
782 elif opt in ('-V', '--version'):
Serhiy Storchakac56894d2013-09-05 17:44:53 +0300783 print(__version__)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000784 sys.exit(0)
785 elif opt in ('-n', '--nosetuid'):
786 options.setuid = 0
787 elif opt in ('-c', '--class'):
788 options.classname = arg
789 elif opt in ('-d', '--debug'):
790 DEBUGSTREAM = sys.stderr
R David Murrayd1a30c92012-05-26 14:33:59 -0400791 elif opt in ('-s', '--size'):
792 try:
793 int_size = int(arg)
794 options.size_limit = int_size
795 except:
796 print('Invalid size: ' + arg, file=sys.stderr)
797 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000798
799 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000800 if len(args) < 1:
801 localspec = 'localhost:8025'
802 remotespec = 'localhost:25'
803 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000804 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000805 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000806 elif len(args) < 3:
807 localspec = args[0]
808 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000809 else:
810 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
811
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000812 # split into host/port pairs
813 i = localspec.find(':')
814 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000815 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000816 options.localhost = localspec[:i]
817 try:
818 options.localport = int(localspec[i+1:])
819 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000820 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000821 i = remotespec.find(':')
822 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000823 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000824 options.remotehost = remotespec[:i]
825 try:
826 options.remoteport = int(remotespec[i+1:])
827 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000828 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000829 return options
830
831
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000832if __name__ == '__main__':
833 options = parseargs()
834 # Become nobody
Florent Xicluna711f87c2011-10-20 23:03:43 +0200835 classname = options.classname
836 if "." in classname:
837 lastdot = classname.rfind(".")
838 mod = __import__(classname[:lastdot], globals(), locals(), [""])
839 classname = classname[lastdot+1:]
840 else:
841 import __main__ as mod
842 class_ = getattr(mod, classname)
843 proxy = class_((options.localhost, options.localport),
R David Murrayd1a30c92012-05-26 14:33:59 -0400844 (options.remotehost, options.remoteport),
845 options.size_limit)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000846 if options.setuid:
847 try:
848 import pwd
Brett Cannoncd171c82013-07-04 17:43:24 -0400849 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000850 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000851 sys.exit(1)
852 nobody = pwd.getpwnam('nobody')[2]
853 try:
854 os.setuid(nobody)
Giampaolo Rodola'0166a282013-02-12 15:14:17 +0100855 except PermissionError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000856 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000857 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000858 try:
859 asyncore.loop()
860 except KeyboardInterrupt:
861 pass