2016-04-04 12:36:30 +00:00
|
|
|
'use strict';
|
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
const util = require('util');
|
|
|
|
const isemail = require('isemail');
|
2018-05-09 02:07:01 +00:00
|
|
|
const path = require('path');
|
2018-09-18 08:30:13 +00:00
|
|
|
const {getPublicUrl} = require('./urls');
|
2016-04-04 12:36:30 +00:00
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
const bluebird = require('bluebird');
|
2016-04-04 12:36:30 +00:00
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
const hasher = require('node-object-hash')();
|
2018-11-10 01:05:26 +00:00
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
const mjml = require('mjml');
|
2018-11-10 01:05:26 +00:00
|
|
|
const mjml2html = mjml.default;
|
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
const hbs = require('hbs');
|
|
|
|
const juice = require('juice');
|
2018-11-17 22:26:45 +00:00
|
|
|
const he = require('he');
|
2018-04-29 16:13:40 +00:00
|
|
|
|
2018-11-17 22:26:45 +00:00
|
|
|
const fs = require('fs-extra');
|
2018-04-29 16:13:40 +00:00
|
|
|
|
2018-11-17 22:26:45 +00:00
|
|
|
const { JSDOM } = require('jsdom');
|
2018-12-15 14:15:48 +00:00
|
|
|
const { tUI, tLog, getLangCodeFromExpressLocale } = require('./translate');
|
2018-04-29 16:13:40 +00:00
|
|
|
|
2018-09-18 08:30:13 +00:00
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
const templates = new Map();
|
2016-04-04 12:36:30 +00:00
|
|
|
|
2018-12-15 14:15:48 +00:00
|
|
|
async function getLocalizedFile(basePath, fileName, language) {
|
|
|
|
try {
|
|
|
|
const locFn = path.join(basePath, language, fileName);
|
|
|
|
const stats = await fs.stat(locFn);
|
|
|
|
|
|
|
|
if (stats.isFile()) {
|
|
|
|
return locFn;
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
if (err.code !== 'ENOENT') {
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return path.join(basePath, fileName)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getTemplate(template, locale) {
|
2018-04-29 16:13:40 +00:00
|
|
|
if (!template) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-04-04 12:36:30 +00:00
|
|
|
|
2018-12-15 14:15:48 +00:00
|
|
|
const key = getLangCodeFromExpressLocale(locale) + ':' + ((typeof template === 'object') ? hasher.hash(template) : template);
|
2016-04-04 12:36:30 +00:00
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
if (templates.has(key)) {
|
|
|
|
return templates.get(key);
|
|
|
|
}
|
|
|
|
|
|
|
|
let source;
|
|
|
|
if (typeof template === 'object') {
|
2018-12-15 14:15:48 +00:00
|
|
|
source = await mergeTemplateIntoLayout(template.template, template.layout, locale);
|
2018-04-29 16:13:40 +00:00
|
|
|
} else {
|
2018-12-15 14:15:48 +00:00
|
|
|
source = await fs.readFile(await getLocalizedFile(path.join(__dirname, '..', 'views'), template, getLangCodeFromExpressLocale(locale)), 'utf-8');
|
2016-04-04 12:36:30 +00:00
|
|
|
}
|
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
if (template.type === 'mjml') {
|
2018-11-10 01:05:26 +00:00
|
|
|
const compiled = mjml2html(source);
|
2018-04-29 16:13:40 +00:00
|
|
|
|
|
|
|
if (compiled.errors.length) {
|
|
|
|
throw new Error(compiled.errors[0].message || compiled.errors[0]);
|
2016-04-04 12:36:30 +00:00
|
|
|
}
|
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
source = compiled.html;
|
|
|
|
}
|
|
|
|
|
2018-05-09 02:07:01 +00:00
|
|
|
const renderer = hbs.handlebars.compile(source);
|
2018-04-29 16:13:40 +00:00
|
|
|
|
2018-12-15 14:15:48 +00:00
|
|
|
const localizedRenderer = (data, options) => {
|
|
|
|
if (!options) {
|
|
|
|
options = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!options.helpers) {
|
|
|
|
options.helpers = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
options.helpers.translate = function (opts) { // eslint-disable-line prefer-arrow-callback
|
|
|
|
const result = tUI(opts.fn(this), locale, opts.hash); // eslint-disable-line no-invalid-this
|
|
|
|
return new hbs.handlebars.SafeString(result);
|
|
|
|
};
|
|
|
|
|
|
|
|
return renderer(data, options);
|
|
|
|
};
|
|
|
|
|
|
|
|
templates.set(key, localizedRenderer);
|
|
|
|
|
|
|
|
return localizedRenderer;
|
2016-04-04 12:36:30 +00:00
|
|
|
}
|
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
|
2018-12-15 14:15:48 +00:00
|
|
|
async function mergeTemplateIntoLayout(template, layout, locale) {
|
2018-04-29 16:13:40 +00:00
|
|
|
layout = layout || '{{{body}}}';
|
|
|
|
|
|
|
|
async function readFile(relPath) {
|
2018-12-15 14:15:48 +00:00
|
|
|
return await fs.readFile(await getLocalizedFile(path.join(__dirname, '..', 'views'), relPath, getLangCodeFromExpressLocale(locale)), 'utf-8');
|
2016-04-04 12:36:30 +00:00
|
|
|
}
|
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
// Please dont end your custom messages with .hbs ...
|
|
|
|
if (layout.endsWith('.hbs')) {
|
|
|
|
layout = await readFile(layout);
|
|
|
|
}
|
2017-04-25 22:49:31 +00:00
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
if (template.endsWith('.hbs')) {
|
2018-05-20 18:27:35 +00:00
|
|
|
template = await readFile(template);
|
2017-04-25 22:49:31 +00:00
|
|
|
}
|
2018-04-29 16:13:40 +00:00
|
|
|
|
|
|
|
const source = layout.replace(/\{\{\{body\}\}\}/g, template);
|
|
|
|
return source;
|
2016-04-04 12:36:30 +00:00
|
|
|
}
|
|
|
|
|
2018-09-01 19:29:10 +00:00
|
|
|
async function validateEmail(address) {
|
2018-04-29 16:13:40 +00:00
|
|
|
const result = await new Promise(resolve => {
|
|
|
|
const result = isemail.validate(address, {
|
|
|
|
checkDNS: true,
|
|
|
|
errorLevel: 1
|
|
|
|
}, resolve);
|
2016-04-04 12:36:30 +00:00
|
|
|
});
|
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
return result;
|
2016-04-26 17:29:57 +00:00
|
|
|
}
|
|
|
|
|
2018-11-17 22:26:45 +00:00
|
|
|
function validateEmailGetMessage(result, address, language) {
|
|
|
|
let t;
|
|
|
|
if (language) {
|
2018-11-18 14:38:52 +00:00
|
|
|
t = (key, args) => tUI(key, language, args);
|
2018-11-17 22:26:45 +00:00
|
|
|
} else {
|
|
|
|
t = (key, args) => tLog(key, args);
|
|
|
|
}
|
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
if (result !== 0) {
|
|
|
|
switch (result) {
|
|
|
|
case 5:
|
2018-11-18 20:31:22 +00:00
|
|
|
return t('invalidEmailAddressEmailMxRecordNotFound', {email: address});
|
2018-04-29 16:13:40 +00:00
|
|
|
case 6:
|
2018-11-18 20:31:22 +00:00
|
|
|
return t('invalidEmailAddressEmailAddressDomainNot', {email: address});
|
2018-04-29 16:13:40 +00:00
|
|
|
case 12:
|
2018-11-18 20:31:22 +00:00
|
|
|
return t('invalidEmailAddressEmailAddressDomain', {email: address});
|
2018-11-17 22:26:45 +00:00
|
|
|
default:
|
|
|
|
return t('invalidEmailGeneric', {email: address});
|
2016-04-04 12:36:30 +00:00
|
|
|
}
|
2018-04-29 16:13:40 +00:00
|
|
|
}
|
2016-04-04 12:36:30 +00:00
|
|
|
}
|
2016-05-03 16:21:01 +00:00
|
|
|
|
2018-11-17 01:54:23 +00:00
|
|
|
function formatMessage(campaign, list, subscription, mergeTags, message, isHTML) {
|
2018-09-27 10:34:54 +00:00
|
|
|
const links = getMessageLinks(campaign, list, subscription);
|
2018-09-18 08:30:13 +00:00
|
|
|
|
2018-12-29 10:21:25 +00:00
|
|
|
const getValue = fullKey => {
|
|
|
|
const keys = (fullKey || '').split('.');
|
|
|
|
|
|
|
|
if (links.hasOwnProperty(keys[0])) {
|
|
|
|
return links[keys[0]];
|
2018-09-18 08:30:13 +00:00
|
|
|
}
|
2018-12-29 10:21:25 +00:00
|
|
|
|
|
|
|
let value = mergeTags;
|
|
|
|
while (keys.length > 0) {
|
|
|
|
let key = keys.shift();
|
|
|
|
if (value.hasOwnProperty(key)) {
|
|
|
|
value = value[key];
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
2018-09-18 08:30:13 +00:00
|
|
|
}
|
2018-12-29 10:21:25 +00:00
|
|
|
|
|
|
|
const containsHTML = /<[a-z][\s\S]*>/.test(value);
|
|
|
|
return isHTML ? he.encode((containsHTML ? value : value.replace(/(?:\r\n|\r|\n)/g, '<br/>')), {
|
|
|
|
useNamedReferences: true,
|
|
|
|
allowUnsafeSymbols: true
|
|
|
|
}) : (containsHTML ? htmlToText.fromString(value) : value);
|
2018-09-18 08:30:13 +00:00
|
|
|
};
|
|
|
|
|
2018-12-29 10:21:25 +00:00
|
|
|
return message.replace(/\[([a-z0-9_.]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => {
|
2018-09-18 08:30:13 +00:00
|
|
|
let value = getValue(identifier);
|
|
|
|
if (value === false) {
|
|
|
|
return match;
|
|
|
|
}
|
|
|
|
value = (value || fallback || '').trim();
|
2018-11-17 01:54:23 +00:00
|
|
|
return value;
|
2018-09-18 08:30:13 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-11-17 01:54:23 +00:00
|
|
|
async function prepareHtml(html) {
|
2016-05-03 16:21:01 +00:00
|
|
|
if (!(html || '').toString().trim()) {
|
2018-04-29 16:13:40 +00:00
|
|
|
return false;
|
2016-05-03 16:21:01 +00:00
|
|
|
}
|
2018-04-29 16:13:40 +00:00
|
|
|
|
2018-11-17 22:26:45 +00:00
|
|
|
const { window } = new JSDOM(html);
|
2018-04-29 16:13:40 +00:00
|
|
|
|
2018-11-17 22:26:45 +00:00
|
|
|
const head = window.document.querySelector('head');
|
2018-04-29 16:13:40 +00:00
|
|
|
let hasCharsetTag = false;
|
2018-11-17 22:26:45 +00:00
|
|
|
const metaTags = window.document.querySelectorAll('meta');
|
2018-04-29 16:13:40 +00:00
|
|
|
if (metaTags) {
|
|
|
|
for (let i = 0; i < metaTags.length; i++) {
|
|
|
|
if (metaTags[i].hasAttribute('charset')) {
|
|
|
|
metaTags[i].setAttribute('charset', 'utf-8');
|
|
|
|
hasCharsetTag = true;
|
|
|
|
break;
|
2016-05-03 16:21:01 +00:00
|
|
|
}
|
|
|
|
}
|
2018-04-29 16:13:40 +00:00
|
|
|
}
|
|
|
|
if (!hasCharsetTag) {
|
2018-11-17 22:26:45 +00:00
|
|
|
const charsetTag = window.document.createElement('meta');
|
2018-04-29 16:13:40 +00:00
|
|
|
charsetTag.setAttribute('charset', 'utf-8');
|
|
|
|
head.appendChild(charsetTag);
|
|
|
|
}
|
2018-11-17 22:26:45 +00:00
|
|
|
const preparedHtml = '<!doctype html><html>' + window.document.documentElement.innerHTML + '</html>';
|
2016-05-03 16:21:01 +00:00
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
return juice(preparedHtml);
|
2016-05-03 16:21:01 +00:00
|
|
|
}
|
2017-03-19 12:36:57 +00:00
|
|
|
|
2018-09-18 08:30:13 +00:00
|
|
|
function getMessageLinks(campaign, list, subscription) {
|
|
|
|
return {
|
|
|
|
LINK_UNSUBSCRIBE: getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscription.cid + '?c=' + campaign.cid),
|
|
|
|
LINK_PREFERENCES: getPublicUrl('/subscription/' + list.cid + '/manage/' + subscription.cid),
|
|
|
|
LINK_BROWSER: getPublicUrl('/archive/' + campaign.cid + '/' + list.cid + '/' + subscription.cid),
|
|
|
|
CAMPAIGN_ID: campaign.cid,
|
|
|
|
LIST_ID: list.cid,
|
|
|
|
SUBSCRIPTION_ID: subscription.cid
|
|
|
|
};
|
|
|
|
}
|
2017-03-19 12:36:57 +00:00
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
module.exports = {
|
|
|
|
validateEmail,
|
|
|
|
validateEmailGetMessage,
|
|
|
|
getTemplate,
|
2018-09-18 08:30:13 +00:00
|
|
|
prepareHtml,
|
2018-09-27 10:34:54 +00:00
|
|
|
getMessageLinks,
|
|
|
|
formatMessage
|
2018-04-29 16:13:40 +00:00
|
|
|
};
|
2017-03-19 12:36:57 +00:00
|
|
|
|