Skip to content

Instantly share code, notes, and snippets.

@coolaj86
Created October 27, 2021 22:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save coolaj86/fd100ff34bf5416f9fa4e19712666ebb to your computer and use it in GitHub Desktop.
Save coolaj86/fd100ff34bf5416f9fa4e19712666ebb to your computer and use it in GitHub Desktop.
'use strict';
let request = require('@root/request');
let Postmark = module.exports;
Postmark._serverToken = process.env.POSTMARK_SERVER_TOKEN || '';
// Divide streams into Transactional vs Bulk and Important vs Casual
Postmark.streams = {
// transactional, important
account: {
id: process.env['POSTMARK_STREAM_ACCOUNT_ID'] || '',
from: process.env['POSTMARK_STREAM_ACCOUNT_FROM'] || '',
},
// transactional, casual
activity: {
id: process.env['POSTMARK_STREAM_ACTIVITY_ID'] || '',
from: process.env['POSTMARK_STREAM_ACTIVITY_FROM'] || '',
},
// bulk, important
service: {
id: process.env['POSTMARK_STREAM_SERVICE_ID'] || '',
from: process.env['POSTMARK_STREAM_SERVICE_FROM'] || '',
},
// bulk, casual
promo: {
id: process.env['POSTMARK_STREAM_PROMO_ID'] || '',
from: process.env['POSTMARK_STREAM_PROMO_FROM'] || '',
},
};
Postmark._defaultStream = Postmark.streams.activity;
// See "Forbidden File Types" in https://postmarkapp.com/developer/user-guide/send-email-with-api
Postmark._forbidden =
'vbs, exe, bin, bat, chm, com, cpl, crt, hlp, hta, inf, ins, isp, jse, lnk, mdb, pcd, pif, reg, scr, sct, shs, vbe, vba, wsf, wsh, wsl, msc, msi, msp, mst'
.split(/[,\s\n\r]+/)
.filter(Boolean);
Postmark.send = async function (mgMsg) {
let pmMsg = await Postmark._mg2pmWithAttachments(mgMsg);
if (mgMsg.stream) {
let stream = Postmark.streams[mgMsg.stream] || Postmark._defaultStream;
pmMsg.MessageStream = stream.id;
pmMsg.From = stream.from;
}
if (!pmMsg.MessageStream) {
pmMsg.MessageStream = Postmark._defaultStream.id;
}
let resp = await request({
url: 'https://api.postmarkapp.com/email',
method: 'POST',
headers: {
'X-Postmark-Server-Token': Postmark._serverToken,
},
json: pmMsg,
});
if (resp.statusCode >= 300) {
var err = new Error('failed to send message');
//@ts-ignore
err.response = resp.toJSON();
//@ts-ignore
err.response.request.headers['X-Postmark-Server-Token'] = err.response.request.headers['X-Postmark-Server-Token'].replace(/\w/g, '*');
throw err;
}
return resp;
};
Postmark._mg2pm = function (mgMsg) {
let trackOpens = mgMsg['o:tracking'] || mgMsg['o:tracking-opens'];
if (trackOpens && !['no', 'false'].includes(trackOpens)) {
trackOpens = true;
}
let trackLinks = mgMsg['o:tracking'] || mgMsg['o:tracking-clicks'];
if (trackLinks && !['no', 'false'].includes(trackLinks)) {
trackLinks = 'HtmlOnly';
}
let pmMsg = {
From: mgMsg.from,
To: mgMsg.to,
Cc: mgMsg.cc,
Bcc: mgMsg.bcc,
Subject: mgMsg.subject,
//Tag: "Invitation",
HtmlBody: mgMsg.html,
TextBody: mgMsg.text,
ReplyTo: mgMsg['h:Reply-To'],
/*
Metadata: { Color: 'blue', 'Client-Id': '12345' },
*/
Headers: [
/* { Name: 'CUSTOM-HEADER', Value: 'value' }, */
],
Attachments: [
/* { "Name": "readme.txt", "Content": "dGVzdCBjb250ZW50", "ContentType": "text/plain" } */
],
TrackOpens: trackOpens,
TrackLinks: trackLinks,
MessageStream: 'outbound',
};
// copy 'h:'-prefixed headers
Object.keys(mgMsg).forEach(function (k) {
if ('h:' !== k.slice(0, 2)) {
return;
}
if ('h:Reply-To' === k) {
return;
}
let header = {
Name: k.slice(2),
Value: mgMsg[k],
};
//@ts-ignore
pmMsg.Headers.push(header);
});
return pmMsg;
};
Postmark._mg2pmWithAttachments = async function (mgMsg) {
let pmMsg = await Postmark._mg2pm(mgMsg);
// this is a very sad way to handle attachments...
if (!mgMsg.attachment || !mgMsg.attachment.length) {
return pmMsg;
}
let Fs = require('fs');
let Path = require('path');
mgMsg.attachment.forEach(function (v) {
let filename = v;
if ('string' !== typeof v) {
filename = v.options.filename;
}
let ext = Path.extname(filename).slice(1).toLowerCase();
if (Postmark._forbidden.includes(ext)) {
let msg = `attachments of type '.${ext}' are not allowed`;
let err = new Error('');
//@ts-ignore
err.code = 'INTERNAL_SERVER_ERROR';
//@ts-ignore
err.source = 'mg2pm';
//@ts-ignore
err.status = 500;
err.message = msg;
throw err;
}
});
await mgMsg.attachment.reduce(async function (p, v) {
await p;
if ('string' === typeof v) {
let r = Fs.createReadStream(v);
let filename = Path.basename(v);
v = {
value: r,
options: {
filename: filename,
},
};
}
let buf = await promisifyStream(v.value);
pmMsg.attachments.push({
Name: v.options.filename || v.value.path,
Content: buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''),
ContentType: v.options.contentType || 'application/octet-stream',
});
}, Promise.resolve());
return pmMsg;
};
async function promisifyStream(r) {
if (r instanceof Buffer) {
return r;
}
return await new Promise(function (resolve) {
let body;
let chunks = [];
r.on('readable', function () {
let chunk;
while ((chunk = r.read())) {
chunks.push(chunk);
}
});
r.on('end', function () {
body = Buffer.concat(chunks);
});
resolve(body);
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment