blob: db7c8675e851a0467620032d85d651c0d5e5af1c [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:
Benjamin Peterson0c803312015-04-05 10:01:48 -0400416 extended = ' [SP <mail-parameters>]'
R David Murrayd1a30c92012-05-26 14:33:59 -0400417 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
Richard Jones803ef8a2010-07-24 09:51:40 +0000479 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000480 self.push('503 Error: nested MAIL command')
481 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400482 params = self._getparams(params.upper())
483 if params is None:
484 self.push(syntaxerr)
485 return
486 size = params.pop('SIZE', None)
487 if size:
488 if not size.isdigit():
489 self.push(syntaxerr)
490 return
491 elif self.data_size_limit and int(size) > self.data_size_limit:
492 self.push('552 Error: message size exceeds fixed maximum message size')
493 return
494 if len(params.keys()) > 0:
495 self.push('555 MAIL FROM parameters not recognized or not implemented')
496 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000497 self.mailfrom = address
498 print('sender:', self.mailfrom, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400499 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000500
501 def smtp_RCPT(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400502 if not self.seen_greeting:
503 self.push('503 Error: send HELO first');
504 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000505 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000506 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000507 self.push('503 Error: need MAIL command')
508 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400509 syntaxerr = '501 Syntax: RCPT TO: <address>'
510 if self.extended_smtp:
511 syntaxerr += ' [SP <mail-parameters>]'
512 if arg is None:
513 self.push(syntaxerr)
514 return
515 arg = self._strip_command_keyword('TO:', arg)
516 address, params = self._getaddr(arg)
517 if not address:
518 self.push(syntaxerr)
519 return
520 if params:
521 if self.extended_smtp:
522 params = self._getparams(params.upper())
523 if params is None:
524 self.push(syntaxerr)
525 return
526 else:
527 self.push(syntaxerr)
528 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400529 if params and len(params.keys()) > 0:
530 self.push('555 RCPT TO parameters not recognized or not implemented')
531 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000532 self.rcpttos.append(address)
533 print('recips:', self.rcpttos, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400534 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000535
536 def smtp_RSET(self, arg):
537 if arg:
538 self.push('501 Syntax: RSET')
539 return
540 # Resets the sender, recipients, and data, but not the greeting
Richard Jones803ef8a2010-07-24 09:51:40 +0000541 self.mailfrom = None
542 self.rcpttos = []
543 self.received_data = ''
544 self.smtp_state = self.COMMAND
R David Murrayd1a30c92012-05-26 14:33:59 -0400545 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000546
547 def smtp_DATA(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400548 if not self.seen_greeting:
549 self.push('503 Error: send HELO first');
550 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000551 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000552 self.push('503 Error: need RCPT command')
553 return
554 if arg:
555 self.push('501 Syntax: DATA')
556 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000557 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000558 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000559 self.push('354 End data with <CR><LF>.<CR><LF>')
560
R David Murrayd1a30c92012-05-26 14:33:59 -0400561 # Commands that have not been implemented
562 def smtp_EXPN(self, arg):
563 self.push('502 EXPN not implemented')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000564
R David Murrayd1a30c92012-05-26 14:33:59 -0400565
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000566class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000567 # SMTPChannel class to use for managing client connections
568 channel_class = SMTPChannel
569
R David Murrayd1a30c92012-05-26 14:33:59 -0400570 def __init__(self, localaddr, remoteaddr,
Vinay Sajip30298b42013-06-07 15:21:41 +0100571 data_size_limit=DATA_SIZE_DEFAULT, map=None):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000572 self._localaddr = localaddr
573 self._remoteaddr = remoteaddr
R David Murrayd1a30c92012-05-26 14:33:59 -0400574 self.data_size_limit = data_size_limit
Vinay Sajip30298b42013-06-07 15:21:41 +0100575 asyncore.dispatcher.__init__(self, map=map)
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000576 try:
577 self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
578 # try to re-use a server port if possible
579 self.set_reuse_addr()
580 self.bind(localaddr)
581 self.listen(5)
582 except:
583 self.close()
584 raise
585 else:
586 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
587 self.__class__.__name__, time.ctime(time.time()),
588 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000589
Giampaolo Rodolà977c7072010-10-04 21:08:36 +0000590 def handle_accepted(self, conn, addr):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000591 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
Vinay Sajip30298b42013-06-07 15:21:41 +0100592 channel = self.channel_class(self, conn, addr, self.data_size_limit,
593 self._map)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000594
595 # API for "doing something useful with the message"
596 def process_message(self, peer, mailfrom, rcpttos, data):
597 """Override this abstract method to handle messages from the client.
598
599 peer is a tuple containing (ipaddr, port) of the client that made the
600 socket connection to our smtp port.
601
602 mailfrom is the raw address the client claims the message is coming
603 from.
604
605 rcpttos is a list of raw addresses the client wishes to deliver the
606 message to.
607
608 data is a string containing the entire full text of the message,
609 headers (if supplied) and all. It has been `de-transparencied'
610 according to RFC 821, Section 4.5.2. In other words, a line
611 containing a `.' followed by other text has had the leading dot
612 removed.
613
614 This function should return None, for a normal `250 Ok' response;
615 otherwise it returns the desired response string in RFC 821 format.
616
617 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000618 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000619
Tim Peters658cba62001-02-09 20:06:00 +0000620
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000621class DebuggingServer(SMTPServer):
622 # Do something with the gathered message
623 def process_message(self, peer, mailfrom, rcpttos, data):
624 inheaders = 1
625 lines = data.split('\n')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000626 print('---------- MESSAGE FOLLOWS ----------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000627 for line in lines:
628 # headers first
629 if inheaders and not line:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000630 print('X-Peer:', peer[0])
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000631 inheaders = 0
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000632 print(line)
633 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000634
635
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000636class PureProxy(SMTPServer):
637 def process_message(self, peer, mailfrom, rcpttos, data):
638 lines = data.split('\n')
639 # Look for the last header
640 i = 0
641 for line in lines:
642 if not line:
643 break
644 i += 1
645 lines.insert(i, 'X-Peer: %s' % peer[0])
646 data = NEWLINE.join(lines)
647 refused = self._deliver(mailfrom, rcpttos, data)
648 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000649 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000650
651 def _deliver(self, mailfrom, rcpttos, data):
652 import smtplib
653 refused = {}
654 try:
655 s = smtplib.SMTP()
656 s.connect(self._remoteaddr[0], self._remoteaddr[1])
657 try:
658 refused = s.sendmail(mailfrom, rcpttos, data)
659 finally:
660 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000661 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000662 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000663 refused = e.recipients
Andrew Svetlov0832af62012-12-18 23:10:48 +0200664 except (OSError, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000665 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000666 # All recipients were refused. If the exception had an associated
667 # error code, use it. Otherwise,fake it with a non-triggering
668 # exception code.
669 errcode = getattr(e, 'smtp_code', -1)
670 errmsg = getattr(e, 'smtp_error', 'ignore')
671 for r in rcpttos:
672 refused[r] = (errcode, errmsg)
673 return refused
674
675
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000676class MailmanProxy(PureProxy):
677 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000678 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000679 from Mailman import Utils
680 from Mailman import Message
681 from Mailman import MailList
682 # If the message is to a Mailman mailing list, then we'll invoke the
683 # Mailman script directly, without going through the real smtpd.
684 # Otherwise we'll forward it to the local proxy for disposition.
685 listnames = []
686 for rcpt in rcpttos:
687 local = rcpt.lower().split('@')[0]
688 # We allow the following variations on the theme
689 # listname
690 # listname-admin
691 # listname-owner
692 # listname-request
693 # listname-join
694 # listname-leave
695 parts = local.split('-')
696 if len(parts) > 2:
697 continue
698 listname = parts[0]
699 if len(parts) == 2:
700 command = parts[1]
701 else:
702 command = ''
703 if not Utils.list_exists(listname) or command not in (
704 '', 'admin', 'owner', 'request', 'join', 'leave'):
705 continue
706 listnames.append((rcpt, listname, command))
707 # Remove all list recipients from rcpttos and forward what we're not
708 # going to take care of ourselves. Linear removal should be fine
709 # since we don't expect a large number of recipients.
710 for rcpt, listname, command in listnames:
711 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000712 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000713 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000714 if rcpttos:
715 refused = self._deliver(mailfrom, rcpttos, data)
716 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000717 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000718 # Now deliver directly to the list commands
719 mlists = {}
720 s = StringIO(data)
721 msg = Message.Message(s)
722 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000723 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000724 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000725 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000726 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000727 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000728 msg['Date'] = time.ctime(time.time())
729 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000730 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000731 mlist = mlists.get(listname)
732 if not mlist:
733 mlist = MailList.MailList(listname, lock=0)
734 mlists[listname] = mlist
735 # dispatch on the type of command
736 if command == '':
737 # post
738 msg.Enqueue(mlist, tolist=1)
739 elif command == 'admin':
740 msg.Enqueue(mlist, toadmin=1)
741 elif command == 'owner':
742 msg.Enqueue(mlist, toowner=1)
743 elif command == 'request':
744 msg.Enqueue(mlist, torequest=1)
745 elif command in ('join', 'leave'):
746 # TBD: this is a hack!
747 if command == 'join':
748 msg['Subject'] = 'subscribe'
749 else:
750 msg['Subject'] = 'unsubscribe'
751 msg.Enqueue(mlist, torequest=1)
752
753
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000754class Options:
755 setuid = 1
756 classname = 'PureProxy'
R David Murrayd1a30c92012-05-26 14:33:59 -0400757 size_limit = None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000758
759
760def parseargs():
761 global DEBUGSTREAM
762 try:
763 opts, args = getopt.getopt(
R David Murrayd1a30c92012-05-26 14:33:59 -0400764 sys.argv[1:], 'nVhc:s:d',
765 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000766 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000767 usage(1, e)
768
769 options = Options()
770 for opt, arg in opts:
771 if opt in ('-h', '--help'):
772 usage(0)
773 elif opt in ('-V', '--version'):
Serhiy Storchakac56894d2013-09-05 17:44:53 +0300774 print(__version__)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000775 sys.exit(0)
776 elif opt in ('-n', '--nosetuid'):
777 options.setuid = 0
778 elif opt in ('-c', '--class'):
779 options.classname = arg
780 elif opt in ('-d', '--debug'):
781 DEBUGSTREAM = sys.stderr
R David Murrayd1a30c92012-05-26 14:33:59 -0400782 elif opt in ('-s', '--size'):
783 try:
784 int_size = int(arg)
785 options.size_limit = int_size
786 except:
787 print('Invalid size: ' + arg, file=sys.stderr)
788 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000789
790 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000791 if len(args) < 1:
792 localspec = 'localhost:8025'
793 remotespec = 'localhost:25'
794 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000795 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000796 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000797 elif len(args) < 3:
798 localspec = args[0]
799 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000800 else:
801 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
802
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000803 # split into host/port pairs
804 i = localspec.find(':')
805 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000806 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000807 options.localhost = localspec[:i]
808 try:
809 options.localport = int(localspec[i+1:])
810 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000811 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000812 i = remotespec.find(':')
813 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000814 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000815 options.remotehost = remotespec[:i]
816 try:
817 options.remoteport = int(remotespec[i+1:])
818 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000819 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000820 return options
821
822
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000823if __name__ == '__main__':
824 options = parseargs()
825 # Become nobody
Florent Xicluna711f87c2011-10-20 23:03:43 +0200826 classname = options.classname
827 if "." in classname:
828 lastdot = classname.rfind(".")
829 mod = __import__(classname[:lastdot], globals(), locals(), [""])
830 classname = classname[lastdot+1:]
831 else:
832 import __main__ as mod
833 class_ = getattr(mod, classname)
834 proxy = class_((options.localhost, options.localport),
R David Murrayd1a30c92012-05-26 14:33:59 -0400835 (options.remotehost, options.remoteport),
836 options.size_limit)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000837 if options.setuid:
838 try:
839 import pwd
Brett Cannoncd171c82013-07-04 17:43:24 -0400840 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000841 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000842 sys.exit(1)
843 nobody = pwd.getpwnam('nobody')[2]
844 try:
845 os.setuid(nobody)
Giampaolo Rodola'0166a282013-02-12 15:14:17 +0100846 except PermissionError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000847 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000848 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000849 try:
850 asyncore.loop()
851 except KeyboardInterrupt:
852 pass