Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
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()
@jsonshen

This comment has been minimized.

Show comment Hide comment
@jsonshen

jsonshen Mar 29, 2012

How to use
Run smtp.py but it does not work
self.state is -2

How to use
Run smtp.py but it does not work
self.state is -2

@koblas

This comment has been minimized.

Show comment Hide comment
@koblas

koblas Apr 9, 2012

Do you have a smtp server running on your localhost?

Owner

koblas commented Apr 9, 2012

Do you have a smtp server running on your localhost?

@jsonshen

This comment has been minimized.

Show comment Hide comment
@jsonshen

jsonshen Apr 10, 2012

Thanks!
I have not run a smtp server in localhost.
Whether to support the Gmail or hotmail?

Thanks!
I have not run a smtp server in localhost.
Whether to support the Gmail or hotmail?

@koblas

This comment has been minimized.

Show comment Hide comment
@koblas

koblas Apr 10, 2012

To support gmail or hotmail as your mail sending agent - that's a very different library.

Owner

koblas commented Apr 10, 2012

To support gmail or hotmail as your mail sending agent - that's a very different library.

@jsonshen

This comment has been minimized.

Show comment Hide comment
@jsonshen

jsonshen Apr 12, 2012

Thanks!
On my localhost running an SMTP server. Send mail sucessfully! But receive are spam. How to set a sender?

Thanks!
On my localhost running an SMTP server. Send mail sucessfully! But receive are spam. How to set a sender?

@philwhln

This comment has been minimized.

Show comment Hide comment
@philwhln

philwhln May 21, 2012

Hi @koblas,

I've added this to a git repo https://github.com/philwhln/tornado-smtp-client

Cheers,
Phil

Hi @koblas,

I've added this to a git repo https://github.com/philwhln/tornado-smtp-client

Cheers,
Phil

@koblas

This comment has been minimized.

Show comment Hide comment
@koblas

koblas May 21, 2012

Owner

koblas commented May 21, 2012

@luismayta

This comment has been minimized.

Show comment Hide comment
@luismayta

luismayta Jul 9, 2012

Cool

Cool

@philwhln

This comment has been minimized.

Show comment Hide comment
@philwhln

philwhln Jul 11, 2012

@koblas

This comment has been minimized.

Show comment Hide comment
@koblas

koblas Jul 11, 2012

Any details on the issues, something that could be fixed in the fragment or something larger?

Owner

koblas commented Jul 11, 2012

Any details on the issues, something that could be fixed in the fragment or something larger?

@philwhln

This comment has been minimized.

Show comment Hide comment
@philwhln

philwhln Jul 11, 2012

Basically messages were not going out. It seemed like something in the SMTP conversation was falling into stalemate.

Sorry, it's a couple of months so bit hazy. I didn't have time at the time to debug it thoroughly and I need some resilient over ioloop integrated.

One thing you should watch is expecting '\r\n' from SMTP servers. Not all servers use the same line endings and this is common gotcha when writing SMTP handlers that will leads stalemate. You'll wait all day for '\r\n' when the client already sent '\n'.

Basically messages were not going out. It seemed like something in the SMTP conversation was falling into stalemate.

Sorry, it's a couple of months so bit hazy. I didn't have time at the time to debug it thoroughly and I need some resilient over ioloop integrated.

One thing you should watch is expecting '\r\n' from SMTP servers. Not all servers use the same line endings and this is common gotcha when writing SMTP handlers that will leads stalemate. You'll wait all day for '\r\n' when the client already sent '\n'.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment