public
Last active

SMTP Client for Tornado

  • Download Gist
smtp.py
Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 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('foo@example.com', ['recipient@example.com'], body)
ioloop.IOLoop.instance().start()

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

Do you have a smtp server running on your localhost?

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

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

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

Hi @koblas,

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

Cheers,
Phil

Cool, thanks.

On 5/21/12 2:19 PM, Phil Whelan wrote:

Hi @koblas,

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

Cheers,
Phil


Reply to this email directly or view it on GitHub:
https://gist.github.com/1358253

On 2012-07-10, at 2:19 AM, Luis Alberto Mayta Mamani wrote:

Cool

Sorry, I kinda abandoned this when was hitting issues I could not quickly resolve.

I'm just using smtplib.SMTP right now and moving it to a separate process if necessary.

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

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'.

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.