blob: f90fae8e15a47abf44a2720922d4bd60b2c8f288 [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'
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000101COMMASPACE = ', '
R David Murrayd1a30c92012-05-26 14:33:59 -0400102DATA_SIZE_DEFAULT = 33554432
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000103
104
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000105def usage(code, msg=''):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000106 print(__doc__ % globals(), file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000107 if msg:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000108 print(msg, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000109 sys.exit(code)
110
111
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000112class SMTPChannel(asynchat.async_chat):
113 COMMAND = 0
114 DATA = 1
115
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000116 command_size_limit = 512
R David Murrayd1a30c92012-05-26 14:33:59 -0400117 command_size_limits = collections.defaultdict(lambda x=command_size_limit: x)
118 command_size_limits.update({
119 'MAIL': command_size_limit + 26,
120 })
121 max_command_size_limit = max(command_size_limits.values())
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000122
Vinay Sajip30298b42013-06-07 15:21:41 +0100123 def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT,
R David Murray554bcbf2014-06-11 11:18:08 -0400124 map=None, decode_data=None):
Vinay Sajip30298b42013-06-07 15:21:41 +0100125 asynchat.async_chat.__init__(self, conn, map=map)
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
R David Murray554bcbf2014-06-11 11:18:08 -0400130 if decode_data is None:
131 warn("The decode_data default of True will change to False in 3.6;"
132 " specify an explicit value for this keyword",
133 DeprecationWarning, 2)
134 decode_data = True
135 self._decode_data = decode_data
136 if decode_data:
137 self._emptystring = ''
138 self._linesep = '\r\n'
139 self._dotsep = '.'
140 self._newline = NEWLINE
141 else:
142 self._emptystring = b''
143 self._linesep = b'\r\n'
144 self._dotsep = b'.'
145 self._newline = b'\n'
Richard Jones803ef8a2010-07-24 09:51:40 +0000146 self.received_lines = []
147 self.smtp_state = self.COMMAND
148 self.seen_greeting = ''
149 self.mailfrom = None
150 self.rcpttos = []
151 self.received_data = ''
152 self.fqdn = socket.getfqdn()
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000153 self.num_bytes = 0
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000154 try:
155 self.peer = conn.getpeername()
Andrew Svetlov0832af62012-12-18 23:10:48 +0200156 except OSError as err:
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000157 # a race condition may occur if the other end is closing
158 # before we can get the peername
159 self.close()
160 if err.args[0] != errno.ENOTCONN:
161 raise
162 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000163 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
164 self.push('220 %s %s' % (self.fqdn, __version__))
Josiah Carlsond74900e2008-07-07 04:15:08 +0000165 self.set_terminator(b'\r\n')
R David Murrayd1a30c92012-05-26 14:33:59 -0400166 self.extended_smtp = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000167
Richard Jones803ef8a2010-07-24 09:51:40 +0000168 # properties for backwards-compatibility
169 @property
170 def __server(self):
171 warn("Access to __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100172 "use 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000173 return self.smtp_server
174 @__server.setter
175 def __server(self, value):
176 warn("Setting __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100177 "set 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000178 self.smtp_server = value
179
180 @property
181 def __line(self):
182 warn("Access to __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100183 "use 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000184 return self.received_lines
185 @__line.setter
186 def __line(self, value):
187 warn("Setting __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100188 "set 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000189 self.received_lines = value
190
191 @property
192 def __state(self):
193 warn("Access to __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100194 "use 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000195 return self.smtp_state
196 @__state.setter
197 def __state(self, value):
198 warn("Setting __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100199 "set 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000200 self.smtp_state = value
201
202 @property
203 def __greeting(self):
204 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100205 "use 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000206 return self.seen_greeting
207 @__greeting.setter
208 def __greeting(self, value):
209 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100210 "set 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000211 self.seen_greeting = value
212
213 @property
214 def __mailfrom(self):
215 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100216 "use 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000217 return self.mailfrom
218 @__mailfrom.setter
219 def __mailfrom(self, value):
220 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100221 "set 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000222 self.mailfrom = value
223
224 @property
225 def __rcpttos(self):
226 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100227 "use 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000228 return self.rcpttos
229 @__rcpttos.setter
230 def __rcpttos(self, value):
231 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100232 "set 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000233 self.rcpttos = value
234
235 @property
236 def __data(self):
237 warn("Access to __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100238 "use 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000239 return self.received_data
240 @__data.setter
241 def __data(self, value):
242 warn("Setting __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100243 "set 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000244 self.received_data = value
245
246 @property
247 def __fqdn(self):
248 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100249 "use 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000250 return self.fqdn
251 @__fqdn.setter
252 def __fqdn(self, value):
253 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100254 "set 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000255 self.fqdn = value
256
257 @property
258 def __peer(self):
259 warn("Access to __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100260 "use 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000261 return self.peer
262 @__peer.setter
263 def __peer(self, value):
264 warn("Setting __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100265 "set 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000266 self.peer = value
267
268 @property
269 def __conn(self):
270 warn("Access to __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100271 "use 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000272 return self.conn
273 @__conn.setter
274 def __conn(self, value):
275 warn("Setting __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100276 "set 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000277 self.conn = value
278
279 @property
280 def __addr(self):
281 warn("Access to __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100282 "use 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000283 return self.addr
284 @__addr.setter
285 def __addr(self, value):
286 warn("Setting __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100287 "set 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000288 self.addr = value
289
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000290 # Overrides base class for convenience
291 def push(self, msg):
Josiah Carlsond74900e2008-07-07 04:15:08 +0000292 asynchat.async_chat.push(self, bytes(msg + '\r\n', 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000293
294 # Implementation of base class abstract method
295 def collect_incoming_data(self, data):
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000296 limit = None
297 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400298 limit = self.max_command_size_limit
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000299 elif self.smtp_state == self.DATA:
300 limit = self.data_size_limit
301 if limit and self.num_bytes > limit:
302 return
303 elif limit:
304 self.num_bytes += len(data)
R David Murray554bcbf2014-06-11 11:18:08 -0400305 if self._decode_data:
306 self.received_lines.append(str(data, 'utf-8'))
307 else:
308 self.received_lines.append(data)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000309
310 # Implementation of base class abstract method
311 def found_terminator(self):
R David Murray554bcbf2014-06-11 11:18:08 -0400312 line = self._emptystring.join(self.received_lines)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000313 print('Data:', repr(line), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000314 self.received_lines = []
315 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400316 sz, self.num_bytes = self.num_bytes, 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000317 if not line:
318 self.push('500 Error: bad syntax')
319 return
320 method = None
R David Murray554bcbf2014-06-11 11:18:08 -0400321 if not self._decode_data:
322 line = str(line, 'utf-8')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000323 i = line.find(' ')
324 if i < 0:
325 command = line.upper()
326 arg = None
327 else:
328 command = line[:i].upper()
329 arg = line[i+1:].strip()
R David Murrayd1a30c92012-05-26 14:33:59 -0400330 max_sz = (self.command_size_limits[command]
331 if self.extended_smtp else self.command_size_limit)
332 if sz > max_sz:
333 self.push('500 Error: line too long')
334 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000335 method = getattr(self, 'smtp_' + command, None)
336 if not method:
R David Murrayd1a30c92012-05-26 14:33:59 -0400337 self.push('500 Error: command "%s" not recognized' % command)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000338 return
339 method(arg)
340 return
341 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000342 if self.smtp_state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000343 self.push('451 Internal confusion')
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000344 self.num_bytes = 0
345 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400346 if self.data_size_limit and self.num_bytes > self.data_size_limit:
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000347 self.push('552 Error: Too much mail data')
348 self.num_bytes = 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000349 return
350 # Remove extraneous carriage returns and de-transparency according
R David Murrayd1a30c92012-05-26 14:33:59 -0400351 # to RFC 5321, Section 4.5.2.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000352 data = []
R David Murray554bcbf2014-06-11 11:18:08 -0400353 for text in line.split(self._linesep):
354 if text and text[0] == self._dotsep:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000355 data.append(text[1:])
356 else:
357 data.append(text)
R David Murray554bcbf2014-06-11 11:18:08 -0400358 self.received_data = self._newline.join(data)
Georg Brandl17e3d692010-07-31 10:08:09 +0000359 status = self.smtp_server.process_message(self.peer,
360 self.mailfrom,
361 self.rcpttos,
362 self.received_data)
Richard Jones803ef8a2010-07-24 09:51:40 +0000363 self.rcpttos = []
364 self.mailfrom = None
365 self.smtp_state = self.COMMAND
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000366 self.num_bytes = 0
Josiah Carlsond74900e2008-07-07 04:15:08 +0000367 self.set_terminator(b'\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000368 if not status:
R David Murrayd1a30c92012-05-26 14:33:59 -0400369 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000370 else:
371 self.push(status)
372
373 # SMTP and ESMTP commands
374 def smtp_HELO(self, arg):
375 if not arg:
376 self.push('501 Syntax: HELO hostname')
377 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000378 if self.seen_greeting:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000379 self.push('503 Duplicate HELO/EHLO')
380 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000381 self.seen_greeting = arg
R David Murrayd1a30c92012-05-26 14:33:59 -0400382 self.extended_smtp = False
Richard Jones803ef8a2010-07-24 09:51:40 +0000383 self.push('250 %s' % self.fqdn)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000384
R David Murrayd1a30c92012-05-26 14:33:59 -0400385 def smtp_EHLO(self, arg):
386 if not arg:
387 self.push('501 Syntax: EHLO hostname')
388 return
389 if self.seen_greeting:
390 self.push('503 Duplicate HELO/EHLO')
391 else:
392 self.seen_greeting = arg
393 self.extended_smtp = True
394 self.push('250-%s' % self.fqdn)
395 if self.data_size_limit:
396 self.push('250-SIZE %s' % self.data_size_limit)
397 self.push('250 HELP')
398
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000399 def smtp_NOOP(self, arg):
400 if arg:
401 self.push('501 Syntax: NOOP')
402 else:
R David Murrayd1a30c92012-05-26 14:33:59 -0400403 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000404
405 def smtp_QUIT(self, arg):
406 # args is ignored
407 self.push('221 Bye')
408 self.close_when_done()
409
R David Murrayd1a30c92012-05-26 14:33:59 -0400410 def _strip_command_keyword(self, keyword, arg):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000411 keylen = len(keyword)
412 if arg[:keylen].upper() == keyword:
R David Murrayd1a30c92012-05-26 14:33:59 -0400413 return arg[keylen:].strip()
414 return ''
415
416 def _getaddr(self, arg):
417 if not arg:
418 return '', ''
419 if arg.lstrip().startswith('<'):
420 address, rest = get_angle_addr(arg)
421 else:
422 address, rest = get_addr_spec(arg)
423 if not address:
424 return address, rest
425 return address.addr_spec, rest
426
427 def _getparams(self, params):
428 # Return any parameters that appear to be syntactically valid according
429 # to RFC 1869, ignore all others. (Postel rule: accept what we can.)
430 params = [param.split('=', 1) for param in params.split()
431 if '=' in param]
432 return {k: v for k, v in params if k.isalnum()}
433
434 def smtp_HELP(self, arg):
435 if arg:
436 extended = ' [SP <mail parameters]'
437 lc_arg = arg.upper()
438 if lc_arg == 'EHLO':
439 self.push('250 Syntax: EHLO hostname')
440 elif lc_arg == 'HELO':
441 self.push('250 Syntax: HELO hostname')
442 elif lc_arg == 'MAIL':
443 msg = '250 Syntax: MAIL FROM: <address>'
444 if self.extended_smtp:
445 msg += extended
446 self.push(msg)
447 elif lc_arg == 'RCPT':
448 msg = '250 Syntax: RCPT TO: <address>'
449 if self.extended_smtp:
450 msg += extended
451 self.push(msg)
452 elif lc_arg == 'DATA':
453 self.push('250 Syntax: DATA')
454 elif lc_arg == 'RSET':
455 self.push('250 Syntax: RSET')
456 elif lc_arg == 'NOOP':
457 self.push('250 Syntax: NOOP')
458 elif lc_arg == 'QUIT':
459 self.push('250 Syntax: QUIT')
460 elif lc_arg == 'VRFY':
461 self.push('250 Syntax: VRFY <address>')
462 else:
463 self.push('501 Supported commands: EHLO HELO MAIL RCPT '
464 'DATA RSET NOOP QUIT VRFY')
465 else:
466 self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '
467 'RSET NOOP QUIT VRFY')
468
469 def smtp_VRFY(self, arg):
470 if arg:
471 address, params = self._getaddr(arg)
472 if address:
473 self.push('252 Cannot VRFY user, but will accept message '
474 'and attempt delivery')
475 else:
476 self.push('502 Could not VRFY %s' % arg)
477 else:
478 self.push('501 Syntax: VRFY <address>')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000479
480 def smtp_MAIL(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400481 if not self.seen_greeting:
482 self.push('503 Error: send HELO first');
483 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000484 print('===> MAIL', arg, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400485 syntaxerr = '501 Syntax: MAIL FROM: <address>'
486 if self.extended_smtp:
487 syntaxerr += ' [SP <mail-parameters>]'
488 if arg is None:
489 self.push(syntaxerr)
490 return
491 arg = self._strip_command_keyword('FROM:', arg)
492 address, params = self._getaddr(arg)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000493 if not address:
R David Murrayd1a30c92012-05-26 14:33:59 -0400494 self.push(syntaxerr)
495 return
496 if not self.extended_smtp and params:
497 self.push(syntaxerr)
498 return
499 if not address:
500 self.push(syntaxerr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000501 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000502 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000503 self.push('503 Error: nested MAIL command')
504 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400505 params = self._getparams(params.upper())
506 if params is None:
507 self.push(syntaxerr)
508 return
509 size = params.pop('SIZE', None)
510 if size:
511 if not size.isdigit():
512 self.push(syntaxerr)
513 return
514 elif self.data_size_limit and int(size) > self.data_size_limit:
515 self.push('552 Error: message size exceeds fixed maximum message size')
516 return
517 if len(params.keys()) > 0:
518 self.push('555 MAIL FROM parameters not recognized or not implemented')
519 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000520 self.mailfrom = address
521 print('sender:', self.mailfrom, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400522 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000523
524 def smtp_RCPT(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400525 if not self.seen_greeting:
526 self.push('503 Error: send HELO first');
527 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000528 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000529 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000530 self.push('503 Error: need MAIL command')
531 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400532 syntaxerr = '501 Syntax: RCPT TO: <address>'
533 if self.extended_smtp:
534 syntaxerr += ' [SP <mail-parameters>]'
535 if arg is None:
536 self.push(syntaxerr)
537 return
538 arg = self._strip_command_keyword('TO:', arg)
539 address, params = self._getaddr(arg)
540 if not address:
541 self.push(syntaxerr)
542 return
543 if params:
544 if self.extended_smtp:
545 params = self._getparams(params.upper())
546 if params is None:
547 self.push(syntaxerr)
548 return
549 else:
550 self.push(syntaxerr)
551 return
552 if not address:
553 self.push(syntaxerr)
554 return
555 if params and len(params.keys()) > 0:
556 self.push('555 RCPT TO parameters not recognized or not implemented')
557 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000558 if not address:
559 self.push('501 Syntax: RCPT TO: <address>')
560 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000561 self.rcpttos.append(address)
562 print('recips:', self.rcpttos, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400563 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000564
565 def smtp_RSET(self, arg):
566 if arg:
567 self.push('501 Syntax: RSET')
568 return
569 # Resets the sender, recipients, and data, but not the greeting
Richard Jones803ef8a2010-07-24 09:51:40 +0000570 self.mailfrom = None
571 self.rcpttos = []
572 self.received_data = ''
573 self.smtp_state = self.COMMAND
R David Murrayd1a30c92012-05-26 14:33:59 -0400574 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000575
576 def smtp_DATA(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400577 if not self.seen_greeting:
578 self.push('503 Error: send HELO first');
579 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000580 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000581 self.push('503 Error: need RCPT command')
582 return
583 if arg:
584 self.push('501 Syntax: DATA')
585 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000586 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000587 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000588 self.push('354 End data with <CR><LF>.<CR><LF>')
589
R David Murrayd1a30c92012-05-26 14:33:59 -0400590 # Commands that have not been implemented
591 def smtp_EXPN(self, arg):
592 self.push('502 EXPN not implemented')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000593
R David Murrayd1a30c92012-05-26 14:33:59 -0400594
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000595class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000596 # SMTPChannel class to use for managing client connections
597 channel_class = SMTPChannel
598
R David Murrayd1a30c92012-05-26 14:33:59 -0400599 def __init__(self, localaddr, remoteaddr,
R David Murray554bcbf2014-06-11 11:18:08 -0400600 data_size_limit=DATA_SIZE_DEFAULT, map=None,
601 decode_data=None):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000602 self._localaddr = localaddr
603 self._remoteaddr = remoteaddr
R David Murrayd1a30c92012-05-26 14:33:59 -0400604 self.data_size_limit = data_size_limit
R David Murray554bcbf2014-06-11 11:18:08 -0400605 if decode_data is None:
606 warn("The decode_data default of True will change to False in 3.6;"
607 " specify an explicit value for this keyword",
608 DeprecationWarning, 2)
609 decode_data = True
610 self._decode_data = decode_data
Vinay Sajip30298b42013-06-07 15:21:41 +0100611 asyncore.dispatcher.__init__(self, map=map)
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000612 try:
R David Murray012a83a2014-06-11 15:17:50 -0400613 gai_results = socket.getaddrinfo(*localaddr,
614 type=socket.SOCK_STREAM)
R David Murray6fe56a32014-06-11 13:48:58 -0400615 self.create_socket(gai_results[0][0], gai_results[0][1])
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000616 # try to re-use a server port if possible
617 self.set_reuse_addr()
618 self.bind(localaddr)
619 self.listen(5)
620 except:
621 self.close()
622 raise
623 else:
624 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
625 self.__class__.__name__, time.ctime(time.time()),
626 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000627
Giampaolo Rodolà977c7072010-10-04 21:08:36 +0000628 def handle_accepted(self, conn, addr):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000629 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
Vinay Sajip30298b42013-06-07 15:21:41 +0100630 channel = self.channel_class(self, conn, addr, self.data_size_limit,
R David Murray554bcbf2014-06-11 11:18:08 -0400631 self._map, self._decode_data)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000632
633 # API for "doing something useful with the message"
634 def process_message(self, peer, mailfrom, rcpttos, data):
635 """Override this abstract method to handle messages from the client.
636
637 peer is a tuple containing (ipaddr, port) of the client that made the
638 socket connection to our smtp port.
639
640 mailfrom is the raw address the client claims the message is coming
641 from.
642
643 rcpttos is a list of raw addresses the client wishes to deliver the
644 message to.
645
646 data is a string containing the entire full text of the message,
647 headers (if supplied) and all. It has been `de-transparencied'
648 according to RFC 821, Section 4.5.2. In other words, a line
649 containing a `.' followed by other text has had the leading dot
650 removed.
651
652 This function should return None, for a normal `250 Ok' response;
653 otherwise it returns the desired response string in RFC 821 format.
654
655 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000656 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000657
Tim Peters658cba62001-02-09 20:06:00 +0000658
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000659class DebuggingServer(SMTPServer):
660 # Do something with the gathered message
661 def process_message(self, peer, mailfrom, rcpttos, data):
662 inheaders = 1
663 lines = data.split('\n')
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000664 print('---------- MESSAGE FOLLOWS ----------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000665 for line in lines:
666 # headers first
667 if inheaders and not line:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000668 print('X-Peer:', peer[0])
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000669 inheaders = 0
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000670 print(line)
671 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000672
673
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000674class PureProxy(SMTPServer):
675 def process_message(self, peer, mailfrom, rcpttos, data):
676 lines = data.split('\n')
677 # Look for the last header
678 i = 0
679 for line in lines:
680 if not line:
681 break
682 i += 1
683 lines.insert(i, 'X-Peer: %s' % peer[0])
684 data = NEWLINE.join(lines)
685 refused = self._deliver(mailfrom, rcpttos, data)
686 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000687 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000688
689 def _deliver(self, mailfrom, rcpttos, data):
690 import smtplib
691 refused = {}
692 try:
693 s = smtplib.SMTP()
694 s.connect(self._remoteaddr[0], self._remoteaddr[1])
695 try:
696 refused = s.sendmail(mailfrom, rcpttos, data)
697 finally:
698 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000699 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000700 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000701 refused = e.recipients
Andrew Svetlov0832af62012-12-18 23:10:48 +0200702 except (OSError, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000703 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000704 # All recipients were refused. If the exception had an associated
705 # error code, use it. Otherwise,fake it with a non-triggering
706 # exception code.
707 errcode = getattr(e, 'smtp_code', -1)
708 errmsg = getattr(e, 'smtp_error', 'ignore')
709 for r in rcpttos:
710 refused[r] = (errcode, errmsg)
711 return refused
712
713
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000714class MailmanProxy(PureProxy):
715 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000716 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000717 from Mailman import Utils
718 from Mailman import Message
719 from Mailman import MailList
720 # If the message is to a Mailman mailing list, then we'll invoke the
721 # Mailman script directly, without going through the real smtpd.
722 # Otherwise we'll forward it to the local proxy for disposition.
723 listnames = []
724 for rcpt in rcpttos:
725 local = rcpt.lower().split('@')[0]
726 # We allow the following variations on the theme
727 # listname
728 # listname-admin
729 # listname-owner
730 # listname-request
731 # listname-join
732 # listname-leave
733 parts = local.split('-')
734 if len(parts) > 2:
735 continue
736 listname = parts[0]
737 if len(parts) == 2:
738 command = parts[1]
739 else:
740 command = ''
741 if not Utils.list_exists(listname) or command not in (
742 '', 'admin', 'owner', 'request', 'join', 'leave'):
743 continue
744 listnames.append((rcpt, listname, command))
745 # Remove all list recipients from rcpttos and forward what we're not
746 # going to take care of ourselves. Linear removal should be fine
747 # since we don't expect a large number of recipients.
748 for rcpt, listname, command in listnames:
749 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000750 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000751 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000752 if rcpttos:
753 refused = self._deliver(mailfrom, rcpttos, data)
754 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000755 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000756 # Now deliver directly to the list commands
757 mlists = {}
758 s = StringIO(data)
759 msg = Message.Message(s)
760 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000761 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000762 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000763 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000764 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000765 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000766 msg['Date'] = time.ctime(time.time())
767 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000768 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000769 mlist = mlists.get(listname)
770 if not mlist:
771 mlist = MailList.MailList(listname, lock=0)
772 mlists[listname] = mlist
773 # dispatch on the type of command
774 if command == '':
775 # post
776 msg.Enqueue(mlist, tolist=1)
777 elif command == 'admin':
778 msg.Enqueue(mlist, toadmin=1)
779 elif command == 'owner':
780 msg.Enqueue(mlist, toowner=1)
781 elif command == 'request':
782 msg.Enqueue(mlist, torequest=1)
783 elif command in ('join', 'leave'):
784 # TBD: this is a hack!
785 if command == 'join':
786 msg['Subject'] = 'subscribe'
787 else:
788 msg['Subject'] = 'unsubscribe'
789 msg.Enqueue(mlist, torequest=1)
790
791
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000792class Options:
793 setuid = 1
794 classname = 'PureProxy'
R David Murrayd1a30c92012-05-26 14:33:59 -0400795 size_limit = None
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000796
797
798def parseargs():
799 global DEBUGSTREAM
800 try:
801 opts, args = getopt.getopt(
R David Murrayd1a30c92012-05-26 14:33:59 -0400802 sys.argv[1:], 'nVhc:s:d',
803 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000804 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000805 usage(1, e)
806
807 options = Options()
808 for opt, arg in opts:
809 if opt in ('-h', '--help'):
810 usage(0)
811 elif opt in ('-V', '--version'):
Serhiy Storchakac56894d2013-09-05 17:44:53 +0300812 print(__version__)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000813 sys.exit(0)
814 elif opt in ('-n', '--nosetuid'):
815 options.setuid = 0
816 elif opt in ('-c', '--class'):
817 options.classname = arg
818 elif opt in ('-d', '--debug'):
819 DEBUGSTREAM = sys.stderr
R David Murrayd1a30c92012-05-26 14:33:59 -0400820 elif opt in ('-s', '--size'):
821 try:
822 int_size = int(arg)
823 options.size_limit = int_size
824 except:
825 print('Invalid size: ' + arg, file=sys.stderr)
826 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000827
828 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000829 if len(args) < 1:
830 localspec = 'localhost:8025'
831 remotespec = 'localhost:25'
832 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000833 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000834 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000835 elif len(args) < 3:
836 localspec = args[0]
837 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000838 else:
839 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
840
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000841 # split into host/port pairs
842 i = localspec.find(':')
843 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000844 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000845 options.localhost = localspec[:i]
846 try:
847 options.localport = int(localspec[i+1:])
848 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000849 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000850 i = remotespec.find(':')
851 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000852 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000853 options.remotehost = remotespec[:i]
854 try:
855 options.remoteport = int(remotespec[i+1:])
856 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000857 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000858 return options
859
860
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000861if __name__ == '__main__':
862 options = parseargs()
863 # Become nobody
Florent Xicluna711f87c2011-10-20 23:03:43 +0200864 classname = options.classname
865 if "." in classname:
866 lastdot = classname.rfind(".")
867 mod = __import__(classname[:lastdot], globals(), locals(), [""])
868 classname = classname[lastdot+1:]
869 else:
870 import __main__ as mod
871 class_ = getattr(mod, classname)
872 proxy = class_((options.localhost, options.localport),
R David Murrayd1a30c92012-05-26 14:33:59 -0400873 (options.remotehost, options.remoteport),
874 options.size_limit)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000875 if options.setuid:
876 try:
877 import pwd
Brett Cannoncd171c82013-07-04 17:43:24 -0400878 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000879 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000880 sys.exit(1)
881 nobody = pwd.getpwnam('nobody')[2]
882 try:
883 os.setuid(nobody)
Giampaolo Rodola'0166a282013-02-12 15:14:17 +0100884 except PermissionError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000885 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000886 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000887 try:
888 asyncore.loop()
889 except KeyboardInterrupt:
890 pass