'use strict'; const config = require('config'); let fs = require('fs'); let path = require('path'); let db = require('./db'); let slugify = require('slugify'); let Isemail = require('isemail'); let urllib = require('url'); let juice = require('juice'); let jsdom = require('jsdom'); let he = require('he'); let _ = require('./translate')._; let util = require('util'); let createDOMPurify = require('dompurify'); let htmlToText = require('html-to-text'); let blockedUsers = ['abuse', 'admin', 'billing', 'compliance', 'devnull', 'dns', 'ftp', 'hostmaster', 'inoc', 'ispfeedback', 'ispsupport', 'listrequest', 'list', 'maildaemon', 'noc', 'noreply', 'noreply', 'null', 'phish', 'phishing', 'postmaster', 'privacy', 'registrar', 'root', 'security', 'spam', 'support', 'sysadmin', 'tech', 'undisclosedrecipients', 'unsubscribe', 'usenet', 'uucp', 'webmaster', 'www']; module.exports = { toDbKey, fromDbKey, convertKeys, queryParams, createSlug, updateMenu, validateEmail, formatMessage, getMessageLinks, prepareHtml, purifyHTML, mergeTemplateIntoLayout, workers: new Set() }; function toDbKey(key) { return key. replace(/[^a-z0-9\-_]/gi, ''). replace(/-+/g, '_'). replace(/[A-Z]/g, c => '_' + c.toLowerCase()). replace(/^_+|_+$/g, ''). replace(/_+/g, '_'). trim(); } function fromDbKey(key) { let prefix = ''; if (key.startsWith('_')) { key = key.substring(1); prefix = '_'; } return prefix + key.replace(/[_-]([a-z])/g, (m, c) => c.toUpperCase()); } function convertKeys(obj, options) { options = options || {}; let response = {}; Object.keys(obj || {}).forEach(key => { let lKey = fromDbKey(key); if (options.skip && options.skip.indexOf(lKey) >= 0) { return; } if (options.keep && options.keep.indexOf(lKey) < 0) { return; } response[lKey] = obj[key]; }); return response; } function queryParams(obj) { return Object.keys(obj). filter(key => key !== '_csrf'). map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])). join('&'); } function createSlug(table, name, callback) { let baseSlug = slugify(name).trim().toLowerCase() || 'list'; let counter = 0; if (baseSlug.length > 80) { baseSlug = baseSlug.substr(0, 80); } db.getConnection((err, connection) => { if (err) { return callback(err); } let finalize = (err, slug) => { connection.release(); if (err) { return callback(err); } return callback(null, slug); }; let trySlug = () => { let currentSlug = baseSlug + (counter === 0 ? '' : '-' + counter); counter++; connection.query('SELECT id FROM ' + table + ' WHERE slug=?', [currentSlug], (err, rows) => { if (err) { return finalize(err); } if (!rows || !rows.length) { return finalize(null, currentSlug); } trySlug(); }); }; trySlug(); }); } function updateMenu(res) { if (!res.locals.menu) { res.locals.menu = []; } res.locals.menu.push({ title: _('Lists'), url: '/lists', key: 'lists' }, { title: _('Templates'), url: '/templates', key: 'templates' }, { title: _('Campaigns'), url: '/campaigns', key: 'campaigns' }, { title: _('Automation'), url: '/triggers', key: 'triggers' }); if (config.reports && config.reports.enabled === true) { res.locals.menu.push({ title: _('Reports'), url: '/reports', key: 'reports' }); } } function validateEmail(address, checkBlocked, callback) { let user = (address || '').toString().split('@').shift().toLowerCase().replace(/[^a-z0-9]/g, ''); if (checkBlocked && blockedUsers.indexOf(user) >= 0) { return callback(new Error(util.format(_('Blocked email address "%s"'), address))); } Isemail.validate(address, { checkDNS: true, errorLevel: 1 }, result => { if (result !== 0) { let message = util.format(_('Invalid email address "%s".'), address); switch (result) { case 5: message += ' ' + _('MX record not found for domain'); break; case 6: message += ' ' + _('Address domain not found'); break; case 12: message += ' ' + _('Address domain name is required'); break; } return callback(new Error(message)); } return callback(); }); } function getMessageLinks(serviceUrl, campaign, list, subscription) { return { LINK_UNSUBSCRIBE: urllib.resolve(serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid + '?c=' + campaign.cid), LINK_PREFERENCES: urllib.resolve(serviceUrl, '/subscription/' + list.cid + '/manage/' + subscription.cid), LINK_BROWSER: urllib.resolve(serviceUrl, '/archive/' + campaign.cid + '/' + list.cid + '/' + subscription.cid), CAMPAIGN_ID: campaign.cid, LIST_ID: list.cid, SUBSCRIPTION_ID: subscription.cid }; } function formatMessage(serviceUrl, campaign, list, subscription, message, filter, isHTML) { filter = typeof filter === 'function' ? filter : (str => str); let links = getMessageLinks(serviceUrl, campaign, list, subscription); let getValue = key => { key = (key || '').toString().toUpperCase().trim(); if (links.hasOwnProperty(key)) { return links[key]; } if (subscription.mergeTags.hasOwnProperty(key)) { let value = (subscription.mergeTags[key] || '').toString(); let containsHTML = /<[a-z][\s\S]*>/.test(value); return isHTML ? he.encode((containsHTML ? value : value.replace(/(?:\r\n|\r|\n)/g, '
')), { useNamedReferences: true, allowUnsafeSymbols: true }) : (containsHTML ? htmlToText.fromString(value) : value); } return false; }; return message.replace(/\[([a-z0-9_]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => { identifier = identifier.toUpperCase(); let value = getValue(identifier); if (value === false) { return match; } value = (value || fallback || '').trim(); return filter(value); }); } function prepareHtml(html, callback) { if (!(html || '').toString().trim()) { return callback(null, false); } jsdom.env(false, false, { html, features: { FetchExternalResources: false, // disables resource loading over HTTP / filesystem ProcessExternalResources: false // do not execute JS within script blocks } }, (err, win) => { if (err) { return callback(err); } let head = win.document.querySelector('head'); let hasCharsetTag = false; let metaTags = win.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) { let charsetTag = win.document.createElement('meta'); charsetTag.setAttribute('charset', 'utf-8'); head.appendChild(charsetTag); } let preparedHtml = '' + win.document.documentElement.innerHTML + ''; return callback(null, juice(preparedHtml)); }); } function purifyHTML(html) { let win = jsdom.jsdom('', { features: { FetchExternalResources: false, // disables resource loading over HTTP / filesystem ProcessExternalResources: false // do not execute JS within script blocks } }).defaultView; let DOMPurify = createDOMPurify(win); return DOMPurify.sanitize(html); } // TODO Simplify! function mergeTemplateIntoLayout(template, layout, callback) { layout = layout || '{{{body}}}'; let readFile = (relPath, callback) => { fs.readFile(path.join(__dirname, '..', 'views', relPath), 'utf-8', (err, source) => { if (err) { return callback(err); } callback(null, source); }); }; let done = (template, layout) => { let source = layout.replace(/\{\{\{body\}\}\}/g, template); return callback(null, source); }; if (layout.endsWith('.hbs')) { readFile(layout, (err, layout) => { if (err) { return callback(err); } // Please dont end your custom messages with .hbs ... if (template.endsWith('.hbs')) { readFile(template, (err, template) => { if (err) { return callback(err); } return done(template, layout); }); } else { return done(template, layout); } }); } else if (template.endsWith('.hbs')) { readFile(template, (err, template) => { if (err) { return callback(err); } return done(template, layout); }); } else { return done(template, layout); } }