blob: bc4333125100975161b4ad154eac63ddca8c498f [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
R David Murrayd1a30c92012-05-26 14:33:59 -040086import collections
Richard Jones803ef8a2010-07-24 09:51:40 +000087from warnings import warn
R David Murrayd1a30c92012-05-26 14:33:59 -040088from email._header_value_parser import get_addr_spec, get_angle_addr
Barry Warsaw7e0d9562001-01-31 22:51:35 +000089
Martin Panter380ef012016-06-06 02:03:11 +000090__all__ = [
91 "SMTPChannel", "SMTPServer", "DebuggingServer", "PureProxy",
92 "MailmanProxy",
93]
Barry Warsaw7e0d9562001-01-31 22:51:35 +000094
Miss Islington (bot)a80a38e2021-06-24 12:57:55 -070095warn(
96 'The smtpd module is deprecated and unmaintained. Please see aiosmtpd '
97 '(https://aiosmtpd.readthedocs.io/) for the recommended replacement.',
98 DeprecationWarning,
99 stacklevel=2)
100
101
102# These are imported after the above warning so that users get the correct
103# deprecation warning.
104import asyncore
105import asynchat
106
107
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000108program = sys.argv[0]
R David Murrayd1a30c92012-05-26 14:33:59 -0400109__version__ = 'Python SMTP proxy version 0.3'
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000110
111
112class Devnull:
113 def write(self, msg): pass
114 def flush(self): pass
115
116
117DEBUGSTREAM = Devnull()
118NEWLINE = '\n'
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000119COMMASPACE = ', '
R David Murrayd1a30c92012-05-26 14:33:59 -0400120DATA_SIZE_DEFAULT = 33554432
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000121
122
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000123def usage(code, msg=''):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000124 print(__doc__ % globals(), file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000125 if msg:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000126 print(msg, file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000127 sys.exit(code)
128
129
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000130class SMTPChannel(asynchat.async_chat):
131 COMMAND = 0
132 DATA = 1
133
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000134 command_size_limit = 512
R David Murrayd1a30c92012-05-26 14:33:59 -0400135 command_size_limits = collections.defaultdict(lambda x=command_size_limit: x)
R David Murray2539e672014-08-09 16:40:49 -0400136
137 @property
138 def max_command_size_limit(self):
139 try:
140 return max(self.command_size_limits.values())
141 except ValueError:
142 return self.command_size_limit
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000143
Vinay Sajip30298b42013-06-07 15:21:41 +0100144 def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT,
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300145 map=None, enable_SMTPUTF8=False, decode_data=False):
Vinay Sajip30298b42013-06-07 15:21:41 +0100146 asynchat.async_chat.__init__(self, conn, map=map)
Richard Jones803ef8a2010-07-24 09:51:40 +0000147 self.smtp_server = server
148 self.conn = conn
149 self.addr = addr
R David Murrayd1a30c92012-05-26 14:33:59 -0400150 self.data_size_limit = data_size_limit
Serhiy Storchakaeb6cd742016-05-29 23:50:56 +0300151 self.enable_SMTPUTF8 = enable_SMTPUTF8
152 self._decode_data = decode_data
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300153 if enable_SMTPUTF8 and decode_data:
154 raise ValueError("decode_data and enable_SMTPUTF8 cannot"
155 " be set to True at the same time")
R David Murray554bcbf2014-06-11 11:18:08 -0400156 if decode_data:
157 self._emptystring = ''
158 self._linesep = '\r\n'
159 self._dotsep = '.'
160 self._newline = NEWLINE
161 else:
162 self._emptystring = b''
163 self._linesep = b'\r\n'
Serhiy Storchakaee4c0b92015-03-20 16:48:02 +0200164 self._dotsep = ord(b'.')
R David Murray554bcbf2014-06-11 11:18:08 -0400165 self._newline = b'\n'
R David Murray2539e672014-08-09 16:40:49 -0400166 self._set_rset_state()
Richard Jones803ef8a2010-07-24 09:51:40 +0000167 self.seen_greeting = ''
R David Murray2539e672014-08-09 16:40:49 -0400168 self.extended_smtp = False
169 self.command_size_limits.clear()
Richard Jones803ef8a2010-07-24 09:51:40 +0000170 self.fqdn = socket.getfqdn()
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000171 try:
172 self.peer = conn.getpeername()
Andrew Svetlov0832af62012-12-18 23:10:48 +0200173 except OSError as err:
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000174 # a race condition may occur if the other end is closing
175 # before we can get the peername
176 self.close()
Serhiy Storchakac4d45ee2020-11-22 10:28:34 +0200177 if err.errno != errno.ENOTCONN:
Giampaolo Rodolà9cf5ef42010-08-23 22:28:13 +0000178 raise
179 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000180 print('Peer:', repr(self.peer), file=DEBUGSTREAM)
181 self.push('220 %s %s' % (self.fqdn, __version__))
R David Murray2539e672014-08-09 16:40:49 -0400182
183 def _set_post_data_state(self):
184 """Reset state variables to their post-DATA state."""
185 self.smtp_state = self.COMMAND
186 self.mailfrom = None
187 self.rcpttos = []
188 self.require_SMTPUTF8 = False
189 self.num_bytes = 0
Josiah Carlsond74900e2008-07-07 04:15:08 +0000190 self.set_terminator(b'\r\n')
R David Murray2539e672014-08-09 16:40:49 -0400191
192 def _set_rset_state(self):
193 """Reset all state variables except the greeting."""
194 self._set_post_data_state()
195 self.received_data = ''
196 self.received_lines = []
197
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000198
Richard Jones803ef8a2010-07-24 09:51:40 +0000199 # properties for backwards-compatibility
200 @property
201 def __server(self):
202 warn("Access to __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100203 "use 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000204 return self.smtp_server
205 @__server.setter
206 def __server(self, value):
207 warn("Setting __server attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100208 "set 'smtp_server' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000209 self.smtp_server = value
210
211 @property
212 def __line(self):
213 warn("Access to __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100214 "use 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000215 return self.received_lines
216 @__line.setter
217 def __line(self, value):
218 warn("Setting __line attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100219 "set 'received_lines' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000220 self.received_lines = value
221
222 @property
223 def __state(self):
224 warn("Access to __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100225 "use 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000226 return self.smtp_state
227 @__state.setter
228 def __state(self, value):
229 warn("Setting __state attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100230 "set 'smtp_state' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000231 self.smtp_state = value
232
233 @property
234 def __greeting(self):
235 warn("Access to __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100236 "use 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000237 return self.seen_greeting
238 @__greeting.setter
239 def __greeting(self, value):
240 warn("Setting __greeting attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100241 "set 'seen_greeting' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000242 self.seen_greeting = value
243
244 @property
245 def __mailfrom(self):
246 warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100247 "use 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000248 return self.mailfrom
249 @__mailfrom.setter
250 def __mailfrom(self, value):
251 warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100252 "set 'mailfrom' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000253 self.mailfrom = value
254
255 @property
256 def __rcpttos(self):
257 warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100258 "use 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000259 return self.rcpttos
260 @__rcpttos.setter
261 def __rcpttos(self, value):
262 warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100263 "set 'rcpttos' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000264 self.rcpttos = value
265
266 @property
267 def __data(self):
268 warn("Access to __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100269 "use 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000270 return self.received_data
271 @__data.setter
272 def __data(self, value):
273 warn("Setting __data attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100274 "set 'received_data' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000275 self.received_data = value
276
277 @property
278 def __fqdn(self):
279 warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100280 "use 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000281 return self.fqdn
282 @__fqdn.setter
283 def __fqdn(self, value):
284 warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100285 "set 'fqdn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000286 self.fqdn = value
287
288 @property
289 def __peer(self):
290 warn("Access to __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100291 "use 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000292 return self.peer
293 @__peer.setter
294 def __peer(self, value):
295 warn("Setting __peer attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100296 "set 'peer' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000297 self.peer = value
298
299 @property
300 def __conn(self):
301 warn("Access to __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100302 "use 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000303 return self.conn
304 @__conn.setter
305 def __conn(self, value):
306 warn("Setting __conn attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100307 "set 'conn' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000308 self.conn = value
309
310 @property
311 def __addr(self):
312 warn("Access to __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100313 "use 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000314 return self.addr
315 @__addr.setter
316 def __addr(self, value):
317 warn("Setting __addr attribute on SMTPChannel is deprecated, "
Florent Xicluna67317752011-12-10 11:07:42 +0100318 "set 'addr' instead", DeprecationWarning, 2)
Richard Jones803ef8a2010-07-24 09:51:40 +0000319 self.addr = value
320
R David Murray2539e672014-08-09 16:40:49 -0400321 # Overrides base class for convenience.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000322 def push(self, msg):
R David Murray2539e672014-08-09 16:40:49 -0400323 asynchat.async_chat.push(self, bytes(
324 msg + '\r\n', 'utf-8' if self.require_SMTPUTF8 else 'ascii'))
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000325
326 # Implementation of base class abstract method
327 def collect_incoming_data(self, data):
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000328 limit = None
329 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400330 limit = self.max_command_size_limit
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000331 elif self.smtp_state == self.DATA:
332 limit = self.data_size_limit
333 if limit and self.num_bytes > limit:
334 return
335 elif limit:
336 self.num_bytes += len(data)
R David Murray554bcbf2014-06-11 11:18:08 -0400337 if self._decode_data:
338 self.received_lines.append(str(data, 'utf-8'))
339 else:
340 self.received_lines.append(data)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000341
342 # Implementation of base class abstract method
343 def found_terminator(self):
R David Murray554bcbf2014-06-11 11:18:08 -0400344 line = self._emptystring.join(self.received_lines)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000345 print('Data:', repr(line), file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000346 self.received_lines = []
347 if self.smtp_state == self.COMMAND:
R David Murrayd1a30c92012-05-26 14:33:59 -0400348 sz, self.num_bytes = self.num_bytes, 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000349 if not line:
350 self.push('500 Error: bad syntax')
351 return
R David Murray554bcbf2014-06-11 11:18:08 -0400352 if not self._decode_data:
353 line = str(line, 'utf-8')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000354 i = line.find(' ')
355 if i < 0:
356 command = line.upper()
357 arg = None
358 else:
359 command = line[:i].upper()
360 arg = line[i+1:].strip()
R David Murrayd1a30c92012-05-26 14:33:59 -0400361 max_sz = (self.command_size_limits[command]
362 if self.extended_smtp else self.command_size_limit)
363 if sz > max_sz:
364 self.push('500 Error: line too long')
365 return
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000366 method = getattr(self, 'smtp_' + command, None)
367 if not method:
R David Murrayd1a30c92012-05-26 14:33:59 -0400368 self.push('500 Error: command "%s" not recognized' % command)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000369 return
370 method(arg)
371 return
372 else:
Richard Jones803ef8a2010-07-24 09:51:40 +0000373 if self.smtp_state != self.DATA:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000374 self.push('451 Internal confusion')
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000375 self.num_bytes = 0
376 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400377 if self.data_size_limit and self.num_bytes > self.data_size_limit:
Georg Brandl1e5c5f82010-12-03 07:38:22 +0000378 self.push('552 Error: Too much mail data')
379 self.num_bytes = 0
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000380 return
381 # Remove extraneous carriage returns and de-transparency according
R David Murrayd1a30c92012-05-26 14:33:59 -0400382 # to RFC 5321, Section 4.5.2.
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000383 data = []
R David Murray554bcbf2014-06-11 11:18:08 -0400384 for text in line.split(self._linesep):
385 if text and text[0] == self._dotsep:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000386 data.append(text[1:])
387 else:
388 data.append(text)
R David Murray554bcbf2014-06-11 11:18:08 -0400389 self.received_data = self._newline.join(data)
R David Murray2539e672014-08-09 16:40:49 -0400390 args = (self.peer, self.mailfrom, self.rcpttos, self.received_data)
R David Murraya33df312015-05-11 12:11:40 -0400391 kwargs = {}
392 if not self._decode_data:
393 kwargs = {
394 'mail_options': self.mail_options,
395 'rcpt_options': self.rcpt_options,
396 }
397 status = self.smtp_server.process_message(*args, **kwargs)
R David Murray2539e672014-08-09 16:40:49 -0400398 self._set_post_data_state()
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000399 if not status:
R David Murrayd1a30c92012-05-26 14:33:59 -0400400 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000401 else:
402 self.push(status)
403
404 # SMTP and ESMTP commands
405 def smtp_HELO(self, arg):
406 if not arg:
407 self.push('501 Syntax: HELO hostname')
408 return
R David Murray2539e672014-08-09 16:40:49 -0400409 # See issue #21783 for a discussion of this behavior.
Richard Jones803ef8a2010-07-24 09:51:40 +0000410 if self.seen_greeting:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000411 self.push('503 Duplicate HELO/EHLO')
R David Murray2539e672014-08-09 16:40:49 -0400412 return
413 self._set_rset_state()
414 self.seen_greeting = arg
415 self.push('250 %s' % self.fqdn)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000416
R David Murrayd1a30c92012-05-26 14:33:59 -0400417 def smtp_EHLO(self, arg):
418 if not arg:
419 self.push('501 Syntax: EHLO hostname')
420 return
R David Murray2539e672014-08-09 16:40:49 -0400421 # See issue #21783 for a discussion of this behavior.
R David Murrayd1a30c92012-05-26 14:33:59 -0400422 if self.seen_greeting:
423 self.push('503 Duplicate HELO/EHLO')
R David Murray2539e672014-08-09 16:40:49 -0400424 return
425 self._set_rset_state()
426 self.seen_greeting = arg
427 self.extended_smtp = True
428 self.push('250-%s' % self.fqdn)
429 if self.data_size_limit:
430 self.push('250-SIZE %s' % self.data_size_limit)
431 self.command_size_limits['MAIL'] += 26
R David Murraya33df312015-05-11 12:11:40 -0400432 if not self._decode_data:
R David Murray2539e672014-08-09 16:40:49 -0400433 self.push('250-8BITMIME')
R David Murraya33df312015-05-11 12:11:40 -0400434 if self.enable_SMTPUTF8:
R David Murray2539e672014-08-09 16:40:49 -0400435 self.push('250-SMTPUTF8')
436 self.command_size_limits['MAIL'] += 10
437 self.push('250 HELP')
R David Murrayd1a30c92012-05-26 14:33:59 -0400438
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000439 def smtp_NOOP(self, arg):
440 if arg:
441 self.push('501 Syntax: NOOP')
442 else:
R David Murrayd1a30c92012-05-26 14:33:59 -0400443 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000444
445 def smtp_QUIT(self, arg):
446 # args is ignored
447 self.push('221 Bye')
448 self.close_when_done()
449
R David Murrayd1a30c92012-05-26 14:33:59 -0400450 def _strip_command_keyword(self, keyword, arg):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000451 keylen = len(keyword)
452 if arg[:keylen].upper() == keyword:
R David Murrayd1a30c92012-05-26 14:33:59 -0400453 return arg[keylen:].strip()
454 return ''
455
456 def _getaddr(self, arg):
457 if not arg:
458 return '', ''
459 if arg.lstrip().startswith('<'):
460 address, rest = get_angle_addr(arg)
461 else:
462 address, rest = get_addr_spec(arg)
463 if not address:
464 return address, rest
465 return address.addr_spec, rest
466
467 def _getparams(self, params):
R David Murraya33df312015-05-11 12:11:40 -0400468 # Return params as dictionary. Return None if not all parameters
469 # appear to be syntactically valid according to RFC 1869.
470 result = {}
471 for param in params:
472 param, eq, value = param.partition('=')
473 if not param.isalnum() or eq and not value:
474 return None
475 result[param] = value if eq else True
476 return result
R David Murrayd1a30c92012-05-26 14:33:59 -0400477
478 def smtp_HELP(self, arg):
479 if arg:
Benjamin Peterson0c803312015-04-05 10:01:48 -0400480 extended = ' [SP <mail-parameters>]'
R David Murrayd1a30c92012-05-26 14:33:59 -0400481 lc_arg = arg.upper()
482 if lc_arg == 'EHLO':
483 self.push('250 Syntax: EHLO hostname')
484 elif lc_arg == 'HELO':
485 self.push('250 Syntax: HELO hostname')
486 elif lc_arg == 'MAIL':
487 msg = '250 Syntax: MAIL FROM: <address>'
488 if self.extended_smtp:
489 msg += extended
490 self.push(msg)
491 elif lc_arg == 'RCPT':
492 msg = '250 Syntax: RCPT TO: <address>'
493 if self.extended_smtp:
494 msg += extended
495 self.push(msg)
496 elif lc_arg == 'DATA':
497 self.push('250 Syntax: DATA')
498 elif lc_arg == 'RSET':
499 self.push('250 Syntax: RSET')
500 elif lc_arg == 'NOOP':
501 self.push('250 Syntax: NOOP')
502 elif lc_arg == 'QUIT':
503 self.push('250 Syntax: QUIT')
504 elif lc_arg == 'VRFY':
505 self.push('250 Syntax: VRFY <address>')
506 else:
507 self.push('501 Supported commands: EHLO HELO MAIL RCPT '
508 'DATA RSET NOOP QUIT VRFY')
509 else:
510 self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '
511 'RSET NOOP QUIT VRFY')
512
513 def smtp_VRFY(self, arg):
514 if arg:
515 address, params = self._getaddr(arg)
516 if address:
517 self.push('252 Cannot VRFY user, but will accept message '
518 'and attempt delivery')
519 else:
520 self.push('502 Could not VRFY %s' % arg)
521 else:
522 self.push('501 Syntax: VRFY <address>')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000523
524 def smtp_MAIL(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400525 if not self.seen_greeting:
R David Murraya33df312015-05-11 12:11:40 -0400526 self.push('503 Error: send HELO first')
R David Murray669b7552012-03-20 16:16:29 -0400527 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000528 print('===> MAIL', arg, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400529 syntaxerr = '501 Syntax: MAIL FROM: <address>'
530 if self.extended_smtp:
531 syntaxerr += ' [SP <mail-parameters>]'
532 if arg is None:
533 self.push(syntaxerr)
534 return
535 arg = self._strip_command_keyword('FROM:', arg)
536 address, params = self._getaddr(arg)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000537 if not address:
R David Murrayd1a30c92012-05-26 14:33:59 -0400538 self.push(syntaxerr)
539 return
540 if not self.extended_smtp and params:
541 self.push(syntaxerr)
542 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000543 if self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000544 self.push('503 Error: nested MAIL command')
545 return
R David Murraya33df312015-05-11 12:11:40 -0400546 self.mail_options = params.upper().split()
547 params = self._getparams(self.mail_options)
R David Murrayd1a30c92012-05-26 14:33:59 -0400548 if params is None:
549 self.push(syntaxerr)
550 return
R David Murraya33df312015-05-11 12:11:40 -0400551 if not self._decode_data:
552 body = params.pop('BODY', '7BIT')
553 if body not in ['7BIT', '8BITMIME']:
554 self.push('501 Error: BODY can only be one of 7BIT, 8BITMIME')
R David Murray2539e672014-08-09 16:40:49 -0400555 return
R David Murraya33df312015-05-11 12:11:40 -0400556 if self.enable_SMTPUTF8:
557 smtputf8 = params.pop('SMTPUTF8', False)
558 if smtputf8 is True:
R David Murray2539e672014-08-09 16:40:49 -0400559 self.require_SMTPUTF8 = True
R David Murraya33df312015-05-11 12:11:40 -0400560 elif smtputf8 is not False:
561 self.push('501 Error: SMTPUTF8 takes no arguments')
562 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400563 size = params.pop('SIZE', None)
564 if size:
565 if not size.isdigit():
566 self.push(syntaxerr)
567 return
568 elif self.data_size_limit and int(size) > self.data_size_limit:
569 self.push('552 Error: message size exceeds fixed maximum message size')
570 return
571 if len(params.keys()) > 0:
572 self.push('555 MAIL FROM parameters not recognized or not implemented')
573 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000574 self.mailfrom = address
575 print('sender:', self.mailfrom, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400576 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000577
578 def smtp_RCPT(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400579 if not self.seen_greeting:
580 self.push('503 Error: send HELO first');
581 return
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000582 print('===> RCPT', arg, file=DEBUGSTREAM)
Richard Jones803ef8a2010-07-24 09:51:40 +0000583 if not self.mailfrom:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000584 self.push('503 Error: need MAIL command')
585 return
R David Murrayd1a30c92012-05-26 14:33:59 -0400586 syntaxerr = '501 Syntax: RCPT TO: <address>'
587 if self.extended_smtp:
588 syntaxerr += ' [SP <mail-parameters>]'
589 if arg is None:
590 self.push(syntaxerr)
591 return
592 arg = self._strip_command_keyword('TO:', arg)
593 address, params = self._getaddr(arg)
594 if not address:
595 self.push(syntaxerr)
596 return
R David Murraya33df312015-05-11 12:11:40 -0400597 if not self.extended_smtp and params:
598 self.push(syntaxerr)
599 return
600 self.rcpt_options = params.upper().split()
601 params = self._getparams(self.rcpt_options)
602 if params is None:
603 self.push(syntaxerr)
604 return
605 # XXX currently there are no options we recognize.
606 if len(params.keys()) > 0:
R David Murrayd1a30c92012-05-26 14:33:59 -0400607 self.push('555 RCPT TO parameters not recognized or not implemented')
608 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000609 self.rcpttos.append(address)
610 print('recips:', self.rcpttos, file=DEBUGSTREAM)
R David Murrayd1a30c92012-05-26 14:33:59 -0400611 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000612
613 def smtp_RSET(self, arg):
614 if arg:
615 self.push('501 Syntax: RSET')
616 return
R David Murray2539e672014-08-09 16:40:49 -0400617 self._set_rset_state()
R David Murrayd1a30c92012-05-26 14:33:59 -0400618 self.push('250 OK')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000619
620 def smtp_DATA(self, arg):
R David Murray669b7552012-03-20 16:16:29 -0400621 if not self.seen_greeting:
622 self.push('503 Error: send HELO first');
623 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000624 if not self.rcpttos:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000625 self.push('503 Error: need RCPT command')
626 return
627 if arg:
628 self.push('501 Syntax: DATA')
629 return
Richard Jones803ef8a2010-07-24 09:51:40 +0000630 self.smtp_state = self.DATA
Josiah Carlsond74900e2008-07-07 04:15:08 +0000631 self.set_terminator(b'\r\n.\r\n')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000632 self.push('354 End data with <CR><LF>.<CR><LF>')
633
R David Murrayd1a30c92012-05-26 14:33:59 -0400634 # Commands that have not been implemented
635 def smtp_EXPN(self, arg):
636 self.push('502 EXPN not implemented')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000637
R David Murrayd1a30c92012-05-26 14:33:59 -0400638
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000639class SMTPServer(asyncore.dispatcher):
Richard Jones803ef8a2010-07-24 09:51:40 +0000640 # SMTPChannel class to use for managing client connections
641 channel_class = SMTPChannel
642
R David Murrayd1a30c92012-05-26 14:33:59 -0400643 def __init__(self, localaddr, remoteaddr,
R David Murray554bcbf2014-06-11 11:18:08 -0400644 data_size_limit=DATA_SIZE_DEFAULT, map=None,
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300645 enable_SMTPUTF8=False, decode_data=False):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000646 self._localaddr = localaddr
647 self._remoteaddr = remoteaddr
R David Murrayd1a30c92012-05-26 14:33:59 -0400648 self.data_size_limit = data_size_limit
Serhiy Storchakaeb6cd742016-05-29 23:50:56 +0300649 self.enable_SMTPUTF8 = enable_SMTPUTF8
650 self._decode_data = decode_data
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300651 if enable_SMTPUTF8 and decode_data:
652 raise ValueError("decode_data and enable_SMTPUTF8 cannot"
653 " be set to True at the same time")
Vinay Sajip30298b42013-06-07 15:21:41 +0100654 asyncore.dispatcher.__init__(self, map=map)
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000655 try:
R David Murray012a83a2014-06-11 15:17:50 -0400656 gai_results = socket.getaddrinfo(*localaddr,
657 type=socket.SOCK_STREAM)
R David Murray6fe56a32014-06-11 13:48:58 -0400658 self.create_socket(gai_results[0][0], gai_results[0][1])
Giampaolo Rodolà610aa4f2010-06-30 17:47:39 +0000659 # try to re-use a server port if possible
660 self.set_reuse_addr()
661 self.bind(localaddr)
662 self.listen(5)
663 except:
664 self.close()
665 raise
666 else:
667 print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
668 self.__class__.__name__, time.ctime(time.time()),
669 localaddr, remoteaddr), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000670
Giampaolo Rodolà977c7072010-10-04 21:08:36 +0000671 def handle_accepted(self, conn, addr):
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000672 print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
R David Murray2539e672014-08-09 16:40:49 -0400673 channel = self.channel_class(self,
674 conn,
675 addr,
676 self.data_size_limit,
677 self._map,
678 self.enable_SMTPUTF8,
679 self._decode_data)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000680
681 # API for "doing something useful with the message"
R David Murraya33df312015-05-11 12:11:40 -0400682 def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000683 """Override this abstract method to handle messages from the client.
684
685 peer is a tuple containing (ipaddr, port) of the client that made the
686 socket connection to our smtp port.
687
688 mailfrom is the raw address the client claims the message is coming
689 from.
690
691 rcpttos is a list of raw addresses the client wishes to deliver the
692 message to.
693
694 data is a string containing the entire full text of the message,
695 headers (if supplied) and all. It has been `de-transparencied'
696 according to RFC 821, Section 4.5.2. In other words, a line
697 containing a `.' followed by other text has had the leading dot
698 removed.
699
Serhiy Storchakacbcc2fd2016-05-16 09:36:31 +0300700 kwargs is a dictionary containing additional information. It is
701 empty if decode_data=True was given as init parameter, otherwise
702 it will contain the following keys:
R David Murraya33df312015-05-11 12:11:40 -0400703 'mail_options': list of parameters to the mail command. All
704 elements are uppercase strings. Example:
705 ['BODY=8BITMIME', 'SMTPUTF8'].
706 'rcpt_options': same, for the rcpt command.
707
R David Murray2539e672014-08-09 16:40:49 -0400708 This function should return None for a normal `250 Ok' response;
709 otherwise, it should return the desired response string in RFC 821
710 format.
711
712 """
713 raise NotImplementedError
714
Tim Peters658cba62001-02-09 20:06:00 +0000715
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000716class DebuggingServer(SMTPServer):
R David Murray2539e672014-08-09 16:40:49 -0400717
718 def _print_message_content(self, peer, data):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000719 inheaders = 1
R David Murray2539e672014-08-09 16:40:49 -0400720 lines = data.splitlines()
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000721 for line in lines:
722 # headers first
723 if inheaders and not line:
R David Murray2539e672014-08-09 16:40:49 -0400724 peerheader = 'X-Peer: ' + peer[0]
725 if not isinstance(data, str):
726 # decoded_data=false; make header match other binary output
727 peerheader = repr(peerheader.encode('utf-8'))
728 print(peerheader)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000729 inheaders = 0
R David Murray2539e672014-08-09 16:40:49 -0400730 if not isinstance(data, str):
731 # Avoid spurious 'str on bytes instance' warning.
732 line = repr(line)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000733 print(line)
R David Murray2539e672014-08-09 16:40:49 -0400734
R David Murraya33df312015-05-11 12:11:40 -0400735 def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
R David Murray2539e672014-08-09 16:40:49 -0400736 print('---------- MESSAGE FOLLOWS ----------')
R David Murraya33df312015-05-11 12:11:40 -0400737 if kwargs:
738 if kwargs.get('mail_options'):
739 print('mail options: %s' % kwargs['mail_options'])
740 if kwargs.get('rcpt_options'):
741 print('rcpt options: %s\n' % kwargs['rcpt_options'])
R David Murray2539e672014-08-09 16:40:49 -0400742 self._print_message_content(peer, data)
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000743 print('------------ END MESSAGE ------------')
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000744
745
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000746class PureProxy(SMTPServer):
R David Murray2539e672014-08-09 16:40:49 -0400747 def __init__(self, *args, **kwargs):
748 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
749 raise ValueError("PureProxy does not support SMTPUTF8.")
750 super(PureProxy, self).__init__(*args, **kwargs)
751
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000752 def process_message(self, peer, mailfrom, rcpttos, data):
753 lines = data.split('\n')
754 # Look for the last header
755 i = 0
756 for line in lines:
757 if not line:
758 break
759 i += 1
760 lines.insert(i, 'X-Peer: %s' % peer[0])
761 data = NEWLINE.join(lines)
762 refused = self._deliver(mailfrom, rcpttos, data)
763 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000764 print('we got some refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000765
766 def _deliver(self, mailfrom, rcpttos, data):
767 import smtplib
768 refused = {}
769 try:
770 s = smtplib.SMTP()
771 s.connect(self._remoteaddr[0], self._remoteaddr[1])
772 try:
773 refused = s.sendmail(mailfrom, rcpttos, data)
774 finally:
775 s.quit()
Guido van Rossumb940e112007-01-10 16:19:56 +0000776 except smtplib.SMTPRecipientsRefused as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000777 print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000778 refused = e.recipients
Andrew Svetlov0832af62012-12-18 23:10:48 +0200779 except (OSError, smtplib.SMTPException) as e:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000780 print('got', e.__class__, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000781 # All recipients were refused. If the exception had an associated
782 # error code, use it. Otherwise,fake it with a non-triggering
783 # exception code.
784 errcode = getattr(e, 'smtp_code', -1)
785 errmsg = getattr(e, 'smtp_error', 'ignore')
786 for r in rcpttos:
787 refused[r] = (errcode, errmsg)
788 return refused
789
790
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000791class MailmanProxy(PureProxy):
R David Murray2539e672014-08-09 16:40:49 -0400792 def __init__(self, *args, **kwargs):
Samuel Colvin822922a2019-10-12 18:24:26 +0100793 warn('MailmanProxy is deprecated and will be removed '
794 'in future', DeprecationWarning, 2)
R David Murray2539e672014-08-09 16:40:49 -0400795 if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
796 raise ValueError("MailmanProxy does not support SMTPUTF8.")
797 super(PureProxy, self).__init__(*args, **kwargs)
798
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000799 def process_message(self, peer, mailfrom, rcpttos, data):
Guido van Rossum68937b42007-05-18 00:51:22 +0000800 from io import StringIO
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000801 from Mailman import Utils
802 from Mailman import Message
803 from Mailman import MailList
804 # If the message is to a Mailman mailing list, then we'll invoke the
805 # Mailman script directly, without going through the real smtpd.
806 # Otherwise we'll forward it to the local proxy for disposition.
807 listnames = []
808 for rcpt in rcpttos:
809 local = rcpt.lower().split('@')[0]
810 # We allow the following variations on the theme
811 # listname
812 # listname-admin
813 # listname-owner
814 # listname-request
815 # listname-join
816 # listname-leave
817 parts = local.split('-')
818 if len(parts) > 2:
819 continue
820 listname = parts[0]
821 if len(parts) == 2:
822 command = parts[1]
823 else:
824 command = ''
825 if not Utils.list_exists(listname) or command not in (
826 '', 'admin', 'owner', 'request', 'join', 'leave'):
827 continue
828 listnames.append((rcpt, listname, command))
829 # Remove all list recipients from rcpttos and forward what we're not
830 # going to take care of ourselves. Linear removal should be fine
831 # since we don't expect a large number of recipients.
832 for rcpt, listname, command in listnames:
833 rcpttos.remove(rcpt)
Tim Peters658cba62001-02-09 20:06:00 +0000834 # If there's any non-list destined recipients left,
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000835 print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000836 if rcpttos:
837 refused = self._deliver(mailfrom, rcpttos, data)
838 # TBD: what to do with refused addresses?
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000839 print('we got refusals:', refused, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000840 # Now deliver directly to the list commands
841 mlists = {}
842 s = StringIO(data)
843 msg = Message.Message(s)
844 # These headers are required for the proper execution of Mailman. All
Mark Dickinson934896d2009-02-21 20:59:32 +0000845 # MTAs in existence seem to add these if the original message doesn't
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000846 # have them.
Barry Warsaw820c1202008-06-12 04:06:45 +0000847 if not msg.get('from'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000848 msg['From'] = mailfrom
Barry Warsaw820c1202008-06-12 04:06:45 +0000849 if not msg.get('date'):
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000850 msg['Date'] = time.ctime(time.time())
851 for rcpt, listname, command in listnames:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000852 print('sending message to', rcpt, file=DEBUGSTREAM)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000853 mlist = mlists.get(listname)
854 if not mlist:
855 mlist = MailList.MailList(listname, lock=0)
856 mlists[listname] = mlist
857 # dispatch on the type of command
858 if command == '':
859 # post
860 msg.Enqueue(mlist, tolist=1)
861 elif command == 'admin':
862 msg.Enqueue(mlist, toadmin=1)
863 elif command == 'owner':
864 msg.Enqueue(mlist, toowner=1)
865 elif command == 'request':
866 msg.Enqueue(mlist, torequest=1)
867 elif command in ('join', 'leave'):
868 # TBD: this is a hack!
869 if command == 'join':
870 msg['Subject'] = 'subscribe'
871 else:
872 msg['Subject'] = 'unsubscribe'
873 msg.Enqueue(mlist, torequest=1)
874
875
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000876class Options:
R David Murray2539e672014-08-09 16:40:49 -0400877 setuid = True
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000878 classname = 'PureProxy'
R David Murrayd1a30c92012-05-26 14:33:59 -0400879 size_limit = None
R David Murray2539e672014-08-09 16:40:49 -0400880 enable_SMTPUTF8 = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000881
882
883def parseargs():
884 global DEBUGSTREAM
885 try:
886 opts, args = getopt.getopt(
R David Murray2539e672014-08-09 16:40:49 -0400887 sys.argv[1:], 'nVhc:s:du',
888 ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug',
889 'smtputf8'])
Guido van Rossumb940e112007-01-10 16:19:56 +0000890 except getopt.error as e:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000891 usage(1, e)
892
893 options = Options()
894 for opt, arg in opts:
895 if opt in ('-h', '--help'):
896 usage(0)
897 elif opt in ('-V', '--version'):
Serhiy Storchakac56894d2013-09-05 17:44:53 +0300898 print(__version__)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000899 sys.exit(0)
900 elif opt in ('-n', '--nosetuid'):
R David Murray2539e672014-08-09 16:40:49 -0400901 options.setuid = False
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000902 elif opt in ('-c', '--class'):
903 options.classname = arg
904 elif opt in ('-d', '--debug'):
905 DEBUGSTREAM = sys.stderr
R David Murray2539e672014-08-09 16:40:49 -0400906 elif opt in ('-u', '--smtputf8'):
907 options.enable_SMTPUTF8 = True
R David Murrayd1a30c92012-05-26 14:33:59 -0400908 elif opt in ('-s', '--size'):
909 try:
910 int_size = int(arg)
911 options.size_limit = int_size
912 except:
913 print('Invalid size: ' + arg, file=sys.stderr)
914 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000915
916 # parse the rest of the arguments
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000917 if len(args) < 1:
918 localspec = 'localhost:8025'
919 remotespec = 'localhost:25'
920 elif len(args) < 2:
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000921 localspec = args[0]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000922 remotespec = 'localhost:25'
Barry Warsawebf54272001-11-04 03:04:25 +0000923 elif len(args) < 3:
924 localspec = args[0]
925 remotespec = args[1]
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000926 else:
927 usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
928
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000929 # split into host/port pairs
930 i = localspec.find(':')
931 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000932 usage(1, 'Bad local spec: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000933 options.localhost = localspec[:i]
934 try:
935 options.localport = int(localspec[i+1:])
936 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000937 usage(1, 'Bad local port: %s' % localspec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000938 i = remotespec.find(':')
939 if i < 0:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000940 usage(1, 'Bad remote spec: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000941 options.remotehost = remotespec[:i]
942 try:
943 options.remoteport = int(remotespec[i+1:])
944 except ValueError:
Barry Warsaw0e8427e2001-10-04 16:27:04 +0000945 usage(1, 'Bad remote port: %s' % remotespec)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000946 return options
947
948
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000949if __name__ == '__main__':
950 options = parseargs()
951 # Become nobody
Florent Xicluna711f87c2011-10-20 23:03:43 +0200952 classname = options.classname
953 if "." in classname:
954 lastdot = classname.rfind(".")
955 mod = __import__(classname[:lastdot], globals(), locals(), [""])
956 classname = classname[lastdot+1:]
957 else:
958 import __main__ as mod
959 class_ = getattr(mod, classname)
960 proxy = class_((options.localhost, options.localport),
R David Murrayd1a30c92012-05-26 14:33:59 -0400961 (options.remotehost, options.remoteport),
R David Murray2539e672014-08-09 16:40:49 -0400962 options.size_limit, enable_SMTPUTF8=options.enable_SMTPUTF8)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000963 if options.setuid:
964 try:
965 import pwd
Brett Cannoncd171c82013-07-04 17:43:24 -0400966 except ImportError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000967 print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000968 sys.exit(1)
969 nobody = pwd.getpwnam('nobody')[2]
970 try:
971 os.setuid(nobody)
Giampaolo Rodola'0166a282013-02-12 15:14:17 +0100972 except PermissionError:
Guido van Rossumbe19ed72007-02-09 05:37:30 +0000973 print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000974 sys.exit(1)
Barry Warsaw7e0d9562001-01-31 22:51:35 +0000975 try:
976 asyncore.loop()
977 except KeyboardInterrupt:
978 pass