blob: 1e2adc87a2bf2ada5495f4734e186ab61564cec8 [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
Martin Panter380ef012016-06-06 02:03:11 +000092__all__ = [
93 "SMTPChannel", "SMTPServer", "DebuggingServer", "PureProxy",
94 "MailmanProxy",
95]
Barry Warsaw7e0d9562001-01-31 22:51:35 +000096
97program = sys.argv[0]
R David Murrayd1a30c92012-05-26 14:33:59 -040098__version__ = 'Python SMTP proxy version 0.3'
Barry Warsaw7e0d9562001-01-31 22:51:35 +000099
100
101class Devnull:
102 def write(self, msg): pass
103 def flush(self): pass
104
105
106DEBUGSTREAM = Devnull()
107NEWLINE = '\n'
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000108COMMASPACE = ', '
R David Murrayd1a30c92012-05-26 14:33:59 -0400109DATA_SIZE_DEFAULT = 33554432
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000110
111
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000112def usage(code, msg=''):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000113 print(__doc__ % globals(), file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000114 if msg:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000115 print(msg, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000116 sys.exit(code)
117
118
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000119class SMTPChannel(asynchat.async_chat):
120 COMMAND = 0
121 DATA = 1
122
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000123 command_size_limit = 512
R David Murrayd1a30c92012-05-26 14:33:59 -0400124 command_size_limits = collections.defaultdict(lambda x=command_size_limit: x)
R David Murray2539e672014-08-09 16:40:49 -0400125
126 @property
127 def max_command_size_limit(self):
128 try:
129 return max(self.command_size_limits.values())
130 except ValueError:
131 return self.command_size_limit
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000132
Vinay Sajip30298b42013-06-07 15:21:41 +0100133 def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT,
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300134 map=None, enable_SMTPUTF8=False, decode_data=False):
Vinay Sajip30298b42013-06-07 15:21:41 +0100135 asynchat.async_chat.__init__(self, conn, map=map)
Richard Jones803ef8a2010-07-24 09:51:40 +0000136 self.smtp_server = server
137 self.conn = conn
138 self.addr = addr
R David Murrayd1a30c92012-05-26 14:33:59 -0400139 self.data_size_limit = data_size_limit
Serhiy Storchakaeb6cd742016-05-29 23:50:56 +0300140 self.enable_SMTPUTF8 = enable_SMTPUTF8
141 self._decode_data = decode_data
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300142 if enable_SMTPUTF8 and decode_data:
143 raise ValueError("decode_data and enable_SMTPUTF8 cannot"
144 " be set to True at the same time")
R David Murray554bcbf2014-06-11 11:18:08 -0400145 if decode_data:
146 self._emptystring = ''
147 self._linesep = '\r\n'
148 self._dotsep = '.'
149 self._newline = NEWLINE
150 else:
151 self._emptystring = b''
152 self._linesep = b'\r\n'
Serhiy Storchakaee4c0b92015-03-20 16:48:02 +0200153 self._dotsep = ord(b'.')
R David Murray554bcbf2014-06-11 11:18:08 -0400154 self._newline = b'\n'
R David Murray2539e672014-08-09 16:40:49 -0400155 self._set_rset_state()
Richard Jones803ef8a2010-07-24 09:51:40 +0000156 self.seen_greeting = ''
R David Murray2539e672014-08-09 16:40:49 -0400157 self.extended_smtp = False
158 self.command_size_limits.clear()
Richard Jones803ef8a2010-07-24 09:51:40 +0000159 self.fqdn = socket.getfqdn()
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000160 try:
161 self.peer = conn.getpeername()
Andrew Svetlov0832af62012-12-18 23:10:48 +0200162 except OSError as err:
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000163 # a race condition may occur if the other end is closing
164 # before we can get the peername
165 self.close()
Serhiy Storchakac4d45ee2020-11-22 10:28:34 +0200166 if err.errno != errno.ENOTCONN:
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000167 raise
168 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000169 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
170 self.push('220 %s %s' % (self.fqdn, __version__))
R David Murray2539e672014-08-09 16:40:49 -0400171
172 def _set_post_data_state(self):
173 """Reset state variables to their post-DATA state."""
174 self.smtp_state = self.COMMAND
175 self.mailfrom = None
176 self.rcpttos = []
177 self.require_SMTPUTF8 = False
178 self.num_bytes = 0
Josiah Carlsond74900e2008-07-07 04:15:08 +0000179 self.set_terminator(b'\r\n')
R David Murray2539e672014-08-09 16:40:49 -0400180
181 def _set_rset_state(self):
182 """Reset all state variables except the greeting."""
183 self._set_post_data_state()
184 self.received_data = ''
185 self.received_lines = []
186
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000187
Richard Jones803ef8a2010-07-24 09:51:40 +0000188 # properties for backwards-compatibility
189 @property
190 def __server(self):
191 warn("Access to __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100192 "use 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000193 return self.smtp_server
194 @__server.setter
195 def __server(self, value):
196 warn("Setting __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100197 "set 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000198 self.smtp_server = value
199
200 @property
201 def __line(self):
202 warn("Access to __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100203 "use 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000204 return self.received_lines
205 @__line.setter
206 def __line(self, value):
207 warn("Setting __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100208 "set 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000209 self.received_lines = value
210
211 @property
212 def __state(self):
213 warn("Access to __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100214 "use 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000215 return self.smtp_state
216 @__state.setter
217 def __state(self, value):
218 warn("Setting __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100219 "set 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000220 self.smtp_state = value
221
222 @property
223 def __greeting(self):
224 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100225 "use 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000226 return self.seen_greeting
227 @__greeting.setter
228 def __greeting(self, value):
229 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100230 "set 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000231 self.seen_greeting = value
232
233 @property
234 def __mailfrom(self):
235 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100236 "use 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000237 return self.mailfrom
238 @__mailfrom.setter
239 def __mailfrom(self, value):
240 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100241 "set 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000242 self.mailfrom = value
243
244 @property
245 def __rcpttos(self):
246 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100247 "use 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000248 return self.rcpttos
249 @__rcpttos.setter
250 def __rcpttos(self, value):
251 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100252 "set 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000253 self.rcpttos = value
254
255 @property
256 def __data(self):
257 warn("Access to __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100258 "use 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000259 return self.received_data
260 @__data.setter
261 def __data(self, value):
262 warn("Setting __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100263 "set 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000264 self.received_data = value
265
266 @property
267 def __fqdn(self):
268 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100269 "use 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000270 return self.fqdn
271 @__fqdn.setter
272 def __fqdn(self, value):
273 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100274 "set 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000275 self.fqdn = value
276
277 @property
278 def __peer(self):
279 warn("Access to __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100280 "use 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000281 return self.peer
282 @__peer.setter
283 def __peer(self, value):
284 warn("Setting __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100285 "set 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000286 self.peer = value
287
288 @property
289 def __conn(self):
290 warn("Access to __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100291 "use 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000292 return self.conn
293 @__conn.setter
294 def __conn(self, value):
295 warn("Setting __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100296 "set 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000297 self.conn = value
298
299 @property
300 def __addr(self):
301 warn("Access to __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100302 "use 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000303 return self.addr
304 @__addr.setter
305 def __addr(self, value):
306 warn("Setting __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100307 "set 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000308 self.addr = value
309
R David Murray2539e672014-08-09 16:40:49 -0400310 # Overrides base class for convenience.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000311 def push(self, msg):
R David Murray2539e672014-08-09 16:40:49 -0400312 asynchat.async_chat.push(self, bytes(
313 msg + '\r\n', 'utf-8' if self.require_SMTPUTF8 else 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000314
315 # Implementation of base class abstract method
316 def collect_incoming_data(self, data):
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000317 limit = None
318 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400319 limit = self.max_command_size_limit
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000320 elif self.smtp_state == self.DATA:
321 limit = self.data_size_limit
322 if limit and self.num_bytes > limit:
323 return
324 elif limit:
325 self.num_bytes += len(data)
R David Murray554bcbf2014-06-11 11:18:08 -0400326 if self._decode_data:
327 self.received_lines.append(str(data, 'utf-8'))
328 else:
329 self.received_lines.append(data)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000330
331 # Implementation of base class abstract method
332 def found_terminator(self):
R David Murray554bcbf2014-06-11 11:18:08 -0400333 line = self._emptystring.join(self.received_lines)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000334 print('Data:', repr(line), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000335 self.received_lines = []
336 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400337 sz, self.num_bytes = self.num_bytes, 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000338 if not line:
339 self.push('500 Error: bad syntax')
340 return
R David Murray554bcbf2014-06-11 11:18:08 -0400341 if not self._decode_data:
342 line = str(line, 'utf-8')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000343 i = line.find(' ')
344 if i < 0:
345 command = line.upper()
346 arg = None
347 else:
348 command = line[:i].upper()
349 arg = line[i+1:].strip()
R David Murrayd1a30c92012-05-26 14:33:59 -0400350 max_sz = (self.command_size_limits[command]
351 if self.extended_smtp else self.command_size_limit)
352 if sz > max_sz:
353 self.push('500 Error: line too long')
354 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000355 method = getattr(self, 'smtp_' + command, None)
356 if not method:
R David Murrayd1a30c92012-05-26 14:33:59 -0400357 self.push('500 Error: command "%s" not recognized' % command)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000358 return
359 method(arg)
360 return
361 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000362 if self.smtp_state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000363 self.push('451 Internal confusion')
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000364 self.num_bytes = 0
365 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400366 if self.data_size_limit and self.num_bytes > self.data_size_limit:
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000367 self.push('552 Error: Too much mail data')
368 self.num_bytes = 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000369 return
370 # Remove extraneous carriage returns and de-transparency according
R David Murrayd1a30c92012-05-26 14:33:59 -0400371 # to RFC 5321, Section 4.5.2.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000372 data = []
R David Murray554bcbf2014-06-11 11:18:08 -0400373 for text in line.split(self._linesep):
374 if text and text[0] == self._dotsep:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000375 data.append(text[1:])
376 else:
377 data.append(text)
R David Murray554bcbf2014-06-11 11:18:08 -0400378 self.received_data = self._newline.join(data)
R David Murray2539e672014-08-09 16:40:49 -0400379 args = (self.peer, self.mailfrom, self.rcpttos, self.received_data)
R David Murraya33df312015-05-11 12:11:40 -0400380 kwargs = {}
381 if not self._decode_data:
382 kwargs = {
383 'mail_options': self.mail_options,
384 'rcpt_options': self.rcpt_options,
385 }
386 status = self.smtp_server.process_message(*args, **kwargs)
R David Murray2539e672014-08-09 16:40:49 -0400387 self._set_post_data_state()
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000388 if not status:
R David Murrayd1a30c92012-05-26 14:33:59 -0400389 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000390 else:
391 self.push(status)
392
393 # SMTP and ESMTP commands
394 def smtp_HELO(self, arg):
395 if not arg:
396 self.push('501 Syntax: HELO hostname')
397 return
R David Murray2539e672014-08-09 16:40:49 -0400398 # See issue #21783 for a discussion of this behavior.
Richard Jones803ef8a2010-07-24 09:51:40 +0000399 if self.seen_greeting:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000400 self.push('503 Duplicate HELO/EHLO')
R David Murray2539e672014-08-09 16:40:49 -0400401 return
402 self._set_rset_state()
403 self.seen_greeting = arg
404 self.push('250 %s' % self.fqdn)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000405
R David Murrayd1a30c92012-05-26 14:33:59 -0400406 def smtp_EHLO(self, arg):
407 if not arg:
408 self.push('501 Syntax: EHLO hostname')
409 return
R David Murray2539e672014-08-09 16:40:49 -0400410 # See issue #21783 for a discussion of this behavior.
R David Murrayd1a30c92012-05-26 14:33:59 -0400411 if self.seen_greeting:
412 self.push('503 Duplicate HELO/EHLO')
R David Murray2539e672014-08-09 16:40:49 -0400413 return
414 self._set_rset_state()
415 self.seen_greeting = arg
416 self.extended_smtp = True
417 self.push('250-%s' % self.fqdn)
418 if self.data_size_limit:
419 self.push('250-SIZE %s' % self.data_size_limit)
420 self.command_size_limits['MAIL'] += 26
R David Murraya33df312015-05-11 12:11:40 -0400421 if not self._decode_data:
R David Murray2539e672014-08-09 16:40:49 -0400422 self.push('250-8BITMIME')
R David Murraya33df312015-05-11 12:11:40 -0400423 if self.enable_SMTPUTF8:
R David Murray2539e672014-08-09 16:40:49 -0400424 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):
R David Murraya33df312015-05-11 12:11:40 -0400457 # Return params as dictionary. Return None if not all parameters
458 # appear to be syntactically valid according to RFC 1869.
459 result = {}
460 for param in params:
461 param, eq, value = param.partition('=')
462 if not param.isalnum() or eq and not value:
463 return None
464 result[param] = value if eq else True
465 return result
R David Murrayd1a30c92012-05-26 14:33:59 -0400466
467 def smtp_HELP(self, arg):
468 if arg:
Benjamin Peterson0c803312015-04-05 10:01:48 -0400469 extended = ' [SP <mail-parameters>]'
R David Murrayd1a30c92012-05-26 14:33:59 -0400470 lc_arg = arg.upper()
471 if lc_arg == 'EHLO':
472 self.push('250 Syntax: EHLO hostname')
473 elif lc_arg == 'HELO':
474 self.push('250 Syntax: HELO hostname')
475 elif lc_arg == 'MAIL':
476 msg = '250 Syntax: MAIL FROM: <address>'
477 if self.extended_smtp:
478 msg += extended
479 self.push(msg)
480 elif lc_arg == 'RCPT':
481 msg = '250 Syntax: RCPT TO: <address>'
482 if self.extended_smtp:
483 msg += extended
484 self.push(msg)
485 elif lc_arg == 'DATA':
486 self.push('250 Syntax: DATA')
487 elif lc_arg == 'RSET':
488 self.push('250 Syntax: RSET')
489 elif lc_arg == 'NOOP':
490 self.push('250 Syntax: NOOP')
491 elif lc_arg == 'QUIT':
492 self.push('250 Syntax: QUIT')
493 elif lc_arg == 'VRFY':
494 self.push('250 Syntax: VRFY <address>')
495 else:
496 self.push('501 Supported commands: EHLO HELO MAIL RCPT '
497 'DATA RSET NOOP QUIT VRFY')
498 else:
499 self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '
500 'RSET NOOP QUIT VRFY')
501
502 def smtp_VRFY(self, arg):
503 if arg:
504 address, params = self._getaddr(arg)
505 if address:
506 self.push('252 Cannot VRFY user, but will accept message '
507 'and attempt delivery')
508 else:
509 self.push('502 Could not VRFY %s' % arg)
510 else:
511 self.push('501 Syntax: VRFY <address>')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000512
513 def smtp_MAIL(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400514 if not self.seen_greeting:
R David Murraya33df312015-05-11 12:11:40 -0400515 self.push('503 Error: send HELO first')
R David Murray669b7552012-03-20 16:16:29 -0400516 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000517 print('===> MAIL', arg, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400518 syntaxerr = '501 Syntax: MAIL FROM: <address>'
519 if self.extended_smtp:
520 syntaxerr += ' [SP <mail-parameters>]'
521 if arg is None:
522 self.push(syntaxerr)
523 return
524 arg = self._strip_command_keyword('FROM:', arg)
525 address, params = self._getaddr(arg)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000526 if not address:
R David Murrayd1a30c92012-05-26 14:33:59 -0400527 self.push(syntaxerr)
528 return
529 if not self.extended_smtp and params:
530 self.push(syntaxerr)
531 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000532 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000533 self.push('503 Error: nested MAIL command')
534 return
R David Murraya33df312015-05-11 12:11:40 -0400535 self.mail_options = params.upper().split()
536 params = self._getparams(self.mail_options)
R David Murrayd1a30c92012-05-26 14:33:59 -0400537 if params is None:
538 self.push(syntaxerr)
539 return
R David Murraya33df312015-05-11 12:11:40 -0400540 if not self._decode_data:
541 body = params.pop('BODY', '7BIT')
542 if body not in ['7BIT', '8BITMIME']:
543 self.push('501 Error: BODY can only be one of 7BIT, 8BITMIME')
R David Murray2539e672014-08-09 16:40:49 -0400544 return
R David Murraya33df312015-05-11 12:11:40 -0400545 if self.enable_SMTPUTF8:
546 smtputf8 = params.pop('SMTPUTF8', False)
547 if smtputf8 is True:
R David Murray2539e672014-08-09 16:40:49 -0400548 self.require_SMTPUTF8 = True
R David Murraya33df312015-05-11 12:11:40 -0400549 elif smtputf8 is not False:
550 self.push('501 Error: SMTPUTF8 takes no arguments')
551 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400552 size = params.pop('SIZE', None)
553 if size:
554 if not size.isdigit():
555 self.push(syntaxerr)
556 return
557 elif self.data_size_limit and int(size) > self.data_size_limit:
558 self.push('552 Error: message size exceeds fixed maximum message size')
559 return
560 if len(params.keys()) > 0:
561 self.push('555 MAIL FROM parameters not recognized or not implemented')
562 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000563 self.mailfrom = address
564 print('sender:', self.mailfrom, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400565 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000566
567 def smtp_RCPT(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400568 if not self.seen_greeting:
569 self.push('503 Error: send HELO first');
570 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000571 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000572 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000573 self.push('503 Error: need MAIL command')
574 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400575 syntaxerr = '501 Syntax: RCPT TO: <address>'
576 if self.extended_smtp:
577 syntaxerr += ' [SP <mail-parameters>]'
578 if arg is None:
579 self.push(syntaxerr)
580 return
581 arg = self._strip_command_keyword('TO:', arg)
582 address, params = self._getaddr(arg)
583 if not address:
584 self.push(syntaxerr)
585 return
R David Murraya33df312015-05-11 12:11:40 -0400586 if not self.extended_smtp and params:
587 self.push(syntaxerr)
588 return
589 self.rcpt_options = params.upper().split()
590 params = self._getparams(self.rcpt_options)
591 if params is None:
592 self.push(syntaxerr)
593 return
594 # XXX currently there are no options we recognize.
595 if len(params.keys()) > 0:
R David Murrayd1a30c92012-05-26 14:33:59 -0400596 self.push('555 RCPT TO parameters not recognized or not implemented')
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,
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300634 enable_SMTPUTF8=False, decode_data=False):
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
Serhiy Storchakaeb6cd742016-05-29 23:50:56 +0300638 self.enable_SMTPUTF8 = enable_SMTPUTF8
639 self._decode_data = decode_data
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300640 if enable_SMTPUTF8 and decode_data:
641 raise ValueError("decode_data and enable_SMTPUTF8 cannot"
642 " be set to True at the same time")
Vinay Sajip30298b42013-06-07 15:21:41 +0100643 asyncore.dispatcher.__init__(self, map=map)
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000644 try:
R David Murray012a83a2014-06-11 15:17:50 -0400645 gai_results = socket.getaddrinfo(*localaddr,
646 type=socket.SOCK_STREAM)
R David Murray6fe56a32014-06-11 13:48:58 -0400647 self.create_socket(gai_results[0][0], gai_results[0][1])
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000648 # try to re-use a server port if possible
649 self.set_reuse_addr()
650 self.bind(localaddr)
651 self.listen(5)
652 except:
653 self.close()
654 raise
655 else:
656 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
657 self.__class__.__name__, time.ctime(time.time()),
658 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000659
Giampaolo Rodolà977c7072010-10-04 21:08:36 +0000660 def handle_accepted(self, conn, addr):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000661 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
R David Murray2539e672014-08-09 16:40:49 -0400662 channel = self.channel_class(self,
663 conn,
664 addr,
665 self.data_size_limit,
666 self._map,
667 self.enable_SMTPUTF8,
668 self._decode_data)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000669
670 # API for "doing something useful with the message"
R David Murraya33df312015-05-11 12:11:40 -0400671 def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000672 """Override this abstract method to handle messages from the client.
673
674 peer is a tuple containing (ipaddr, port) of the client that made the
675 socket connection to our smtp port.
676
677 mailfrom is the raw address the client claims the message is coming
678 from.
679
680 rcpttos is a list of raw addresses the client wishes to deliver the
681 message to.
682
683 data is a string containing the entire full text of the message,
684 headers (if supplied) and all. It has been `de-transparencied'
685 according to RFC 821, Section 4.5.2. In other words, a line
686 containing a `.' followed by other text has had the leading dot
687 removed.
688
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300689 kwargs is a dictionary containing additional information. It is
690 empty if decode_data=True was given as init parameter, otherwise
691 it will contain the following keys:
R David Murraya33df312015-05-11 12:11:40 -0400692 'mail_options': list of parameters to the mail command. All
693 elements are uppercase strings. Example:
694 ['BODY=8BITMIME', 'SMTPUTF8'].
695 'rcpt_options': same, for the rcpt command.
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
Tim Peters658cba62001-02-09 20:06:00 +0000704
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000705class DebuggingServer(SMTPServer):
R David Murray2539e672014-08-09 16:40:49 -0400706
707 def _print_message_content(self, peer, data):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000708 inheaders = 1
R David Murray2539e672014-08-09 16:40:49 -0400709 lines = data.splitlines()
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000710 for line in lines:
711 # headers first
712 if inheaders and not line:
R David Murray2539e672014-08-09 16:40:49 -0400713 peerheader = 'X-Peer: ' + peer[0]
714 if not isinstance(data, str):
715 # decoded_data=false; make header match other binary output
716 peerheader = repr(peerheader.encode('utf-8'))
717 print(peerheader)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000718 inheaders = 0
R David Murray2539e672014-08-09 16:40:49 -0400719 if not isinstance(data, str):
720 # Avoid spurious 'str on bytes instance' warning.
721 line = repr(line)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000722 print(line)
R David Murray2539e672014-08-09 16:40:49 -0400723
R David Murraya33df312015-05-11 12:11:40 -0400724 def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
R David Murray2539e672014-08-09 16:40:49 -0400725 print('---------- MESSAGE FOLLOWS ----------')
R David Murraya33df312015-05-11 12:11:40 -0400726 if kwargs:
727 if kwargs.get('mail_options'):
728 print('mail options: %s' % kwargs['mail_options'])
729 if kwargs.get('rcpt_options'):
730 print('rcpt options: %s\n' % kwargs['rcpt_options'])
R David Murray2539e672014-08-09 16:40:49 -0400731 self._print_message_content(peer, data)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000732 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000733
734
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000735class PureProxy(SMTPServer):
R David Murray2539e672014-08-09 16:40:49 -0400736 def __init__(self, *args, **kwargs):
737 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
738 raise ValueError("PureProxy does not support SMTPUTF8.")
739 super(PureProxy, self).__init__(*args, **kwargs)
740
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000741 def process_message(self, peer, mailfrom, rcpttos, data):
742 lines = data.split('\n')
743 # Look for the last header
744 i = 0
745 for line in lines:
746 if not line:
747 break
748 i += 1
749 lines.insert(i, 'X-Peer: %s' % peer[0])
750 data = NEWLINE.join(lines)
751 refused = self._deliver(mailfrom, rcpttos, data)
752 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000753 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000754
755 def _deliver(self, mailfrom, rcpttos, data):
756 import smtplib
757 refused = {}
758 try:
759 s = smtplib.SMTP()
760 s.connect(self._remoteaddr[0], self._remoteaddr[1])
761 try:
762 refused = s.sendmail(mailfrom, rcpttos, data)
763 finally:
764 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000765 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000766 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000767 refused = e.recipients
Andrew Svetlov0832af62012-12-18 23:10:48 +0200768 except (OSError, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000769 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000770 # All recipients were refused. If the exception had an associated
771 # error code, use it. Otherwise,fake it with a non-triggering
772 # exception code.
773 errcode = getattr(e, 'smtp_code', -1)
774 errmsg = getattr(e, 'smtp_error', 'ignore')
775 for r in rcpttos:
776 refused[r] = (errcode, errmsg)
777 return refused
778
779
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000780class MailmanProxy(PureProxy):
R David Murray2539e672014-08-09 16:40:49 -0400781 def __init__(self, *args, **kwargs):
Samuel Colvin822922a2019-10-12 18:24:26 +0100782 warn('MailmanProxy is deprecated and will be removed '
783 'in future', DeprecationWarning, 2)
R David Murray2539e672014-08-09 16:40:49 -0400784 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
785 raise ValueError("MailmanProxy does not support SMTPUTF8.")
786 super(PureProxy, self).__init__(*args, **kwargs)
787
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000788 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000789 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000790 from Mailman import Utils
791 from Mailman import Message
792 from Mailman import MailList
793 # If the message is to a Mailman mailing list, then we'll invoke the
794 # Mailman script directly, without going through the real smtpd.
795 # Otherwise we'll forward it to the local proxy for disposition.
796 listnames = []
797 for rcpt in rcpttos:
798 local = rcpt.lower().split('@')[0]
799 # We allow the following variations on the theme
800 # listname
801 # listname-admin
802 # listname-owner
803 # listname-request
804 # listname-join
805 # listname-leave
806 parts = local.split('-')
807 if len(parts) > 2:
808 continue
809 listname = parts[0]
810 if len(parts) == 2:
811 command = parts[1]
812 else:
813 command = ''
814 if not Utils.list_exists(listname) or command not in (
815 '', 'admin', 'owner', 'request', 'join', 'leave'):
816 continue
817 listnames.append((rcpt, listname, command))
818 # Remove all list recipients from rcpttos and forward what we're not
819 # going to take care of ourselves. Linear removal should be fine
820 # since we don't expect a large number of recipients.
821 for rcpt, listname, command in listnames:
822 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000823 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000824 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000825 if rcpttos:
826 refused = self._deliver(mailfrom, rcpttos, data)
827 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000828 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000829 # Now deliver directly to the list commands
830 mlists = {}
831 s = StringIO(data)
832 msg = Message.Message(s)
833 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000834 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000835 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000836 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000837 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000838 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000839 msg['Date'] = time.ctime(time.time())
840 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000841 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000842 mlist = mlists.get(listname)
843 if not mlist:
844 mlist = MailList.MailList(listname, lock=0)
845 mlists[listname] = mlist
846 # dispatch on the type of command
847 if command == '':
848 # post
849 msg.Enqueue(mlist, tolist=1)
850 elif command == 'admin':
851 msg.Enqueue(mlist, toadmin=1)
852 elif command == 'owner':
853 msg.Enqueue(mlist, toowner=1)
854 elif command == 'request':
855 msg.Enqueue(mlist, torequest=1)
856 elif command in ('join', 'leave'):
857 # TBD: this is a hack!
858 if command == 'join':
859 msg['Subject'] = 'subscribe'
860 else:
861 msg['Subject'] = 'unsubscribe'
862 msg.Enqueue(mlist, torequest=1)
863
864
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000865class Options:
R David Murray2539e672014-08-09 16:40:49 -0400866 setuid = True
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000867 classname = 'PureProxy'
R David Murrayd1a30c92012-05-26 14:33:59 -0400868 size_limit = None
R David Murray2539e672014-08-09 16:40:49 -0400869 enable_SMTPUTF8 = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000870
871
872def parseargs():
873 global DEBUGSTREAM
874 try:
875 opts, args = getopt.getopt(
R David Murray2539e672014-08-09 16:40:49 -0400876 sys.argv[1:], 'nVhc:s:du',
877 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug',
878 'smtputf8'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000879 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000880 usage(1, e)
881
882 options = Options()
883 for opt, arg in opts:
884 if opt in ('-h', '--help'):
885 usage(0)
886 elif opt in ('-V', '--version'):
Serhiy Storchakac56894d2013-09-05 17:44:53 +0300887 print(__version__)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000888 sys.exit(0)
889 elif opt in ('-n', '--nosetuid'):
R David Murray2539e672014-08-09 16:40:49 -0400890 options.setuid = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000891 elif opt in ('-c', '--class'):
892 options.classname = arg
893 elif opt in ('-d', '--debug'):
894 DEBUGSTREAM = sys.stderr
R David Murray2539e672014-08-09 16:40:49 -0400895 elif opt in ('-u', '--smtputf8'):
896 options.enable_SMTPUTF8 = True
R David Murrayd1a30c92012-05-26 14:33:59 -0400897 elif opt in ('-s', '--size'):
898 try:
899 int_size = int(arg)
900 options.size_limit = int_size
901 except:
902 print('Invalid size: ' + arg, file=sys.stderr)
903 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000904
905 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000906 if len(args) < 1:
907 localspec = 'localhost:8025'
908 remotespec = 'localhost:25'
909 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000910 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000911 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000912 elif len(args) < 3:
913 localspec = args[0]
914 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000915 else:
916 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
917
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000918 # split into host/port pairs
919 i = localspec.find(':')
920 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000921 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000922 options.localhost = localspec[:i]
923 try:
924 options.localport = int(localspec[i+1:])
925 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000926 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000927 i = remotespec.find(':')
928 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000929 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000930 options.remotehost = remotespec[:i]
931 try:
932 options.remoteport = int(remotespec[i+1:])
933 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000934 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000935 return options
936
937
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000938if __name__ == '__main__':
939 options = parseargs()
940 # Become nobody
Florent Xicluna711f87c2011-10-20 23:03:43 +0200941 classname = options.classname
942 if "." in classname:
943 lastdot = classname.rfind(".")
944 mod = __import__(classname[:lastdot], globals(), locals(), [""])
945 classname = classname[lastdot+1:]
946 else:
947 import __main__ as mod
948 class_ = getattr(mod, classname)
949 proxy = class_((options.localhost, options.localport),
R David Murrayd1a30c92012-05-26 14:33:59 -0400950 (options.remotehost, options.remoteport),
R David Murray2539e672014-08-09 16:40:49 -0400951 options.size_limit, enable_SMTPUTF8=options.enable_SMTPUTF8)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000952 if options.setuid:
953 try:
954 import pwd
Brett Cannoncd171c82013-07-04 17:43:24 -0400955 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000956 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000957 sys.exit(1)
958 nobody = pwd.getpwnam('nobody')[2]
959 try:
960 os.setuid(nobody)
Giampaolo Rodola'0166a282013-02-12 15:14:17 +0100961 except PermissionError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000962 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000963 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000964 try:
965 asyncore.loop()
966 except KeyboardInterrupt:
967 pass