blob: d63ae95d464e1613a9fff5bd3561b3f32784f69a [file] [log] [blame]
Benjamin Peterson90f5ba52010-03-11 22:53:45 +00001#! /usr/bin/env python3
R David Murray2539e672014-08-09 16:40:49 -04002"""An RFC 5321 smtp proxy with optional RFC 1870 and RFC 6531 extensions.
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
R David Murray2539e672014-08-09 16:40:49 -040028 --smtputf8
29 -u
30 Enable the SMTPUTF8 extension and behave as an RFC 6531 smtp proxy.
31
Barry Warsaw7e0d9562001-01-31 22:51:35 +000032 --debug
33 -d
34 Turn on debugging prints.
35
36 --help
37 -h
38 Print this message and exit.
39
40Version: %(__version__)s
41
Barry Warsaw0e8427e2001-10-04 16:27:04 +000042If localhost is not given then `localhost' is used, and if localport is not
43given then 8025 is used. If remotehost is not given then `localhost' is used,
44and if remoteport is not given, then 25 is used.
Barry Warsaw7e0d9562001-01-31 22:51:35 +000045"""
46
47# Overview:
48#
R David Murrayd1a30c92012-05-26 14:33:59 -040049# This file implements the minimal SMTP protocol as defined in RFC 5321. It
Barry Warsaw7e0d9562001-01-31 22:51:35 +000050# has a hierarchy of classes which implement the backend functionality for the
51# smtpd. A number of classes are provided:
52#
Guido van Rossumb8b45ea2001-04-15 13:06:04 +000053# SMTPServer - the base class for the backend. Raises NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +000054# if you try to use it.
55#
56# DebuggingServer - simply prints each message it receives on stdout.
57#
58# PureProxy - Proxies all messages to a real smtpd which does final
59# delivery. One known problem with this class is that it doesn't handle
60# SMTP errors from the backend server at all. This should be fixed
61# (contributions are welcome!).
62#
63# MailmanProxy - An experimental hack to work with GNU Mailman
64# <www.list.org>. Using this server as your real incoming smtpd, your
65# mailhost will automatically recognize and accept mail destined to Mailman
66# lists when those lists are created. Every message not destined for a list
67# gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
68# are not handled correctly yet.
69#
Barry Warsaw7e0d9562001-01-31 22:51:35 +000070#
Barry Warsawb1027642004-07-12 23:10:08 +000071# Author: Barry Warsaw <barry@python.org>
Barry Warsaw7e0d9562001-01-31 22:51:35 +000072#
73# TODO:
74#
75# - support mailbox delivery
76# - alias files
R David Murrayd1a30c92012-05-26 14:33:59 -040077# - Handle more ESMTP extensions
Barry Warsaw7e0d9562001-01-31 22:51:35 +000078# - handle error codes from the backend smtpd
79
80import sys
81import os
82import errno
83import getopt
84import time
85import socket
86import asyncore
87import asynchat
R David Murrayd1a30c92012-05-26 14:33:59 -040088import collections
Richard Jones803ef8a2010-07-24 09:51:40 +000089from warnings import warn
R David Murrayd1a30c92012-05-26 14:33:59 -040090from email._header_value_parser import get_addr_spec, get_angle_addr
Barry Warsaw7e0d9562001-01-31 22:51:35 +000091
Skip Montanaro0de65802001-02-15 22:15:14 +000092__all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
Barry Warsaw7e0d9562001-01-31 22:51:35 +000093
94program = sys.argv[0]
R David Murrayd1a30c92012-05-26 14:33:59 -040095__version__ = 'Python SMTP proxy version 0.3'
Barry Warsaw7e0d9562001-01-31 22:51:35 +000096
97
98class Devnull:
99 def write(self, msg): pass
100 def flush(self): pass
101
102
103DEBUGSTREAM = Devnull()
104NEWLINE = '\n'
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000105COMMASPACE = ', '
R David Murrayd1a30c92012-05-26 14:33:59 -0400106DATA_SIZE_DEFAULT = 33554432
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000107
108
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000109def usage(code, msg=''):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000110 print(__doc__ % globals(), file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000111 if msg:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000112 print(msg, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000113 sys.exit(code)
114
115
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000116class SMTPChannel(asynchat.async_chat):
117 COMMAND = 0
118 DATA = 1
119
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000120 command_size_limit = 512
R David Murrayd1a30c92012-05-26 14:33:59 -0400121 command_size_limits = collections.defaultdict(lambda x=command_size_limit: x)
R David Murray2539e672014-08-09 16:40:49 -0400122
123 @property
124 def max_command_size_limit(self):
125 try:
126 return max(self.command_size_limits.values())
127 except ValueError:
128 return self.command_size_limit
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000129
Vinay Sajip30298b42013-06-07 15:21:41 +0100130 def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT,
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300131 map=None, enable_SMTPUTF8=False, decode_data=False):
Vinay Sajip30298b42013-06-07 15:21:41 +0100132 asynchat.async_chat.__init__(self, conn, map=map)
Richard Jones803ef8a2010-07-24 09:51:40 +0000133 self.smtp_server = server
134 self.conn = conn
135 self.addr = addr
R David Murrayd1a30c92012-05-26 14:33:59 -0400136 self.data_size_limit = data_size_limit
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300137 self.enable_SMTPUTF8 = enable_SMTPUTF8 = bool(enable_SMTPUTF8)
138 self._decode_data = decode_data = bool(decode_data)
139 if enable_SMTPUTF8 and decode_data:
140 raise ValueError("decode_data and enable_SMTPUTF8 cannot"
141 " be set to True at the same time")
R David Murray554bcbf2014-06-11 11:18:08 -0400142 if decode_data:
143 self._emptystring = ''
144 self._linesep = '\r\n'
145 self._dotsep = '.'
146 self._newline = NEWLINE
147 else:
148 self._emptystring = b''
149 self._linesep = b'\r\n'
Serhiy Storchakaee4c0b92015-03-20 16:48:02 +0200150 self._dotsep = ord(b'.')
R David Murray554bcbf2014-06-11 11:18:08 -0400151 self._newline = b'\n'
R David Murray2539e672014-08-09 16:40:49 -0400152 self._set_rset_state()
Richard Jones803ef8a2010-07-24 09:51:40 +0000153 self.seen_greeting = ''
R David Murray2539e672014-08-09 16:40:49 -0400154 self.extended_smtp = False
155 self.command_size_limits.clear()
Richard Jones803ef8a2010-07-24 09:51:40 +0000156 self.fqdn = socket.getfqdn()
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000157 try:
158 self.peer = conn.getpeername()
Andrew Svetlov0832af62012-12-18 23:10:48 +0200159 except OSError as err:
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000160 # a race condition may occur if the other end is closing
161 # before we can get the peername
162 self.close()
163 if err.args[0] != errno.ENOTCONN:
164 raise
165 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000166 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
167 self.push('220 %s %s' % (self.fqdn, __version__))
R David Murray2539e672014-08-09 16:40:49 -0400168
169 def _set_post_data_state(self):
170 """Reset state variables to their post-DATA state."""
171 self.smtp_state = self.COMMAND
172 self.mailfrom = None
173 self.rcpttos = []
174 self.require_SMTPUTF8 = False
175 self.num_bytes = 0
Josiah Carlsond74900e2008-07-07 04:15:08 +0000176 self.set_terminator(b'\r\n')
R David Murray2539e672014-08-09 16:40:49 -0400177
178 def _set_rset_state(self):
179 """Reset all state variables except the greeting."""
180 self._set_post_data_state()
181 self.received_data = ''
182 self.received_lines = []
183
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000184
Richard Jones803ef8a2010-07-24 09:51:40 +0000185 # properties for backwards-compatibility
186 @property
187 def __server(self):
188 warn("Access to __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100189 "use 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000190 return self.smtp_server
191 @__server.setter
192 def __server(self, value):
193 warn("Setting __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100194 "set 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000195 self.smtp_server = value
196
197 @property
198 def __line(self):
199 warn("Access to __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100200 "use 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000201 return self.received_lines
202 @__line.setter
203 def __line(self, value):
204 warn("Setting __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100205 "set 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000206 self.received_lines = value
207
208 @property
209 def __state(self):
210 warn("Access to __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100211 "use 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000212 return self.smtp_state
213 @__state.setter
214 def __state(self, value):
215 warn("Setting __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100216 "set 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000217 self.smtp_state = value
218
219 @property
220 def __greeting(self):
221 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100222 "use 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000223 return self.seen_greeting
224 @__greeting.setter
225 def __greeting(self, value):
226 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100227 "set 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000228 self.seen_greeting = value
229
230 @property
231 def __mailfrom(self):
232 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100233 "use 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000234 return self.mailfrom
235 @__mailfrom.setter
236 def __mailfrom(self, value):
237 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100238 "set 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000239 self.mailfrom = value
240
241 @property
242 def __rcpttos(self):
243 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100244 "use 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000245 return self.rcpttos
246 @__rcpttos.setter
247 def __rcpttos(self, value):
248 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100249 "set 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000250 self.rcpttos = value
251
252 @property
253 def __data(self):
254 warn("Access to __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100255 "use 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000256 return self.received_data
257 @__data.setter
258 def __data(self, value):
259 warn("Setting __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100260 "set 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000261 self.received_data = value
262
263 @property
264 def __fqdn(self):
265 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100266 "use 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000267 return self.fqdn
268 @__fqdn.setter
269 def __fqdn(self, value):
270 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100271 "set 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000272 self.fqdn = value
273
274 @property
275 def __peer(self):
276 warn("Access to __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100277 "use 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000278 return self.peer
279 @__peer.setter
280 def __peer(self, value):
281 warn("Setting __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100282 "set 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000283 self.peer = value
284
285 @property
286 def __conn(self):
287 warn("Access to __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100288 "use 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000289 return self.conn
290 @__conn.setter
291 def __conn(self, value):
292 warn("Setting __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100293 "set 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000294 self.conn = value
295
296 @property
297 def __addr(self):
298 warn("Access to __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100299 "use 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000300 return self.addr
301 @__addr.setter
302 def __addr(self, value):
303 warn("Setting __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100304 "set 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000305 self.addr = value
306
R David Murray2539e672014-08-09 16:40:49 -0400307 # Overrides base class for convenience.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000308 def push(self, msg):
R David Murray2539e672014-08-09 16:40:49 -0400309 asynchat.async_chat.push(self, bytes(
310 msg + '\r\n', 'utf-8' if self.require_SMTPUTF8 else 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000311
312 # Implementation of base class abstract method
313 def collect_incoming_data(self, data):
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000314 limit = None
315 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400316 limit = self.max_command_size_limit
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000317 elif self.smtp_state == self.DATA:
318 limit = self.data_size_limit
319 if limit and self.num_bytes > limit:
320 return
321 elif limit:
322 self.num_bytes += len(data)
R David Murray554bcbf2014-06-11 11:18:08 -0400323 if self._decode_data:
324 self.received_lines.append(str(data, 'utf-8'))
325 else:
326 self.received_lines.append(data)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000327
328 # Implementation of base class abstract method
329 def found_terminator(self):
R David Murray554bcbf2014-06-11 11:18:08 -0400330 line = self._emptystring.join(self.received_lines)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000331 print('Data:', repr(line), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000332 self.received_lines = []
333 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400334 sz, self.num_bytes = self.num_bytes, 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000335 if not line:
336 self.push('500 Error: bad syntax')
337 return
R David Murray554bcbf2014-06-11 11:18:08 -0400338 if not self._decode_data:
339 line = str(line, 'utf-8')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000340 i = line.find(' ')
341 if i < 0:
342 command = line.upper()
343 arg = None
344 else:
345 command = line[:i].upper()
346 arg = line[i+1:].strip()
R David Murrayd1a30c92012-05-26 14:33:59 -0400347 max_sz = (self.command_size_limits[command]
348 if self.extended_smtp else self.command_size_limit)
349 if sz > max_sz:
350 self.push('500 Error: line too long')
351 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000352 method = getattr(self, 'smtp_' + command, None)
353 if not method:
R David Murrayd1a30c92012-05-26 14:33:59 -0400354 self.push('500 Error: command "%s" not recognized' % command)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000355 return
356 method(arg)
357 return
358 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000359 if self.smtp_state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000360 self.push('451 Internal confusion')
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000361 self.num_bytes = 0
362 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400363 if self.data_size_limit and self.num_bytes > self.data_size_limit:
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000364 self.push('552 Error: Too much mail data')
365 self.num_bytes = 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000366 return
367 # Remove extraneous carriage returns and de-transparency according
R David Murrayd1a30c92012-05-26 14:33:59 -0400368 # to RFC 5321, Section 4.5.2.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000369 data = []
R David Murray554bcbf2014-06-11 11:18:08 -0400370 for text in line.split(self._linesep):
371 if text and text[0] == self._dotsep:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000372 data.append(text[1:])
373 else:
374 data.append(text)
R David Murray554bcbf2014-06-11 11:18:08 -0400375 self.received_data = self._newline.join(data)
R David Murray2539e672014-08-09 16:40:49 -0400376 args = (self.peer, self.mailfrom, self.rcpttos, self.received_data)
R David Murraya33df312015-05-11 12:11:40 -0400377 kwargs = {}
378 if not self._decode_data:
379 kwargs = {
380 'mail_options': self.mail_options,
381 'rcpt_options': self.rcpt_options,
382 }
383 status = self.smtp_server.process_message(*args, **kwargs)
R David Murray2539e672014-08-09 16:40:49 -0400384 self._set_post_data_state()
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000385 if not status:
R David Murrayd1a30c92012-05-26 14:33:59 -0400386 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000387 else:
388 self.push(status)
389
390 # SMTP and ESMTP commands
391 def smtp_HELO(self, arg):
392 if not arg:
393 self.push('501 Syntax: HELO hostname')
394 return
R David Murray2539e672014-08-09 16:40:49 -0400395 # See issue #21783 for a discussion of this behavior.
Richard Jones803ef8a2010-07-24 09:51:40 +0000396 if self.seen_greeting:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000397 self.push('503 Duplicate HELO/EHLO')
R David Murray2539e672014-08-09 16:40:49 -0400398 return
399 self._set_rset_state()
400 self.seen_greeting = arg
401 self.push('250 %s' % self.fqdn)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000402
R David Murrayd1a30c92012-05-26 14:33:59 -0400403 def smtp_EHLO(self, arg):
404 if not arg:
405 self.push('501 Syntax: EHLO hostname')
406 return
R David Murray2539e672014-08-09 16:40:49 -0400407 # See issue #21783 for a discussion of this behavior.
R David Murrayd1a30c92012-05-26 14:33:59 -0400408 if self.seen_greeting:
409 self.push('503 Duplicate HELO/EHLO')
R David Murray2539e672014-08-09 16:40:49 -0400410 return
411 self._set_rset_state()
412 self.seen_greeting = arg
413 self.extended_smtp = True
414 self.push('250-%s' % self.fqdn)
415 if self.data_size_limit:
416 self.push('250-SIZE %s' % self.data_size_limit)
417 self.command_size_limits['MAIL'] += 26
R David Murraya33df312015-05-11 12:11:40 -0400418 if not self._decode_data:
R David Murray2539e672014-08-09 16:40:49 -0400419 self.push('250-8BITMIME')
R David Murraya33df312015-05-11 12:11:40 -0400420 if self.enable_SMTPUTF8:
R David Murray2539e672014-08-09 16:40:49 -0400421 self.push('250-SMTPUTF8')
422 self.command_size_limits['MAIL'] += 10
423 self.push('250 HELP')
R David Murrayd1a30c92012-05-26 14:33:59 -0400424
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000425 def smtp_NOOP(self, arg):
426 if arg:
427 self.push('501 Syntax: NOOP')
428 else:
R David Murrayd1a30c92012-05-26 14:33:59 -0400429 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000430
431 def smtp_QUIT(self, arg):
432 # args is ignored
433 self.push('221 Bye')
434 self.close_when_done()
435
R David Murrayd1a30c92012-05-26 14:33:59 -0400436 def _strip_command_keyword(self, keyword, arg):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000437 keylen = len(keyword)
438 if arg[:keylen].upper() == keyword:
R David Murrayd1a30c92012-05-26 14:33:59 -0400439 return arg[keylen:].strip()
440 return ''
441
442 def _getaddr(self, arg):
443 if not arg:
444 return '', ''
445 if arg.lstrip().startswith('<'):
446 address, rest = get_angle_addr(arg)
447 else:
448 address, rest = get_addr_spec(arg)
449 if not address:
450 return address, rest
451 return address.addr_spec, rest
452
453 def _getparams(self, params):
R David Murraya33df312015-05-11 12:11:40 -0400454 # Return params as dictionary. Return None if not all parameters
455 # appear to be syntactically valid according to RFC 1869.
456 result = {}
457 for param in params:
458 param, eq, value = param.partition('=')
459 if not param.isalnum() or eq and not value:
460 return None
461 result[param] = value if eq else True
462 return result
R David Murrayd1a30c92012-05-26 14:33:59 -0400463
464 def smtp_HELP(self, arg):
465 if arg:
Benjamin Peterson0c803312015-04-05 10:01:48 -0400466 extended = ' [SP <mail-parameters>]'
R David Murrayd1a30c92012-05-26 14:33:59 -0400467 lc_arg = arg.upper()
468 if lc_arg == 'EHLO':
469 self.push('250 Syntax: EHLO hostname')
470 elif lc_arg == 'HELO':
471 self.push('250 Syntax: HELO hostname')
472 elif lc_arg == 'MAIL':
473 msg = '250 Syntax: MAIL FROM: <address>'
474 if self.extended_smtp:
475 msg += extended
476 self.push(msg)
477 elif lc_arg == 'RCPT':
478 msg = '250 Syntax: RCPT TO: <address>'
479 if self.extended_smtp:
480 msg += extended
481 self.push(msg)
482 elif lc_arg == 'DATA':
483 self.push('250 Syntax: DATA')
484 elif lc_arg == 'RSET':
485 self.push('250 Syntax: RSET')
486 elif lc_arg == 'NOOP':
487 self.push('250 Syntax: NOOP')
488 elif lc_arg == 'QUIT':
489 self.push('250 Syntax: QUIT')
490 elif lc_arg == 'VRFY':
491 self.push('250 Syntax: VRFY <address>')
492 else:
493 self.push('501 Supported commands: EHLO HELO MAIL RCPT '
494 'DATA RSET NOOP QUIT VRFY')
495 else:
496 self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '
497 'RSET NOOP QUIT VRFY')
498
499 def smtp_VRFY(self, arg):
500 if arg:
501 address, params = self._getaddr(arg)
502 if address:
503 self.push('252 Cannot VRFY user, but will accept message '
504 'and attempt delivery')
505 else:
506 self.push('502 Could not VRFY %s' % arg)
507 else:
508 self.push('501 Syntax: VRFY <address>')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000509
510 def smtp_MAIL(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400511 if not self.seen_greeting:
R David Murraya33df312015-05-11 12:11:40 -0400512 self.push('503 Error: send HELO first')
R David Murray669b7552012-03-20 16:16:29 -0400513 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000514 print('===> MAIL', arg, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400515 syntaxerr = '501 Syntax: MAIL FROM: <address>'
516 if self.extended_smtp:
517 syntaxerr += ' [SP <mail-parameters>]'
518 if arg is None:
519 self.push(syntaxerr)
520 return
521 arg = self._strip_command_keyword('FROM:', arg)
522 address, params = self._getaddr(arg)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000523 if not address:
R David Murrayd1a30c92012-05-26 14:33:59 -0400524 self.push(syntaxerr)
525 return
526 if not self.extended_smtp and params:
527 self.push(syntaxerr)
528 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000529 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000530 self.push('503 Error: nested MAIL command')
531 return
R David Murraya33df312015-05-11 12:11:40 -0400532 self.mail_options = params.upper().split()
533 params = self._getparams(self.mail_options)
R David Murrayd1a30c92012-05-26 14:33:59 -0400534 if params is None:
535 self.push(syntaxerr)
536 return
R David Murraya33df312015-05-11 12:11:40 -0400537 if not self._decode_data:
538 body = params.pop('BODY', '7BIT')
539 if body not in ['7BIT', '8BITMIME']:
540 self.push('501 Error: BODY can only be one of 7BIT, 8BITMIME')
R David Murray2539e672014-08-09 16:40:49 -0400541 return
R David Murraya33df312015-05-11 12:11:40 -0400542 if self.enable_SMTPUTF8:
543 smtputf8 = params.pop('SMTPUTF8', False)
544 if smtputf8 is True:
R David Murray2539e672014-08-09 16:40:49 -0400545 self.require_SMTPUTF8 = True
R David Murraya33df312015-05-11 12:11:40 -0400546 elif smtputf8 is not False:
547 self.push('501 Error: SMTPUTF8 takes no arguments')
548 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400549 size = params.pop('SIZE', None)
550 if size:
551 if not size.isdigit():
552 self.push(syntaxerr)
553 return
554 elif self.data_size_limit and int(size) > self.data_size_limit:
555 self.push('552 Error: message size exceeds fixed maximum message size')
556 return
557 if len(params.keys()) > 0:
558 self.push('555 MAIL FROM parameters not recognized or not implemented')
559 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000560 self.mailfrom = address
561 print('sender:', self.mailfrom, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400562 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000563
564 def smtp_RCPT(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400565 if not self.seen_greeting:
566 self.push('503 Error: send HELO first');
567 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000568 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000569 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000570 self.push('503 Error: need MAIL command')
571 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400572 syntaxerr = '501 Syntax: RCPT TO: <address>'
573 if self.extended_smtp:
574 syntaxerr += ' [SP <mail-parameters>]'
575 if arg is None:
576 self.push(syntaxerr)
577 return
578 arg = self._strip_command_keyword('TO:', arg)
579 address, params = self._getaddr(arg)
580 if not address:
581 self.push(syntaxerr)
582 return
R David Murraya33df312015-05-11 12:11:40 -0400583 if not self.extended_smtp and params:
584 self.push(syntaxerr)
585 return
586 self.rcpt_options = params.upper().split()
587 params = self._getparams(self.rcpt_options)
588 if params is None:
589 self.push(syntaxerr)
590 return
591 # XXX currently there are no options we recognize.
592 if len(params.keys()) > 0:
R David Murrayd1a30c92012-05-26 14:33:59 -0400593 self.push('555 RCPT TO parameters not recognized or not implemented')
594 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000595 self.rcpttos.append(address)
596 print('recips:', self.rcpttos, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400597 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000598
599 def smtp_RSET(self, arg):
600 if arg:
601 self.push('501 Syntax: RSET')
602 return
R David Murray2539e672014-08-09 16:40:49 -0400603 self._set_rset_state()
R David Murrayd1a30c92012-05-26 14:33:59 -0400604 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000605
606 def smtp_DATA(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400607 if not self.seen_greeting:
608 self.push('503 Error: send HELO first');
609 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000610 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000611 self.push('503 Error: need RCPT command')
612 return
613 if arg:
614 self.push('501 Syntax: DATA')
615 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000616 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000617 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000618 self.push('354 End data with <CR><LF>.<CR><LF>')
619
R David Murrayd1a30c92012-05-26 14:33:59 -0400620 # Commands that have not been implemented
621 def smtp_EXPN(self, arg):
622 self.push('502 EXPN not implemented')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000623
R David Murrayd1a30c92012-05-26 14:33:59 -0400624
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000625class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000626 # SMTPChannel class to use for managing client connections
627 channel_class = SMTPChannel
628
R David Murrayd1a30c92012-05-26 14:33:59 -0400629 def __init__(self, localaddr, remoteaddr,
R David Murray554bcbf2014-06-11 11:18:08 -0400630 data_size_limit=DATA_SIZE_DEFAULT, map=None,
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300631 enable_SMTPUTF8=False, decode_data=False):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000632 self._localaddr = localaddr
633 self._remoteaddr = remoteaddr
R David Murrayd1a30c92012-05-26 14:33:59 -0400634 self.data_size_limit = data_size_limit
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300635 self.enable_SMTPUTF8 = enable_SMTPUTF8 = bool(enable_SMTPUTF8)
636 self._decode_data = decode_data = bool(decode_data)
637 if enable_SMTPUTF8 and decode_data:
638 raise ValueError("decode_data and enable_SMTPUTF8 cannot"
639 " be set to True at the same time")
Vinay Sajip30298b42013-06-07 15:21:41 +0100640 asyncore.dispatcher.__init__(self, map=map)
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000641 try:
R David Murray012a83a2014-06-11 15:17:50 -0400642 gai_results = socket.getaddrinfo(*localaddr,
643 type=socket.SOCK_STREAM)
R David Murray6fe56a32014-06-11 13:48:58 -0400644 self.create_socket(gai_results[0][0], gai_results[0][1])
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000645 # try to re-use a server port if possible
646 self.set_reuse_addr()
647 self.bind(localaddr)
648 self.listen(5)
649 except:
650 self.close()
651 raise
652 else:
653 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
654 self.__class__.__name__, time.ctime(time.time()),
655 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000656
Giampaolo Rodolà977c7072010-10-04 21:08:36 +0000657 def handle_accepted(self, conn, addr):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000658 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
R David Murray2539e672014-08-09 16:40:49 -0400659 channel = self.channel_class(self,
660 conn,
661 addr,
662 self.data_size_limit,
663 self._map,
664 self.enable_SMTPUTF8,
665 self._decode_data)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000666
667 # API for "doing something useful with the message"
R David Murraya33df312015-05-11 12:11:40 -0400668 def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000669 """Override this abstract method to handle messages from the client.
670
671 peer is a tuple containing (ipaddr, port) of the client that made the
672 socket connection to our smtp port.
673
674 mailfrom is the raw address the client claims the message is coming
675 from.
676
677 rcpttos is a list of raw addresses the client wishes to deliver the
678 message to.
679
680 data is a string containing the entire full text of the message,
681 headers (if supplied) and all. It has been `de-transparencied'
682 according to RFC 821, Section 4.5.2. In other words, a line
683 containing a `.' followed by other text has had the leading dot
684 removed.
685
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300686 kwargs is a dictionary containing additional information. It is
687 empty if decode_data=True was given as init parameter, otherwise
688 it will contain the following keys:
R David Murraya33df312015-05-11 12:11:40 -0400689 'mail_options': list of parameters to the mail command. All
690 elements are uppercase strings. Example:
691 ['BODY=8BITMIME', 'SMTPUTF8'].
692 'rcpt_options': same, for the rcpt command.
693
R David Murray2539e672014-08-09 16:40:49 -0400694 This function should return None for a normal `250 Ok' response;
695 otherwise, it should return the desired response string in RFC 821
696 format.
697
698 """
699 raise NotImplementedError
700
Tim Peters658cba62001-02-09 20:06:00 +0000701
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000702class DebuggingServer(SMTPServer):
R David Murray2539e672014-08-09 16:40:49 -0400703
704 def _print_message_content(self, peer, data):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000705 inheaders = 1
R David Murray2539e672014-08-09 16:40:49 -0400706 lines = data.splitlines()
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000707 for line in lines:
708 # headers first
709 if inheaders and not line:
R David Murray2539e672014-08-09 16:40:49 -0400710 peerheader = 'X-Peer: ' + peer[0]
711 if not isinstance(data, str):
712 # decoded_data=false; make header match other binary output
713 peerheader = repr(peerheader.encode('utf-8'))
714 print(peerheader)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000715 inheaders = 0
R David Murray2539e672014-08-09 16:40:49 -0400716 if not isinstance(data, str):
717 # Avoid spurious 'str on bytes instance' warning.
718 line = repr(line)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000719 print(line)
R David Murray2539e672014-08-09 16:40:49 -0400720
R David Murraya33df312015-05-11 12:11:40 -0400721 def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
R David Murray2539e672014-08-09 16:40:49 -0400722 print('---------- MESSAGE FOLLOWS ----------')
R David Murraya33df312015-05-11 12:11:40 -0400723 if kwargs:
724 if kwargs.get('mail_options'):
725 print('mail options: %s' % kwargs['mail_options'])
726 if kwargs.get('rcpt_options'):
727 print('rcpt options: %s\n' % kwargs['rcpt_options'])
R David Murray2539e672014-08-09 16:40:49 -0400728 self._print_message_content(peer, data)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000729 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000730
731
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000732class PureProxy(SMTPServer):
R David Murray2539e672014-08-09 16:40:49 -0400733 def __init__(self, *args, **kwargs):
734 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
735 raise ValueError("PureProxy does not support SMTPUTF8.")
736 super(PureProxy, self).__init__(*args, **kwargs)
737
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000738 def process_message(self, peer, mailfrom, rcpttos, data):
739 lines = data.split('\n')
740 # Look for the last header
741 i = 0
742 for line in lines:
743 if not line:
744 break
745 i += 1
746 lines.insert(i, 'X-Peer: %s' % peer[0])
747 data = NEWLINE.join(lines)
748 refused = self._deliver(mailfrom, rcpttos, data)
749 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000750 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000751
752 def _deliver(self, mailfrom, rcpttos, data):
753 import smtplib
754 refused = {}
755 try:
756 s = smtplib.SMTP()
757 s.connect(self._remoteaddr[0], self._remoteaddr[1])
758 try:
759 refused = s.sendmail(mailfrom, rcpttos, data)
760 finally:
761 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000762 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000763 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000764 refused = e.recipients
Andrew Svetlov0832af62012-12-18 23:10:48 +0200765 except (OSError, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000766 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000767 # All recipients were refused. If the exception had an associated
768 # error code, use it. Otherwise,fake it with a non-triggering
769 # exception code.
770 errcode = getattr(e, 'smtp_code', -1)
771 errmsg = getattr(e, 'smtp_error', 'ignore')
772 for r in rcpttos:
773 refused[r] = (errcode, errmsg)
774 return refused
775
776
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000777class MailmanProxy(PureProxy):
R David Murray2539e672014-08-09 16:40:49 -0400778 def __init__(self, *args, **kwargs):
779 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
780 raise ValueError("MailmanProxy does not support SMTPUTF8.")
781 super(PureProxy, self).__init__(*args, **kwargs)
782
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000783 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000784 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000785 from Mailman import Utils
786 from Mailman import Message
787 from Mailman import MailList
788 # If the message is to a Mailman mailing list, then we'll invoke the
789 # Mailman script directly, without going through the real smtpd.
790 # Otherwise we'll forward it to the local proxy for disposition.
791 listnames = []
792 for rcpt in rcpttos:
793 local = rcpt.lower().split('@')[0]
794 # We allow the following variations on the theme
795 # listname
796 # listname-admin
797 # listname-owner
798 # listname-request
799 # listname-join
800 # listname-leave
801 parts = local.split('-')
802 if len(parts) > 2:
803 continue
804 listname = parts[0]
805 if len(parts) == 2:
806 command = parts[1]
807 else:
808 command = ''
809 if not Utils.list_exists(listname) or command not in (
810 '', 'admin', 'owner', 'request', 'join', 'leave'):
811 continue
812 listnames.append((rcpt, listname, command))
813 # Remove all list recipients from rcpttos and forward what we're not
814 # going to take care of ourselves. Linear removal should be fine
815 # since we don't expect a large number of recipients.
816 for rcpt, listname, command in listnames:
817 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000818 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000819 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000820 if rcpttos:
821 refused = self._deliver(mailfrom, rcpttos, data)
822 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000823 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000824 # Now deliver directly to the list commands
825 mlists = {}
826 s = StringIO(data)
827 msg = Message.Message(s)
828 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000829 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000830 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000831 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000832 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000833 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000834 msg['Date'] = time.ctime(time.time())
835 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000836 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000837 mlist = mlists.get(listname)
838 if not mlist:
839 mlist = MailList.MailList(listname, lock=0)
840 mlists[listname] = mlist
841 # dispatch on the type of command
842 if command == '':
843 # post
844 msg.Enqueue(mlist, tolist=1)
845 elif command == 'admin':
846 msg.Enqueue(mlist, toadmin=1)
847 elif command == 'owner':
848 msg.Enqueue(mlist, toowner=1)
849 elif command == 'request':
850 msg.Enqueue(mlist, torequest=1)
851 elif command in ('join', 'leave'):
852 # TBD: this is a hack!
853 if command == 'join':
854 msg['Subject'] = 'subscribe'
855 else:
856 msg['Subject'] = 'unsubscribe'
857 msg.Enqueue(mlist, torequest=1)
858
859
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000860class Options:
R David Murray2539e672014-08-09 16:40:49 -0400861 setuid = True
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000862 classname = 'PureProxy'
R David Murrayd1a30c92012-05-26 14:33:59 -0400863 size_limit = None
R David Murray2539e672014-08-09 16:40:49 -0400864 enable_SMTPUTF8 = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000865
866
867def parseargs():
868 global DEBUGSTREAM
869 try:
870 opts, args = getopt.getopt(
R David Murray2539e672014-08-09 16:40:49 -0400871 sys.argv[1:], 'nVhc:s:du',
872 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug',
873 'smtputf8'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000874 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000875 usage(1, e)
876
877 options = Options()
878 for opt, arg in opts:
879 if opt in ('-h', '--help'):
880 usage(0)
881 elif opt in ('-V', '--version'):
Serhiy Storchakac56894d2013-09-05 17:44:53 +0300882 print(__version__)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000883 sys.exit(0)
884 elif opt in ('-n', '--nosetuid'):
R David Murray2539e672014-08-09 16:40:49 -0400885 options.setuid = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000886 elif opt in ('-c', '--class'):
887 options.classname = arg
888 elif opt in ('-d', '--debug'):
889 DEBUGSTREAM = sys.stderr
R David Murray2539e672014-08-09 16:40:49 -0400890 elif opt in ('-u', '--smtputf8'):
891 options.enable_SMTPUTF8 = True
R David Murrayd1a30c92012-05-26 14:33:59 -0400892 elif opt in ('-s', '--size'):
893 try:
894 int_size = int(arg)
895 options.size_limit = int_size
896 except:
897 print('Invalid size: ' + arg, file=sys.stderr)
898 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000899
900 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000901 if len(args) < 1:
902 localspec = 'localhost:8025'
903 remotespec = 'localhost:25'
904 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000905 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000906 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000907 elif len(args) < 3:
908 localspec = args[0]
909 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000910 else:
911 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
912
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000913 # split into host/port pairs
914 i = localspec.find(':')
915 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000916 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000917 options.localhost = localspec[:i]
918 try:
919 options.localport = int(localspec[i+1:])
920 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000921 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000922 i = remotespec.find(':')
923 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000924 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000925 options.remotehost = remotespec[:i]
926 try:
927 options.remoteport = int(remotespec[i+1:])
928 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000929 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000930 return options
931
932
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000933if __name__ == '__main__':
934 options = parseargs()
935 # Become nobody
Florent Xicluna711f87c2011-10-20 23:03:43 +0200936 classname = options.classname
937 if "." in classname:
938 lastdot = classname.rfind(".")
939 mod = __import__(classname[:lastdot], globals(), locals(), [""])
940 classname = classname[lastdot+1:]
941 else:
942 import __main__ as mod
943 class_ = getattr(mod, classname)
944 proxy = class_((options.localhost, options.localport),
R David Murrayd1a30c92012-05-26 14:33:59 -0400945 (options.remotehost, options.remoteport),
R David Murray2539e672014-08-09 16:40:49 -0400946 options.size_limit, enable_SMTPUTF8=options.enable_SMTPUTF8)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000947 if options.setuid:
948 try:
949 import pwd
Brett Cannoncd171c82013-07-04 17:43:24 -0400950 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000951 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000952 sys.exit(1)
953 nobody = pwd.getpwnam('nobody')[2]
954 try:
955 os.setuid(nobody)
Giampaolo Rodola'0166a282013-02-12 15:14:17 +0100956 except PermissionError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000957 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000958 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000959 try:
960 asyncore.loop()
961 except KeyboardInterrupt:
962 pass