blob: 8103ca9af0d7b3f2fb20a02b34182d97ef30a8e3 [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()
166 if err.args[0] != errno.ENOTCONN:
167 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):
782 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
783 raise ValueError("MailmanProxy does not support SMTPUTF8.")
784 super(PureProxy, self).__init__(*args, **kwargs)
785
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000786 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000787 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000788 from Mailman import Utils
789 from Mailman import Message
790 from Mailman import MailList
791 # If the message is to a Mailman mailing list, then we'll invoke the
792 # Mailman script directly, without going through the real smtpd.
793 # Otherwise we'll forward it to the local proxy for disposition.
794 listnames = []
795 for rcpt in rcpttos:
796 local = rcpt.lower().split('@')[0]
797 # We allow the following variations on the theme
798 # listname
799 # listname-admin
800 # listname-owner
801 # listname-request
802 # listname-join
803 # listname-leave
804 parts = local.split('-')
805 if len(parts) > 2:
806 continue
807 listname = parts[0]
808 if len(parts) == 2:
809 command = parts[1]
810 else:
811 command = ''
812 if not Utils.list_exists(listname) or command not in (
813 '', 'admin', 'owner', 'request', 'join', 'leave'):
814 continue
815 listnames.append((rcpt, listname, command))
816 # Remove all list recipients from rcpttos and forward what we're not
817 # going to take care of ourselves. Linear removal should be fine
818 # since we don't expect a large number of recipients.
819 for rcpt, listname, command in listnames:
820 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000821 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000822 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000823 if rcpttos:
824 refused = self._deliver(mailfrom, rcpttos, data)
825 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000826 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000827 # Now deliver directly to the list commands
828 mlists = {}
829 s = StringIO(data)
830 msg = Message.Message(s)
831 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000832 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000833 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000834 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000835 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000836 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000837 msg['Date'] = time.ctime(time.time())
838 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000839 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000840 mlist = mlists.get(listname)
841 if not mlist:
842 mlist = MailList.MailList(listname, lock=0)
843 mlists[listname] = mlist
844 # dispatch on the type of command
845 if command == '':
846 # post
847 msg.Enqueue(mlist, tolist=1)
848 elif command == 'admin':
849 msg.Enqueue(mlist, toadmin=1)
850 elif command == 'owner':
851 msg.Enqueue(mlist, toowner=1)
852 elif command == 'request':
853 msg.Enqueue(mlist, torequest=1)
854 elif command in ('join', 'leave'):
855 # TBD: this is a hack!
856 if command == 'join':
857 msg['Subject'] = 'subscribe'
858 else:
859 msg['Subject'] = 'unsubscribe'
860 msg.Enqueue(mlist, torequest=1)
861
862
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000863class Options:
R David Murray2539e672014-08-09 16:40:49 -0400864 setuid = True
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000865 classname = 'PureProxy'
R David Murrayd1a30c92012-05-26 14:33:59 -0400866 size_limit = None
R David Murray2539e672014-08-09 16:40:49 -0400867 enable_SMTPUTF8 = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000868
869
870def parseargs():
871 global DEBUGSTREAM
872 try:
873 opts, args = getopt.getopt(
R David Murray2539e672014-08-09 16:40:49 -0400874 sys.argv[1:], 'nVhc:s:du',
875 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug',
876 'smtputf8'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000877 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000878 usage(1, e)
879
880 options = Options()
881 for opt, arg in opts:
882 if opt in ('-h', '--help'):
883 usage(0)
884 elif opt in ('-V', '--version'):
Serhiy Storchakac56894d2013-09-05 17:44:53 +0300885 print(__version__)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000886 sys.exit(0)
887 elif opt in ('-n', '--nosetuid'):
R David Murray2539e672014-08-09 16:40:49 -0400888 options.setuid = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000889 elif opt in ('-c', '--class'):
890 options.classname = arg
891 elif opt in ('-d', '--debug'):
892 DEBUGSTREAM = sys.stderr
R David Murray2539e672014-08-09 16:40:49 -0400893 elif opt in ('-u', '--smtputf8'):
894 options.enable_SMTPUTF8 = True
R David Murrayd1a30c92012-05-26 14:33:59 -0400895 elif opt in ('-s', '--size'):
896 try:
897 int_size = int(arg)
898 options.size_limit = int_size
899 except:
900 print('Invalid size: ' + arg, file=sys.stderr)
901 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000902
903 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000904 if len(args) < 1:
905 localspec = 'localhost:8025'
906 remotespec = 'localhost:25'
907 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000908 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000909 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000910 elif len(args) < 3:
911 localspec = args[0]
912 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000913 else:
914 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
915
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000916 # split into host/port pairs
917 i = localspec.find(':')
918 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000919 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000920 options.localhost = localspec[:i]
921 try:
922 options.localport = int(localspec[i+1:])
923 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000924 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000925 i = remotespec.find(':')
926 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000927 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000928 options.remotehost = remotespec[:i]
929 try:
930 options.remoteport = int(remotespec[i+1:])
931 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000932 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000933 return options
934
935
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000936if __name__ == '__main__':
937 options = parseargs()
938 # Become nobody
Florent Xicluna711f87c2011-10-20 23:03:43 +0200939 classname = options.classname
940 if "." in classname:
941 lastdot = classname.rfind(".")
942 mod = __import__(classname[:lastdot], globals(), locals(), [""])
943 classname = classname[lastdot+1:]
944 else:
945 import __main__ as mod
946 class_ = getattr(mod, classname)
947 proxy = class_((options.localhost, options.localport),
R David Murrayd1a30c92012-05-26 14:33:59 -0400948 (options.remotehost, options.remoteport),
R David Murray2539e672014-08-09 16:40:49 -0400949 options.size_limit, enable_SMTPUTF8=options.enable_SMTPUTF8)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000950 if options.setuid:
951 try:
952 import pwd
Brett Cannoncd171c82013-07-04 17:43:24 -0400953 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000954 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000955 sys.exit(1)
956 nobody = pwd.getpwnam('nobody')[2]
957 try:
958 os.setuid(nobody)
Giampaolo Rodola'0166a282013-02-12 15:14:17 +0100959 except PermissionError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000960 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000961 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000962 try:
963 asyncore.loop()
964 except KeyboardInterrupt:
965 pass