Skip to content

Instantly share code, notes, and snippets.

@koblas
Created November 11, 2011 15:23

Revisions

  1. @invalid-email-address Anonymous created this gist Nov 11, 2011.
    149 changes: 149 additions & 0 deletions smtp.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,149 @@
    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('[email protected]', ['[email protected]'], body)
    ioloop.IOLoop.instance().start()