Using SMTP as a User Interface
SMTP is more than just a protocol for routing messages between people; it can also be used for communications in which the recipient is a computer program. Many server applications use SMTP to allow people to publish information via email. By sending mail to a special address, users can post to their weblogs, add a page to a Wiki, or import image files into their photo sharing services. Offering an SMTP interface to your application makes getting data in and out as easy as sending and receiving email. This lets people use your application as part of their existing email workflow, which makes it more likely that they're actually going to use that program you worked so hard to write. SMTP is also supported by many mobile phones, so it can be an easy way to enable mobile access to your applications.
8.2.1. How Do I Do That?
Example 8-2 shows how you might offer an SMTP interface to an application. smtpgoogle.py is an SMTP server that takes email as input and uses it to run a Google "I'm Feeling Lucky" search. Then it sends a reply containing the search result. The code is similar to the basic SMTP server in Example 8-1, but with application-specific classes implementing smtp.IMessage and smtp.ImessageDelivery.
Example 8-2. smtpgoogle.py
from twisted.mail import smtp from twisted.web import google, client from zope.interface import implements from twisted.internet import protocol, reactor, defer import os, email, email.Utils from email import Header, MIMEBase, MIMEMultipart, MIMEText class GoogleQueryPageFetcher(object): def fetchFirstResult(self, query): """ Given a query, finds the first Google result, and downloads that page. Returns a Deferred, which will be called back with a tuple containing (firstMatchUrl, firstMatchPageData) """ # the twisted.web.google.checkGoogle function does an # "I'm feeling lucky" search on Google return google.checkGoogle(query).addCallback( self._gotUrlFromGoogle) def _gotUrlFromGoogle(self, url): # grab the page return client.getPage(url).addCallback( self._downloadedPage, url) def _downloadedPage(self, pageContents, url): # return a tuple containing both the url and page data return (url, pageContents) class ReplyFromGoogle(object): implements(smtp.IMessage) def _ _init_ _(self, address, upstreamServer): self.address = address self.lines = [] self.upstreamServer = upstreamServer def lineReceived(self, line): self.lines.append(line) def eomReceived(self): message = email.message_from_string(" ".join(self.lines)) fromAddr = email.Utils.parseaddr(message['From'])[1] query = message['Subject'] searcher = GoogleQueryPageFetcher( ) return searcher.fetchFirstResult(query).addCallback( self._sendPageToEmailAddress, query, fromAddr) def _sendPageToEmailAddress(self, pageData, query, destAddress): pageUrl, pageContents = pageData msg = MIMEMultipart.MIMEMultipart( ) msg['From'] = self.address msg['To'] = destAddress msg['Subject'] = "First Google result for '%s'" % query body = MIMEText.MIMEText( "The first Google result for '%s' is: %s" % ( query, pageUrl)) # create a text/html attachment with the page data attachment = MIMEBase.MIMEBase("text", "html") attachment['Content-Location'] = pageUrl attachment.set_payload(pageContents) msg.attach(body) msg.attach(attachment) return smtp.sendmail( self.upstreamServer, self.address, destAddress, msg) def connectionLost(self): pass class GoogleMessageDelivery(object): implements(smtp.IMessageDelivery) def _ _init_ _(self, googleSearchAddress, upstreamSMTPServer): self.googleSearchAddress = googleSearchAddress self.upstreamServer = upstreamSMTPServer def receivedHeader(self, helo, origin, recipients): myHostname, clientIP = helo headerValue = "by %s from %s with ESMTP ; %s" % ( myHostname, clientIP, smtp.rfc822date( )) # email.Header.Header used for automatic wrapping of long lines return "Received: %s" % Header.Header(headerValue) def validateTo(self, user): if not str(user.dest).lower( ) == self.googleSearchAddress: raise smtp.SMTPBadRcpt(user.dest) else: return lambda: ReplyFromGoogle(self.googleSearchAddress, self.upstreamServer) def validateFrom(self, helo, originAddress): # accept mail from anywhere. To reject an address, raise # smtp.SMTPBadSender here. return originAddress class SMTPServerFactory(protocol.ServerFactory): def _ _init_ _(self, googleSearchAddress, upstreamSMTPServer): self.googleSearchAddress = googleSearchAddress self.upstreamSMTPServer = upstreamSMTPServer def buildProtocol(self, addr): delivery = GoogleMessageDelivery(self.googleSearchAddress, self.upstreamSMTPServer) smtpProtocol = smtp.SMTP(delivery) smtpProtocol.factory = self return smtpProtocol if __name__ == "_ _main_ _": import sys googleSearchAddress = sys.argv[1] upstreamSMTPServer = sys.argv[2] reactor.listenTCP( 25, SMTPServerFactory(googleSearchAddress, upstreamSMTPServer)) reactor.run( )
Run smtpgoogle.py from the command line with two arguments. The first is the email address you want to use for Google searches: you'll send mail to this address to search Google, and you'll get a reply from this address with the result. The second argument is the upstream SMTP server you want to use for sending the reply emails; typically this is your ISP's mail server:
$ python smtpgoogle.py google@myhostname.mydomain smtp.myisp.com
If you're running this command behind a firewall, you might have to fake the address you use for Google searches in order to send the reply emails. The address google@localhost, for example, might be rejected by your upstream mail server as an invalid sender address. As long as you're using a mail client configured to send all mail through localhost, you should be able to use any address; smtpgoogle.py will use that address for Google searches, and refuse to accept mail for any other address.
Once the server is running, try sending an email to the address you specified. The subject of the email should be the query you want to send to Google. Figure 8-2 shows a query email being sent.
Figure 8-2. Sending a query to the smtpgoogle.py search service
smtpgoogle.py should process your query and send a reply to whatever From: address you used when sending the email. Check your mail, and you should get an email similar to that in Figure 8-3 with the first Google result.
Figure 8-3. Reply containing result of a Google search
8.2.2. How Does That Work?
Example 8-2 defines the GoogleQueryPageFetcher class, which does the work of finding the first Google result for a query and then downloading that page. The function fetchFirstResult returns a Deferred object that will be called back with a tuple containing the URL and contents of the first matching page.
The ReplyFromGoogle class takes this feature and makes it available to SMTP by implementing smtp.IMessage. The key is the eomReceived function, which is called once the entire message has been received. The smtp.IMessage interface says that eomReceived should return a Deferred. Because of this, ReplyFromGoogle is free to run a series of asynchronous operations while processing the email, as long as it eventually returns a successful value or raises an exception. Rather than just writing the message data to disk, ReplyFromGoogle.eomReceived runs the Google search and sets up a callback to take the search result and send it out by email. It constructs a message using classes in Python's email module, and sends it back to the address that the query came from.
This technique of doing all the work in the context of eomReceived is a good approach to use when you expect that it won't take more than a second or two to process the incoming message. At other times, you might need to do something that's going to take a while. In this case, you don't want to leave the SMTP client hanging there waiting for a response, especially because it will eventually get tired of waiting and time out. So you might choose to add the incoming message to a queue for processing, and to immediately return a successful result for eomReceived. If something goes wrong later, you can always send an email back to the sender informing them of what happened.