Skip to content

Instantly share code, notes, and snippets.

What would you like to do?

Stream email attachments to S3 using Mailgun, node.js


1. Mailgun setup

First define a Mailgun Route using the store action:

Filter Expression: match_recipient("save@YOUR_DOMAIN")
Action: store(notify="YOUR_ROUTE")

We could forward the email directly to the handler, however this approach allows us to verify the email prior to doing anything with the attachments.

2. Express setup

See email.js.

var _ = require('underscore')
var assert = require('assert')
var async = require('async')
var connectBusboy = require('connect-busboy')
var express = require('express')
var request = require('request')
var Uploader = require('s3-streaming-upload').Uploader
var server = module.exports = express()
var parser = connectBusboy({
immediate: true,
files: 0
}), [parser], postEmail)
function postEmail(req, res) {
parse(req.busboy, function(err, _res, body){
if (err) throw err
if (_res.statusCode !== 200) throw new Error('unable to retrieve message')
var msg = JSON.parse(body)
console.log('saving work for ' + msg.sender + '...')
upload(msg, function(err, attachments){
if (err) throw err
console.log('work saved')
return res.send({ attachments:, function(a){ return a.location }) })
function parse(busboy, callback) {
var url
.on('error', function(err){
return callback(err)
.on('field', function(name, value){
if (name !== 'message-url') return
url = value
.on('end', function(){
if (!url) return callback(new Error('message url does not exist'))
return request.get(url, callback).auth('api', YOUR_MAILGUN_API_KEY, false)
function upload(msg, callback) {
var tasks
try {
assert(_.isObject(msg), 'message must be an object')
assert(_.isArray(msg.attachments), 'message must have an attachments array')
tasks =, function(attachment){
return async.apply(uploadAttachment, attachment)
} catch (ex) {
return callback(ex)
return async.parallel(tasks, callback)
function uploadAttachment(attachment, callback) {
var readStream
var options
try {
assert(_.isObject(attachment), 'must have an attachment')
assert(_.isString(attachment.url), 'attachment must have a url')
assert(_.isString(attachment['content-type']), 'attachment must have a content type')
assert(_.isString(, 'attachment must have a name')
readStream = request.get(attachment['url']).auth('api', YOUR_MAILGUN_API_KEY, false)
options = {
accessKey: YOUR_AWS_KEY,
objectParams: {
ACL: 'public-read',
ContentType: attachment['content-type']
stream: readStream
} catch (ex) {
return callback(ex)
new Uploader(options)
.on('completed', callback)
.on('failed', callback)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.