logo

oasis-root

Compiled tree of Oasis Linux based on own branch at <https://hacktivis.me/git/oasis/> git clone https://anongit.hacktivis.me/git/oasis-root.git

smtpd.py (35123B)


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