PyXR

c:\python24\lib \ smtpd.py



0001 #! /usr/bin/env python
0002 """An RFC 2821 smtp proxy.
0003 
0004 Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
0005 
0006 Options:
0007 
0008     --nosetuid
0009     -n
0010         This program generally tries to setuid `nobody', unless this flag is
0011         set.  The setuid call will fail if this program is not run as root (in
0012         which case, use this flag).
0013 
0014     --version
0015     -V
0016         Print the version number and exit.
0017 
0018     --class classname
0019     -c classname
0020         Use `classname' as the concrete SMTP proxy class.  Uses `PureProxy' by
0021         default.
0022 
0023     --debug
0024     -d
0025         Turn on debugging prints.
0026 
0027     --help
0028     -h
0029         Print this message and exit.
0030 
0031 Version: %(__version__)s
0032 
0033 If localhost is not given then `localhost' is used, and if localport is not
0034 given then 8025 is used.  If remotehost is not given then `localhost' is used,
0035 and if remoteport is not given, then 25 is used.
0036 """
0037 
0038 
0039 # Overview:
0040 #
0041 # This file implements the minimal SMTP protocol as defined in RFC 821.  It
0042 # has a hierarchy of classes which implement the backend functionality for the
0043 # smtpd.  A number of classes are provided:
0044 #
0045 #   SMTPServer - the base class for the backend.  Raises NotImplementedError
0046 #   if you try to use it.
0047 #
0048 #   DebuggingServer - simply prints each message it receives on stdout.
0049 #
0050 #   PureProxy - Proxies all messages to a real smtpd which does final
0051 #   delivery.  One known problem with this class is that it doesn't handle
0052 #   SMTP errors from the backend server at all.  This should be fixed
0053 #   (contributions are welcome!).
0054 #
0055 #   MailmanProxy - An experimental hack to work with GNU Mailman
0056 #   <www.list.org>.  Using this server as your real incoming smtpd, your
0057 #   mailhost will automatically recognize and accept mail destined to Mailman
0058 #   lists when those lists are created.  Every message not destined for a list
0059 #   gets forwarded to a real backend smtpd, as with PureProxy.  Again, errors
0060 #   are not handled correctly yet.
0061 #
0062 # Please note that this script requires Python 2.0
0063 #
0064 # Author: Barry Warsaw <barry@python.org>
0065 #
0066 # TODO:
0067 #
0068 # - support mailbox delivery
0069 # - alias files
0070 # - ESMTP
0071 # - handle error codes from the backend smtpd
0072 
0073 import sys
0074 import os
0075 import errno
0076 import getopt
0077 import time
0078 import socket
0079 import asyncore
0080 import asynchat
0081 
0082 __all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
0083 
0084 program = sys.argv[0]
0085 __version__ = 'Python SMTP proxy version 0.2'
0086 
0087 
0088 class Devnull:
0089     def write(self, msg): pass
0090     def flush(self): pass
0091 
0092 
0093 DEBUGSTREAM = Devnull()
0094 NEWLINE = '\n'
0095 EMPTYSTRING = ''
0096 COMMASPACE = ', '
0097 
0098 
0099 
0100 def usage(code, msg=''):
0101     print >> sys.stderr, __doc__ % globals()
0102     if msg:
0103         print >> sys.stderr, msg
0104     sys.exit(code)
0105 
0106 
0107 
0108 class SMTPChannel(asynchat.async_chat):
0109     COMMAND = 0
0110     DATA = 1
0111 
0112     def __init__(self, server, conn, addr):
0113         asynchat.async_chat.__init__(self, conn)
0114         self.__server = server
0115         self.__conn = conn
0116         self.__addr = addr
0117         self.__line = []
0118         self.__state = self.COMMAND
0119         self.__greeting = 0
0120         self.__mailfrom = None
0121         self.__rcpttos = []
0122         self.__data = ''
0123         self.__fqdn = socket.getfqdn()
0124         self.__peer = conn.getpeername()
0125         print >> DEBUGSTREAM, 'Peer:', repr(self.__peer)
0126         self.push('220 %s %s' % (self.__fqdn, __version__))
0127         self.set_terminator('\r\n')
0128 
0129     # Overrides base class for convenience
0130     def push(self, msg):
0131         asynchat.async_chat.push(self, msg + '\r\n')
0132 
0133     # Implementation of base class abstract method
0134     def collect_incoming_data(self, data):
0135         self.__line.append(data)
0136 
0137     # Implementation of base class abstract method
0138     def found_terminator(self):
0139         line = EMPTYSTRING.join(self.__line)
0140         print >> DEBUGSTREAM, 'Data:', repr(line)
0141         self.__line = []
0142         if self.__state == self.COMMAND:
0143             if not line:
0144                 self.push('500 Error: bad syntax')
0145                 return
0146             method = None
0147             i = line.find(' ')
0148             if i < 0:
0149                 command = line.upper()
0150                 arg = None
0151             else:
0152                 command = line[:i].upper()
0153                 arg = line[i+1:].strip()
0154             method = getattr(self, 'smtp_' + command, None)
0155             if not method:
0156                 self.push('502 Error: command "%s" not implemented' % command)
0157                 return
0158             method(arg)
0159             return
0160         else:
0161             if self.__state != self.DATA:
0162                 self.push('451 Internal confusion')
0163                 return
0164             # Remove extraneous carriage returns and de-transparency according
0165             # to RFC 821, Section 4.5.2.
0166             data = []
0167             for text in line.split('\r\n'):
0168                 if text and text[0] == '.':
0169                     data.append(text[1:])
0170                 else:
0171                     data.append(text)
0172             self.__data = NEWLINE.join(data)
0173             status = self.__server.process_message(self.__peer,
0174                                                    self.__mailfrom,
0175                                                    self.__rcpttos,
0176                                                    self.__data)
0177             self.__rcpttos = []
0178             self.__mailfrom = None
0179             self.__state = self.COMMAND
0180             self.set_terminator('\r\n')
0181             if not status:
0182                 self.push('250 Ok')
0183             else:
0184                 self.push(status)
0185 
0186     # SMTP and ESMTP commands
0187     def smtp_HELO(self, arg):
0188         if not arg:
0189             self.push('501 Syntax: HELO hostname')
0190             return
0191         if self.__greeting:
0192             self.push('503 Duplicate HELO/EHLO')
0193         else:
0194             self.__greeting = arg
0195             self.push('250 %s' % self.__fqdn)
0196 
0197     def smtp_NOOP(self, arg):
0198         if arg:
0199             self.push('501 Syntax: NOOP')
0200         else:
0201             self.push('250 Ok')
0202 
0203     def smtp_QUIT(self, arg):
0204         # args is ignored
0205         self.push('221 Bye')
0206         self.close_when_done()
0207 
0208     # factored
0209     def __getaddr(self, keyword, arg):
0210         address = None
0211         keylen = len(keyword)
0212         if arg[:keylen].upper() == keyword:
0213             address = arg[keylen:].strip()
0214             if not address:
0215                 pass
0216             elif address[0] == '<' and address[-1] == '>' and address != '<>':
0217                 # Addresses can be in the form <person@dom.com> but watch out
0218                 # for null address, e.g. <>
0219                 address = address[1:-1]
0220         return address
0221 
0222     def smtp_MAIL(self, arg):
0223         print >> DEBUGSTREAM, '===> MAIL', arg
0224         address = self.__getaddr('FROM:', arg)
0225         if not address:
0226             self.push('501 Syntax: MAIL FROM:<address>')
0227             return
0228         if self.__mailfrom:
0229             self.push('503 Error: nested MAIL command')
0230             return
0231         self.__mailfrom = address
0232         print >> DEBUGSTREAM, 'sender:', self.__mailfrom
0233         self.push('250 Ok')
0234 
0235     def smtp_RCPT(self, arg):
0236         print >> DEBUGSTREAM, '===> RCPT', arg
0237         if not self.__mailfrom:
0238             self.push('503 Error: need MAIL command')
0239             return
0240         address = self.__getaddr('TO:', arg)
0241         if not address:
0242             self.push('501 Syntax: RCPT TO: <address>')
0243             return
0244         self.__rcpttos.append(address)
0245         print >> DEBUGSTREAM, 'recips:', self.__rcpttos
0246         self.push('250 Ok')
0247 
0248     def smtp_RSET(self, arg):
0249         if arg:
0250             self.push('501 Syntax: RSET')
0251             return
0252         # Resets the sender, recipients, and data, but not the greeting
0253         self.__mailfrom = None
0254         self.__rcpttos = []
0255         self.__data = ''
0256         self.__state = self.COMMAND
0257         self.push('250 Ok')
0258 
0259     def smtp_DATA(self, arg):
0260         if not self.__rcpttos:
0261             self.push('503 Error: need RCPT command')
0262             return
0263         if arg:
0264             self.push('501 Syntax: DATA')
0265             return
0266         self.__state = self.DATA
0267         self.set_terminator('\r\n.\r\n')
0268         self.push('354 End data with <CR><LF>.<CR><LF>')
0269 
0270 
0271 
0272 class SMTPServer(asyncore.dispatcher):
0273     def __init__(self, localaddr, remoteaddr):
0274         self._localaddr = localaddr
0275         self._remoteaddr = remoteaddr
0276         asyncore.dispatcher.__init__(self)
0277         self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
0278         # try to re-use a server port if possible
0279         self.set_reuse_addr()
0280         self.bind(localaddr)
0281         self.listen(5)
0282         print >> DEBUGSTREAM, \
0283               '%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
0284             self.__class__.__name__, time.ctime(time.time()),
0285             localaddr, remoteaddr)
0286 
0287     def handle_accept(self):
0288         conn, addr = self.accept()
0289         print >> DEBUGSTREAM, 'Incoming connection from %s' % repr(addr)
0290         channel = SMTPChannel(self, conn, addr)
0291 
0292     # API for "doing something useful with the message"
0293     def process_message(self, peer, mailfrom, rcpttos, data):
0294         """Override this abstract method to handle messages from the client.
0295 
0296         peer is a tuple containing (ipaddr, port) of the client that made the
0297         socket connection to our smtp port.
0298 
0299         mailfrom is the raw address the client claims the message is coming
0300         from.
0301 
0302         rcpttos is a list of raw addresses the client wishes to deliver the
0303         message to.
0304 
0305         data is a string containing the entire full text of the message,
0306         headers (if supplied) and all.  It has been `de-transparencied'
0307         according to RFC 821, Section 4.5.2.  In other words, a line
0308         containing a `.' followed by other text has had the leading dot
0309         removed.
0310 
0311         This function should return None, for a normal `250 Ok' response;
0312         otherwise it returns the desired response string in RFC 821 format.
0313 
0314         """
0315         raise NotImplementedError
0316 
0317 
0318 
0319 class DebuggingServer(SMTPServer):
0320     # Do something with the gathered message
0321     def process_message(self, peer, mailfrom, rcpttos, data):
0322         inheaders = 1
0323         lines = data.split('\n')
0324         print '---------- MESSAGE FOLLOWS ----------'
0325         for line in lines:
0326             # headers first
0327             if inheaders and not line:
0328                 print 'X-Peer:', peer[0]
0329                 inheaders = 0
0330             print line
0331         print '------------ END MESSAGE ------------'
0332 
0333 
0334 
0335 class PureProxy(SMTPServer):
0336     def process_message(self, peer, mailfrom, rcpttos, data):
0337         lines = data.split('\n')
0338         # Look for the last header
0339         i = 0
0340         for line in lines:
0341             if not line:
0342                 break
0343             i += 1
0344         lines.insert(i, 'X-Peer: %s' % peer[0])
0345         data = NEWLINE.join(lines)
0346         refused = self._deliver(mailfrom, rcpttos, data)
0347         # TBD: what to do with refused addresses?
0348         print >> DEBUGSTREAM, 'we got some refusals:', refused
0349 
0350     def _deliver(self, mailfrom, rcpttos, data):
0351         import smtplib
0352         refused = {}
0353         try:
0354             s = smtplib.SMTP()
0355             s.connect(self._remoteaddr[0], self._remoteaddr[1])
0356             try:
0357                 refused = s.sendmail(mailfrom, rcpttos, data)
0358             finally:
0359                 s.quit()
0360         except smtplib.SMTPRecipientsRefused, e:
0361             print >> DEBUGSTREAM, 'got SMTPRecipientsRefused'
0362             refused = e.recipients
0363         except (socket.error, smtplib.SMTPException), e:
0364             print >> DEBUGSTREAM, 'got', e.__class__
0365             # All recipients were refused.  If the exception had an associated
0366             # error code, use it.  Otherwise,fake it with a non-triggering
0367             # exception code.
0368             errcode = getattr(e, 'smtp_code', -1)
0369             errmsg = getattr(e, 'smtp_error', 'ignore')
0370             for r in rcpttos:
0371                 refused[r] = (errcode, errmsg)
0372         return refused
0373 
0374 
0375 
0376 class MailmanProxy(PureProxy):
0377     def process_message(self, peer, mailfrom, rcpttos, data):
0378         from cStringIO import StringIO
0379         from Mailman import Utils
0380         from Mailman import Message
0381         from Mailman import MailList
0382         # If the message is to a Mailman mailing list, then we'll invoke the
0383         # Mailman script directly, without going through the real smtpd.
0384         # Otherwise we'll forward it to the local proxy for disposition.
0385         listnames = []
0386         for rcpt in rcpttos:
0387             local = rcpt.lower().split('@')[0]
0388             # We allow the following variations on the theme
0389             #   listname
0390             #   listname-admin
0391             #   listname-owner
0392             #   listname-request
0393             #   listname-join
0394             #   listname-leave
0395             parts = local.split('-')
0396             if len(parts) > 2:
0397                 continue
0398             listname = parts[0]
0399             if len(parts) == 2:
0400                 command = parts[1]
0401             else:
0402                 command = ''
0403             if not Utils.list_exists(listname) or command not in (
0404                     '', 'admin', 'owner', 'request', 'join', 'leave'):
0405                 continue
0406             listnames.append((rcpt, listname, command))
0407         # Remove all list recipients from rcpttos and forward what we're not
0408         # going to take care of ourselves.  Linear removal should be fine
0409         # since we don't expect a large number of recipients.
0410         for rcpt, listname, command in listnames:
0411             rcpttos.remove(rcpt)
0412         # If there's any non-list destined recipients left,
0413         print >> DEBUGSTREAM, 'forwarding recips:', ' '.join(rcpttos)
0414         if rcpttos:
0415             refused = self._deliver(mailfrom, rcpttos, data)
0416             # TBD: what to do with refused addresses?
0417             print >> DEBUGSTREAM, 'we got refusals:', refused
0418         # Now deliver directly to the list commands
0419         mlists = {}
0420         s = StringIO(data)
0421         msg = Message.Message(s)
0422         # These headers are required for the proper execution of Mailman.  All
0423         # MTAs in existance seem to add these if the original message doesn't
0424         # have them.
0425         if not msg.getheader('from'):
0426             msg['From'] = mailfrom
0427         if not msg.getheader('date'):
0428             msg['Date'] = time.ctime(time.time())
0429         for rcpt, listname, command in listnames:
0430             print >> DEBUGSTREAM, 'sending message to', rcpt
0431             mlist = mlists.get(listname)
0432             if not mlist:
0433                 mlist = MailList.MailList(listname, lock=0)
0434                 mlists[listname] = mlist
0435             # dispatch on the type of command
0436             if command == '':
0437                 # post
0438                 msg.Enqueue(mlist, tolist=1)
0439             elif command == 'admin':
0440                 msg.Enqueue(mlist, toadmin=1)
0441             elif command == 'owner':
0442                 msg.Enqueue(mlist, toowner=1)
0443             elif command == 'request':
0444                 msg.Enqueue(mlist, torequest=1)
0445             elif command in ('join', 'leave'):
0446                 # TBD: this is a hack!
0447                 if command == 'join':
0448                     msg['Subject'] = 'subscribe'
0449                 else:
0450                     msg['Subject'] = 'unsubscribe'
0451                 msg.Enqueue(mlist, torequest=1)
0452 
0453 
0454 
0455 class Options:
0456     setuid = 1
0457     classname = 'PureProxy'
0458 
0459 
0460 
0461 def parseargs():
0462     global DEBUGSTREAM
0463     try:
0464         opts, args = getopt.getopt(
0465             sys.argv[1:], 'nVhc:d',
0466             ['class=', 'nosetuid', 'version', 'help', 'debug'])
0467     except getopt.error, e:
0468         usage(1, e)
0469 
0470     options = Options()
0471     for opt, arg in opts:
0472         if opt in ('-h', '--help'):
0473             usage(0)
0474         elif opt in ('-V', '--version'):
0475             print >> sys.stderr, __version__
0476             sys.exit(0)
0477         elif opt in ('-n', '--nosetuid'):
0478             options.setuid = 0
0479         elif opt in ('-c', '--class'):
0480             options.classname = arg
0481         elif opt in ('-d', '--debug'):
0482             DEBUGSTREAM = sys.stderr
0483 
0484     # parse the rest of the arguments
0485     if len(args) < 1:
0486         localspec = 'localhost:8025'
0487         remotespec = 'localhost:25'
0488     elif len(args) < 2:
0489         localspec = args[0]
0490         remotespec = 'localhost:25'
0491     elif len(args) < 3:
0492         localspec = args[0]
0493         remotespec = args[1]
0494     else:
0495         usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
0496 
0497     # split into host/port pairs
0498     i = localspec.find(':')
0499     if i < 0:
0500         usage(1, 'Bad local spec: %s' % localspec)
0501     options.localhost = localspec[:i]
0502     try:
0503         options.localport = int(localspec[i+1:])
0504     except ValueError:
0505         usage(1, 'Bad local port: %s' % localspec)
0506     i = remotespec.find(':')
0507     if i < 0:
0508         usage(1, 'Bad remote spec: %s' % remotespec)
0509     options.remotehost = remotespec[:i]
0510     try:
0511         options.remoteport = int(remotespec[i+1:])
0512     except ValueError:
0513         usage(1, 'Bad remote port: %s' % remotespec)
0514     return options
0515 
0516 
0517 
0518 if __name__ == '__main__':
0519     options = parseargs()
0520     # Become nobody
0521     if options.setuid:
0522         try:
0523             import pwd
0524         except ImportError:
0525             print >> sys.stderr, \
0526                   'Cannot import module "pwd"; try running with -n option.'
0527             sys.exit(1)
0528         nobody = pwd.getpwnam('nobody')[2]
0529         try:
0530             os.setuid(nobody)
0531         except OSError, e:
0532             if e.errno != errno.EPERM: raise
0533             print >> sys.stderr, \
0534                   'Cannot setuid "nobody"; try running with -n option.'
0535             sys.exit(1)
0536     classname = options.classname
0537     if "." in classname:
0538         lastdot = classname.rfind(".")
0539         mod = __import__(classname[:lastdot], globals(), locals(), [""])
0540         classname = classname[lastdot+1:]
0541     else:
0542         import __main__ as mod
0543     class_ = getattr(mod, classname)
0544     proxy = class_((options.localhost, options.localport),
0545                    (options.remotehost, options.remoteport))
0546     try:
0547         asyncore.loop()
0548     except KeyboardInterrupt:
0549         pass
0550 

Generated by PyXR 0.9.4
SourceForge.net Logo