Skip to content

Instantly share code, notes, and snippets.

@lepture
Forked from koblas/smtp.py
Created December 14, 2011 16:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lepture/1477164 to your computer and use it in GitHub Desktop.
Save lepture/1477164 to your computer and use it in GitHub Desktop.
SMTP Client for Tornado
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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment