python-2.5.2/win32/Lib/smtpd.py
changeset 0 ae805ac0140d
equal deleted inserted replaced
-1:000000000000 0:ae805ac0140d
       
     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 `PureProxy' 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@python.org>
       
    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) if arg else None
       
   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) if arg else None
       
   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     classname = options.classname
       
   537     if "." in classname:
       
   538         lastdot = classname.rfind(".")
       
   539         mod = __import__(classname[:lastdot], globals(), locals(), [""])
       
   540         classname = classname[lastdot+1:]
       
   541     else:
       
   542         import __main__ as mod
       
   543     class_ = getattr(mod, classname)
       
   544     proxy = class_((options.localhost, options.localport),
       
   545                    (options.remotehost, options.remoteport))
       
   546     try:
       
   547         asyncore.loop()
       
   548     except KeyboardInterrupt:
       
   549         pass