view smtpserver.py @ 4:87317bd93890 default tip

The server now has a separate thread that forwards queued mail to an external server.
author Atul Varma <varmaa@toolness.com>
date Tue, 18 Mar 2008 22:25:31 -0500
parents e033c476f97a
children
line wrap: on
line source

import os
import sys
import smtpd
import smtplib
import asyncore
import grp
import pwd
import logging
import traceback
import threading
import Queue

class MockSMTP( object ):
    def __init__( self, server, port ):
        pass

    def login( self, username, password ):
        pass

    def sendmail( self, mailfrom, rcpttos, data ):
        pass

    def quit( self ):
        pass

def mailThread( queue, config, smtpClass ):
    done = False

    try:
        while not done:
            item = queue.get()
            if item["cmd"] == "quit":
                logging.info( "quit signal received, exiting." )
                done = True
            elif item["cmd"] == "sendmsg":
                # TODO: If the sending of the message fails, we should
                # try to re-queue it and re-send it later.
                logging.info(
                    "sending message from %s to %s" % ( item["mailfrom"],
                                                        item["rcpttos"] )
                    )
                server = smtpClass( config.server, config.port )
                server.login( config.username, config.password )
                server.sendmail( item["mailfrom"],
                                 item["rcpttos"],
                                 item["data"] )
                server.quit()
                logging.info( "done." )
            else:
                raise AssertionError( "Bad cmd: %s" % item["cmd"] )
    except:
        logging.error( "exception in mailThread()." )
        logging.error( traceback.format_exc() )

class MyServer( smtpd.SMTPServer ):
    import smtpconfig as config

    def __init__( self, smtpClass ):
        self.smtpClass = smtpClass
        self.queue = Queue.Queue()
        smtpd.SMTPServer.__init__( self, self.config.my_addr, None )

    def bind( self, addr ):
        retval = smtpd.SMTPServer.bind( self, addr )
        gid = grp.getgrnam( self.config.gid )[2]
        uid = pwd.getpwnam( self.config.uid )[2]
        os.setgid( gid )
        os.setuid( uid )
        logging.info(
            "Bound to %s and changed user to %s." % ( addr,
                                                      self.config.uid )
            )

        self.mailThread = threading.Thread(
            target = mailThread,
            args = (self.queue, self.config, self.smtpClass)
            )
        self.mailThread.start()

        return retval

    def finalize( self ):
        self.queue.put( {"cmd" : "quit"} )

    def process_message( self, peer, mailfrom, rcpttos, data ):
        if peer[0] != "127.0.0.1":
            logging.warn( "not from localhost: %s" % peer )
            return

        if not self.mailThread.isAlive():
            logging.info( "mailThread is no longer alive! exiting." )
            raise asyncore.ExitNow()

        logging.info(
            "queuing message from %s to %s on %s (length %d)." %
            (mailfrom, rcpttos, peer, len(data))
            )

        self.queue.put( { "cmd" : "sendmsg",
                          "mailfrom" : mailfrom,
                          "rcpttos" : rcpttos,
                          "data" : data } )

if __name__ == "__main__":
    if os.getuid() != 0:
        print "This program must be run as root."
        sys.exit( -1 )

    logging.basicConfig(
        filename = "smtpserver.log",
        format = "%(asctime)-15s %(levelname)s: %(message)s",
        level = logging.INFO
        )

    if len( sys.argv ) == 2 and sys.argv[1] == "test":
        msg = "Test mode enabled."
        logging.info( msg )
        print msg

        smtpClass = MockSMTP
    else:
        smtpClass = smtplib.SMTP

    server = MyServer( smtpClass )

    msg = "Listening for incoming connections on port %s." % \
        server.config.my_addr[1]
    logging.info( msg )
    print msg

    try:
        asyncore.loop()
    finally:
        server.finalize()