mailtrain/server/lib/tools.js
Tomas Bures e786964411 Some fixes in RSS feed processing.
It is now possible to have hierarchical merge tags (separated by "."). The merge tags are now case sensitive.
Mailtrain allows passing element "mt:entries-json" in the RSS item feed. If this is detected, it parses the json structure and makes it available through RSS_ENTRY_CUSTOM_TAGS. Then it can be used as [RSS_ENTRY_CUSTOM_TAGS.field_quote_text.rendered]
2018-12-29 11:21:25 +01:00

235 lines
6.6 KiB
JavaScript

'use strict';
const util = require('util');
const isemail = require('isemail');
const path = require('path');
const {getPublicUrl} = require('./urls');
const bluebird = require('bluebird');
const hasher = require('node-object-hash')();
const mjml = require('mjml');
const mjml2html = mjml.default;
const hbs = require('hbs');
const juice = require('juice');
const he = require('he');
const fs = require('fs-extra');
const { JSDOM } = require('jsdom');
const { tUI, tLog, getLangCodeFromExpressLocale } = require('./translate');
const templates = new Map();
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) {
if (!template) {
return false;
}
const key = getLangCodeFromExpressLocale(locale) + ':' + ((typeof template === 'object') ? hasher.hash(template) : template);
if (templates.has(key)) {
return templates.get(key);
}
let source;
if (typeof template === 'object') {
source = await mergeTemplateIntoLayout(template.template, template.layout, locale);
} else {
source = await fs.readFile(await getLocalizedFile(path.join(__dirname, '..', 'views'), template, getLangCodeFromExpressLocale(locale)), 'utf-8');
}
if (template.type === 'mjml') {
const compiled = mjml2html(source);
if (compiled.errors.length) {
throw new Error(compiled.errors[0].message || compiled.errors[0]);
}
source = compiled.html;
}
const renderer = hbs.handlebars.compile(source);
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;
}
async function mergeTemplateIntoLayout(template, layout, locale) {
layout = layout || '{{{body}}}';
async function readFile(relPath) {
return await fs.readFile(await getLocalizedFile(path.join(__dirname, '..', 'views'), relPath, getLangCodeFromExpressLocale(locale)), 'utf-8');
}
// Please dont end your custom messages with .hbs ...
if (layout.endsWith('.hbs')) {
layout = await readFile(layout);
}
if (template.endsWith('.hbs')) {
template = await readFile(template);
}
const source = layout.replace(/\{\{\{body\}\}\}/g, template);
return source;
}
async function validateEmail(address) {
const result = await new Promise(resolve => {
const result = isemail.validate(address, {
checkDNS: true,
errorLevel: 1
}, resolve);
});
return result;
}
function validateEmailGetMessage(result, address, language) {
let t;
if (language) {
t = (key, args) => tUI(key, language, args);
} else {
t = (key, args) => tLog(key, args);
}
if (result !== 0) {
switch (result) {
case 5:
return t('invalidEmailAddressEmailMxRecordNotFound', {email: address});
case 6:
return t('invalidEmailAddressEmailAddressDomainNot', {email: address});
case 12:
return t('invalidEmailAddressEmailAddressDomain', {email: address});
default:
return t('invalidEmailGeneric', {email: address});
}
}
}
function formatMessage(campaign, list, subscription, mergeTags, message, isHTML) {
const links = getMessageLinks(campaign, list, subscription);
const getValue = fullKey => {
const keys = (fullKey || '').split('.');
if (links.hasOwnProperty(keys[0])) {
return links[keys[0]];
}
let value = mergeTags;
while (keys.length > 0) {
let key = keys.shift();
if (value.hasOwnProperty(key)) {
value = value[key];
} else {
return false;
}
}
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);
};
return message.replace(/\[([a-z0-9_.]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => {
let value = getValue(identifier);
if (value === false) {
return match;
}
value = (value || fallback || '').trim();
return value;
});
}
async function prepareHtml(html) {
if (!(html || '').toString().trim()) {
return false;
}
const { window } = new JSDOM(html);
const head = window.document.querySelector('head');
let hasCharsetTag = false;
const metaTags = window.document.querySelectorAll('meta');
if (metaTags) {
for (let i = 0; i < metaTags.length; i++) {
if (metaTags[i].hasAttribute('charset')) {
metaTags[i].setAttribute('charset', 'utf-8');
hasCharsetTag = true;
break;
}
}
}
if (!hasCharsetTag) {
const charsetTag = window.document.createElement('meta');
charsetTag.setAttribute('charset', 'utf-8');
head.appendChild(charsetTag);
}
const preparedHtml = '<!doctype html><html>' + window.document.documentElement.innerHTML + '</html>';
return juice(preparedHtml);
}
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
};
}
module.exports = {
validateEmail,
validateEmailGetMessage,
getTemplate,
prepareHtml,
getMessageLinks,
formatMessage
};