home *** CD-ROM | disk | FTP | other *** search
/ PC World 2002 May / PCWorld_2002-05_cd.bin / Software / TemaCD / activepython / ActivePython-2.1.1.msi / Python21_Lib_smtpd.py < prev    next >
Encoding:
Python Source  |  2001-07-26  |  17.6 KB  |  533 lines

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