blob: dd410b8e169b84da2c15e68b96e4ba90ee41b465 [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'
Serhiy Storchakaee4c0b92015-03-20 16:48:02 +0200157 self._dotsep = ord(b'.')
R David Murray554bcbf2014-06-11 11:18:08 -0400158 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:
Benjamin Peterson0c803312015-04-05 10:01:48 -0400465 extended = ' [SP <mail-parameters>]'
R David Murrayd1a30c92012-05-26 14:33:59 -0400466 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
Richard Jones803ef8a2010-07-24 09:51:40 +0000528 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000529 self.push('503 Error: nested MAIL command')
530 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400531 params = self._getparams(params.upper())
532 if params is None:
533 self.push(syntaxerr)
534 return
R David Murray2539e672014-08-09 16:40:49 -0400535 body = params.pop('BODY', '7BIT')
536 if self.enable_SMTPUTF8 and params.pop('SMTPUTF8', False):
537 if body != '8BITMIME':
538 self.push('501 Syntax: MAIL FROM: <address>'
539 ' [BODY=8BITMIME SMTPUTF8]')
540 return
541 else:
542 self.require_SMTPUTF8 = True
R David Murrayd1a30c92012-05-26 14:33:59 -0400543 size = params.pop('SIZE', None)
544 if size:
545 if not size.isdigit():
546 self.push(syntaxerr)
547 return
548 elif self.data_size_limit and int(size) > self.data_size_limit:
549 self.push('552 Error: message size exceeds fixed maximum message size')
550 return
551 if len(params.keys()) > 0:
552 self.push('555 MAIL FROM parameters not recognized or not implemented')
553 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000554 self.mailfrom = address
555 print('sender:', self.mailfrom, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400556 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000557
558 def smtp_RCPT(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400559 if not self.seen_greeting:
560 self.push('503 Error: send HELO first');
561 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000562 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000563 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000564 self.push('503 Error: need MAIL command')
565 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400566 syntaxerr = '501 Syntax: RCPT TO: <address>'
567 if self.extended_smtp:
568 syntaxerr += ' [SP <mail-parameters>]'
569 if arg is None:
570 self.push(syntaxerr)
571 return
572 arg = self._strip_command_keyword('TO:', arg)
573 address, params = self._getaddr(arg)
574 if not address:
575 self.push(syntaxerr)
576 return
577 if params:
578 if self.extended_smtp:
579 params = self._getparams(params.upper())
580 if params is None:
581 self.push(syntaxerr)
582 return
583 else:
584 self.push(syntaxerr)
585 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400586 if params and len(params.keys()) > 0:
587 self.push('555 RCPT TO parameters not recognized or not implemented')
588 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000589 self.rcpttos.append(address)
590 print('recips:', self.rcpttos, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400591 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000592
593 def smtp_RSET(self, arg):
594 if arg:
595 self.push('501 Syntax: RSET')
596 return
R David Murray2539e672014-08-09 16:40:49 -0400597 self._set_rset_state()
R David Murrayd1a30c92012-05-26 14:33:59 -0400598 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000599
600 def smtp_DATA(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400601 if not self.seen_greeting:
602 self.push('503 Error: send HELO first');
603 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000604 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000605 self.push('503 Error: need RCPT command')
606 return
607 if arg:
608 self.push('501 Syntax: DATA')
609 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000610 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000611 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000612 self.push('354 End data with <CR><LF>.<CR><LF>')
613
R David Murrayd1a30c92012-05-26 14:33:59 -0400614 # Commands that have not been implemented
615 def smtp_EXPN(self, arg):
616 self.push('502 EXPN not implemented')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000617
R David Murrayd1a30c92012-05-26 14:33:59 -0400618
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000619class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000620 # SMTPChannel class to use for managing client connections
621 channel_class = SMTPChannel
622
R David Murrayd1a30c92012-05-26 14:33:59 -0400623 def __init__(self, localaddr, remoteaddr,
R David Murray554bcbf2014-06-11 11:18:08 -0400624 data_size_limit=DATA_SIZE_DEFAULT, map=None,
R David Murray2539e672014-08-09 16:40:49 -0400625 enable_SMTPUTF8=False, decode_data=None):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000626 self._localaddr = localaddr
627 self._remoteaddr = remoteaddr
R David Murrayd1a30c92012-05-26 14:33:59 -0400628 self.data_size_limit = data_size_limit
R David Murray2539e672014-08-09 16:40:49 -0400629 self.enable_SMTPUTF8 = enable_SMTPUTF8
630 if enable_SMTPUTF8:
631 if decode_data:
632 raise ValueError("The decode_data and enable_SMTPUTF8"
633 " parameters cannot be set to True at the"
634 " same time.")
635 decode_data = False
R David Murray554bcbf2014-06-11 11:18:08 -0400636 if decode_data is None:
637 warn("The decode_data default of True will change to False in 3.6;"
638 " specify an explicit value for this keyword",
639 DeprecationWarning, 2)
640 decode_data = True
641 self._decode_data = decode_data
Vinay Sajip30298b42013-06-07 15:21:41 +0100642 asyncore.dispatcher.__init__(self, map=map)
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000643 try:
R David Murray012a83a2014-06-11 15:17:50 -0400644 gai_results = socket.getaddrinfo(*localaddr,
645 type=socket.SOCK_STREAM)
R David Murray6fe56a32014-06-11 13:48:58 -0400646 self.create_socket(gai_results[0][0], gai_results[0][1])
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000647 # try to re-use a server port if possible
648 self.set_reuse_addr()
649 self.bind(localaddr)
650 self.listen(5)
651 except:
652 self.close()
653 raise
654 else:
655 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
656 self.__class__.__name__, time.ctime(time.time()),
657 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000658
Giampaolo Rodolà977c7072010-10-04 21:08:36 +0000659 def handle_accepted(self, conn, addr):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000660 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
R David Murray2539e672014-08-09 16:40:49 -0400661 channel = self.channel_class(self,
662 conn,
663 addr,
664 self.data_size_limit,
665 self._map,
666 self.enable_SMTPUTF8,
667 self._decode_data)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000668
669 # API for "doing something useful with the message"
670 def process_message(self, peer, mailfrom, rcpttos, data):
671 """Override this abstract method to handle messages from the client.
672
673 peer is a tuple containing (ipaddr, port) of the client that made the
674 socket connection to our smtp port.
675
676 mailfrom is the raw address the client claims the message is coming
677 from.
678
679 rcpttos is a list of raw addresses the client wishes to deliver the
680 message to.
681
682 data is a string containing the entire full text of the message,
683 headers (if supplied) and all. It has been `de-transparencied'
684 according to RFC 821, Section 4.5.2. In other words, a line
685 containing a `.' followed by other text has had the leading dot
686 removed.
687
R David Murray2539e672014-08-09 16:40:49 -0400688 This function should return None for a normal `250 Ok' response;
689 otherwise, it should return the desired response string in RFC 821
690 format.
691
692 """
693 raise NotImplementedError
694
695 # API for processing messeges needing Unicode support (RFC 6531, RFC 6532).
696 def process_smtputf8_message(self, peer, mailfrom, rcpttos, data):
697 """Same as ``process_message`` but for messages for which the client
698 has sent the SMTPUTF8 parameter with the MAIL command (see the
699 enable_SMTPUTF8 parameter of the constructor).
700
701 This function should return None for a normal `250 Ok' response;
702 otherwise, it should return the desired response string in RFC 6531
703 format.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000704
705 """
Guido van Rossumb8b45ea2001-04-15 13:06:04 +0000706 raise NotImplementedError
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000707
Tim Peters658cba62001-02-09 20:06:00 +0000708
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000709class DebuggingServer(SMTPServer):
R David Murray2539e672014-08-09 16:40:49 -0400710
711 def _print_message_content(self, peer, data):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000712 inheaders = 1
R David Murray2539e672014-08-09 16:40:49 -0400713 lines = data.splitlines()
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000714 for line in lines:
715 # headers first
716 if inheaders and not line:
R David Murray2539e672014-08-09 16:40:49 -0400717 peerheader = 'X-Peer: ' + peer[0]
718 if not isinstance(data, str):
719 # decoded_data=false; make header match other binary output
720 peerheader = repr(peerheader.encode('utf-8'))
721 print(peerheader)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000722 inheaders = 0
R David Murray2539e672014-08-09 16:40:49 -0400723 if not isinstance(data, str):
724 # Avoid spurious 'str on bytes instance' warning.
725 line = repr(line)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000726 print(line)
R David Murray2539e672014-08-09 16:40:49 -0400727
728 def process_message(self, peer, mailfrom, rcpttos, data):
729 print('---------- MESSAGE FOLLOWS ----------')
730 self._print_message_content(peer, data)
731 print('------------ END MESSAGE ------------')
732
733 def process_smtputf8_message(self, peer, mailfrom, rcpttos, data):
734 print('----- SMTPUTF8 MESSAGE FOLLOWS ------')
735 self._print_message_content(peer, data)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000736 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000737
738
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000739class PureProxy(SMTPServer):
R David Murray2539e672014-08-09 16:40:49 -0400740 def __init__(self, *args, **kwargs):
741 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
742 raise ValueError("PureProxy does not support SMTPUTF8.")
743 super(PureProxy, self).__init__(*args, **kwargs)
744
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000745 def process_message(self, peer, mailfrom, rcpttos, data):
746 lines = data.split('\n')
747 # Look for the last header
748 i = 0
749 for line in lines:
750 if not line:
751 break
752 i += 1
753 lines.insert(i, 'X-Peer: %s' % peer[0])
754 data = NEWLINE.join(lines)
755 refused = self._deliver(mailfrom, rcpttos, data)
756 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000757 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000758
759 def _deliver(self, mailfrom, rcpttos, data):
760 import smtplib
761 refused = {}
762 try:
763 s = smtplib.SMTP()
764 s.connect(self._remoteaddr[0], self._remoteaddr[1])
765 try:
766 refused = s.sendmail(mailfrom, rcpttos, data)
767 finally:
768 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000769 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000770 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000771 refused = e.recipients
Andrew Svetlov0832af62012-12-18 23:10:48 +0200772 except (OSError, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000773 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000774 # All recipients were refused. If the exception had an associated
775 # error code, use it. Otherwise,fake it with a non-triggering
776 # exception code.
777 errcode = getattr(e, 'smtp_code', -1)
778 errmsg = getattr(e, 'smtp_error', 'ignore')
779 for r in rcpttos:
780 refused[r] = (errcode, errmsg)
781 return refused
782
783
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000784class MailmanProxy(PureProxy):
R David Murray2539e672014-08-09 16:40:49 -0400785 def __init__(self, *args, **kwargs):
786 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
787 raise ValueError("MailmanProxy does not support SMTPUTF8.")
788 super(PureProxy, self).__init__(*args, **kwargs)
789
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000790 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000791 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000792 from Mailman import Utils
793 from Mailman import Message
794 from Mailman import MailList
795 # If the message is to a Mailman mailing list, then we'll invoke the
796 # Mailman script directly, without going through the real smtpd.
797 # Otherwise we'll forward it to the local proxy for disposition.
798 listnames = []
799 for rcpt in rcpttos:
800 local = rcpt.lower().split('@')[0]
801 # We allow the following variations on the theme
802 # listname
803 # listname-admin
804 # listname-owner
805 # listname-request
806 # listname-join
807 # listname-leave
808 parts = local.split('-')
809 if len(parts) > 2:
810 continue
811 listname = parts[0]
812 if len(parts) == 2:
813 command = parts[1]
814 else:
815 command = ''
816 if not Utils.list_exists(listname) or command not in (
817 '', 'admin', 'owner', 'request', 'join', 'leave'):
818 continue
819 listnames.append((rcpt, listname, command))
820 # Remove all list recipients from rcpttos and forward what we're not
821 # going to take care of ourselves. Linear removal should be fine
822 # since we don't expect a large number of recipients.
823 for rcpt, listname, command in listnames:
824 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000825 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000826 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000827 if rcpttos:
828 refused = self._deliver(mailfrom, rcpttos, data)
829 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000830 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000831 # Now deliver directly to the list commands
832 mlists = {}
833 s = StringIO(data)
834 msg = Message.Message(s)
835 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000836 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000837 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000838 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000839 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000840 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000841 msg['Date'] = time.ctime(time.time())
842 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000843 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000844 mlist = mlists.get(listname)
845 if not mlist:
846 mlist = MailList.MailList(listname, lock=0)
847 mlists[listname] = mlist
848 # dispatch on the type of command
849 if command == '':
850 # post
851 msg.Enqueue(mlist, tolist=1)
852 elif command == 'admin':
853 msg.Enqueue(mlist, toadmin=1)
854 elif command == 'owner':
855 msg.Enqueue(mlist, toowner=1)
856 elif command == 'request':
857 msg.Enqueue(mlist, torequest=1)
858 elif command in ('join', 'leave'):
859 # TBD: this is a hack!
860 if command == 'join':
861 msg['Subject'] = 'subscribe'
862 else:
863 msg['Subject'] = 'unsubscribe'
864 msg.Enqueue(mlist, torequest=1)
865
866
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000867class Options:
R David Murray2539e672014-08-09 16:40:49 -0400868 setuid = True
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000869 classname = 'PureProxy'
R David Murrayd1a30c92012-05-26 14:33:59 -0400870 size_limit = None
R David Murray2539e672014-08-09 16:40:49 -0400871 enable_SMTPUTF8 = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000872
873
874def parseargs():
875 global DEBUGSTREAM
876 try:
877 opts, args = getopt.getopt(
R David Murray2539e672014-08-09 16:40:49 -0400878 sys.argv[1:], 'nVhc:s:du',
879 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug',
880 'smtputf8'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000881 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000882 usage(1, e)
883
884 options = Options()
885 for opt, arg in opts:
886 if opt in ('-h', '--help'):
887 usage(0)
888 elif opt in ('-V', '--version'):
Serhiy Storchakac56894d2013-09-05 17:44:53 +0300889 print(__version__)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000890 sys.exit(0)
891 elif opt in ('-n', '--nosetuid'):
R David Murray2539e672014-08-09 16:40:49 -0400892 options.setuid = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000893 elif opt in ('-c', '--class'):
894 options.classname = arg
895 elif opt in ('-d', '--debug'):
896 DEBUGSTREAM = sys.stderr
R David Murray2539e672014-08-09 16:40:49 -0400897 elif opt in ('-u', '--smtputf8'):
898 options.enable_SMTPUTF8 = True
R David Murrayd1a30c92012-05-26 14:33:59 -0400899 elif opt in ('-s', '--size'):
900 try:
901 int_size = int(arg)
902 options.size_limit = int_size
903 except:
904 print('Invalid size: ' + arg, file=sys.stderr)
905 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000906
907 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000908 if len(args) < 1:
909 localspec = 'localhost:8025'
910 remotespec = 'localhost:25'
911 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000912 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000913 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000914 elif len(args) < 3:
915 localspec = args[0]
916 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000917 else:
918 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
919
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000920 # split into host/port pairs
921 i = localspec.find(':')
922 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000923 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000924 options.localhost = localspec[:i]
925 try:
926 options.localport = int(localspec[i+1:])
927 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000928 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000929 i = remotespec.find(':')
930 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000931 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000932 options.remotehost = remotespec[:i]
933 try:
934 options.remoteport = int(remotespec[i+1:])
935 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000936 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000937 return options
938
939
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000940if __name__ == '__main__':
941 options = parseargs()
942 # Become nobody
Florent Xicluna711f87c2011-10-20 23:03:43 +0200943 classname = options.classname
944 if "." in classname:
945 lastdot = classname.rfind(".")
946 mod = __import__(classname[:lastdot], globals(), locals(), [""])
947 classname = classname[lastdot+1:]
948 else:
949 import __main__ as mod
950 class_ = getattr(mod, classname)
951 proxy = class_((options.localhost, options.localport),
R David Murrayd1a30c92012-05-26 14:33:59 -0400952 (options.remotehost, options.remoteport),
R David Murray2539e672014-08-09 16:40:49 -0400953 options.size_limit, enable_SMTPUTF8=options.enable_SMTPUTF8)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000954 if options.setuid:
955 try:
956 import pwd
Brett Cannoncd171c82013-07-04 17:43:24 -0400957 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000958 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000959 sys.exit(1)
960 nobody = pwd.getpwnam('nobody')[2]
961 try:
962 os.setuid(nobody)
Giampaolo Rodola'0166a282013-02-12 15:14:17 +0100963 except PermissionError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000964 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000965 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000966 try:
967 asyncore.loop()
968 except KeyboardInterrupt:
969 pass