postmailer
Version:
HTTP POST -> SMTP proxy, as Express middleware
218 lines (199 loc) • 7.2 kB
JavaScript
var stream = require('stream');
var crypto = require('crypto');
var fs = require('fs');
var express = require('express');
var bodyParser = require('body-parser');
var nodemailer = require('nodemailer');
var directTransport = require('nodemailer-direct-transport');
var mime = require('mime');
var parseHeader = require('parse-http-header');
var UriTemplate = require('uri-templates');
var Mapping = require('./mapping');
// Pipe through to another stream without changes
// - otherwise the "path" property screws things up when using the request object/stream
function copyStream(source) {
var content = new stream.Transform();
content._transform = function (chunk, encoding, callback) {
this.push(chunk, encoding);
callback();
};
source.pipe(content);
return content;
}
function commaSplit(string) {
return string.match(/([^,]|"([^"]|\\.)*")*/g);
}
function htmlEscape(value) {
return value.replace(/&/g, '&').replace(/</g, '<');
}
function httpMail(options) {
options = Object.create(options || {});
options.open = options.open || false;
var maxInline = options.maxInline || 500*1024; // 500kb maximum inline document
var mapping = new Mapping(options.mapping);
var mailTransport = nodemailer.createTransport(options.transport || directTransport());
mailTransport.on('log', function (message) {
console.log(message.type + ': ' + message.message);
});
var domains = [''].concat(options.domain || []);
// TODO: shift this logic into the mapping?
function emailForUrl(url, mustBeOpen) {
var urls = [];
if (url.indexOf('://') !== -1) {
urls.push(url);
domains.forEach(function (domain) {
if (url.substring(0, domain.length + 1) == domain + '/') {
urls.push(url.substring(domain.length));
}
});
} else {
url = '/' + url.replace(/^\//, '');
domains.forEach(function (domain) {
urls.push(domain + url);
});
}
return mapping.emailForUrl(urls, mustBeOpen);
}
// HTML generator for GET requests (web form)
var htmlFunction;
if (options.webForm !== false) {
if (!options.webForm || options.webForm === true) {
options.webForm = __dirname + '/form-template.html';
}
if (typeof options.webForm === 'string') {
var template;
try {
template = fs.readFileSync(options.webForm, {encoding: 'utf-8'});
if (options.minifyForm) {
try {
template = require('html-minifier').minify(template, {
collapseWhitespace: true,
minifyJS: true,
minifyCSS: true
});
} catch (e) {
// Nothing
}
}
} catch (e) {
template = '<B>Error</B>: could not load template';
}
htmlFunction = function (username, url) {
return template.replace(/\{\{url\}\}/g, htmlEscape(url)).replace(/\{\{name\}\}/g, htmlEscape(username));
};
} else if (typeof options.webForm === 'function') {
htmlFunction = options.webForm;
} else {
htmlFunction = function () {
return '<B>Error</B>: invalid template argument: ' + JSON.stringify(options.webForm).replace(/&/g, '&').replace(/</g, '<');
};
}
};
var router = express.Router({strict: true});
router.use('',
function (request, response, next) {
// Detect email address, or reject
var url = request.originalUrl;
var mappedEmail = request.mappedEmail = emailForUrl(url, options.open);
request.mappedName = mapping.nameForUrl(url) || url;
if (!mappedEmail) return next('route'); // Skip CORS and bodyParser stuff
next();
},
function (request, response, next) {
response.header("Access-Control-Allow-Origin", "*");
//response.header("Access-Control-Request-Method", "GET, POST, OPTIONS");
response.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Link, From, Subject");
next();
},
bodyParser.urlencoded({extended: false}),
function (request, response, next) {
var email = request.mappedEmail, displayName = request.mappedName;
if (!email) {
console.log("No email found, but somehow passed by filter");
return next();
}
if (request.method === 'GET' && htmlFunction) {
response.setHeader('Content-Type', 'text/html');
var email = request.mappedEmail, displayName = request.mappedName;
if (!displayName || !email) return next();
var url = request.protocol + '://' + request.get('host') + request.originalUrl;
response.send(htmlFunction(displayName, url));
} else if (request.method === 'POST') {
var messageHeaders = {};
function transformField(value) {
if (Array.isArray(value)) return value.map(transformField);
return value.replace(/<([^<>]+\:\/\/[^<>]+)>/g, function (match, url) {
var email = emailForUrl(url, true);
if (email) return '<' + email + '>';
return match;
});
}
for (var key in request.headers) {
messageHeaders[key] = transformField(request.headers[key]);
}
if (!messageHeaders['from']) return response.status(400).json('"From" is required');
var contentType = messageHeaders['content-type'];
var filename = 'attachment', cid;
var inline = !(parseFloat(messageHeaders['content-length']) > options.maxInline);
if (messageHeaders['content-disposition']) {
var parsed = parseHeader(messageHeaders['content-disposition']);
filename = parsed.filename || filename;
inline = inline && (parsed[0] == 'inline');
}
// Envelope sender/target
var sender = (messageHeaders['sender'] || commaSplit(messageHeaders['from'])[0]);
var target = email;
// strip name and <>
sender = sender.replace(/^\s*"([^"\\]|\\.)*"/, '').replace(/(^[^<]*<|>[^>]*$)/g, '');
target = target.replace(/^\s*"([^"\\]|\\.)*"/, '').replace(/(^[^<]*<|>[^>]*$)/g, '');
var mail = {
from: messageHeaders['from'],
to: [email],
cc: [],
subject: messageHeaders['subject'] || '',
text: '',
headers: {},
envelope: { /* only send to the "to" address, don't include CC/BCC */
from: sender,
to: target
}
};
if (messageHeaders['cc']) {
mail.cc = mail.cc.concat(commaSplit(messageHeaders['cc']));
}
// TODO: copy over all suitable headers from request
if (inline && /^text\/plain($|\s|;)/.test(contentType)) {
mail.text = copyStream(request);
} else if (inline && /^text\/html($|\s|;)/.test(contentType)) {
delete mail.text;
mail.html = copyStream(request);
} else {
if (/^image\//.test(contentType) && inline) {
delete mail.text;
cid = crypto.pseudoRandomBytes(16).toString('hex') + '-' + email;
mail.html = '<img src="cid:' + cid + '" style="max-width: 100%; max-height: 100%" alt="' + filename.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<') + '"/>';
}
mail.attachments = [{
contentType: contentType,
content: copyStream(request),
cid: cid,
filename: filename
}];
}
mailTransport.sendMail(mail, function (error, info) {
if (error) {
return response.status(500).json({
code: error.code,
error: error.message
});
}
return response.json('Message accepted');
});
} else {
next();
}
}
);
return router;
}
module.exports = httpMail;