home *** CD-ROM | disk | FTP | other *** search
/ PC World 2005 June / PCWorld_2005-06_cd.bin / software / vyzkuste / firewally / firewally.exe / framework-2.3.exe / smtpd.py < prev    next >
Text File  |  2003-12-30  |  18KB  |  544 lines

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