from tornado import ioloop
from tornado import iostream
import socket

class Envelope(object):
    def __init__(self, sender, rcpt, body, callback):
        self.sender = sender
        self.rcpt   = rcpt[:]
        self.body   = body
        self.callback = callback

class SMTPClient(object):
    CLOSED = -2
    CONNECTED = -1
    IDLE = 0
    EHLO = 1
    MAIL = 2
    RCPT = 3
    DATA = 4
    DATA_DONE = 5
    QUIT = 6

    def __init__(self, host='localhost', port=25):
        self.host = host
        self.port = port
        self.msgs = []
        self.stream = None
        self.state = self.CLOSED

    def send_message(self, msg, callback=None):
        """Message is a django style EmailMessage object"""

        if not msg:
            return

        self.msgs.append(Envelope(msg.from_email, msg.recipients(), msg.message().as_string(), callback))

        self.begin()

    def send(self, sender=None, rcpt=[], body="", callback=None):
        """Very simple sender, just take the necessary parameters to create an envelope"""
        self.msgs.append(Envelope(sender, rcpt, body, callback))

        self.begin()

    def begin(self):
        """Start the sending of a message, if we need a connection open it"""
        if not self.stream:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)
            self.stream = iostream.IOStream(s)
            self.stream.connect((self.host, self.port), self.connected)
        else:
            self.work_or_quit(self.process)

    def work_or_quit(self, callback=None):
        """
           callback is provided, for the startup case where we're not in the main processing loop
        """
        if self.state == self.IDLE:
            if self.msgs:
                self.state = self.MAIL
                self.stream.write('MAIL FROM: <%s>\r\n' % self.msgs[0].sender) 
            else:
                self.state = self.QUIT
                self.stream.write('QUIT\r\n') 
            if callback:
                self.stream.read_until('\r\n', callback)

    def connected(self):
        """Socket connect callback"""
        self.state = self.CONNECTED
        self.stream.read_until('\r\n', self.process)

    def process(self, data):
        # print self.state, data, 
        code = int(data[0:3])
        if data[3] not in (' ', '\r', '\n'):
            self.stream.read_until('\r\n', self.process)
            return

        if self.state == self.CONNECTED:
            if not 200 <= code < 300:
                return self.error("Unexpected status %d from CONNECT: %s" % (code, data.strip()))
            self.state = self.EHLO
            self.stream.write('EHLO localhost\r\n')
        elif self.state == self.EHLO:
            if not 200 <= code < 300:
                return self.error("Unexpected status %d from EHLO: %s" % (code, data.strip()))
            self.state = self.IDLE
            self.work_or_quit()
        elif self.state == self.MAIL:
            if not 200 <= code < 300:
                return self.error("Unexpected status %d from MAIL: %s" % (code, data.strip()))
            if self.msgs[0].rcpt:
                self.stream.write('RCPT TO: <%s>\r\n' % self.msgs[0].rcpt.pop(0))
            self.state = self.RCPT
        elif self.state == self.RCPT:
            if not 200 <= code < 300:
                return self.error("Unexpected status %d from RCPT: %s" % (code, data.strip()))
            if self.msgs[0].rcpt:
                self.stream.write('RCPT TO: <%s>\r\n' % self.msgs[0].rcpt.pop(0))
            else:
                self.stream.write('DATA\r\n')
                self.state = self.DATA
        elif self.state == self.DATA:
            if code not in (354,) :
                return self.error("Unexpected status %d from DATA: %s" % (code, data.strip()))
            self.stream.write(self.msgs[0].body)
            if self.msgs[0].body[-2:] != '\r\n':
                self.stream.write('\r\n')
            self.stream.write('.\r\n')
            self.state = self.DATA_DONE
        elif self.state == self.DATA_DONE:
            if not 200 <= code < 300:
                return self.error("Unexpected status %d from DATA END: %s" % (code, data.strip()))
            if self.msgs[0].callback:
                self.msgs[0].callback(True)

            self.msgs.pop(0)

            self.state = self.IDLE
            self.work_or_quit()
        elif self.state == self.QUIT:
            if not 200 <= code < 300:
                return self.error("Unexpected status %d from QUIT: %s" % (code, data.strip()))
            self.close()

        if self.stream:
            self.stream.read_until('\r\n', self.process)

    def error(self, msg):
        self.close()

    def close(self):
        for msg in self.msgs:
            if msg.callback:
                msg.callback(False)
        self.stream.close()
        self.stream = None
        self.state = self.CLOSED

if __name__ == '__main__':
    client = SMTPClient('localhost', 25)
    body = """Subject: Testing

Just a test
    """
    client.send('foo@example.com', ['recipient@example.com'], body)
    ioloop.IOLoop.instance().start()