Permalink
Browse files

Works.

  • Loading branch information...
0 parents commit 3e626f05cc5d5d77d21d5330d28081383d3bdf22 Toby White committed Jul 7, 2010
Showing with 260 additions and 0 deletions.
  1. +2 −0 .gitignore
  2. +4 −0 README
  3. +9 −0 app.yaml
  4. +68 −0 gmail.py
  5. +35 −0 google.mime.types
  6. +11 −0 index.yaml
  7. +111 −0 mail.py
  8. +14 −0 main.py
  9. +6 −0 rfc822
2 .gitignore
@@ -0,0 +1,2 @@
+*.pyc
+GMAIL_SECRET_KEY
4 README
@@ -0,0 +1,4 @@
+* This is a limited RFC2822-over-HTTP proxy for running in Google App Engine.
+
+Emails sent as RFC-2822 documents (with an accompanying signature) are decoded & sent onwards using GAE's mail-sending capabilities.
+
9 app.yaml
@@ -0,0 +1,9 @@
+application: timetric-mail-rfc2822
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+
+- url: .*
+ script: main.py
68 gmail.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python
+
+import base64, email.message, email.mime, email.parser, email.utils, hashlib, hmac, optparse, os, sys, urllib
+
+
+class MessageSendingFailure(Exception):
+ pass
+
+
+class Signer(object):
+ def __init__(self):
+ try:
+ self.SECRET_KEY = os.environ['GMAIL_SECRET_KEY']
+ except KeyError:
+ try:
+ self.SECRET_KEY = open('/etc/envdir/GMAIL_SECRET_KEY').readline().rstrip()
+ except OSError:
+ raise EnvironmentError("GMAIL_SECRET_KEY is not set.")
+
+ def generate_signature(self, msg):
+ return base64.encodestring(hmac.new(self.SECRET_KEY, msg, hashlib.sha1).digest()).strip()
+
+
+class Connection(object):
+ def __init__(self):
+ import httplib2
+ self.h = httplib2.Http()
+ try:
+ self.EMAIL_APPENGINE_PROXY_URL = os.environ['GMAIL_PROXY_URL']
+ except KeyError:
+ try:
+ self.EMAIL_APPENGINE_PROXY_URL = open('/etc/envdir/GMAIL_PROXY_URL').readline().rstrip()
+ except OSError:
+ raise EnvironmentError("GMAIL_PROXY_URL is not set.")
+
+ def make_request(self, data):
+ return self.h.request(self.EMAIL_APPENGINE_PROXY_URL, "POST", body=data)
+
+
+def send_mail(msg):
+ values = {'msg':msg.as_string(),
+ 'signature':Signer().generate_signature(msg.as_string())}
+ data = urllib.urlencode([(k, v.encode('utf-8')) for k, v in values.items()])
+ r, c = Connection().make_request(data)
+
+ if r.status != 204:
+ raise MessageSendingFailure(c)
+
+
+if __name__ == '__main__':
+ """mail -s [space-separated to-addresses] to-address
+ and the message on stdin"""
+ parser = optparse.OptionParser()
+ parser.add_option("-s", dest="subject", help="subject of message")
+ options, to_addresses = parser.parse_args()
+ if to_addresses:
+ msg = email.message.Message()
+ msg['From'] = os.environ['USER']
+ msg['To'] = ",".join(to_addresses) # escaping necessary?
+ msg['Subject'] = options.subject
+ msg.set_payload(sys.stdin.read())
+ else:
+ # We're expecting a whole message on stdin:
+ msg = email.parser.Parser().parse(sys.stdin)
+ recipient = os.environ.get('RECIPIENT')
+ if recipient:
+ msg['To'] = recipient
+ send_mail(msg)
35 google.mime.types
@@ -0,0 +1,35 @@
+application/msword doc
+application/pdf pdf
+application/rss+xml rss
+application/vnd.ms-excel xls
+application/vnd.ms-powerpoint pps ppt
+application/vnd.oasis.opendocument.presentation odp
+application/vnd.oasis.opendocument.spreadsheet ods
+application/vnd.oasis.opendocument.text odt
+application/vnd.sun.xml.calc sxc
+application/vnd.sun.xml.writer sxw
+audio/basic au snd
+audio/flac flac
+audio/mid mid rmi
+audio/mp4 m4a
+audio/mpeg mp3
+audio/ogg oga ogg
+audio/x-aiff aif aifc aiff
+audio/x-wav wav
+image/gif gif
+image/jpeg jpeg jpg jpe
+image/png png
+image/tiff tiff tif
+image/vnd.wap.wbmp wbmp
+image/x-ms-bmp bmp
+text/calendar ics
+text/comma-separated-values csv
+text/css css
+text/html htm html
+text/plain text txt asc diff pot
+text/x-vcard vcf
+video/mp4 mp4
+video/mpeg mpeg mpg mpe
+video/ogg ogv
+video/quicktime qt mov
+video/x-msvideo avi
11 index.yaml
@@ -0,0 +1,11 @@
+indexes:
+
+# AUTOGENERATED
+
+# This index.yaml is automatically updated whenever the dev_appserver
+# detects that a new type of query is run. If you want to manage the
+# index.yaml file manually, remove the above marker line (the line
+# saying "# AUTOGENERATED"). If you want to manage some indexes
+# manually, move them above the marker line. The index.yaml file is
+# automatically uploaded to the admin console when you next deploy
+# your application using appcfg.py.
111 mail.py
@@ -0,0 +1,111 @@
+import email.parser, logging, os
+
+from google.appengine.api.mail import EmailMessage, InvalidSenderError
+from google.appengine.ext.webapp import RequestHandler
+
+from gmail import Signer
+
+
+suffixes = dict(line.strip().split()[:2] for line in open('google.mime.types'))
+
+
+class BadRequestError(ValueError):
+ pass
+
+
+class BadMessageError(ValueError):
+ pass
+
+
+def get_filename(part):
+ filename = part.get_filename()
+ if not filename:
+ content_type = part.get_content_type()
+ try:
+ filename = "file.%s" % suffixes[content_type]
+ except KeyError:
+ raise BadMessageError("Google won't let us send content of type '%s'" % content_type)
+ return filename
+
+def send_message(msg):
+ sender = msg.get_unixfrom() or msg['From']
+ if not sender:
+ raise BadMessageError("No sender specified")
+ to = msg['To']
+ if not to:
+ raise BadMessageError("No destination addresses specified")
+ message = EmailMessage(sender=sender or msg['From'], to=to)
+
+ # Go through all the headers which Google will let us use
+ cc = msg['Cc']
+ if cc:
+ message.cc = cc
+ bcc = msg['Bcc']
+ if bcc:
+ message.bcc = cc
+ reply_to = msg['Reply-To']
+ if reply_to:
+ message.reply_to = reply_to
+ subject = msg['Subject']
+ if subject:
+ message.subject = subject
+
+ # If there's just a plain text body, use that, otherwise
+ # iterate over all the attachments
+ payload = msg.get_payload()
+ if isinstance(payload, basestring):
+ message.body = payload
+ else:
+ # GAW demands we specify the body explicitly - we use the first textual attachment.
+ payload = msg.walk()
+ while True:
+ try:
+ firstpart = payload.next()
+ except StopIteration:
+ raise BadMessageError("Message consists only of multipart attachments")
+ if not firstpart.get_content_type().startswith('multipart'):
+ break
+ if not firstpart.get_content_type() == 'text/plain':
+ raise BadMessageError("No text body found for message")
+ message.body = firstpart.get_payload()
+ message.attachments = [(get_filename(part), part.as_string()) for part in payload
+ if not part.get_content_type().startswith('multipart')]
+ try:
+ message.send()
+ except InvalidSenderError:
+ raise BadMessageError("Unauthorized message sender '%s'" % sender)
+
+def check_signature(msg, signature):
+ os.environ['GMAIL_SECRET_KEY'] = open('GMAIL_SECRET_KEY').read().strip()
+ return Signer().generate_signature(msg) == signature
+
+def parse_args(request):
+ msg = request.get('msg')
+ if not msg:
+ raise BadRequestError("No message found")
+ signature = request.get('signature')
+ if not signature:
+ raise BadRequestError("No signature found")
+ if not check_signature(msg, signature):
+ raise BadRequestError("Signature doesn't match")
+ return str(msg) # email.parser barfs on unicode
+
+
+class SendMail(RequestHandler):
+ def post(self):
+ try:
+ msg = parse_args(self.request)
+ send_message(email.parser.Parser().parsestr(msg))
+ logging.info("Sent message ok\n%s" % msg)
+ self.error(204)
+ except BadRequestError, e:
+ logging.error("Malformed request")
+ self.error(400)
+ self.response.out.write(e.args[0])
+ except BadMessageError, e:
+ logging.error("Failed to send message\n%s" % msg)
+ self.error(400)
+ self.response.out.write(e.args[0])
+ except Exception, e:
+ logging.exception("Failed to process request\n%s" % self.request)
+ self.error(500)
14 main.py
@@ -0,0 +1,14 @@
+from google.appengine.ext.webapp import WSGIApplication
+from google.appengine.ext.webapp.util import run_wsgi_app
+
+from mail import SendMail
+
+application = WSGIApplication([('/', SendMail),
+ ],
+ debug=True)
+
+def main():
+ run_wsgi_app(application)
+
+if __name__ == '__main__':
+ main()
6 rfc822
@@ -0,0 +1,6 @@
+From: Toby White <support@timetric.com>
+To: toby.o.h.white@gmail.com
+Subject: As basic as it gets
+
+This is the plain text body of the message. Note the blank line
+between the header information and the body of the message.

0 comments on commit 3e626f0

Please sign in to comment.