@@ -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 ('foo@example.com' , ['recipient@example.com' ], body )
ioloop .IOLoop .instance ().start ()