UNPKG

postmailer

Version:

HTTP POST -> SMTP proxy, as Express middleware

218 lines (199 loc) 7.2 kB
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, '&amp;').replace(/</g, '&lt;'); } 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, '&amp;').replace(/</g, '&lt;'); }; } }; 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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt') + '"/>'; } 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;