smtpd.py 34 KB

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