PageRenderTime 85ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/lib-python/2.7/smtpd.py

https://bitbucket.org/dac_io/pypy
Python | 555 lines | 472 code | 14 blank | 69 comment | 21 complexity | 95b3c89f0cda61a1828bf06630407044 MD5 | raw file
  1. #! /usr/bin/env python
  2. """An RFC 2821 smtp proxy.
  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. --debug
  18. -d
  19. Turn on debugging prints.
  20. --help
  21. -h
  22. Print this message and exit.
  23. Version: %(__version__)s
  24. If localhost is not given then `localhost' is used, and if localport is not
  25. given then 8025 is used. If remotehost is not given then `localhost' is used,
  26. and if remoteport is not given, then 25 is used.
  27. """
  28. # Overview:
  29. #
  30. # This file implements the minimal SMTP protocol as defined in RFC 821. It
  31. # has a hierarchy of classes which implement the backend functionality for the
  32. # smtpd. A number of classes are provided:
  33. #
  34. # SMTPServer - the base class for the backend. Raises NotImplementedError
  35. # if you try to use it.
  36. #
  37. # DebuggingServer - simply prints each message it receives on stdout.
  38. #
  39. # PureProxy - Proxies all messages to a real smtpd which does final
  40. # delivery. One known problem with this class is that it doesn't handle
  41. # SMTP errors from the backend server at all. This should be fixed
  42. # (contributions are welcome!).
  43. #
  44. # MailmanProxy - An experimental hack to work with GNU Mailman
  45. # <www.list.org>. Using this server as your real incoming smtpd, your
  46. # mailhost will automatically recognize and accept mail destined to Mailman
  47. # lists when those lists are created. Every message not destined for a list
  48. # gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
  49. # are not handled correctly yet.
  50. #
  51. # Please note that this script requires Python 2.0
  52. #
  53. # Author: Barry Warsaw <barry@python.org>
  54. #
  55. # TODO:
  56. #
  57. # - support mailbox delivery
  58. # - alias files
  59. # - ESMTP
  60. # - handle error codes from the backend smtpd
  61. import sys
  62. import os
  63. import errno
  64. import getopt
  65. import time
  66. import socket
  67. import asyncore
  68. import asynchat
  69. __all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
  70. program = sys.argv[0]
  71. __version__ = 'Python SMTP proxy version 0.2'
  72. class Devnull:
  73. def write(self, msg): pass
  74. def flush(self): pass
  75. DEBUGSTREAM = Devnull()
  76. NEWLINE = '\n'
  77. EMPTYSTRING = ''
  78. COMMASPACE = ', '
  79. def usage(code, msg=''):
  80. print >> sys.stderr, __doc__ % globals()
  81. if msg:
  82. print >> sys.stderr, msg
  83. sys.exit(code)
  84. class SMTPChannel(asynchat.async_chat):
  85. COMMAND = 0
  86. DATA = 1
  87. def __init__(self, server, conn, addr):
  88. asynchat.async_chat.__init__(self, conn)
  89. self.__server = server
  90. self.__conn = conn
  91. self.__addr = addr
  92. self.__line = []
  93. self.__state = self.COMMAND
  94. self.__greeting = 0
  95. self.__mailfrom = None
  96. self.__rcpttos = []
  97. self.__data = ''
  98. self.__fqdn = socket.getfqdn()
  99. try:
  100. self.__peer = conn.getpeername()
  101. except socket.error, err:
  102. # a race condition may occur if the other end is closing
  103. # before we can get the peername
  104. self.close()
  105. if err[0] != errno.ENOTCONN:
  106. raise
  107. return
  108. print >> DEBUGSTREAM, 'Peer:', repr(self.__peer)
  109. self.push('220 %s %s' % (self.__fqdn, __version__))
  110. self.set_terminator('\r\n')
  111. # Overrides base class for convenience
  112. def push(self, msg):
  113. asynchat.async_chat.push(self, msg + '\r\n')
  114. # Implementation of base class abstract method
  115. def collect_incoming_data(self, data):
  116. self.__line.append(data)
  117. # Implementation of base class abstract method
  118. def found_terminator(self):
  119. line = EMPTYSTRING.join(self.__line)
  120. print >> DEBUGSTREAM, 'Data:', repr(line)
  121. self.__line = []
  122. if self.__state == self.COMMAND:
  123. if not line:
  124. self.push('500 Error: bad syntax')
  125. return
  126. method = None
  127. i = line.find(' ')
  128. if i < 0:
  129. command = line.upper()
  130. arg = None
  131. else:
  132. command = line[:i].upper()
  133. arg = line[i+1:].strip()
  134. method = getattr(self, 'smtp_' + command, None)
  135. if not method:
  136. self.push('502 Error: command "%s" not implemented' % command)
  137. return
  138. method(arg)
  139. return
  140. else:
  141. if self.__state != self.DATA:
  142. self.push('451 Internal confusion')
  143. return
  144. # Remove extraneous carriage returns and de-transparency according
  145. # to RFC 821, Section 4.5.2.
  146. data = []
  147. for text in line.split('\r\n'):
  148. if text and text[0] == '.':
  149. data.append(text[1:])
  150. else:
  151. data.append(text)
  152. self.__data = NEWLINE.join(data)
  153. status = self.__server.process_message(self.__peer,
  154. self.__mailfrom,
  155. self.__rcpttos,
  156. self.__data)
  157. self.__rcpttos = []
  158. self.__mailfrom = None
  159. self.__state = self.COMMAND
  160. self.set_terminator('\r\n')
  161. if not status:
  162. self.push('250 Ok')
  163. else:
  164. self.push(status)
  165. # SMTP and ESMTP commands
  166. def smtp_HELO(self, arg):
  167. if not arg:
  168. self.push('501 Syntax: HELO hostname')
  169. return
  170. if self.__greeting:
  171. self.push('503 Duplicate HELO/EHLO')
  172. else:
  173. self.__greeting = arg
  174. self.push('250 %s' % self.__fqdn)
  175. def smtp_NOOP(self, arg):
  176. if arg:
  177. self.push('501 Syntax: NOOP')
  178. else:
  179. self.push('250 Ok')
  180. def smtp_QUIT(self, arg):
  181. # args is ignored
  182. self.push('221 Bye')
  183. self.close_when_done()
  184. # factored
  185. def __getaddr(self, keyword, arg):
  186. address = None
  187. keylen = len(keyword)
  188. if arg[:keylen].upper() == keyword:
  189. address = arg[keylen:].strip()
  190. if not address:
  191. pass
  192. elif address[0] == '<' and address[-1] == '>' and address != '<>':
  193. # Addresses can be in the form <person@dom.com> but watch out
  194. # for null address, e.g. <>
  195. address = address[1:-1]
  196. return address
  197. def smtp_MAIL(self, arg):
  198. print >> DEBUGSTREAM, '===> MAIL', arg
  199. address = self.__getaddr('FROM:', arg) if arg else None
  200. if not address:
  201. self.push('501 Syntax: MAIL FROM:<address>')
  202. return
  203. if self.__mailfrom:
  204. self.push('503 Error: nested MAIL command')
  205. return
  206. self.__mailfrom = address
  207. print >> DEBUGSTREAM, 'sender:', self.__mailfrom
  208. self.push('250 Ok')
  209. def smtp_RCPT(self, arg):
  210. print >> DEBUGSTREAM, '===> RCPT', arg
  211. if not self.__mailfrom:
  212. self.push('503 Error: need MAIL command')
  213. return
  214. address = self.__getaddr('TO:', arg) if arg else None
  215. if not address:
  216. self.push('501 Syntax: RCPT TO: <address>')
  217. return
  218. self.__rcpttos.append(address)
  219. print >> DEBUGSTREAM, 'recips:', self.__rcpttos
  220. self.push('250 Ok')
  221. def smtp_RSET(self, arg):
  222. if arg:
  223. self.push('501 Syntax: RSET')
  224. return
  225. # Resets the sender, recipients, and data, but not the greeting
  226. self.__mailfrom = None
  227. self.__rcpttos = []
  228. self.__data = ''
  229. self.__state = self.COMMAND
  230. self.push('250 Ok')
  231. def smtp_DATA(self, arg):
  232. if not self.__rcpttos:
  233. self.push('503 Error: need RCPT command')
  234. return
  235. if arg:
  236. self.push('501 Syntax: DATA')
  237. return
  238. self.__state = self.DATA
  239. self.set_terminator('\r\n.\r\n')
  240. self.push('354 End data with <CR><LF>.<CR><LF>')
  241. class SMTPServer(asyncore.dispatcher):
  242. def __init__(self, localaddr, remoteaddr):
  243. self._localaddr = localaddr
  244. self._remoteaddr = remoteaddr
  245. asyncore.dispatcher.__init__(self)
  246. try:
  247. self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
  248. # try to re-use a server port if possible
  249. self.set_reuse_addr()
  250. self.bind(localaddr)
  251. self.listen(5)
  252. except:
  253. # cleanup asyncore.socket_map before raising
  254. self.close()
  255. raise
  256. else:
  257. print >> DEBUGSTREAM, \
  258. '%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
  259. self.__class__.__name__, time.ctime(time.time()),
  260. localaddr, remoteaddr)
  261. def handle_accept(self):
  262. pair = self.accept()
  263. if pair is not None:
  264. conn, addr = pair
  265. print >> DEBUGSTREAM, 'Incoming connection from %s' % repr(addr)
  266. channel = SMTPChannel(self, conn, addr)
  267. # API for "doing something useful with the message"
  268. def process_message(self, peer, mailfrom, rcpttos, data):
  269. """Override this abstract method to handle messages from the client.
  270. peer is a tuple containing (ipaddr, port) of the client that made the
  271. socket connection to our smtp port.
  272. mailfrom is the raw address the client claims the message is coming
  273. from.
  274. rcpttos is a list of raw addresses the client wishes to deliver the
  275. message to.
  276. data is a string containing the entire full text of the message,
  277. headers (if supplied) and all. It has been `de-transparencied'
  278. according to RFC 821, Section 4.5.2. In other words, a line
  279. containing a `.' followed by other text has had the leading dot
  280. removed.
  281. This function should return None, for a normal `250 Ok' response;
  282. otherwise it returns the desired response string in RFC 821 format.
  283. """
  284. raise NotImplementedError
  285. class DebuggingServer(SMTPServer):
  286. # Do something with the gathered message
  287. def process_message(self, peer, mailfrom, rcpttos, data):
  288. inheaders = 1
  289. lines = data.split('\n')
  290. print '---------- MESSAGE FOLLOWS ----------'
  291. for line in lines:
  292. # headers first
  293. if inheaders and not line:
  294. print 'X-Peer:', peer[0]
  295. inheaders = 0
  296. print line
  297. print '------------ END MESSAGE ------------'
  298. class PureProxy(SMTPServer):
  299. def process_message(self, peer, mailfrom, rcpttos, data):
  300. lines = data.split('\n')
  301. # Look for the last header
  302. i = 0
  303. for line in lines:
  304. if not line:
  305. break
  306. i += 1
  307. lines.insert(i, 'X-Peer: %s' % peer[0])
  308. data = NEWLINE.join(lines)
  309. refused = self._deliver(mailfrom, rcpttos, data)
  310. # TBD: what to do with refused addresses?
  311. print >> DEBUGSTREAM, 'we got some refusals:', refused
  312. def _deliver(self, mailfrom, rcpttos, data):
  313. import smtplib
  314. refused = {}
  315. try:
  316. s = smtplib.SMTP()
  317. s.connect(self._remoteaddr[0], self._remoteaddr[1])
  318. try:
  319. refused = s.sendmail(mailfrom, rcpttos, data)
  320. finally:
  321. s.quit()
  322. except smtplib.SMTPRecipientsRefused, e:
  323. print >> DEBUGSTREAM, 'got SMTPRecipientsRefused'
  324. refused = e.recipients
  325. except (socket.error, smtplib.SMTPException), e:
  326. print >> DEBUGSTREAM, 'got', e.__class__
  327. # All recipients were refused. If the exception had an associated
  328. # error code, use it. Otherwise,fake it with a non-triggering
  329. # exception code.
  330. errcode = getattr(e, 'smtp_code', -1)
  331. errmsg = getattr(e, 'smtp_error', 'ignore')
  332. for r in rcpttos:
  333. refused[r] = (errcode, errmsg)
  334. return refused
  335. class MailmanProxy(PureProxy):
  336. def process_message(self, peer, mailfrom, rcpttos, data):
  337. from cStringIO import StringIO
  338. from Mailman import Utils
  339. from Mailman import Message
  340. from Mailman import MailList
  341. # If the message is to a Mailman mailing list, then we'll invoke the
  342. # Mailman script directly, without going through the real smtpd.
  343. # Otherwise we'll forward it to the local proxy for disposition.
  344. listnames = []
  345. for rcpt in rcpttos:
  346. local = rcpt.lower().split('@')[0]
  347. # We allow the following variations on the theme
  348. # listname
  349. # listname-admin
  350. # listname-owner
  351. # listname-request
  352. # listname-join
  353. # listname-leave
  354. parts = local.split('-')
  355. if len(parts) > 2:
  356. continue
  357. listname = parts[0]
  358. if len(parts) == 2:
  359. command = parts[1]
  360. else:
  361. command = ''
  362. if not Utils.list_exists(listname) or command not in (
  363. '', 'admin', 'owner', 'request', 'join', 'leave'):
  364. continue
  365. listnames.append((rcpt, listname, command))
  366. # Remove all list recipients from rcpttos and forward what we're not
  367. # going to take care of ourselves. Linear removal should be fine
  368. # since we don't expect a large number of recipients.
  369. for rcpt, listname, command in listnames:
  370. rcpttos.remove(rcpt)
  371. # If there's any non-list destined recipients left,
  372. print >> DEBUGSTREAM, 'forwarding recips:', ' '.join(rcpttos)
  373. if rcpttos:
  374. refused = self._deliver(mailfrom, rcpttos, data)
  375. # TBD: what to do with refused addresses?
  376. print >> DEBUGSTREAM, 'we got refusals:', refused
  377. # Now deliver directly to the list commands
  378. mlists = {}
  379. s = StringIO(data)
  380. msg = Message.Message(s)
  381. # These headers are required for the proper execution of Mailman. All
  382. # MTAs in existence seem to add these if the original message doesn't
  383. # have them.
  384. if not msg.getheader('from'):
  385. msg['From'] = mailfrom
  386. if not msg.getheader('date'):
  387. msg['Date'] = time.ctime(time.time())
  388. for rcpt, listname, command in listnames:
  389. print >> DEBUGSTREAM, 'sending message to', rcpt
  390. mlist = mlists.get(listname)
  391. if not mlist:
  392. mlist = MailList.MailList(listname, lock=0)
  393. mlists[listname] = mlist
  394. # dispatch on the type of command
  395. if command == '':
  396. # post
  397. msg.Enqueue(mlist, tolist=1)
  398. elif command == 'admin':
  399. msg.Enqueue(mlist, toadmin=1)
  400. elif command == 'owner':
  401. msg.Enqueue(mlist, toowner=1)
  402. elif command == 'request':
  403. msg.Enqueue(mlist, torequest=1)
  404. elif command in ('join', 'leave'):
  405. # TBD: this is a hack!
  406. if command == 'join':
  407. msg['Subject'] = 'subscribe'
  408. else:
  409. msg['Subject'] = 'unsubscribe'
  410. msg.Enqueue(mlist, torequest=1)
  411. class Options:
  412. setuid = 1
  413. classname = 'PureProxy'
  414. def parseargs():
  415. global DEBUGSTREAM
  416. try:
  417. opts, args = getopt.getopt(
  418. sys.argv[1:], 'nVhc:d',
  419. ['class=', 'nosetuid', 'version', 'help', 'debug'])
  420. except getopt.error, e:
  421. usage(1, e)
  422. options = Options()
  423. for opt, arg in opts:
  424. if opt in ('-h', '--help'):
  425. usage(0)
  426. elif opt in ('-V', '--version'):
  427. print >> sys.stderr, __version__
  428. sys.exit(0)
  429. elif opt in ('-n', '--nosetuid'):
  430. options.setuid = 0
  431. elif opt in ('-c', '--class'):
  432. options.classname = arg
  433. elif opt in ('-d', '--debug'):
  434. DEBUGSTREAM = sys.stderr
  435. # parse the rest of the arguments
  436. if len(args) < 1:
  437. localspec = 'localhost:8025'
  438. remotespec = 'localhost:25'
  439. elif len(args) < 2:
  440. localspec = args[0]
  441. remotespec = 'localhost:25'
  442. elif len(args) < 3:
  443. localspec = args[0]
  444. remotespec = args[1]
  445. else:
  446. usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
  447. # split into host/port pairs
  448. i = localspec.find(':')
  449. if i < 0:
  450. usage(1, 'Bad local spec: %s' % localspec)
  451. options.localhost = localspec[:i]
  452. try:
  453. options.localport = int(localspec[i+1:])
  454. except ValueError:
  455. usage(1, 'Bad local port: %s' % localspec)
  456. i = remotespec.find(':')
  457. if i < 0:
  458. usage(1, 'Bad remote spec: %s' % remotespec)
  459. options.remotehost = remotespec[:i]
  460. try:
  461. options.remoteport = int(remotespec[i+1:])
  462. except ValueError:
  463. usage(1, 'Bad remote port: %s' % remotespec)
  464. return options
  465. if __name__ == '__main__':
  466. options = parseargs()
  467. # Become nobody
  468. if options.setuid:
  469. try:
  470. import pwd
  471. except ImportError:
  472. print >> sys.stderr, \
  473. 'Cannot import module "pwd"; try running with -n option.'
  474. sys.exit(1)
  475. nobody = pwd.getpwnam('nobody')[2]
  476. try:
  477. os.setuid(nobody)
  478. except OSError, e:
  479. if e.errno != errno.EPERM: raise
  480. print >> sys.stderr, \
  481. 'Cannot setuid "nobody"; try running with -n option.'
  482. sys.exit(1)
  483. classname = options.classname
  484. if "." in classname:
  485. lastdot = classname.rfind(".")
  486. mod = __import__(classname[:lastdot], globals(), locals(), [""])
  487. classname = classname[lastdot+1:]
  488. else:
  489. import __main__ as mod
  490. class_ = getattr(mod, classname)
  491. proxy = class_((options.localhost, options.localport),
  492. (options.remotehost, options.remoteport))
  493. try:
  494. asyncore.loop()
  495. except KeyboardInterrupt:
  496. pass