blob: 33653d4fbf4ae64702a62967bdbebc920eb5a45f [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,
R David Murray2539e672014-08-09 16:40:49 -0400131 map=None, enable_SMTPUTF8=False, decode_data=None):
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
R David Murray2539e672014-08-09 16:40:49 -0400137 self.enable_SMTPUTF8 = enable_SMTPUTF8
138 if enable_SMTPUTF8:
139 if decode_data:
140 ValueError("decode_data and enable_SMTPUTF8 cannot be set to"
141 " True at the same time")
142 decode_data = False
R David Murray554bcbf2014-06-11 11:18:08 -0400143 if decode_data is None:
144 warn("The decode_data default of True will change to False in 3.6;"
145 " specify an explicit value for this keyword",
146 DeprecationWarning, 2)
147 decode_data = True
148 self._decode_data = decode_data
149 if decode_data:
150 self._emptystring = ''
151 self._linesep = '\r\n'
152 self._dotsep = '.'
153 self._newline = NEWLINE
154 else:
155 self._emptystring = b''
156 self._linesep = b'\r\n'
157 self._dotsep = b'.'
158 self._newline = b'\n'
R David Murray2539e672014-08-09 16:40:49 -0400159 self._set_rset_state()
Richard Jones803ef8a2010-07-24 09:51:40 +0000160 self.seen_greeting = ''
R David Murray2539e672014-08-09 16:40:49 -0400161 self.extended_smtp = False
162 self.command_size_limits.clear()
Richard Jones803ef8a2010-07-24 09:51:40 +0000163 self.fqdn = socket.getfqdn()
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000164 try:
165 self.peer = conn.getpeername()
Andrew Svetlov0832af62012-12-18 23:10:48 +0200166 except OSError as err:
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000167 # a race condition may occur if the other end is closing
168 # before we can get the peername
169 self.close()
170 if err.args[0] != errno.ENOTCONN:
171 raise
172 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000173 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
174 self.push('220 %s %s' % (self.fqdn, __version__))
R David Murray2539e672014-08-09 16:40:49 -0400175
176 def _set_post_data_state(self):
177 """Reset state variables to their post-DATA state."""
178 self.smtp_state = self.COMMAND
179 self.mailfrom = None
180 self.rcpttos = []
181 self.require_SMTPUTF8 = False
182 self.num_bytes = 0
Josiah Carlsond74900e2008-07-07 04:15:08 +0000183 self.set_terminator(b'\r\n')
R David Murray2539e672014-08-09 16:40:49 -0400184
185 def _set_rset_state(self):
186 """Reset all state variables except the greeting."""
187 self._set_post_data_state()
188 self.received_data = ''
189 self.received_lines = []
190
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000191
Richard Jones803ef8a2010-07-24 09:51:40 +0000192 # properties for backwards-compatibility
193 @property
194 def __server(self):
195 warn("Access to __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100196 "use 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000197 return self.smtp_server
198 @__server.setter
199 def __server(self, value):
200 warn("Setting __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100201 "set 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000202 self.smtp_server = value
203
204 @property
205 def __line(self):
206 warn("Access to __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100207 "use 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000208 return self.received_lines
209 @__line.setter
210 def __line(self, value):
211 warn("Setting __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100212 "set 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000213 self.received_lines = value
214
215 @property
216 def __state(self):
217 warn("Access to __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100218 "use 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000219 return self.smtp_state
220 @__state.setter
221 def __state(self, value):
222 warn("Setting __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100223 "set 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000224 self.smtp_state = value
225
226 @property
227 def __greeting(self):
228 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100229 "use 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000230 return self.seen_greeting
231 @__greeting.setter
232 def __greeting(self, value):
233 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100234 "set 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000235 self.seen_greeting = value
236
237 @property
238 def __mailfrom(self):
239 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100240 "use 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000241 return self.mailfrom
242 @__mailfrom.setter
243 def __mailfrom(self, value):
244 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100245 "set 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000246 self.mailfrom = value
247
248 @property
249 def __rcpttos(self):
250 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100251 "use 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000252 return self.rcpttos
253 @__rcpttos.setter
254 def __rcpttos(self, value):
255 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100256 "set 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000257 self.rcpttos = value
258
259 @property
260 def __data(self):
261 warn("Access to __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100262 "use 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000263 return self.received_data
264 @__data.setter
265 def __data(self, value):
266 warn("Setting __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100267 "set 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000268 self.received_data = value
269
270 @property
271 def __fqdn(self):
272 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100273 "use 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000274 return self.fqdn
275 @__fqdn.setter
276 def __fqdn(self, value):
277 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100278 "set 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000279 self.fqdn = value
280
281 @property
282 def __peer(self):
283 warn("Access to __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100284 "use 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000285 return self.peer
286 @__peer.setter
287 def __peer(self, value):
288 warn("Setting __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100289 "set 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000290 self.peer = value
291
292 @property
293 def __conn(self):
294 warn("Access to __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100295 "use 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000296 return self.conn
297 @__conn.setter
298 def __conn(self, value):
299 warn("Setting __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100300 "set 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000301 self.conn = value
302
303 @property
304 def __addr(self):
305 warn("Access to __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100306 "use 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000307 return self.addr
308 @__addr.setter
309 def __addr(self, value):
310 warn("Setting __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100311 "set 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000312 self.addr = value
313
R David Murray2539e672014-08-09 16:40:49 -0400314 # Overrides base class for convenience.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000315 def push(self, msg):
R David Murray2539e672014-08-09 16:40:49 -0400316 asynchat.async_chat.push(self, bytes(
317 msg + '\r\n', 'utf-8' if self.require_SMTPUTF8 else 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000318
319 # Implementation of base class abstract method
320 def collect_incoming_data(self, data):
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000321 limit = None
322 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400323 limit = self.max_command_size_limit
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000324 elif self.smtp_state == self.DATA:
325 limit = self.data_size_limit
326 if limit and self.num_bytes > limit:
327 return
328 elif limit:
329 self.num_bytes += len(data)
R David Murray554bcbf2014-06-11 11:18:08 -0400330 if self._decode_data:
331 self.received_lines.append(str(data, 'utf-8'))
332 else:
333 self.received_lines.append(data)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000334
335 # Implementation of base class abstract method
336 def found_terminator(self):
R David Murray554bcbf2014-06-11 11:18:08 -0400337 line = self._emptystring.join(self.received_lines)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000338 print('Data:', repr(line), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000339 self.received_lines = []
340 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400341 sz, self.num_bytes = self.num_bytes, 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000342 if not line:
343 self.push('500 Error: bad syntax')
344 return
R David Murray554bcbf2014-06-11 11:18:08 -0400345 if not self._decode_data:
346 line = str(line, 'utf-8')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000347 i = line.find(' ')
348 if i < 0:
349 command = line.upper()
350 arg = None
351 else:
352 command = line[:i].upper()
353 arg = line[i+1:].strip()
R David Murrayd1a30c92012-05-26 14:33:59 -0400354 max_sz = (self.command_size_limits[command]
355 if self.extended_smtp else self.command_size_limit)
356 if sz > max_sz:
357 self.push('500 Error: line too long')
358 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000359 method = getattr(self, 'smtp_' + command, None)
360 if not method:
R David Murrayd1a30c92012-05-26 14:33:59 -0400361 self.push('500 Error: command "%s" not recognized' % command)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000362 return
363 method(arg)
364 return
365 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000366 if self.smtp_state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000367 self.push('451 Internal confusion')
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000368 self.num_bytes = 0
369 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400370 if self.data_size_limit and self.num_bytes > self.data_size_limit:
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000371 self.push('552 Error: Too much mail data')
372 self.num_bytes = 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000373 return
374 # Remove extraneous carriage returns and de-transparency according
R David Murrayd1a30c92012-05-26 14:33:59 -0400375 # to RFC 5321, Section 4.5.2.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000376 data = []
R David Murray554bcbf2014-06-11 11:18:08 -0400377 for text in line.split(self._linesep):
378 if text and text[0] == self._dotsep:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000379 data.append(text[1:])
380 else:
381 data.append(text)
R David Murray554bcbf2014-06-11 11:18:08 -0400382 self.received_data = self._newline.join(data)
R David Murray2539e672014-08-09 16:40:49 -0400383 args = (self.peer, self.mailfrom, self.rcpttos, self.received_data)
384 if self.require_SMTPUTF8:
385 status = self.smtp_server.process_smtputf8_message(*args)
386 else:
387 status = self.smtp_server.process_message(*args)
388 self._set_post_data_state()
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000389 if not status:
R David Murrayd1a30c92012-05-26 14:33:59 -0400390 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000391 else:
392 self.push(status)
393
394 # SMTP and ESMTP commands
395 def smtp_HELO(self, arg):
396 if not arg:
397 self.push('501 Syntax: HELO hostname')
398 return
R David Murray2539e672014-08-09 16:40:49 -0400399 # See issue #21783 for a discussion of this behavior.
Richard Jones803ef8a2010-07-24 09:51:40 +0000400 if self.seen_greeting:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000401 self.push('503 Duplicate HELO/EHLO')
R David Murray2539e672014-08-09 16:40:49 -0400402 return
403 self._set_rset_state()
404 self.seen_greeting = arg
405 self.push('250 %s' % self.fqdn)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000406
R David Murrayd1a30c92012-05-26 14:33:59 -0400407 def smtp_EHLO(self, arg):
408 if not arg:
409 self.push('501 Syntax: EHLO hostname')
410 return
R David Murray2539e672014-08-09 16:40:49 -0400411 # See issue #21783 for a discussion of this behavior.
R David Murrayd1a30c92012-05-26 14:33:59 -0400412 if self.seen_greeting:
413 self.push('503 Duplicate HELO/EHLO')
R David Murray2539e672014-08-09 16:40:49 -0400414 return
415 self._set_rset_state()
416 self.seen_greeting = arg
417 self.extended_smtp = True
418 self.push('250-%s' % self.fqdn)
419 if self.data_size_limit:
420 self.push('250-SIZE %s' % self.data_size_limit)
421 self.command_size_limits['MAIL'] += 26
422 if self.enable_SMTPUTF8:
423 self.push('250-8BITMIME')
424 self.push('250-SMTPUTF8')
425 self.command_size_limits['MAIL'] += 10
426 self.push('250 HELP')
R David Murrayd1a30c92012-05-26 14:33:59 -0400427
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000428 def smtp_NOOP(self, arg):
429 if arg:
430 self.push('501 Syntax: NOOP')
431 else:
R David Murrayd1a30c92012-05-26 14:33:59 -0400432 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000433
434 def smtp_QUIT(self, arg):
435 # args is ignored
436 self.push('221 Bye')
437 self.close_when_done()
438
R David Murrayd1a30c92012-05-26 14:33:59 -0400439 def _strip_command_keyword(self, keyword, arg):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000440 keylen = len(keyword)
441 if arg[:keylen].upper() == keyword:
R David Murrayd1a30c92012-05-26 14:33:59 -0400442 return arg[keylen:].strip()
443 return ''
444
445 def _getaddr(self, arg):
446 if not arg:
447 return '', ''
448 if arg.lstrip().startswith('<'):
449 address, rest = get_angle_addr(arg)
450 else:
451 address, rest = get_addr_spec(arg)
452 if not address:
453 return address, rest
454 return address.addr_spec, rest
455
456 def _getparams(self, params):
457 # Return any parameters that appear to be syntactically valid according
458 # to RFC 1869, ignore all others. (Postel rule: accept what we can.)
R David Murray2539e672014-08-09 16:40:49 -0400459 params = [param.split('=', 1) if '=' in param else (param, True)
460 for param in params.split()]
R David Murrayd1a30c92012-05-26 14:33:59 -0400461 return {k: v for k, v in params if k.isalnum()}
462
463 def smtp_HELP(self, arg):
464 if arg:
465 extended = ' [SP <mail parameters]'
466 lc_arg = arg.upper()
467 if lc_arg == 'EHLO':
468 self.push('250 Syntax: EHLO hostname')
469 elif lc_arg == 'HELO':
470 self.push('250 Syntax: HELO hostname')
471 elif lc_arg == 'MAIL':
472 msg = '250 Syntax: MAIL FROM: <address>'
473 if self.extended_smtp:
474 msg += extended
475 self.push(msg)
476 elif lc_arg == 'RCPT':
477 msg = '250 Syntax: RCPT TO: <address>'
478 if self.extended_smtp:
479 msg += extended
480 self.push(msg)
481 elif lc_arg == 'DATA':
482 self.push('250 Syntax: DATA')
483 elif lc_arg == 'RSET':
484 self.push('250 Syntax: RSET')
485 elif lc_arg == 'NOOP':
486 self.push('250 Syntax: NOOP')
487 elif lc_arg == 'QUIT':
488 self.push('250 Syntax: QUIT')
489 elif lc_arg == 'VRFY':
490 self.push('250 Syntax: VRFY <address>')
491 else:
492 self.push('501 Supported commands: EHLO HELO MAIL RCPT '
493 'DATA RSET NOOP QUIT VRFY')
494 else:
495 self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '
496 'RSET NOOP QUIT VRFY')
497
498 def smtp_VRFY(self, arg):
499 if arg:
500 address, params = self._getaddr(arg)
501 if address:
502 self.push('252 Cannot VRFY user, but will accept message '
503 'and attempt delivery')
504 else:
505 self.push('502 Could not VRFY %s' % arg)
506 else:
507 self.push('501 Syntax: VRFY <address>')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000508
509 def smtp_MAIL(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400510 if not self.seen_greeting:
511 self.push('503 Error: send HELO first');
512 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000513 print('===> MAIL', arg, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400514 syntaxerr = '501 Syntax: MAIL FROM: <address>'
515 if self.extended_smtp:
516 syntaxerr += ' [SP <mail-parameters>]'
517 if arg is None:
518 self.push(syntaxerr)
519 return
520 arg = self._strip_command_keyword('FROM:', arg)
521 address, params = self._getaddr(arg)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000522 if not address:
R David Murrayd1a30c92012-05-26 14:33:59 -0400523 self.push(syntaxerr)
524 return
525 if not self.extended_smtp and params:
526 self.push(syntaxerr)
527 return
528 if not address:
529 self.push(syntaxerr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000530 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000531 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000532 self.push('503 Error: nested MAIL command')
533 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400534 params = self._getparams(params.upper())
535 if params is None:
536 self.push(syntaxerr)
537 return
R David Murray2539e672014-08-09 16:40:49 -0400538 body = params.pop('BODY', '7BIT')
539 if self.enable_SMTPUTF8 and params.pop('SMTPUTF8', False):
540 if body != '8BITMIME':
541 self.push('501 Syntax: MAIL FROM: <address>'
542 ' [BODY=8BITMIME SMTPUTF8]')
543 return
544 else:
545 self.require_SMTPUTF8 = True
R David Murrayd1a30c92012-05-26 14:33:59 -0400546 size = params.pop('SIZE', None)
547 if size:
548 if not size.isdigit():
549 self.push(syntaxerr)
550 return
551 elif self.data_size_limit and int(size) > self.data_size_limit:
552 self.push('552 Error: message size exceeds fixed maximum message size')
553 return
554 if len(params.keys()) > 0:
555 self.push('555 MAIL FROM parameters not recognized or not implemented')
556 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000557 self.mailfrom = address
558 print('sender:', self.mailfrom, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400559 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000560
561 def smtp_RCPT(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400562 if not self.seen_greeting:
563 self.push('503 Error: send HELO first');
564 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000565 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000566 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000567 self.push('503 Error: need MAIL command')
568 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400569 syntaxerr = '501 Syntax: RCPT TO: <address>'
570 if self.extended_smtp:
571 syntaxerr += ' [SP <mail-parameters>]'
572 if arg is None:
573 self.push(syntaxerr)
574 return
575 arg = self._strip_command_keyword('TO:', arg)
576 address, params = self._getaddr(arg)
577 if not address:
578 self.push(syntaxerr)
579 return
580 if params:
581 if self.extended_smtp:
582 params = self._getparams(params.upper())
583 if params is None:
584 self.push(syntaxerr)
585 return
586 else:
587 self.push(syntaxerr)
588 return
589 if not address:
590 self.push(syntaxerr)
591 return
592 if params and len(params.keys()) > 0:
593 self.push('555 RCPT TO parameters not recognized or not implemented')
594 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000595 if not address:
596 self.push('501 Syntax: RCPT TO: <address>')
597 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000598 self.rcpttos.append(address)
599 print('recips:', self.rcpttos, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400600 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000601
602 def smtp_RSET(self, arg):
603 if arg:
604 self.push('501 Syntax: RSET')
605 return
R David Murray2539e672014-08-09 16:40:49 -0400606 self._set_rset_state()
R David Murrayd1a30c92012-05-26 14:33:59 -0400607 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000608
609 def smtp_DATA(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400610 if not self.seen_greeting:
611 self.push('503 Error: send HELO first');
612 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000613 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000614 self.push('503 Error: need RCPT command')
615 return
616 if arg:
617 self.push('501 Syntax: DATA')
618 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000619 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000620 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000621 self.push('354 End data with <CR><LF>.<CR><LF>')
622
R David Murrayd1a30c92012-05-26 14:33:59 -0400623 # Commands that have not been implemented
624 def smtp_EXPN(self, arg):
625 self.push('502 EXPN not implemented')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000626
R David Murrayd1a30c92012-05-26 14:33:59 -0400627
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000628class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000629 # SMTPChannel class to use for managing client connections
630 channel_class = SMTPChannel
631
R David Murrayd1a30c92012-05-26 14:33:59 -0400632 def __init__(self, localaddr, remoteaddr,
R David Murray554bcbf2014-06-11 11:18:08 -0400633 data_size_limit=DATA_SIZE_DEFAULT, map=None,
R David Murray2539e672014-08-09 16:40:49 -0400634 enable_SMTPUTF8=False, decode_data=None):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000635 self._localaddr = localaddr
636 self._remoteaddr = remoteaddr
R David Murrayd1a30c92012-05-26 14:33:59 -0400637 self.data_size_limit = data_size_limit
R David Murray2539e672014-08-09 16:40:49 -0400638 self.enable_SMTPUTF8 = enable_SMTPUTF8
639 if enable_SMTPUTF8:
640 if decode_data:
641 raise ValueError("The decode_data and enable_SMTPUTF8"
642 " parameters cannot be set to True at the"
643 " same time.")
644 decode_data = False
R David Murray554bcbf2014-06-11 11:18:08 -0400645 if decode_data is None:
646 warn("The decode_data default of True will change to False in 3.6;"
647 " specify an explicit value for this keyword",
648 DeprecationWarning, 2)
649 decode_data = True
650 self._decode_data = decode_data
Vinay Sajip30298b42013-06-07 15:21:41 +0100651 asyncore.dispatcher.__init__(self, map=map)
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000652 try:
R David Murray012a83a2014-06-11 15:17:50 -0400653 gai_results = socket.getaddrinfo(*localaddr,
654 type=socket.SOCK_STREAM)
R David Murray6fe56a32014-06-11 13:48:58 -0400655 self.create_socket(gai_results[0][0], gai_results[0][1])
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000656 # try to re-use a server port if possible
657 self.set_reuse_addr()
658 self.bind(localaddr)
659 self.listen(5)
660 except:
661 self.close()
662 raise
663 else:
664 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
665 self.__class__.__name__, time.ctime(time.time()),
666 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000667
Giampaolo Rodolà977c7072010-10-04 21:08:36 +0000668 def handle_accepted(self, conn, addr):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000669 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
R David Murray2539e672014-08-09 16:40:49 -0400670 channel = self.channel_class(self,
671 conn,
672 addr,
673 self.data_size_limit,
674 self._map,
675 self.enable_SMTPUTF8,
676 self._decode_data)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000677
678 # API for "doing something useful with the message"
679 def process_message(self, peer, mailfrom, rcpttos, data):
680 """Override this abstract method to handle messages from the client.
681
682 peer is a tuple containing (ipaddr, port) of the client that made the
683 socket connection to our smtp port.
684
685 mailfrom is the raw address the client claims the message is coming
686 from.
687
688 rcpttos is a list of raw addresses the client wishes to deliver the
689 message to.
690
691 data is a string containing the entire full text of the message,
692 headers (if supplied) and all. It has been `de-transparencied'
693 according to RFC 821, Section 4.5.2. In other words, a line
694 containing a `.' followed by other text has had the leading dot
695 removed.
696
R David Murray2539e672014-08-09 16:40:49 -0400697 This function should return None for a normal `250 Ok' response;
698 otherwise, it should return the desired response string in RFC 821
699 format.
700
701 """
702 raise NotImplementedError
703
704 # API for processing messeges needing Unicode support (RFC 6531, RFC 6532).
705 def process_smtputf8_message(self, peer, mailfrom, rcpttos, data):
706 """Same as ``process_message`` but for messages for which the client
707 has sent the SMTPUTF8 parameter with the MAIL command (see the
708 enable_SMTPUTF8 parameter of the constructor).
709
710 This function should return None for a normal `250 Ok' response;
711 otherwise, it should return the desired response string in RFC 6531
712 format.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000713
714 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000715 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000716
Tim Peters658cba62001-02-09 20:06:00 +0000717
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000718class DebuggingServer(SMTPServer):
R David Murray2539e672014-08-09 16:40:49 -0400719
720 def _print_message_content(self, peer, data):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000721 inheaders = 1
R David Murray2539e672014-08-09 16:40:49 -0400722 lines = data.splitlines()
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000723 for line in lines:
724 # headers first
725 if inheaders and not line:
R David Murray2539e672014-08-09 16:40:49 -0400726 peerheader = 'X-Peer: ' + peer[0]
727 if not isinstance(data, str):
728 # decoded_data=false; make header match other binary output
729 peerheader = repr(peerheader.encode('utf-8'))
730 print(peerheader)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000731 inheaders = 0
R David Murray2539e672014-08-09 16:40:49 -0400732 if not isinstance(data, str):
733 # Avoid spurious 'str on bytes instance' warning.
734 line = repr(line)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000735 print(line)
R David Murray2539e672014-08-09 16:40:49 -0400736
737 def process_message(self, peer, mailfrom, rcpttos, data):
738 print('---------- MESSAGE FOLLOWS ----------')
739 self._print_message_content(peer, data)
740 print('------------ END MESSAGE ------------')
741
742 def process_smtputf8_message(self, peer, mailfrom, rcpttos, data):
743 print('----- SMTPUTF8 MESSAGE FOLLOWS ------')
744 self._print_message_content(peer, data)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000745 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000746
747
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000748class PureProxy(SMTPServer):
R David Murray2539e672014-08-09 16:40:49 -0400749 def __init__(self, *args, **kwargs):
750 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
751 raise ValueError("PureProxy does not support SMTPUTF8.")
752 super(PureProxy, self).__init__(*args, **kwargs)
753
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000754 def process_message(self, peer, mailfrom, rcpttos, data):
755 lines = data.split('\n')
756 # Look for the last header
757 i = 0
758 for line in lines:
759 if not line:
760 break
761 i += 1
762 lines.insert(i, 'X-Peer: %s' % peer[0])
763 data = NEWLINE.join(lines)
764 refused = self._deliver(mailfrom, rcpttos, data)
765 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000766 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000767
768 def _deliver(self, mailfrom, rcpttos, data):
769 import smtplib
770 refused = {}
771 try:
772 s = smtplib.SMTP()
773 s.connect(self._remoteaddr[0], self._remoteaddr[1])
774 try:
775 refused = s.sendmail(mailfrom, rcpttos, data)
776 finally:
777 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000778 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000779 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000780 refused = e.recipients
Andrew Svetlov0832af62012-12-18 23:10:48 +0200781 except (OSError, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000782 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000783 # All recipients were refused. If the exception had an associated
784 # error code, use it. Otherwise,fake it with a non-triggering
785 # exception code.
786 errcode = getattr(e, 'smtp_code', -1)
787 errmsg = getattr(e, 'smtp_error', 'ignore')
788 for r in rcpttos:
789 refused[r] = (errcode, errmsg)
790 return refused
791
792
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000793class MailmanProxy(PureProxy):
R David Murray2539e672014-08-09 16:40:49 -0400794 def __init__(self, *args, **kwargs):
795 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
796 raise ValueError("MailmanProxy does not support SMTPUTF8.")
797 super(PureProxy, self).__init__(*args, **kwargs)
798
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000799 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000800 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000801 from Mailman import Utils
802 from Mailman import Message
803 from Mailman import MailList
804 # If the message is to a Mailman mailing list, then we'll invoke the
805 # Mailman script directly, without going through the real smtpd.
806 # Otherwise we'll forward it to the local proxy for disposition.
807 listnames = []
808 for rcpt in rcpttos:
809 local = rcpt.lower().split('@')[0]
810 # We allow the following variations on the theme
811 # listname
812 # listname-admin
813 # listname-owner
814 # listname-request
815 # listname-join
816 # listname-leave
817 parts = local.split('-')
818 if len(parts) > 2:
819 continue
820 listname = parts[0]
821 if len(parts) == 2:
822 command = parts[1]
823 else:
824 command = ''
825 if not Utils.list_exists(listname) or command not in (
826 '', 'admin', 'owner', 'request', 'join', 'leave'):
827 continue
828 listnames.append((rcpt, listname, command))
829 # Remove all list recipients from rcpttos and forward what we're not
830 # going to take care of ourselves. Linear removal should be fine
831 # since we don't expect a large number of recipients.
832 for rcpt, listname, command in listnames:
833 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000834 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000835 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000836 if rcpttos:
837 refused = self._deliver(mailfrom, rcpttos, data)
838 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000839 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000840 # Now deliver directly to the list commands
841 mlists = {}
842 s = StringIO(data)
843 msg = Message.Message(s)
844 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000845 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000846 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000847 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000848 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000849 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000850 msg['Date'] = time.ctime(time.time())
851 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000852 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000853 mlist = mlists.get(listname)
854 if not mlist:
855 mlist = MailList.MailList(listname, lock=0)
856 mlists[listname] = mlist
857 # dispatch on the type of command
858 if command == '':
859 # post
860 msg.Enqueue(mlist, tolist=1)
861 elif command == 'admin':
862 msg.Enqueue(mlist, toadmin=1)
863 elif command == 'owner':
864 msg.Enqueue(mlist, toowner=1)
865 elif command == 'request':
866 msg.Enqueue(mlist, torequest=1)
867 elif command in ('join', 'leave'):
868 # TBD: this is a hack!
869 if command == 'join':
870 msg['Subject'] = 'subscribe'
871 else:
872 msg['Subject'] = 'unsubscribe'
873 msg.Enqueue(mlist, torequest=1)
874
875
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000876class Options:
R David Murray2539e672014-08-09 16:40:49 -0400877 setuid = True
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000878 classname = 'PureProxy'
R David Murrayd1a30c92012-05-26 14:33:59 -0400879 size_limit = None
R David Murray2539e672014-08-09 16:40:49 -0400880 enable_SMTPUTF8 = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000881
882
883def parseargs():
884 global DEBUGSTREAM
885 try:
886 opts, args = getopt.getopt(
R David Murray2539e672014-08-09 16:40:49 -0400887 sys.argv[1:], 'nVhc:s:du',
888 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug',
889 'smtputf8'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000890 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000891 usage(1, e)
892
893 options = Options()
894 for opt, arg in opts:
895 if opt in ('-h', '--help'):
896 usage(0)
897 elif opt in ('-V', '--version'):
Serhiy Storchakac56894d2013-09-05 17:44:53 +0300898 print(__version__)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000899 sys.exit(0)
900 elif opt in ('-n', '--nosetuid'):
R David Murray2539e672014-08-09 16:40:49 -0400901 options.setuid = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000902 elif opt in ('-c', '--class'):
903 options.classname = arg
904 elif opt in ('-d', '--debug'):
905 DEBUGSTREAM = sys.stderr
R David Murray2539e672014-08-09 16:40:49 -0400906 elif opt in ('-u', '--smtputf8'):
907 options.enable_SMTPUTF8 = True
R David Murrayd1a30c92012-05-26 14:33:59 -0400908 elif opt in ('-s', '--size'):
909 try:
910 int_size = int(arg)
911 options.size_limit = int_size
912 except:
913 print('Invalid size: ' + arg, file=sys.stderr)
914 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000915
916 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000917 if len(args) < 1:
918 localspec = 'localhost:8025'
919 remotespec = 'localhost:25'
920 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000921 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000922 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000923 elif len(args) < 3:
924 localspec = args[0]
925 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000926 else:
927 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
928
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000929 # split into host/port pairs
930 i = localspec.find(':')
931 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000932 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000933 options.localhost = localspec[:i]
934 try:
935 options.localport = int(localspec[i+1:])
936 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000937 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000938 i = remotespec.find(':')
939 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000940 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000941 options.remotehost = remotespec[:i]
942 try:
943 options.remoteport = int(remotespec[i+1:])
944 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000945 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000946 return options
947
948
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000949if __name__ == '__main__':
950 options = parseargs()
951 # Become nobody
Florent Xicluna711f87c2011-10-20 23:03:43 +0200952 classname = options.classname
953 if "." in classname:
954 lastdot = classname.rfind(".")
955 mod = __import__(classname[:lastdot], globals(), locals(), [""])
956 classname = classname[lastdot+1:]
957 else:
958 import __main__ as mod
959 class_ = getattr(mod, classname)
960 proxy = class_((options.localhost, options.localport),
R David Murrayd1a30c92012-05-26 14:33:59 -0400961 (options.remotehost, options.remoteport),
R David Murray2539e672014-08-09 16:40:49 -0400962 options.size_limit, enable_SMTPUTF8=options.enable_SMTPUTF8)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000963 if options.setuid:
964 try:
965 import pwd
Brett Cannoncd171c82013-07-04 17:43:24 -0400966 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000967 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000968 sys.exit(1)
969 nobody = pwd.getpwnam('nobody')[2]
970 try:
971 os.setuid(nobody)
Giampaolo Rodola'0166a282013-02-12 15:14:17 +0100972 except PermissionError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000973 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000974 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000975 try:
976 asyncore.loop()
977 except KeyboardInterrupt:
978 pass