smtpd.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  1. #! /usr/bin/env python3
  2. """An RFC 5321 smtp proxy with optional RFC 1870 and RFC 6531 extensions.
  3. Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
  4. Options:
  5. --nosetuid
  6. -n
  7. This program generally tries to setuid `nobody', unless this flag is
  8. set. The setuid call will fail if this program is not run as root (in
  9. which case, use this flag).
  10. --version
  11. -V
  12. Print the version number and exit.
  13. --class classname
  14. -c classname
  15. Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by
  16. default.
  17. --size limit
  18. -s limit
  19. Restrict the total size of the incoming message to "limit" number of
  20. bytes via the RFC 1870 SIZE extension. Defaults to 33554432 bytes.
  21. --smtputf8
  22. -u
  23. Enable the SMTPUTF8 extension and behave as an RFC 6531 smtp proxy.
  24. --debug
  25. -d
  26. Turn on debugging prints.
  27. --help
  28. -h
  29. Print this message and exit.
  30. Version: %(__version__)s
  31. If localhost is not given then `localhost' is used, and if localport is not
  32. given then 8025 is used. If remotehost is not given then `localhost' is used,
  33. and if remoteport is not given, then 25 is used.
  34. """
  35. # Overview:
  36. #
  37. # This file implements the minimal SMTP protocol as defined in RFC 5321. It
  38. # has a hierarchy of classes which implement the backend functionality for the
  39. # smtpd. A number of classes are provided:
  40. #
  41. # SMTPServer - the base class for the backend. Raises NotImplementedError
  42. # if you try to use it.
  43. #
  44. # DebuggingServer - simply prints each message it receives on stdout.
  45. #
  46. # PureProxy - Proxies all messages to a real smtpd which does final
  47. # delivery. One known problem with this class is that it doesn't handle
  48. # SMTP errors from the backend server at all. This should be fixed
  49. # (contributions are welcome!).
  50. #
  51. #
  52. # Author: Barry Warsaw <barry@python.org>
  53. #
  54. # TODO:
  55. #
  56. # - support mailbox delivery
  57. # - alias files
  58. # - Handle more ESMTP extensions
  59. # - handle error codes from the backend smtpd
  60. import sys
  61. import os
  62. import errno
  63. import getopt
  64. import time
  65. import socket
  66. import collections
  67. from warnings import _deprecated, warn
  68. from email._header_value_parser import get_addr_spec, get_angle_addr
  69. __all__ = [
  70. "SMTPChannel", "SMTPServer", "DebuggingServer", "PureProxy",
  71. ]
  72. _DEPRECATION_MSG = ('The {name} module is deprecated and unmaintained and will '
  73. 'be removed in Python {remove}. Please see aiosmtpd '
  74. '(https://aiosmtpd.readthedocs.io/) for the recommended '
  75. 'replacement.')
  76. _deprecated(__name__, _DEPRECATION_MSG, remove=(3, 12))
  77. # These are imported after the above warning so that users get the correct
  78. # deprecation warning.
  79. import asyncore
  80. import asynchat
  81. program = sys.argv[0]
  82. __version__ = 'Python SMTP proxy version 0.3'
  83. class Devnull:
  84. def write(self, msg): pass
  85. def flush(self): pass
  86. DEBUGSTREAM = Devnull()
  87. NEWLINE = '\n'
  88. COMMASPACE = ', '
  89. DATA_SIZE_DEFAULT = 33554432
  90. def usage(code, msg=''):
  91. print(__doc__ % globals(), file=sys.stderr)
  92. if msg:
  93. print(msg, file=sys.stderr)
  94. sys.exit(code)
  95. class SMTPChannel(asynchat.async_chat):
  96. COMMAND = 0
  97. DATA = 1
  98. command_size_limit = 512
  99. command_size_limits = collections.defaultdict(lambda x=command_size_limit: x)
  100. @property
  101. def max_command_size_limit(self):
  102. try:
  103. return max(self.command_size_limits.values())
  104. except ValueError:
  105. return self.command_size_limit
  106. def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT,
  107. map=None, enable_SMTPUTF8=False, decode_data=False):
  108. asynchat.async_chat.__init__(self, conn, map=map)
  109. self.smtp_server = server
  110. self.conn = conn
  111. self.addr = addr
  112. self.data_size_limit = data_size_limit
  113. self.enable_SMTPUTF8 = enable_SMTPUTF8
  114. self._decode_data = decode_data
  115. if enable_SMTPUTF8 and decode_data:
  116. raise ValueError("decode_data and enable_SMTPUTF8 cannot"
  117. " be set to True at the same time")
  118. if decode_data:
  119. self._emptystring = ''
  120. self._linesep = '\r\n'
  121. self._dotsep = '.'
  122. self._newline = NEWLINE
  123. else:
  124. self._emptystring = b''
  125. self._linesep = b'\r\n'
  126. self._dotsep = ord(b'.')
  127. self._newline = b'\n'
  128. self._set_rset_state()
  129. self.seen_greeting = ''
  130. self.extended_smtp = False
  131. self.command_size_limits.clear()
  132. self.fqdn = socket.getfqdn()
  133. try:
  134. self.peer = conn.getpeername()
  135. except OSError as err:
  136. # a race condition may occur if the other end is closing
  137. # before we can get the peername
  138. self.close()
  139. if err.errno != errno.ENOTCONN:
  140. raise
  141. return
  142. print('Peer:', repr(self.peer), file=DEBUGSTREAM)
  143. self.push('220 %s %s' % (self.fqdn, __version__))
  144. def _set_post_data_state(self):
  145. """Reset state variables to their post-DATA state."""
  146. self.smtp_state = self.COMMAND
  147. self.mailfrom = None
  148. self.rcpttos = []
  149. self.require_SMTPUTF8 = False
  150. self.num_bytes = 0
  151. self.set_terminator(b'\r\n')
  152. def _set_rset_state(self):
  153. """Reset all state variables except the greeting."""
  154. self._set_post_data_state()
  155. self.received_data = ''
  156. self.received_lines = []
  157. # properties for backwards-compatibility
  158. @property
  159. def __server(self):
  160. warn("Access to __server attribute on SMTPChannel is deprecated, "
  161. "use 'smtp_server' instead", DeprecationWarning, 2)
  162. return self.smtp_server
  163. @__server.setter
  164. def __server(self, value):
  165. warn("Setting __server attribute on SMTPChannel is deprecated, "
  166. "set 'smtp_server' instead", DeprecationWarning, 2)
  167. self.smtp_server = value
  168. @property
  169. def __line(self):
  170. warn("Access to __line attribute on SMTPChannel is deprecated, "
  171. "use 'received_lines' instead", DeprecationWarning, 2)
  172. return self.received_lines
  173. @__line.setter
  174. def __line(self, value):
  175. warn("Setting __line attribute on SMTPChannel is deprecated, "
  176. "set 'received_lines' instead", DeprecationWarning, 2)
  177. self.received_lines = value
  178. @property
  179. def __state(self):
  180. warn("Access to __state attribute on SMTPChannel is deprecated, "
  181. "use 'smtp_state' instead", DeprecationWarning, 2)
  182. return self.smtp_state
  183. @__state.setter
  184. def __state(self, value):
  185. warn("Setting __state attribute on SMTPChannel is deprecated, "
  186. "set 'smtp_state' instead", DeprecationWarning, 2)
  187. self.smtp_state = value
  188. @property
  189. def __greeting(self):
  190. warn("Access to __greeting attribute on SMTPChannel is deprecated, "
  191. "use 'seen_greeting' instead", DeprecationWarning, 2)
  192. return self.seen_greeting
  193. @__greeting.setter
  194. def __greeting(self, value):
  195. warn("Setting __greeting attribute on SMTPChannel is deprecated, "
  196. "set 'seen_greeting' instead", DeprecationWarning, 2)
  197. self.seen_greeting = value
  198. @property
  199. def __mailfrom(self):
  200. warn("Access to __mailfrom attribute on SMTPChannel is deprecated, "
  201. "use 'mailfrom' instead", DeprecationWarning, 2)
  202. return self.mailfrom
  203. @__mailfrom.setter
  204. def __mailfrom(self, value):
  205. warn("Setting __mailfrom attribute on SMTPChannel is deprecated, "
  206. "set 'mailfrom' instead", DeprecationWarning, 2)
  207. self.mailfrom = value
  208. @property
  209. def __rcpttos(self):
  210. warn("Access to __rcpttos attribute on SMTPChannel is deprecated, "
  211. "use 'rcpttos' instead", DeprecationWarning, 2)
  212. return self.rcpttos
  213. @__rcpttos.setter
  214. def __rcpttos(self, value):
  215. warn("Setting __rcpttos attribute on SMTPChannel is deprecated, "
  216. "set 'rcpttos' instead", DeprecationWarning, 2)
  217. self.rcpttos = value
  218. @property
  219. def __data(self):
  220. warn("Access to __data attribute on SMTPChannel is deprecated, "
  221. "use 'received_data' instead", DeprecationWarning, 2)
  222. return self.received_data
  223. @__data.setter
  224. def __data(self, value):
  225. warn("Setting __data attribute on SMTPChannel is deprecated, "
  226. "set 'received_data' instead", DeprecationWarning, 2)
  227. self.received_data = value
  228. @property
  229. def __fqdn(self):
  230. warn("Access to __fqdn attribute on SMTPChannel is deprecated, "
  231. "use 'fqdn' instead", DeprecationWarning, 2)
  232. return self.fqdn
  233. @__fqdn.setter
  234. def __fqdn(self, value):
  235. warn("Setting __fqdn attribute on SMTPChannel is deprecated, "
  236. "set 'fqdn' instead", DeprecationWarning, 2)
  237. self.fqdn = value
  238. @property
  239. def __peer(self):
  240. warn("Access to __peer attribute on SMTPChannel is deprecated, "
  241. "use 'peer' instead", DeprecationWarning, 2)
  242. return self.peer
  243. @__peer.setter
  244. def __peer(self, value):
  245. warn("Setting __peer attribute on SMTPChannel is deprecated, "
  246. "set 'peer' instead", DeprecationWarning, 2)
  247. self.peer = value
  248. @property
  249. def __conn(self):
  250. warn("Access to __conn attribute on SMTPChannel is deprecated, "
  251. "use 'conn' instead", DeprecationWarning, 2)
  252. return self.conn
  253. @__conn.setter
  254. def __conn(self, value):
  255. warn("Setting __conn attribute on SMTPChannel is deprecated, "
  256. "set 'conn' instead", DeprecationWarning, 2)
  257. self.conn = value
  258. @property
  259. def __addr(self):
  260. warn("Access to __addr attribute on SMTPChannel is deprecated, "
  261. "use 'addr' instead", DeprecationWarning, 2)
  262. return self.addr
  263. @__addr.setter
  264. def __addr(self, value):
  265. warn("Setting __addr attribute on SMTPChannel is deprecated, "
  266. "set 'addr' instead", DeprecationWarning, 2)
  267. self.addr = value
  268. # Overrides base class for convenience.
  269. def push(self, msg):
  270. asynchat.async_chat.push(self, bytes(
  271. msg + '\r\n', 'utf-8' if self.require_SMTPUTF8 else 'ascii'))
  272. # Implementation of base class abstract method
  273. def collect_incoming_data(self, data):
  274. limit = None
  275. if self.smtp_state == self.COMMAND:
  276. limit = self.max_command_size_limit
  277. elif self.smtp_state == self.DATA:
  278. limit = self.data_size_limit
  279. if limit and self.num_bytes > limit:
  280. return
  281. elif limit:
  282. self.num_bytes += len(data)
  283. if self._decode_data:
  284. self.received_lines.append(str(data, 'utf-8'))
  285. else:
  286. self.received_lines.append(data)
  287. # Implementation of base class abstract method
  288. def found_terminator(self):
  289. line = self._emptystring.join(self.received_lines)
  290. print('Data:', repr(line), file=DEBUGSTREAM)
  291. self.received_lines = []
  292. if self.smtp_state == self.COMMAND:
  293. sz, self.num_bytes = self.num_bytes, 0
  294. if not line:
  295. self.push('500 Error: bad syntax')
  296. return
  297. if not self._decode_data:
  298. line = str(line, 'utf-8')
  299. i = line.find(' ')
  300. if i < 0:
  301. command = line.upper()
  302. arg = None
  303. else:
  304. command = line[:i].upper()
  305. arg = line[i+1:].strip()
  306. max_sz = (self.command_size_limits[command]
  307. if self.extended_smtp else self.command_size_limit)
  308. if sz > max_sz:
  309. self.push('500 Error: line too long')
  310. return
  311. method = getattr(self, 'smtp_' + command, None)
  312. if not method:
  313. self.push('500 Error: command "%s" not recognized' % command)
  314. return
  315. method(arg)
  316. return
  317. else:
  318. if self.smtp_state != self.DATA:
  319. self.push('451 Internal confusion')
  320. self.num_bytes = 0
  321. return
  322. if self.data_size_limit and self.num_bytes > self.data_size_limit:
  323. self.push('552 Error: Too much mail data')
  324. self.num_bytes = 0
  325. return
  326. # Remove extraneous carriage returns and de-transparency according
  327. # to RFC 5321, Section 4.5.2.
  328. data = []
  329. for text in line.split(self._linesep):
  330. if text and text[0] == self._dotsep:
  331. data.append(text[1:])
  332. else:
  333. data.append(text)
  334. self.received_data = self._newline.join(data)
  335. args = (self.peer, self.mailfrom, self.rcpttos, self.received_data)
  336. kwargs = {}
  337. if not self._decode_data:
  338. kwargs = {
  339. 'mail_options': self.mail_options,
  340. 'rcpt_options': self.rcpt_options,
  341. }
  342. status = self.smtp_server.process_message(*args, **kwargs)
  343. self._set_post_data_state()
  344. if not status:
  345. self.push('250 OK')
  346. else:
  347. self.push(status)
  348. # SMTP and ESMTP commands
  349. def smtp_HELO(self, arg):
  350. if not arg:
  351. self.push('501 Syntax: HELO hostname')
  352. return
  353. # See issue #21783 for a discussion of this behavior.
  354. if self.seen_greeting:
  355. self.push('503 Duplicate HELO/EHLO')
  356. return
  357. self._set_rset_state()
  358. self.seen_greeting = arg
  359. self.push('250 %s' % self.fqdn)
  360. def smtp_EHLO(self, arg):
  361. if not arg:
  362. self.push('501 Syntax: EHLO hostname')
  363. return
  364. # See issue #21783 for a discussion of this behavior.
  365. if self.seen_greeting:
  366. self.push('503 Duplicate HELO/EHLO')
  367. return
  368. self._set_rset_state()
  369. self.seen_greeting = arg
  370. self.extended_smtp = True
  371. self.push('250-%s' % self.fqdn)
  372. if self.data_size_limit:
  373. self.push('250-SIZE %s' % self.data_size_limit)
  374. self.command_size_limits['MAIL'] += 26
  375. if not self._decode_data:
  376. self.push('250-8BITMIME')
  377. if self.enable_SMTPUTF8:
  378. self.push('250-SMTPUTF8')
  379. self.command_size_limits['MAIL'] += 10
  380. self.push('250 HELP')
  381. def smtp_NOOP(self, arg):
  382. if arg:
  383. self.push('501 Syntax: NOOP')
  384. else:
  385. self.push('250 OK')
  386. def smtp_QUIT(self, arg):
  387. # args is ignored
  388. self.push('221 Bye')
  389. self.close_when_done()
  390. def _strip_command_keyword(self, keyword, arg):
  391. keylen = len(keyword)
  392. if arg[:keylen].upper() == keyword:
  393. return arg[keylen:].strip()
  394. return ''
  395. def _getaddr(self, arg):
  396. if not arg:
  397. return '', ''
  398. if arg.lstrip().startswith('<'):
  399. address, rest = get_angle_addr(arg)
  400. else:
  401. address, rest = get_addr_spec(arg)
  402. if not address:
  403. return address, rest
  404. return address.addr_spec, rest
  405. def _getparams(self, params):
  406. # Return params as dictionary. Return None if not all parameters
  407. # appear to be syntactically valid according to RFC 1869.
  408. result = {}
  409. for param in params:
  410. param, eq, value = param.partition('=')
  411. if not param.isalnum() or eq and not value:
  412. return None
  413. result[param] = value if eq else True
  414. return result
  415. def smtp_HELP(self, arg):
  416. if arg:
  417. extended = ' [SP <mail-parameters>]'
  418. lc_arg = arg.upper()
  419. if lc_arg == 'EHLO':
  420. self.push('250 Syntax: EHLO hostname')
  421. elif lc_arg == 'HELO':
  422. self.push('250 Syntax: HELO hostname')
  423. elif lc_arg == 'MAIL':
  424. msg = '250 Syntax: MAIL FROM: <address>'
  425. if self.extended_smtp:
  426. msg += extended
  427. self.push(msg)
  428. elif lc_arg == 'RCPT':
  429. msg = '250 Syntax: RCPT TO: <address>'
  430. if self.extended_smtp:
  431. msg += extended
  432. self.push(msg)
  433. elif lc_arg == 'DATA':
  434. self.push('250 Syntax: DATA')
  435. elif lc_arg == 'RSET':
  436. self.push('250 Syntax: RSET')
  437. elif lc_arg == 'NOOP':
  438. self.push('250 Syntax: NOOP')
  439. elif lc_arg == 'QUIT':
  440. self.push('250 Syntax: QUIT')
  441. elif lc_arg == 'VRFY':
  442. self.push('250 Syntax: VRFY <address>')
  443. else:
  444. self.push('501 Supported commands: EHLO HELO MAIL RCPT '
  445. 'DATA RSET NOOP QUIT VRFY')
  446. else:
  447. self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA '
  448. 'RSET NOOP QUIT VRFY')
  449. def smtp_VRFY(self, arg):
  450. if arg:
  451. address, params = self._getaddr(arg)
  452. if address:
  453. self.push('252 Cannot VRFY user, but will accept message '
  454. 'and attempt delivery')
  455. else:
  456. self.push('502 Could not VRFY %s' % arg)
  457. else:
  458. self.push('501 Syntax: VRFY <address>')
  459. def smtp_MAIL(self, arg):
  460. if not self.seen_greeting:
  461. self.push('503 Error: send HELO first')
  462. return
  463. print('===> MAIL', arg, file=DEBUGSTREAM)
  464. syntaxerr = '501 Syntax: MAIL FROM: <address>'
  465. if self.extended_smtp:
  466. syntaxerr += ' [SP <mail-parameters>]'
  467. if arg is None:
  468. self.push(syntaxerr)
  469. return
  470. arg = self._strip_command_keyword('FROM:', arg)
  471. address, params = self._getaddr(arg)
  472. if not address:
  473. self.push(syntaxerr)
  474. return
  475. if not self.extended_smtp and params:
  476. self.push(syntaxerr)
  477. return
  478. if self.mailfrom:
  479. self.push('503 Error: nested MAIL command')
  480. return
  481. self.mail_options = params.upper().split()
  482. params = self._getparams(self.mail_options)
  483. if params is None:
  484. self.push(syntaxerr)
  485. return
  486. if not self._decode_data:
  487. body = params.pop('BODY', '7BIT')
  488. if body not in ['7BIT', '8BITMIME']:
  489. self.push('501 Error: BODY can only be one of 7BIT, 8BITMIME')
  490. return
  491. if self.enable_SMTPUTF8:
  492. smtputf8 = params.pop('SMTPUTF8', False)
  493. if smtputf8 is True:
  494. self.require_SMTPUTF8 = True
  495. elif smtputf8 is not False:
  496. self.push('501 Error: SMTPUTF8 takes no arguments')
  497. return
  498. size = params.pop('SIZE', None)
  499. if size:
  500. if not size.isdigit():
  501. self.push(syntaxerr)
  502. return
  503. elif self.data_size_limit and int(size) > self.data_size_limit:
  504. self.push('552 Error: message size exceeds fixed maximum message size')
  505. return
  506. if len(params.keys()) > 0:
  507. self.push('555 MAIL FROM parameters not recognized or not implemented')
  508. return
  509. self.mailfrom = address
  510. print('sender:', self.mailfrom, file=DEBUGSTREAM)
  511. self.push('250 OK')
  512. def smtp_RCPT(self, arg):
  513. if not self.seen_greeting:
  514. self.push('503 Error: send HELO first');
  515. return
  516. print('===> RCPT', arg, file=DEBUGSTREAM)
  517. if not self.mailfrom:
  518. self.push('503 Error: need MAIL command')
  519. return
  520. syntaxerr = '501 Syntax: RCPT TO: <address>'
  521. if self.extended_smtp:
  522. syntaxerr += ' [SP <mail-parameters>]'
  523. if arg is None:
  524. self.push(syntaxerr)
  525. return
  526. arg = self._strip_command_keyword('TO:', arg)
  527. address, params = self._getaddr(arg)
  528. if not address:
  529. self.push(syntaxerr)
  530. return
  531. if not self.extended_smtp and params:
  532. self.push(syntaxerr)
  533. return
  534. self.rcpt_options = params.upper().split()
  535. params = self._getparams(self.rcpt_options)
  536. if params is None:
  537. self.push(syntaxerr)
  538. return
  539. # XXX currently there are no options we recognize.
  540. if len(params.keys()) > 0:
  541. self.push('555 RCPT TO parameters not recognized or not implemented')
  542. return
  543. self.rcpttos.append(address)
  544. print('recips:', self.rcpttos, file=DEBUGSTREAM)
  545. self.push('250 OK')
  546. def smtp_RSET(self, arg):
  547. if arg:
  548. self.push('501 Syntax: RSET')
  549. return
  550. self._set_rset_state()
  551. self.push('250 OK')
  552. def smtp_DATA(self, arg):
  553. if not self.seen_greeting:
  554. self.push('503 Error: send HELO first');
  555. return
  556. if not self.rcpttos:
  557. self.push('503 Error: need RCPT command')
  558. return
  559. if arg:
  560. self.push('501 Syntax: DATA')
  561. return
  562. self.smtp_state = self.DATA
  563. self.set_terminator(b'\r\n.\r\n')
  564. self.push('354 End data with <CR><LF>.<CR><LF>')
  565. # Commands that have not been implemented
  566. def smtp_EXPN(self, arg):
  567. self.push('502 EXPN not implemented')
  568. class SMTPServer(asyncore.dispatcher):
  569. # SMTPChannel class to use for managing client connections
  570. channel_class = SMTPChannel
  571. def __init__(self, localaddr, remoteaddr,
  572. data_size_limit=DATA_SIZE_DEFAULT, map=None,
  573. enable_SMTPUTF8=False, decode_data=False):
  574. self._localaddr = localaddr
  575. self._remoteaddr = remoteaddr
  576. self.data_size_limit = data_size_limit
  577. self.enable_SMTPUTF8 = enable_SMTPUTF8
  578. self._decode_data = decode_data
  579. if enable_SMTPUTF8 and decode_data:
  580. raise ValueError("decode_data and enable_SMTPUTF8 cannot"
  581. " be set to True at the same time")
  582. asyncore.dispatcher.__init__(self, map=map)
  583. try:
  584. gai_results = socket.getaddrinfo(*localaddr,
  585. type=socket.SOCK_STREAM)
  586. self.create_socket(gai_results[0][0], gai_results[0][1])
  587. # try to re-use a server port if possible
  588. self.set_reuse_addr()
  589. self.bind(localaddr)
  590. self.listen(5)
  591. except:
  592. self.close()
  593. raise
  594. else:
  595. print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
  596. self.__class__.__name__, time.ctime(time.time()),
  597. localaddr, remoteaddr), file=DEBUGSTREAM)
  598. def handle_accepted(self, conn, addr):
  599. print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
  600. channel = self.channel_class(self,
  601. conn,
  602. addr,
  603. self.data_size_limit,
  604. self._map,
  605. self.enable_SMTPUTF8,
  606. self._decode_data)
  607. # API for "doing something useful with the message"
  608. def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
  609. """Override this abstract method to handle messages from the client.
  610. peer is a tuple containing (ipaddr, port) of the client that made the
  611. socket connection to our smtp port.
  612. mailfrom is the raw address the client claims the message is coming
  613. from.
  614. rcpttos is a list of raw addresses the client wishes to deliver the
  615. message to.
  616. data is a string containing the entire full text of the message,
  617. headers (if supplied) and all. It has been `de-transparencied'
  618. according to RFC 821, Section 4.5.2. In other words, a line
  619. containing a `.' followed by other text has had the leading dot
  620. removed.
  621. kwargs is a dictionary containing additional information. It is
  622. empty if decode_data=True was given as init parameter, otherwise
  623. it will contain the following keys:
  624. 'mail_options': list of parameters to the mail command. All
  625. elements are uppercase strings. Example:
  626. ['BODY=8BITMIME', 'SMTPUTF8'].
  627. 'rcpt_options': same, for the rcpt command.
  628. This function should return None for a normal `250 Ok' response;
  629. otherwise, it should return the desired response string in RFC 821
  630. format.
  631. """
  632. raise NotImplementedError
  633. class DebuggingServer(SMTPServer):
  634. def _print_message_content(self, peer, data):
  635. inheaders = 1
  636. lines = data.splitlines()
  637. for line in lines:
  638. # headers first
  639. if inheaders and not line:
  640. peerheader = 'X-Peer: ' + peer[0]
  641. if not isinstance(data, str):
  642. # decoded_data=false; make header match other binary output
  643. peerheader = repr(peerheader.encode('utf-8'))
  644. print(peerheader)
  645. inheaders = 0
  646. if not isinstance(data, str):
  647. # Avoid spurious 'str on bytes instance' warning.
  648. line = repr(line)
  649. print(line)
  650. def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
  651. print('---------- MESSAGE FOLLOWS ----------')
  652. if kwargs:
  653. if kwargs.get('mail_options'):
  654. print('mail options: %s' % kwargs['mail_options'])
  655. if kwargs.get('rcpt_options'):
  656. print('rcpt options: %s\n' % kwargs['rcpt_options'])
  657. self._print_message_content(peer, data)
  658. print('------------ END MESSAGE ------------')
  659. class PureProxy(SMTPServer):
  660. def __init__(self, *args, **kwargs):
  661. if 'enable_SMTPUTF8' in kwargs and kwargs['enable_SMTPUTF8']:
  662. raise ValueError("PureProxy does not support SMTPUTF8.")
  663. super(PureProxy, self).__init__(*args, **kwargs)
  664. def process_message(self, peer, mailfrom, rcpttos, data):
  665. lines = data.split('\n')
  666. # Look for the last header
  667. i = 0
  668. for line in lines:
  669. if not line:
  670. break
  671. i += 1
  672. lines.insert(i, 'X-Peer: %s' % peer[0])
  673. data = NEWLINE.join(lines)
  674. refused = self._deliver(mailfrom, rcpttos, data)
  675. # TBD: what to do with refused addresses?
  676. print('we got some refusals:', refused, file=DEBUGSTREAM)
  677. def _deliver(self, mailfrom, rcpttos, data):
  678. import smtplib
  679. refused = {}
  680. try:
  681. s = smtplib.SMTP()
  682. s.connect(self._remoteaddr[0], self._remoteaddr[1])
  683. try:
  684. refused = s.sendmail(mailfrom, rcpttos, data)
  685. finally:
  686. s.quit()
  687. except smtplib.SMTPRecipientsRefused as e:
  688. print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
  689. refused = e.recipients
  690. except (OSError, smtplib.SMTPException) as e:
  691. print('got', e.__class__, file=DEBUGSTREAM)
  692. # All recipients were refused. If the exception had an associated
  693. # error code, use it. Otherwise,fake it with a non-triggering
  694. # exception code.
  695. errcode = getattr(e, 'smtp_code', -1)
  696. errmsg = getattr(e, 'smtp_error', 'ignore')
  697. for r in rcpttos:
  698. refused[r] = (errcode, errmsg)
  699. return refused
  700. class Options:
  701. setuid = True
  702. classname = 'PureProxy'
  703. size_limit = None
  704. enable_SMTPUTF8 = False
  705. def parseargs():
  706. global DEBUGSTREAM
  707. try:
  708. opts, args = getopt.getopt(
  709. sys.argv[1:], 'nVhc:s:du',
  710. ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug',
  711. 'smtputf8'])
  712. except getopt.error as e:
  713. usage(1, e)
  714. options = Options()
  715. for opt, arg in opts:
  716. if opt in ('-h', '--help'):
  717. usage(0)
  718. elif opt in ('-V', '--version'):
  719. print(__version__)
  720. sys.exit(0)
  721. elif opt in ('-n', '--nosetuid'):
  722. options.setuid = False
  723. elif opt in ('-c', '--class'):
  724. options.classname = arg
  725. elif opt in ('-d', '--debug'):
  726. DEBUGSTREAM = sys.stderr
  727. elif opt in ('-u', '--smtputf8'):
  728. options.enable_SMTPUTF8 = True
  729. elif opt in ('-s', '--size'):
  730. try:
  731. int_size = int(arg)
  732. options.size_limit = int_size
  733. except:
  734. print('Invalid size: ' + arg, file=sys.stderr)
  735. sys.exit(1)
  736. # parse the rest of the arguments
  737. if len(args) < 1:
  738. localspec = 'localhost:8025'
  739. remotespec = 'localhost:25'
  740. elif len(args) < 2:
  741. localspec = args[0]
  742. remotespec = 'localhost:25'
  743. elif len(args) < 3:
  744. localspec = args[0]
  745. remotespec = args[1]
  746. else:
  747. usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
  748. # split into host/port pairs
  749. i = localspec.find(':')
  750. if i < 0:
  751. usage(1, 'Bad local spec: %s' % localspec)
  752. options.localhost = localspec[:i]
  753. try:
  754. options.localport = int(localspec[i+1:])
  755. except ValueError:
  756. usage(1, 'Bad local port: %s' % localspec)
  757. i = remotespec.find(':')
  758. if i < 0:
  759. usage(1, 'Bad remote spec: %s' % remotespec)
  760. options.remotehost = remotespec[:i]
  761. try:
  762. options.remoteport = int(remotespec[i+1:])
  763. except ValueError:
  764. usage(1, 'Bad remote port: %s' % remotespec)
  765. return options
  766. if __name__ == '__main__':
  767. options = parseargs()
  768. # Become nobody
  769. classname = options.classname
  770. if "." in classname:
  771. lastdot = classname.rfind(".")
  772. mod = __import__(classname[:lastdot], globals(), locals(), [""])
  773. classname = classname[lastdot+1:]
  774. else:
  775. import __main__ as mod
  776. class_ = getattr(mod, classname)
  777. proxy = class_((options.localhost, options.localport),
  778. (options.remotehost, options.remoteport),
  779. options.size_limit, enable_SMTPUTF8=options.enable_SMTPUTF8)
  780. if options.setuid:
  781. try:
  782. import pwd
  783. except ImportError:
  784. print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
  785. sys.exit(1)
  786. nobody = pwd.getpwnam('nobody')[2]
  787. try:
  788. os.setuid(nobody)
  789. except PermissionError:
  790. print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
  791. sys.exit(1)
  792. try:
  793. asyncore.loop()
  794. except KeyboardInterrupt:
  795. pass