postmailer
Version:
HTTP POST -> SMTP proxy, as Express middleware
127 lines (112 loc) • 4.18 kB
JavaScript
var fs = require('fs');
var stream = require('stream');
var parseHeader = require('parse-http-header');
function htmlEscape(value) {
return value.replace(/&/g, '&').replace(/</g, '<').replace(/"/g, '"');
}
function enableCors(request, response) {
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Link, From, Subject, CC");
if (request.method === 'OPTIONS') {
var methods = "HEAD,GET,POST,OPTIONS";
response.setHeader("Access-Control-Allow-Methods", methods);
response.setHeader("Access-Control-Allow-Headers", request.header("Access-Control-Request-Headers"));
}
}
function loadHtmlTemplateSync(file, minify) {
var template = fs.readFileSync(file, {encoding: 'utf-8'});
if (minify) {
try {
template = require('html-minifier').minify(template, {
collapseWhitespace: true,
minifyJS: true,
minifyCSS: true
});
} catch (e) {
console.error(e);
// Nothing - the HTML minifier module might not be installed
}
}
return function (match, httpUrl, wsUrl) {
var subs = {
url: httpUrl,
name: match.name,
'ws-url': wsUrl
}
return template.replace(/\{\{(.*?)\}\}/g, function (match, key) {
return htmlEscape((subs[key] || "") + "");
});
};
}
// Pipe through to another stream without changes
// - otherwise the "path" property can screw things up
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 expressHandler(hub, options) {
options = options || {};
var htmlTemplate = null;
var form = options.webForm || options.form;
if (form !== false) {
if (typeof form === 'function') {
htmlTemplate = form;
} else {
var templateFile = (typeof form === 'string') ? form : 'rich';
if (/^[^\/]+$/.test(templateFile) && fs.existsSync(__dirname + '/templates/' + templateFile + '.html')) {
templateFile = __dirname + '/templates/' + templateFile + '.html';
}
htmlTemplate = loadHtmlTemplateSync(templateFile, options.minifyForm);
}
}
return function (request, response, next) {
// TODO: additionally add 'https' if it's secure
var url = 'http://' + request.get('host') + request.originalUrl.replace(/\?.*/, ''); // Reconstruct (without query)
// WebSocket version (used for template only)
var wsUrl = 'ws://' + request.get('host') + request.originalUrl.replace(/\?.*/, '');
var match = hub.match(url); // TODO: filter on "acceptPost" flag
if (!match) return next();
enableCors(request, response);
function textResult(code, text) {
response.setHeader('Content-Type', 'text/plain');
response.status(code).send(text);
}
if (request.method === 'OPTIONS') {
return textResult(200, 'This endpoint implements the "HTTP POST mail" principle.\r\n\r\nhttps://www.npmjs.com/package/postmailer');
} else if (request.method === 'GET') {
if (!htmlTemplate) return next();
response.setHeader('Content-Type', 'text/html');
var wsMatch = hub.match(wsUrl);
response.send(htmlTemplate(match, url, wsMatch && wsUrl));
} else if (request.method === 'POST') {
var fromHeader = request.header('from');
if (!fromHeader) {
return textResult(400, '"From" header is required');
} else if (!/@/.test(fromHeader) && !/\:\/\//.test(fromHeader)) {
return textResult(400, '"From" header must contain URL or email address');
}
var filename = null;
if (request.header('content-disposition')) {
var parsed = parseHeader(request.header('content-disposition'));
if (parsed[0] !== 'inline' && parsed.filename) filename = parsed.filename;
}
hub.emit('mail', {
target: match,
headers: request.headers,
filename: filename,
message: copyStream(request),
length: parseFloat(request.header['content-length']) || null
});
var messageHeaders = request.headers;
return textResult(200, "Message accepted");
} else {
return textResult(405, 'Unknown HTTP method: ' + request.method);
}
}
}
module.exports = expressHandler;