Python Tutorial: How ImgPage lets you upload 25 MB photos to S3 and Cloud Files with just an email
This post is written by Paul Finn. Paul is a Portsmouth, NH-based Python developer, business owner (http://route3software.com/) and beer enthusiast (https://hoppypress.com/beer-art). Follow his blog at http://www.pfinn.net/, his tweets at @paulfinn and his code at https://github.com/pfinn
My favorite web development projects always involve building a tool around a newly discovered API and I’ve been a big fan of Mailgun for a while now. Not only does their API tackle one of the most difficult-things-to-get-right, email parsing, it does it very well. It’s a great example of what a modern, developer-friendly API should be: it’s RESTful, solves a real problem, has solid documentation and it’s fast to get started with.
Building a easy-to-use image upload and hosting app
I turned to Mailgun a few weeks ago when I decided to build a quick image host that could receive images via email and not rely on a traditional client-side uploading process. Many of today’s smartphones don’t have native support for uploads in the browser so it can be a bit tricky to find a way to get photos off your phone and share them with just a link (and bypassing any social networks).
I created ImgPage (“Image Page”) to solve that problem: simply attach a photo (or a few) to an email and send it to email@example.com. Wait a few seconds and you’ll get email response from ImgPage with a unique link to clean, clutter-free page featuring just your image(s) like below.
Below, I’ll show some simplified sample code on how ImgPage uses Mailgun to upload these images to the cloud.
Parsing incoming emails using the Mailgun Routes API
After creating a git repository for ImgPage, the next step was creating the firstname.lastname@example.org route within Mailgun. A route is what Mailgun calls the logic that tells their system what to do with each incoming email it receives. For ImgPage, I configured Mailgun to parse each incoming email received at email@example.com and POST the details to my server.
requests.post("https://api.mailgun.net/v2/routes", auth=("api", os.environ.get("MAILGUN_KEY", "your-api-key"))), data=MultiDict([("description", "ImgPage Inbox Processing"), ("expression", "match_recipient('firstname.lastname@example.org')"), ("action", "forward('http://imgpage.com/mailgun-endpoint/')"), ("action", "stop()")]))
A cool trick about Mailgun: you aren’t limited by just matching on email recipients. You can match on email headers like subject line and also use regular expressions. You can create routes programmatically, as demonstrated above, or you can create routes using the Mailgun dashboard after logging in. Here are the full inbound email parsing API specs.
When it comes to the attachments, Mailgun does the heavy lifting for us and will POST the information we need about any and all attachments up to 25MB (actually the max size is 1 or 2 KB less than 25 MB since space needs to be reserved for email headers). In this simplified example, I’m checking out the request to verify that there is at least one attachment and the attachment has a common photo file extension that we are expecting (if not, return an HTTP 415 error).
@app.route('/mailgun-endpoint', methods=['POST']) def mailgun_endpoint(): if(len(request.files) > 0): for attachment in request.files.values(): file_name, file_extension = os.path.splitext(attachment.filename) if file_extension.lower() not in ['.jpg', '.gif', '.png', '.bmp']: abort(415) #Unsupported Media Type
Uploading photos to Rackspace Cloud Files & Amazon S3
Now I had a few options for reliable cloud storage, the most popular being Amazon S3. I’m always up for trying a new service and learning a new API and I decided to give Rackspace Cloud Files a shot as well (Rackspace recently acquired Mailgun so over time they will probably build some native integrations). I had plenty of experience using S3 and I was pleasantly surprised at how easy it was to get started with Cloud Files. The basic concepts, things like containers and buckets, are quite similar between the two services.
import cloudfiles import uuid import os conn = cloudfiles.get_connection(username=hsimpson, api_key='kwyjibo') images = conn.create_container('images') #Create a unique ID for a filename image_attachment = images.create_object('%s%s') % (str(uuid.uuid4()), file_extension) image_attachment.load_from_filename(attachment.filename)
In the case of ImgPage, it’s important that everything inside the images container is publicly reachable with a unique URL. After uploading, you can request the public URI from Cloud Files.
As you can see, Cloud Files is easy and simple to work with. If you want to stick with Amazon S3, here is how you could handle uploading the attachments using the boto Python library.
from boto.s3.key import Key from boto.s3.connection import S3Connection conn = S3Connection(my-access-key, my-secret-key) bucket = conn.get_bucket('images') key = Key(bucket) key = ('%s%s') % (str(uuid.uuid4()), file_extension) key.set_contents_from_filename(attachment.filename)
(When you’re done with handling the POST, Mailgun expects to see a 200 OK HTTP response. Otherwise this action will be logged as an error in your Mailgun logs.)
Routing incoming emails and parsing attachments is a breeze with Mailgun. I was able to pair Mailgun’s recipient matching and forwarding actions with both Cloud Files and Amazon S3 to create ImgPage. Without having Mailgun, I probably wouldn’t have tackled this project to begin with due to the difficulties of parsing incoming email. Mailgun gets two thumbs up from me for solving a tough problem and creating a stable and well-documented API to go with it.