WiP on mailers
This commit is contained in:
parent
e97415c237
commit
a4ee1534cc
46 changed files with 1263 additions and 529 deletions
114
lib/db.js
114
lib/db.js
|
@ -1,114 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let config = require('config');
|
||||
let mysql = require('mysql2');
|
||||
let redis = require('redis');
|
||||
let Lock = require('redfour');
|
||||
let stringifyDate = require('json-stringify-date');
|
||||
let senders = require('./senders');
|
||||
|
||||
module.exports = mysql.createPool(config.mysql);
|
||||
if (config.redis && config.redis.enabled) {
|
||||
|
||||
module.exports.redis = redis.createClient(config.redis);
|
||||
|
||||
let queueLock = new Lock({
|
||||
redis: config.redis,
|
||||
namespace: 'mailtrain:lock'
|
||||
});
|
||||
|
||||
module.exports.getLock = (id, callback) => {
|
||||
queueLock.waitAcquireLock(id, 60 * 1000 /* Lock expires after 60sec */ , 10 * 1000 /* Wait for lock for up to 10sec */ , (err, lock) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!lock) {
|
||||
return callback(null, false);
|
||||
}
|
||||
return callback(null, {
|
||||
lock,
|
||||
release(done) {
|
||||
queueLock.releaseLock(lock, done);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.clearCache = (key, callback) => {
|
||||
module.exports.redis.del('mailtrain:cache:' + key, err => callback(err));
|
||||
};
|
||||
|
||||
module.exports.addToCache = (key, value, callback) => {
|
||||
if (!value) {
|
||||
return setImmediate(() => callback());
|
||||
}
|
||||
module.exports.redis.multi().
|
||||
lpush('mailtrain:cache:' + key, stringifyDate.stringify(value)).
|
||||
expire('mailtrain:cache:' + key, 24 * 3600).
|
||||
exec(err => callback(err));
|
||||
};
|
||||
|
||||
module.exports.getFromCache = (key, callback) => {
|
||||
module.exports.redis.rpop('mailtrain:cache:' + key, (err, value) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
try {
|
||||
value = stringifyDate.parse(value);
|
||||
} catch (E) {
|
||||
return callback(E);
|
||||
}
|
||||
|
||||
return callback(null, value);
|
||||
});
|
||||
};
|
||||
|
||||
} else {
|
||||
// fakelock. does not lock anything
|
||||
module.exports.getLock = (id, callback) => {
|
||||
setImmediate(() => callback(null, {
|
||||
lock: false,
|
||||
release(done) {
|
||||
setImmediate(done);
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
let caches = new Map();
|
||||
|
||||
module.exports.clearCache = (key, callback) => {
|
||||
caches.delete(key);
|
||||
senders.workers.forEach(child => {
|
||||
child.send({
|
||||
cmd: 'db.clearCache',
|
||||
key
|
||||
});
|
||||
});
|
||||
setImmediate(() => callback());
|
||||
};
|
||||
|
||||
process.on('message', m => {
|
||||
if (m && m.cmd === 'db.clearCache' && m.key) {
|
||||
caches.delete(m.key);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports.addToCache = (key, value, callback) => {
|
||||
if (!caches.has(key)) {
|
||||
caches.set(key, []);
|
||||
}
|
||||
caches.get(key).push(value);
|
||||
setImmediate(() => callback());
|
||||
};
|
||||
|
||||
module.exports.getFromCache = (key, callback) => {
|
||||
let value;
|
||||
if (caches.has(key)) {
|
||||
value = caches.get(key).shift();
|
||||
if (!caches.get(key).length) {
|
||||
caches.delete(key);
|
||||
}
|
||||
}
|
||||
setImmediate(() => callback(null, value));
|
||||
};
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
// DEPRECATED
|
||||
|
||||
'use strict';
|
||||
|
||||
let _ = require('../lib/translate')._;
|
||||
let helpers = require('../lib/helpers');
|
||||
let templates = require('../lib/models/templates');
|
||||
let campaigns = require('../lib/models/campaigns');
|
||||
|
||||
module.exports = {
|
||||
getResource,
|
||||
getMergeTagsForResource
|
||||
};
|
||||
|
||||
function getResource(type, id, callback) {
|
||||
if (type === 'template') {
|
||||
templates.get(id, (err, template) => {
|
||||
if (err || !template) {
|
||||
return callback(err && err.message || err || _('Could not find template with specified ID'));
|
||||
}
|
||||
|
||||
getMergeTagsForResource(template, (err, mergeTags) => {
|
||||
if (err) {
|
||||
return callback(err.message || err);
|
||||
}
|
||||
|
||||
template.mergeTags = mergeTags;
|
||||
return callback(null, template);
|
||||
});
|
||||
});
|
||||
|
||||
} else if (type === 'campaign') {
|
||||
campaigns.get(id, false, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
return callback(err && err.message || err || _('Could not find campaign with specified ID'));
|
||||
}
|
||||
|
||||
getMergeTagsForResource(campaign, (err, mergeTags) => {
|
||||
if (err) {
|
||||
return callback(err.message || err);
|
||||
}
|
||||
|
||||
campaign.mergeTags = mergeTags;
|
||||
return callback(null, campaign);
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
return callback(_('Invalid resource type'));
|
||||
}
|
||||
}
|
||||
|
||||
function getMergeTagsForResource(resource, callback) {
|
||||
helpers.getDefaultMergeTags((err, defaultMergeTags) => {
|
||||
if (err) {
|
||||
return callback(err.message || err);
|
||||
}
|
||||
|
||||
if (!Number(resource.list)) {
|
||||
return callback(null, defaultMergeTags);
|
||||
}
|
||||
|
||||
helpers.getListMergeTags(resource.list, (err, listMergeTags) => {
|
||||
if (err) {
|
||||
return callback(err.message || err);
|
||||
}
|
||||
|
||||
if (resource.type !== 2) {
|
||||
return callback(null, defaultMergeTags.concat(listMergeTags));
|
||||
}
|
||||
|
||||
helpers.getRSSMergeTags((err, rssMergeTags) => {
|
||||
if (err) {
|
||||
return callback(err.message || err);
|
||||
}
|
||||
|
||||
callback(null, defaultMergeTags.concat(listMergeTags, rssMergeTags));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
308
lib/mailer.js
308
lib/mailer.js
|
@ -1,308 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { nodeifyFunction } = require('./nodeify');
|
||||
const getSettings = nodeifyFunction(require('../models/settings').get);
|
||||
|
||||
let log = require('npmlog');
|
||||
let config = require('config');
|
||||
let nodemailer = require('nodemailer');
|
||||
let openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt;
|
||||
let tools = require('./tools');
|
||||
let db = require('./db');
|
||||
let Handlebars = require('handlebars');
|
||||
let fs = require('fs');
|
||||
let path = require('path');
|
||||
let templates = new Map();
|
||||
let htmlToText = require('html-to-text');
|
||||
let aws = require('aws-sdk');
|
||||
let objectHash = require('object-hash');
|
||||
let mjml = require('mjml');
|
||||
|
||||
let _ = require('./translate')._;
|
||||
let util = require('util');
|
||||
|
||||
Handlebars.registerHelper('translate', function (context, options) { // eslint-disable-line prefer-arrow-callback
|
||||
if (typeof options === 'undefined' && context) {
|
||||
options = context;
|
||||
context = false;
|
||||
}
|
||||
|
||||
let result = _(options.fn(this)); // eslint-disable-line no-invalid-this
|
||||
|
||||
if (Array.isArray(context)) {
|
||||
result = util.format(result, ...context);
|
||||
}
|
||||
return new Handlebars.SafeString(result);
|
||||
});
|
||||
|
||||
module.exports.transport = false;
|
||||
|
||||
module.exports.update = () => {
|
||||
createMailer(() => false);
|
||||
};
|
||||
|
||||
module.exports.getMailer = callback => {
|
||||
if (!module.exports.transport) {
|
||||
return createMailer(callback);
|
||||
}
|
||||
callback(null, module.exports.transport);
|
||||
};
|
||||
|
||||
module.exports.sendMail = (mail, template, callback) => {
|
||||
if (!callback && typeof template === 'function') {
|
||||
callback = template;
|
||||
template = false;
|
||||
}
|
||||
|
||||
if (!module.exports.transport) {
|
||||
return createMailer(err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return module.exports.sendMail(mail, template, callback);
|
||||
});
|
||||
}
|
||||
|
||||
if (!mail.headers) {
|
||||
mail.headers = {};
|
||||
}
|
||||
mail.headers['X-Sending-Zone'] = 'transactional';
|
||||
|
||||
getTemplate(template.html, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (htmlRenderer) {
|
||||
mail.html = htmlRenderer(template.data || {});
|
||||
}
|
||||
|
||||
tools.prepareHtml(mail.html, (err, prepareHtml) => {
|
||||
if (err) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (prepareHtml) {
|
||||
mail.html = prepareHtml;
|
||||
}
|
||||
|
||||
getTemplate(template.text, (err, textRenderer) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (textRenderer) {
|
||||
mail.text = textRenderer(template.data || {});
|
||||
} else if (mail.html) {
|
||||
mail.text = htmlToText.fromString(mail.html, {
|
||||
wordwrap: 130
|
||||
});
|
||||
}
|
||||
|
||||
let tryCount = 0;
|
||||
let trySend = () => {
|
||||
tryCount++;
|
||||
|
||||
module.exports.transport.sendMail(mail, (err, info) => {
|
||||
if (err) {
|
||||
log.error('Mail', err);
|
||||
if (err.responseCode && err.responseCode >= 400 && err.responseCode < 500 && tryCount <= 5) {
|
||||
// temporary error, try again
|
||||
log.verbose('Mail', 'Retrying after %s sec. ...', tryCount);
|
||||
return setTimeout(trySend, tryCount * 1000);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, info);
|
||||
});
|
||||
};
|
||||
setImmediate(trySend);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
function getTemplate(template, callback) {
|
||||
if (!template) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let key = (typeof template === 'object') ? objectHash(template) : template;
|
||||
|
||||
if (templates.has(key)) {
|
||||
return callback(null, templates.get(key));
|
||||
}
|
||||
|
||||
let done = (source, isMjml = false) => {
|
||||
if (isMjml) {
|
||||
let compiled;
|
||||
try {
|
||||
compiled = mjml.mjml2html(source);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (compiled.errors.length) {
|
||||
return callback(compiled.errors[0].message || compiled.errors[0]);
|
||||
}
|
||||
source = compiled.html;
|
||||
}
|
||||
let renderer = Handlebars.compile(source);
|
||||
templates.set(key, renderer);
|
||||
callback(null, renderer);
|
||||
};
|
||||
|
||||
if (typeof template === 'object') {
|
||||
tools.mergeTemplateIntoLayout(template.template, template.layout, (err, source) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let isMjml = template.type === 'mjml';
|
||||
done(source, isMjml);
|
||||
});
|
||||
} else {
|
||||
fs.readFile(path.join(__dirname, '..', 'views', template), 'utf-8', (err, source) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
done(source);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createMailer(callback) {
|
||||
// FIXME
|
||||
module.exports.transport = {
|
||||
on: () => {},
|
||||
isIdle: () => true,
|
||||
checkThrottling: next => next()
|
||||
};
|
||||
return callback(null, module.exports.transport);
|
||||
|
||||
|
||||
getSettings(['smtpHostname', 'smtpPort', 'smtpEncryption', 'smtpUser', 'smtpPass', 'smtpLog', 'smtpDisableAuth', 'smtpMaxConnections', 'smtpMaxMessages', 'smtpSelfSigned', 'pgpPrivateKey', 'pgpPassphrase', 'smtpThrottling', 'mailTransport', 'sesKey', 'sesSecret', 'sesRegion'], (err, configItems) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let oldListeners = [];
|
||||
if (module.exports.transport) {
|
||||
oldListeners = module.exports.transport.listeners('idle');
|
||||
module.exports.transport.removeAllListeners('idle');
|
||||
module.exports.transport.removeAllListeners('stream');
|
||||
module.exports.transport.checkThrottling = null;
|
||||
}
|
||||
|
||||
let sendingRate = Number(configItems.smtpThrottling) || 0;
|
||||
if (sendingRate) {
|
||||
// convert to messages/second
|
||||
sendingRate = sendingRate / 3600;
|
||||
}
|
||||
|
||||
let transportOptions;
|
||||
|
||||
let logfunc = function () {
|
||||
let args = [].slice.call(arguments);
|
||||
let level = args.shift();
|
||||
args.shift();
|
||||
args.unshift('Mail');
|
||||
log[level](...args);
|
||||
};
|
||||
|
||||
if (configItems.mailTransport === 'smtp' || !configItems.mailTransport) {
|
||||
transportOptions = {
|
||||
pool: true,
|
||||
host: configItems.smtpHostname,
|
||||
port: Number(configItems.smtpPort) || false,
|
||||
secure: configItems.smtpEncryption === 'TLS',
|
||||
ignoreTLS: configItems.smtpEncryption === 'NONE',
|
||||
auth: configItems.smtpDisableAuth ? false : {
|
||||
user: configItems.smtpUser,
|
||||
pass: configItems.smtpPass
|
||||
},
|
||||
debug: !!configItems.smtpLog,
|
||||
logger: !configItems.smtpLog ? false : {
|
||||
debug: logfunc.bind(null, 'verbose'),
|
||||
info: logfunc.bind(null, 'info'),
|
||||
error: logfunc.bind(null, 'error')
|
||||
},
|
||||
maxConnections: Number(configItems.smtpMaxConnections),
|
||||
maxMessages: Number(configItems.smtpMaxMessages),
|
||||
tls: {
|
||||
rejectUnauthorized: !configItems.smtpSelfSigned
|
||||
}
|
||||
};
|
||||
} else if (configItems.mailTransport === 'ses') {
|
||||
transportOptions = {
|
||||
SES: new aws.SES({
|
||||
apiVersion: '2010-12-01',
|
||||
accessKeyId: configItems.sesKey,
|
||||
secretAccessKey: configItems.sesSecret,
|
||||
region: configItems.sesRegion
|
||||
}),
|
||||
debug: !!configItems.smtpLog,
|
||||
logger: !configItems.smtpLog ? false : {
|
||||
debug: logfunc.bind(null, 'verbose'),
|
||||
info: logfunc.bind(null, 'info'),
|
||||
error: logfunc.bind(null, 'error')
|
||||
},
|
||||
maxConnections: Number(configItems.smtpMaxConnections),
|
||||
sendingRate,
|
||||
tls: {
|
||||
rejectUnauthorized: !configItems.smtpSelfSigned
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return callback(new Error(_('Invalid mail transport')));
|
||||
}
|
||||
|
||||
module.exports.transport = nodemailer.createTransport(transportOptions, config.nodemailer);
|
||||
|
||||
module.exports.transport.use('stream', openpgpEncrypt({
|
||||
signingKey: configItems.pgpPrivateKey,
|
||||
passphrase: configItems.pgpPassphrase
|
||||
}));
|
||||
|
||||
if (oldListeners.length) {
|
||||
log.info('Mail', 'Reattaching %s idle listeners', oldListeners.length);
|
||||
oldListeners.forEach(listener => module.exports.transport.on('idle', listener));
|
||||
}
|
||||
|
||||
|
||||
if (configItems.mailTransport === 'smtp' || !configItems.mailTransport) {
|
||||
|
||||
let throttling = Number(configItems.smtpThrottling) || 0;
|
||||
if (throttling) {
|
||||
throttling = 1 / (throttling / (3600 * 1000));
|
||||
}
|
||||
|
||||
let lastCheck = Date.now();
|
||||
|
||||
module.exports.transport.checkThrottling = function (next) {
|
||||
if (!throttling) {
|
||||
return next();
|
||||
}
|
||||
let nextCheck = Date.now();
|
||||
let checkDiff = (nextCheck - lastCheck);
|
||||
if (checkDiff < throttling) {
|
||||
log.verbose('Mail', 'Throttling next message in %s sec.', (throttling - checkDiff) / 1000);
|
||||
setTimeout(() => {
|
||||
lastCheck = Date.now();
|
||||
next();
|
||||
}, throttling - checkDiff);
|
||||
} else {
|
||||
lastCheck = nextCheck;
|
||||
next();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
module.exports.transport.checkThrottling = next => next();
|
||||
}
|
||||
|
||||
db.clearCache('sender', () => {
|
||||
callback(null, module.exports.transport);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.getTemplate = getTemplate;
|
232
lib/mailers.js
Normal file
232
lib/mailers.js
Normal file
|
@ -0,0 +1,232 @@
|
|||
'use strict';
|
||||
|
||||
const log = require('npmlog');
|
||||
const config = require('config');
|
||||
|
||||
const Handlebars = require('handlebars');
|
||||
const util = require('util');
|
||||
const nodemailer = require('nodemailer');
|
||||
const aws = require('aws-sdk');
|
||||
const openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt;
|
||||
const sendConfigurations = require('../models/send-configurations');
|
||||
|
||||
const contextHelpers = require('./context-helpers');
|
||||
const settings = require('../models/settings');
|
||||
const tools = require('./tools');
|
||||
const htmlToText = require('html-to-text');
|
||||
|
||||
const bluebird = require('bluebird');
|
||||
|
||||
const _ = require('./translate')._;
|
||||
|
||||
Handlebars.registerHelper('translate', function (context, options) { // eslint-disable-line prefer-arrow-callback
|
||||
if (typeof options === 'undefined' && context) {
|
||||
options = context;
|
||||
context = false;
|
||||
}
|
||||
|
||||
let result = _(options.fn(this)); // eslint-disable-line no-invalid-this
|
||||
|
||||
if (Array.isArray(context)) {
|
||||
result = util.format(result, ...context);
|
||||
}
|
||||
return new Handlebars.SafeString(result);
|
||||
});
|
||||
|
||||
const transports = new Map();
|
||||
|
||||
async function getOrCreateMailer(sendConfiguration) {
|
||||
if (!sendConfiguration) {
|
||||
sendConfiguration = sendConfigurations.getSystemSendConfiguration();
|
||||
}
|
||||
|
||||
const transport = transports.get(sendConfiguration.id) || await _createTransport(sendConfiguration);
|
||||
return transport.mailer;
|
||||
}
|
||||
|
||||
function invalidateMailer(sendConfiguration) {
|
||||
transports.delete(sendConfiguration.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function _sendMail(transport, mail, template) {
|
||||
if (!mail.headers) {
|
||||
mail.headers = {};
|
||||
}
|
||||
mail.headers['X-Sending-Zone'] = 'transactional';
|
||||
|
||||
const htmlRenderer = await tools.getTemplate(template.html);
|
||||
|
||||
if (htmlRenderer) {
|
||||
mail.html = htmlRenderer(template.data || {});
|
||||
}
|
||||
|
||||
const preparedHtml = await tools.prepareHtml(mail.html);
|
||||
|
||||
if (prepareHtml) {
|
||||
mail.html = prepareHtml;
|
||||
}
|
||||
|
||||
const textRenderer = await tools.getTemplate(template.text);
|
||||
|
||||
if (textRenderer) {
|
||||
mail.text = textRenderer(template.data || {});
|
||||
} else if (mail.html) {
|
||||
mail.text = htmlToText.fromString(mail.html, {
|
||||
wordwrap: 130
|
||||
});
|
||||
}
|
||||
|
||||
let tryCount = 0;
|
||||
const trySend = (callback) => {
|
||||
tryCount++;
|
||||
transport.sendMail(mail, (err, info) => {
|
||||
if (err) {
|
||||
log.error('Mail', err);
|
||||
if (err.responseCode && err.responseCode >= 400 && err.responseCode < 500 && tryCount <= 5) {
|
||||
// temporary error, try again
|
||||
log.verbose('Mail', 'Retrying after %s sec. ...', tryCount);
|
||||
return setTimeout(trySend, tryCount * 1000);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, info);
|
||||
});
|
||||
};
|
||||
|
||||
const trySendAsync = bluebird.promisify(trySend);
|
||||
return await trySendAsync();
|
||||
}
|
||||
|
||||
|
||||
async function _createTransport(sendConfiguration) {
|
||||
const mailerSettings = sendConfiguration.mailer_settings;
|
||||
const mailerType = sendConfiguration.mailer_type;
|
||||
const configItems = await settings.get(contextHelpers.getAdminContext(), ['pgpPrivateKey', 'pgpPassphrase']);
|
||||
|
||||
const existingTransport = transports.get(sendConfiguration.id);
|
||||
|
||||
let existingListeners = [];
|
||||
if (existingTransport) {
|
||||
existingListeners = existingTransport.listeners('idle');
|
||||
existingTransport.removeAllListeners('idle');
|
||||
existingTransport.removeAllListeners('stream');
|
||||
existingTransport.checkThrottling = null;
|
||||
}
|
||||
|
||||
const logFunc = (...args) => {
|
||||
const level = args.shift();
|
||||
args.shift();
|
||||
args.unshift('Mail');
|
||||
log[level](...args);
|
||||
};
|
||||
|
||||
|
||||
let transportOptions;
|
||||
|
||||
if (mailerType === sendConfigurations.MailerType.GENERIC_SMTP || mailerType === sendConfigurations.MailerType.ZONE_MTA) {
|
||||
transportOptions = {
|
||||
pool: true,
|
||||
host: mailerSettings.hostname,
|
||||
port: mailerSettings.port || false,
|
||||
secure: mailerSettings.encryption === 'TLS',
|
||||
ignoreTLS: mailerSettings.encryption === 'NONE',
|
||||
auth: mailerSettings.useAuth ? {
|
||||
user: mailerSettings.user,
|
||||
pass: mailerSettings.password
|
||||
} : false,
|
||||
debug: mailerSettings.logTransactions,
|
||||
logger: mailerSettings.logTransactions ? {
|
||||
debug: logFunc.bind(null, 'verbose'),
|
||||
info: logFunc.bind(null, 'info'),
|
||||
error: logFunc.bind(null, 'error')
|
||||
} : false,
|
||||
maxConnections: mailerSettings.maxConnections,
|
||||
maxMessages: mailerSettings.maxMessages,
|
||||
tls: {
|
||||
rejectUnauthorized: !mailerSettings.allowSelfSigned
|
||||
}
|
||||
};
|
||||
|
||||
} else if (mailerType === sendConfigurations.MailerType.AWS_SES) {
|
||||
const sendingRate = mailerSettings.throttling / 3600; // convert to messages/second
|
||||
|
||||
transportOptions = {
|
||||
SES: new aws.SES({
|
||||
apiVersion: '2010-12-01',
|
||||
accessKeyId: mailerSettings.key,
|
||||
secretAccessKey: mailerSettings.secret,
|
||||
region: mailerSettings.region
|
||||
}),
|
||||
debug: mailerSettings.logTransactions,
|
||||
logger: mailerSettings.logTransactions ? {
|
||||
debug: logFunc.bind(null, 'verbose'),
|
||||
info: logFunc.bind(null, 'info'),
|
||||
error: logFunc.bind(null, 'error')
|
||||
} : false,
|
||||
maxConnections: mailerSettings.maxConnections,
|
||||
sendingRate
|
||||
};
|
||||
|
||||
} else {
|
||||
throw new Error('Invalid mail transport');
|
||||
}
|
||||
|
||||
const transport = nodemailer.createTransport(transportOptions, config.nodemailer);
|
||||
|
||||
transport.use('stream', openpgpEncrypt({
|
||||
signingKey: configItems.pgpPrivateKey,
|
||||
passphrase: configItems.pgpPassphrase
|
||||
}));
|
||||
|
||||
if (existingListeners.length) {
|
||||
log.info('Mail', 'Reattaching %s idle listeners', existingListeners.length);
|
||||
existingListeners.forEach(listener => transport.on('idle', listener));
|
||||
}
|
||||
|
||||
let checkThrottling;
|
||||
|
||||
if (mailerType === sendConfigurations.MailerType.GENERIC_SMTP || mailerType === sendConfigurations.MailerType.ZONE_MTA) {
|
||||
let throttling = mailerSettings.throttling;
|
||||
if (throttling) {
|
||||
throttling = 1 / (throttling / (3600 * 1000));
|
||||
}
|
||||
|
||||
let lastCheck = Date.now();
|
||||
|
||||
checkThrottling = function (next) {
|
||||
if (!throttling) {
|
||||
return next();
|
||||
}
|
||||
let nextCheck = Date.now();
|
||||
let checkDiff = (nextCheck - lastCheck);
|
||||
if (checkDiff < throttling) {
|
||||
log.verbose('Mail', 'Throttling next message in %s sec.', (throttling - checkDiff) / 1000);
|
||||
setTimeout(() => {
|
||||
lastCheck = Date.now();
|
||||
next();
|
||||
}, throttling - checkDiff);
|
||||
} else {
|
||||
lastCheck = nextCheck;
|
||||
next();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
checkThrottling = next => next();
|
||||
}
|
||||
|
||||
transport.mailer = {
|
||||
checkThrottling,
|
||||
sendMail: async (mail, template) => await _sendMail(transport, mail, template)
|
||||
};
|
||||
|
||||
transports.set(sendConfiguration.id, transport);
|
||||
return transport;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOrCreateMailer,
|
||||
invalidateMailer
|
||||
};
|
|
@ -1,86 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
|
||||
module.exports.get = (start, limit, search, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
search = '%' + search + '%';
|
||||
connection.query('SELECT SQL_CALC_FOUND_ROWS `email` FROM blacklist WHERE `email` LIKE ? ORDER BY `email` LIMIT ? OFFSET ?', [search, limit, start], (err, rows) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT FOUND_ROWS() AS total', (err, total) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let emails = [];
|
||||
rows.forEach(email => {
|
||||
emails.push(email.email);
|
||||
});
|
||||
return callback(null, emails, total && total[0] && total[0].total);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.add = (email, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('INSERT IGNORE INTO `blacklist` (`email`) VALUES(?)', email, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.release();
|
||||
return callback(null, null);
|
||||
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (email, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM `blacklist` WHERE `email`=?', email, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.release();
|
||||
return callback(null, null);
|
||||
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.isblacklisted = (email, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT `email` FROM blacklist WHERE `email`=?', email, (err, rows) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.release();
|
||||
if (rows.length > 0) {
|
||||
return callback(null, true);
|
||||
} else {
|
||||
return callback(null, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
File diff suppressed because it is too large
Load diff
|
@ -1,91 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let shortid = require('shortid');
|
||||
let helpers = require('../helpers');
|
||||
let _ = require('../translate')._;
|
||||
|
||||
/*
|
||||
Adds new entry to the confirmations tables. Generates confirmation cid, which it returns.
|
||||
*/
|
||||
module.exports.addConfirmation = (listId, action, ip, data, callback) => {
|
||||
let cid = shortid.generate();
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO confirmations (cid, list, action, ip, data) VALUES (?,?,?,?,?)';
|
||||
connection.query(query, [cid, listId, action, ip, JSON.stringify(data || {})], (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!result || !result.affectedRows) {
|
||||
return callback(new Error(_('Could not store confirmation data')));
|
||||
}
|
||||
|
||||
return callback(null, cid);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Atomically retrieves confirmation from the database, removes it from the database and returns it.
|
||||
*/
|
||||
module.exports.takeConfirmation = (cid, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT cid, list, action, ip, data FROM confirmations WHERE cid=? LIMIT 1';
|
||||
connection.query(query, [cid], (err, rows) => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(null, false));
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM confirmations WHERE `cid`=? LIMIT 1', [cid], () => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(rows[0].data);
|
||||
} catch (E) {
|
||||
data = {};
|
||||
}
|
||||
|
||||
const result = {
|
||||
listId: rows[0].list,
|
||||
action: rows[0].action,
|
||||
ip: rows[0].ip,
|
||||
data
|
||||
};
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -1,617 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let tools = require('../tools');
|
||||
let slugify = require('slugify');
|
||||
let lists = require('./lists');
|
||||
let shortid = require('shortid');
|
||||
let Handlebars = require('handlebars');
|
||||
let _ = require('../translate')._;
|
||||
let util = require('util');
|
||||
|
||||
let allowedKeys = ['name', 'key', 'default_value', 'group', 'group_template', 'visible'];
|
||||
let allowedTypes;
|
||||
|
||||
module.exports.grouped = ['radio', 'checkbox', 'dropdown'];
|
||||
module.exports.types = {
|
||||
text: _('Text'),
|
||||
website: _('Website'),
|
||||
longtext: _('Multi-line text'),
|
||||
gpg: _('GPG Public Key'),
|
||||
number: _('Number'),
|
||||
radio: _('Radio Buttons'),
|
||||
checkbox: _('Checkboxes'),
|
||||
dropdown: _('Drop Down'),
|
||||
'date-us': _('Date (MM/DD/YYY)'),
|
||||
'date-eur': _('Date (DD/MM/YYYY)'),
|
||||
'birthday-us': _('Birthday (MM/DD)'),
|
||||
'birthday-eur': _('Birthday (DD/MM)'),
|
||||
json: _('JSON value for custom rendering'),
|
||||
option: _('Option')
|
||||
};
|
||||
|
||||
module.exports.allowedTypes = allowedTypes = Object.keys(module.exports.types);
|
||||
|
||||
module.exports.genericTypes = {
|
||||
text: 'string',
|
||||
website: 'string',
|
||||
longtext: 'textarea',
|
||||
gpg: 'textarea',
|
||||
json: 'textarea',
|
||||
number: 'number',
|
||||
'date-us': 'date',
|
||||
'date-eur': 'date',
|
||||
'birthday-us': 'birthday',
|
||||
'birthday-eur': 'birthday',
|
||||
option: 'boolean'
|
||||
};
|
||||
|
||||
module.exports.list = (listId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM custom_fields WHERE list=? ORDER BY id';
|
||||
connection.query(query, [listId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let fieldList = rows && rows.map(row => tools.convertKeys(row)) || [];
|
||||
let groups = new Map();
|
||||
|
||||
// remove grouped rows
|
||||
for (let i = fieldList.length - 1; i >= 0; i--) {
|
||||
let field = fieldList[i];
|
||||
if (module.exports.grouped.indexOf(field.type) >= 0) {
|
||||
if (!groups.has(field.id)) {
|
||||
groups.set(field.id, []);
|
||||
}
|
||||
field.options = groups.get(field.id);
|
||||
} else if (field.group && field.type === 'option') {
|
||||
if (!groups.has(field.group)) {
|
||||
groups.set(field.group, [field]);
|
||||
} else {
|
||||
groups.get(field.group).unshift(field);
|
||||
}
|
||||
fieldList.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return callback(null, fieldList);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM custom_fields WHERE id=? LIMIT 1';
|
||||
connection.query(query, [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let field = rows && rows[0] && tools.convertKeys(rows[0]) || false;
|
||||
field.isGroup = module.exports.grouped.indexOf(field.type) >= 0 || field.type === 'json';
|
||||
return callback(null, field);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.create = (listId, field, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
field = tools.convertKeys(field);
|
||||
|
||||
if (field.type === 'option' && !field.group) {
|
||||
return callback(new Error(_('Option field requires a group to be selected')));
|
||||
}
|
||||
|
||||
if (field.type !== 'option') {
|
||||
field.group = null;
|
||||
}
|
||||
|
||||
field.defaultValue = (field.defaultValue || '').toString().trim() || null;
|
||||
field.groupTemplate = (field.groupTemplate || '').toString().trim() || null;
|
||||
|
||||
addCustomField(listId, field.name, field.defaultValue, field.type, field.group, field.groupTemplate, field.visible, callback);
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
updates = updates || {};
|
||||
id = Number(id) || 0;
|
||||
|
||||
updates = tools.convertKeys(updates);
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Field ID')));
|
||||
}
|
||||
|
||||
if (!(updates.name || '').toString().trim()) {
|
||||
return callback(new Error(_('Field Name must be set')));
|
||||
}
|
||||
|
||||
if (updates.key) {
|
||||
updates.key = slugify(updates.key, '_').toUpperCase();
|
||||
}
|
||||
|
||||
updates.defaultValue = (updates.defaultValue || '').toString().trim() || null;
|
||||
updates.groupTemplate = (updates.groupTemplate || '').toString().trim() || null;
|
||||
|
||||
updates.visible = updates.visible ? 1 : 0;
|
||||
|
||||
let name = (updates.name || '').toString().trim();
|
||||
let keys = ['name'];
|
||||
let values = [name];
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
let value = typeof updates[key] === 'string' ? updates[key].trim() : updates[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
|
||||
connection.query('UPDATE custom_fields SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (fieldId, callback) => {
|
||||
fieldId = Number(fieldId) || 0;
|
||||
|
||||
if (fieldId < 1) {
|
||||
return callback(new Error(_('Missing Field ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM custom_fields WHERE id=? LIMIT 1';
|
||||
connection.query(query, [fieldId], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
connection.release();
|
||||
return callback(new Error(_('Custom field not found')));
|
||||
}
|
||||
|
||||
let field = tools.convertKeys(rows[0]);
|
||||
if (field.column) {
|
||||
connection.query('ALTER TABLE `subscription__' + field.list + '` DROP COLUMN `' + field.column + '`', err => {
|
||||
if (err && err.code !== 'ER_CANT_DROP_FIELD_OR_KEY') {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('DELETE FROM custom_fields WHERE id=? LIMIT 1', [fieldId], err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('DELETE FROM segment_rules WHERE column=? LIMIT 1', [field.column], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
// ignore
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// delete all subfields in this group
|
||||
let query = 'SELECT id FROM custom_fields WHERE `group`=?';
|
||||
connection.query(query, [fieldId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
rows = [];
|
||||
}
|
||||
|
||||
let pos = 0;
|
||||
let deleteNext = () => {
|
||||
if (pos >= rows.length) {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('DELETE FROM custom_fields WHERE id=? LIMIT 1', [fieldId], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
module.exports.delete(rows[pos++].id, deleteNext);
|
||||
};
|
||||
|
||||
deleteNext();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function addCustomField(listId, name, defaultValue, type, group, groupTemplate, visible, callback) {
|
||||
type = (type || '').toString().trim().toLowerCase();
|
||||
group = Number(group) || null;
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
let column = null;
|
||||
let key = slugify('merge ' + name, '_').toUpperCase();
|
||||
|
||||
if (allowedTypes.indexOf(type) < 0) {
|
||||
return callback(new Error(util.format(_('Unknown column type %s'), type)));
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return callback(new Error(_('Missing column name')));
|
||||
}
|
||||
|
||||
if (listId <= 0) {
|
||||
return callback(new Error(_('Missing list ID')));
|
||||
}
|
||||
|
||||
lists.get(listId, (err, list) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!list) {
|
||||
return callback(_('Provided List ID not found'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (module.exports.grouped.indexOf(type) < 0) {
|
||||
column = ('custom_' + slugify(name, '_') + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '');
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO custom_fields (`list`, `name`, `key`,`default_value`, `type`, `group`, `group_template`, `column`, `visible`) VALUES(?,?,?,?,?,?,?,?,?)';
|
||||
connection.query(query, [listId, name, key, defaultValue, type, group, groupTemplate, column, visible ? 1 : 0], (err, result) => {
|
||||
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let fieldId = result && result.insertId;
|
||||
let indexQuery;
|
||||
|
||||
switch (type) {
|
||||
case 'text':
|
||||
case 'website':
|
||||
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` VARCHAR(255) DEFAULT NULL';
|
||||
indexQuery = 'CREATE INDEX ' + column + '_index ON `subscription__' + listId + '` (`column`);';
|
||||
break;
|
||||
case 'gpg':
|
||||
case 'longtext':
|
||||
case 'json':
|
||||
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` TEXT DEFAULT NULL';
|
||||
break;
|
||||
case 'number':
|
||||
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` INT(11) DEFAULT NULL';
|
||||
indexQuery = 'CREATE INDEX ' + column + '_index ON `subscription__' + listId + '` (`column`);';
|
||||
break;
|
||||
case 'option':
|
||||
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` TINYINT(4) UNSIGNED NOT NULL DEFAULT \'0\'';
|
||||
indexQuery = 'CREATE INDEX ' + column + '_index ON `subscription__' + listId + '` (`column`);';
|
||||
break;
|
||||
case 'date-us':
|
||||
case 'date-eur':
|
||||
case 'birthday-us':
|
||||
case 'birthday-eur':
|
||||
query = 'ALTER TABLE `subscription__' + listId + '` ADD COLUMN `' + column + '` DATETIME NULL DEFAULT NULL';
|
||||
indexQuery = 'CREATE INDEX ' + column + '_index ON `subscription__' + listId + '` (`column`);';
|
||||
break;
|
||||
default:
|
||||
connection.release();
|
||||
return callback(null, fieldId, key);
|
||||
}
|
||||
|
||||
connection.query(query, err => {
|
||||
if (err) {
|
||||
connection.query('DELETE FROM custom_fields WHERE id=? LIMIT 1', [fieldId], () => connection.release());
|
||||
return callback(err);
|
||||
}
|
||||
if (!indexQuery) {
|
||||
connection.release();
|
||||
return callback(null, fieldId, key);
|
||||
} else {
|
||||
connection.query(query, err => {
|
||||
if (err) {
|
||||
// ignore index errors
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, fieldId, key);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
||||
let valueList = {};
|
||||
let row = [];
|
||||
|
||||
Object.keys(values || {}).forEach(key => {
|
||||
let value = values[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (key.indexOf('custom_') === 0) {
|
||||
valueList[key] = value;
|
||||
} else if (key.indexOf('group_g') === 0 && value.indexOf('custom_') === 0) {
|
||||
valueList[tools.toDbKey(value)] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
fieldList.filter(field => showAll || field.visible).forEach(field => {
|
||||
if (onlyExisting && field.column && !valueList.hasOwnProperty(field.column)) {
|
||||
// ignore missing values
|
||||
return;
|
||||
}
|
||||
/* eslint-disable indent */
|
||||
switch (field.type) {
|
||||
case 'text':
|
||||
case 'website':
|
||||
case 'gpg':
|
||||
case 'longtext':
|
||||
{
|
||||
let item = {
|
||||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
column: field.column,
|
||||
value: (valueList[field.column] || '').toString().trim(),
|
||||
visible: !!field.visible,
|
||||
mergeTag: field.key,
|
||||
mergeValue: (valueList[field.column] || '').toString().trim() || field.defaultValue,
|
||||
['type' + (field.type || '').toString().trim().replace(/(?:^|-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
||||
};
|
||||
row.push(item);
|
||||
break;
|
||||
}
|
||||
case 'json':
|
||||
{
|
||||
let value;
|
||||
let json = (valueList[field.column] || '').toString().trim();
|
||||
try {
|
||||
let parsed = JSON.parse(json);
|
||||
if (Array.isArray(parsed)) {
|
||||
parsed = {
|
||||
values: parsed
|
||||
};
|
||||
}
|
||||
value = json ? render(field.groupTemplate, parsed) : '';
|
||||
} catch (E) {
|
||||
value = E.message;
|
||||
}
|
||||
|
||||
let item = {
|
||||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
column: field.column,
|
||||
value: (valueList[field.column] || '').toString().trim(),
|
||||
visible: !!field.visible,
|
||||
mergeTag: field.key,
|
||||
mergeValue: value || field.defaultValue,
|
||||
['type' + (field.type || '').toString().trim().replace(/(?:^|-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
||||
};
|
||||
row.push(item);
|
||||
break;
|
||||
}
|
||||
case 'number':
|
||||
{
|
||||
let item = {
|
||||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
column: field.column,
|
||||
value: Number(valueList[field.column]) || 0,
|
||||
visible: !!field.visible,
|
||||
mergeTag: field.key,
|
||||
mergeValue: (Number(valueList[field.column]) || Number(field.defaultValue) || 0).toString(),
|
||||
['type' + (field.type || '').toString().trim().replace(/(?:^|-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
||||
};
|
||||
row.push(item);
|
||||
break;
|
||||
}
|
||||
case 'dropdown':
|
||||
case 'radio':
|
||||
case 'checkbox':
|
||||
{
|
||||
let hasSelectedOption = (field.options || []).some(subField => subField.column && valueList[subField.column]);
|
||||
let item = {
|
||||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
visible: !!field.visible,
|
||||
key: 'group-g' + field.id,
|
||||
mergeTag: field.key,
|
||||
mergeValue: field.defaultValue,
|
||||
['type' + (field.type || '').toString().trim().replace(/(?:^|-)([a-z])/g, (m, c) => c.toUpperCase())]: true,
|
||||
groupTemplate: field.groupTemplate,
|
||||
options: (field.options || []).map(subField => {
|
||||
if (onlyExisting && subField.column && !valueList.hasOwnProperty(subField.column)) {
|
||||
if (hasSelectedOption && field.type !== 'checkbox') {
|
||||
// Set all radio or dropdown options if a selection for the group is present
|
||||
} else if (field.type === 'checkbox' && values['originGroupG' + field.id] === 'webform') {
|
||||
// Set all checkbox options if origin is webform (subscribe, manage, or admin edit) #333
|
||||
// Atomic updates via API call or CSV import still possible
|
||||
} else {
|
||||
// ignore missing values
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: subField.type,
|
||||
name: subField.name,
|
||||
column: subField.column,
|
||||
value: valueList[subField.column] ? 1 : 0,
|
||||
visible: !!subField.visible,
|
||||
mergeTag: subField.key,
|
||||
mergeValue: valueList[subField.column] ? subField.name : subField.defaultValue
|
||||
};
|
||||
}).filter(subField => subField)
|
||||
};
|
||||
let subItems = item.options.filter(subField => (showAll || subField.visible) && subField.value).map(subField => subField.name);
|
||||
item.value = field.groupTemplate ? render(field.groupTemplate, {
|
||||
values: subItems
|
||||
}) : subItems.join(', ');
|
||||
item.mergeValue = item.value || field.defaultValue;
|
||||
row.push(item);
|
||||
break;
|
||||
}
|
||||
case 'date-eur':
|
||||
case 'birthday-eur':
|
||||
case 'date-us':
|
||||
case 'birthday-us':
|
||||
{
|
||||
let isUs = /-us$/.test(field.type);
|
||||
let isYear = field.type.indexOf('date-') === 0;
|
||||
let value = valueList[field.column];
|
||||
let day, month, year;
|
||||
let formatted;
|
||||
|
||||
if (value && typeof value.getUTCFullYear === 'function') {
|
||||
day = value.getUTCDate();
|
||||
month = value.getUTCMonth() + 1;
|
||||
year = value.getUTCFullYear();
|
||||
} else {
|
||||
value = (value || '').toString().trim();
|
||||
|
||||
// try international format first YYYY-MM-DD
|
||||
let parts = value.match(/(\d{4})\D+(\d{2})(?:\D+(\d{2})\b)?/);
|
||||
if (parts) {
|
||||
year = Number(parts[1]) || 2000;
|
||||
month = Number(parts[2]) || 0;
|
||||
day = Number(parts[3]) || 0;
|
||||
value = new Date(Date.UTC(year, month - 1, day));
|
||||
} else {
|
||||
parts = value.match(/(\d+)\D+(\d+)(?:\D+(\d+)\b)?/);
|
||||
if (!parts) {
|
||||
value = null;
|
||||
} else {
|
||||
day = Number(parts[isUs ? 2 : 1]) || 0;
|
||||
month = Number(parts[isUs ? 1 : 2]) || 0;
|
||||
year = Number(parts[3]) || 2000;
|
||||
|
||||
if (!day || !month) {
|
||||
value = null;
|
||||
} else {
|
||||
value = new Date(Date.UTC(year, month - 1, day));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (day && month) {
|
||||
if (isUs) {
|
||||
formatted = (month < 10 ? '0' : '') + month + '/' + (day < 10 ? '0' : '') + day;
|
||||
} else {
|
||||
formatted = (day < 10 ? '0' : '') + day + '/' + (month < 10 ? '0' : '') + month;
|
||||
}
|
||||
|
||||
if (isYear) {
|
||||
formatted += '/' + year;
|
||||
}
|
||||
} else {
|
||||
formatted = null;
|
||||
}
|
||||
|
||||
let item = {
|
||||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
column: field.column,
|
||||
value: useDate ? value : formatted,
|
||||
visible: !!field.visible,
|
||||
mergeTag: field.key,
|
||||
mergeValue: (useDate ? value : formatted) || field.defaultValue,
|
||||
['type' + (field.type || '').toString().trim().replace(/(?:^|-)([a-z])/g, (m, c) => c.toUpperCase())]: true
|
||||
};
|
||||
row.push(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* eslint-enable indent */
|
||||
});
|
||||
|
||||
return row;
|
||||
};
|
||||
|
||||
module.exports.getValues = (row, showAll) => {
|
||||
let result = [];
|
||||
row.filter(field => showAll || field.visible).forEach(field => {
|
||||
if (field.column) {
|
||||
result.push({
|
||||
key: field.column,
|
||||
value: field.value
|
||||
});
|
||||
} else if (field.options) {
|
||||
field.options.filter(field => showAll || field.visible).forEach(subField => {
|
||||
result.push({
|
||||
key: subField.column,
|
||||
value: subField.value
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
function render(template, options) {
|
||||
let renderer = Handlebars.compile(template);
|
||||
return renderer(options);
|
||||
}
|
|
@ -1,418 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let fs = require('fs');
|
||||
let path = require('path');
|
||||
let tools = require('../tools');
|
||||
let mjml = require('mjml');
|
||||
let _ = require('../translate')._;
|
||||
|
||||
let allowedKeys = [
|
||||
'name',
|
||||
'description',
|
||||
'fields_shown_on_subscribe',
|
||||
'fields_shown_on_manage',
|
||||
'layout',
|
||||
'form_input_style',
|
||||
'web_subscribe',
|
||||
'web_confirm_subscription_notice',
|
||||
'mail_confirm_subscription_html',
|
||||
'mail_confirm_subscription_text',
|
||||
'mail_already_subscribed_html',
|
||||
'mail_already_subscribed_text',
|
||||
'web_subscribed_notice',
|
||||
'mail_subscription_confirmed_html',
|
||||
'mail_subscription_confirmed_text',
|
||||
'web_manage',
|
||||
'web_manage_address',
|
||||
'web_updated_notice',
|
||||
'web_unsubscribe',
|
||||
'web_confirm_unsubscription_notice',
|
||||
'mail_confirm_unsubscription_html',
|
||||
'mail_confirm_unsubscription_text',
|
||||
'mail_confirm_address_change_html',
|
||||
'mail_confirm_address_change_text',
|
||||
'web_unsubscribed_notice',
|
||||
'mail_unsubscription_confirmed_html',
|
||||
'mail_unsubscription_confirmed_text',
|
||||
'web_manual_unsubscribe_notice'
|
||||
];
|
||||
|
||||
|
||||
module.exports.list = (listId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM custom_forms WHERE list=? ORDER BY id', [listId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let formList = rows && rows.map(row => tools.convertKeys(row)) || [];
|
||||
return callback(null, formList);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Form ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM custom_forms WHERE id=? LIMIT 1', [id], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let form = rows && rows[0] && tools.convertKeys(rows[0]) || false;
|
||||
|
||||
if (!form) {
|
||||
connection.release();
|
||||
return callback(new Error('Selected form not found'));
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM custom_forms_data WHERE form=?', [id], (err, data_rows = []) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
data_rows.forEach(data_row => {
|
||||
let modelKey = tools.fromDbKey(data_row.data_key);
|
||||
form[modelKey] = data_row.data_value;
|
||||
});
|
||||
|
||||
return callback(null, form);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports.create = (listId, form, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing Form ID')));
|
||||
}
|
||||
|
||||
form = tools.convertKeys(form);
|
||||
form = setDefaultValues(form);
|
||||
form.name = (form.name || '').toString().trim();
|
||||
|
||||
if (!form.name) {
|
||||
return callback(new Error(_('Form Name must be set')));
|
||||
}
|
||||
|
||||
let keys = ['list'];
|
||||
let values = [listId];
|
||||
|
||||
Object.keys(form).forEach(key => {
|
||||
let value = form[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let filtered = filterKeysAndValues(keys, values, 'exclude', ['mail_', 'web_']);
|
||||
let query = 'INSERT INTO custom_forms (' + filtered.keys.join(', ') + ') VALUES (' + filtered.values.map(() => '?').join(',') + ')';
|
||||
|
||||
connection.query(query, filtered.values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let formId = result && result.insertId;
|
||||
|
||||
if (!formId) {
|
||||
return callback(new Error('Invalid custom_forms insertId'));
|
||||
}
|
||||
|
||||
let jobs = 1;
|
||||
let error = null;
|
||||
|
||||
let done = err => {
|
||||
jobs--;
|
||||
error = err ? err : error; // One's enough
|
||||
jobs === 0 && callback(error, formId);
|
||||
};
|
||||
|
||||
filtered = filterKeysAndValues(keys, values, 'include', ['mail_', 'web_']);
|
||||
|
||||
filtered.keys.forEach((key, index) => {
|
||||
jobs++;
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
connection.query('INSERT INTO custom_forms_data (form, data_key, data_value) VALUES (?, ?, ?)', [formId, key, filtered.values[index]], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
return done(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
done(null);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
updates = updates || {};
|
||||
id = Number(id) || 0;
|
||||
|
||||
updates = tools.convertKeys(updates);
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Form ID')));
|
||||
}
|
||||
|
||||
if (!(updates.name || '').toString().trim()) {
|
||||
return callback(new Error(_('Form Name must be set')));
|
||||
}
|
||||
|
||||
let keys = [];
|
||||
let values = [];
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
let value = typeof updates[key] === 'string' ? updates[key].trim() : updates[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let filtered = filterKeysAndValues(keys, values, 'exclude', ['mail_', 'web_']);
|
||||
let query = 'UPDATE custom_forms SET ' + filtered.keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1';
|
||||
|
||||
connection.query(query, filtered.values.concat(id), (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let affectedRows = result && result.affectedRows;
|
||||
|
||||
let jobs = 1;
|
||||
let error = null;
|
||||
|
||||
let done = err => {
|
||||
jobs--;
|
||||
error = err ? err : error; // One's enough
|
||||
|
||||
if (jobs === 0) {
|
||||
if (error) {
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
// Save then validate, as otherwise their work get's lost ...
|
||||
err = testForMjmlErrors(keys, values);
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, affectedRows);
|
||||
}
|
||||
};
|
||||
|
||||
filtered = filterKeysAndValues(keys, values, 'include', ['mail_', 'web_']);
|
||||
|
||||
filtered.keys.forEach((key, index) => {
|
||||
jobs++;
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
connection.query('UPDATE custom_forms_data SET data_value=? WHERE data_key=? AND form=?', [filtered.values[index], key, id], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
return done(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
done(null);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (formId, callback) => {
|
||||
formId = Number(formId) || 0;
|
||||
|
||||
if (formId < 1) {
|
||||
return callback(new Error(_('Missing Form ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM custom_forms WHERE id=? LIMIT 1', [formId], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
connection.release();
|
||||
return callback(new Error(_('Custom form not found')));
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM custom_forms WHERE id=? LIMIT 1', [formId], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function setDefaultValues(form) {
|
||||
let getContents = fileName => {
|
||||
try {
|
||||
let basePath = path.join(__dirname, '..', '..');
|
||||
let template = fs.readFileSync(path.join(basePath, fileName), 'utf8');
|
||||
return template.replace(/\{\{#translate\}\}(.*?)\{\{\/translate\}\}/g, (m, s) => _(s));
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
allowedKeys.forEach(key => {
|
||||
let modelKey = tools.fromDbKey(key);
|
||||
let base = 'views/subscription/' + key.replace(/_/g, '-');
|
||||
|
||||
if (key.startsWith('mail') || key.startsWith('web')) {
|
||||
form[modelKey] = getContents(base + '.mjml.hbs') || getContents(base + '.hbs') || '';
|
||||
}
|
||||
});
|
||||
|
||||
form.layout = getContents('views/subscription/layout.mjml.hbs') || '';
|
||||
form.formInputStyle = getContents('public/subscription/form-input-style.css') || '@import url(/subscription/form-input-style.css);';
|
||||
|
||||
return form;
|
||||
}
|
||||
|
||||
function filterKeysAndValues(keysIn, valuesIn, method = 'include', prefixes = []) {
|
||||
let values = [];
|
||||
|
||||
let prefixMatch = key => (
|
||||
prefixes.some(prefix => key.startsWith(prefix))
|
||||
);
|
||||
|
||||
let keys = keysIn.filter((key, index) => {
|
||||
if ((method === 'include' && prefixMatch(key)) || (method === 'exclude' && !prefixMatch(key))) {
|
||||
values.push(valuesIn[index]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
keys,
|
||||
values
|
||||
};
|
||||
}
|
||||
|
||||
function testForMjmlErrors(keys, values) {
|
||||
|
||||
let errors = [];
|
||||
let testLayout = '<mjml><mj-body><mj-container>{{{body}}}</mj-container></mj-body></mjml>';
|
||||
|
||||
let hasMjmlError = (template, layout = testLayout) => {
|
||||
let source = layout.replace(/\{\{\{body\}\}\}/g, template);
|
||||
let compiled;
|
||||
|
||||
try {
|
||||
compiled = mjml.mjml2html(source);
|
||||
} catch (err) {
|
||||
return err;
|
||||
}
|
||||
|
||||
if (compiled.errors.length) {
|
||||
return compiled.errors[0].message || compiled.errors[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
keys.forEach((key, index) => {
|
||||
if (key.startsWith('mail_') || key.startsWith('web_')) {
|
||||
|
||||
let template = values[index];
|
||||
let err = hasMjmlError(template);
|
||||
|
||||
err && errors.push(key + ': ' + (err.message || err));
|
||||
key === 'mail_confirm_html' && !template.includes('{{confirmUrl}}') && errors.push(key + ': Missing {{confirmUrl}}');
|
||||
|
||||
} else if (key === 'layout') {
|
||||
|
||||
let layout = values[index];
|
||||
let err = hasMjmlError('', layout);
|
||||
|
||||
err && errors.push('layout: ' + (err.message || err));
|
||||
!layout.includes('{{{body}}}') && errors.push('layout: {{{body}}} not found');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (errors.length) {
|
||||
errors.forEach((err, index) => {
|
||||
errors[index] = (index + 1) + ') ' + err;
|
||||
});
|
||||
return 'Please fix these MJML errors:\n\n' + errors.join('\n');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
|
@ -1,364 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let shortid = require('shortid');
|
||||
let util = require('util');
|
||||
let _ = require('../translate')._;
|
||||
|
||||
let geoip = require('geoip-ultralight');
|
||||
let campaigns = require('./campaigns');
|
||||
let subscriptions = require('./subscriptions');
|
||||
let lists = require('./lists');
|
||||
|
||||
let log = require('npmlog');
|
||||
let urllib = require('url');
|
||||
let he = require('he');
|
||||
let ua_parser = require('device');
|
||||
|
||||
module.exports.resolve = (linkCid, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'SELECT id, url FROM links WHERE `cid`=? LIMIT 1';
|
||||
connection.query(query, [linkCid], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (rows && rows.length) {
|
||||
return callback(null, rows[0].id, rows[0].url);
|
||||
}
|
||||
|
||||
return callback(null, false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.countClick = (remoteIp, useragent, campaignCid, listCid, subscriptionCid, linkId, callback) => {
|
||||
getSubscriptionData(campaignCid, listCid, subscriptionCid, (err, data) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!data || data.campaign.clickTrackingDisabled) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let country = geoip.lookupCountry(remoteIp) || null;
|
||||
let device = ua_parser(useragent, { unknownUserAgentDeviceType: 'desktop', emptyUserAgentDeviceType: 'desktop' });
|
||||
let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `device_type`, `country`) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE `count`=`count`+1';
|
||||
connection.query(query, [data.list.id, data.subscription.id, linkId, remoteIp, device.type, country], (err, result) => {
|
||||
if (err && err.code !== 'ER_DUP_ENTRY') {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
if (err && err.code === 'ER_DUP_ENTRY' || result.affectedRows > 1) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
});
|
||||
}
|
||||
|
||||
let query = 'UPDATE `subscription__' + data.list.id + '` SET `latest_click`=NOW(), `latest_open`=NOW() WHERE id=?';
|
||||
connection.query(query, [data.subscription.id], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
let query = 'UPDATE links SET clicks = clicks + 1 WHERE id=?';
|
||||
connection.query(query, [linkId], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `device_type`, `country`) VALUES (?,?,?,?,?,?)';
|
||||
connection.query(query, [data.list.id, data.subscription.id, 0, remoteIp, device.type, country], err => {
|
||||
if (err && err.code !== 'ER_DUP_ENTRY') {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
if (err && err.code === 'ER_DUP_ENTRY') {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
});
|
||||
}
|
||||
|
||||
let query = 'UPDATE campaigns SET clicks = clicks + 1 WHERE id=?';
|
||||
connection.query(query, [data.campaign.id], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
});
|
||||
});
|
||||
|
||||
// also count clicks as open events in case beacon image was blocked
|
||||
module.exports.countOpen(remoteIp, useragent, campaignCid, listCid, subscriptionCid, () => false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.countOpen = (remoteIp, useragent, campaignCid, listCid, subscriptionCid, callback) => {
|
||||
getSubscriptionData(campaignCid, listCid, subscriptionCid, (err, data) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!data || data.campaign.openTrackingDisabled) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let country = geoip.lookupCountry(remoteIp) || null;
|
||||
let device = ua_parser(useragent, { unknownUserAgentDeviceType: 'desktop', emptyUserAgentDeviceType: 'desktop' });
|
||||
let query = 'INSERT INTO `campaign_tracker__' + data.campaign.id + '` (`list`, `subscriber`, `link`, `ip`, `device_type`, `country`) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE `count`=`count`+1';
|
||||
connection.query(query, [data.list.id, data.subscription.id, -1, remoteIp, device.type, country], (err, result) => {
|
||||
if (err && err.code !== 'ER_DUP_ENTRY') {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
if (err && err.code === 'ER_DUP_ENTRY' || result.affectedRows > 1) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
});
|
||||
}
|
||||
|
||||
let query = 'UPDATE `subscription__' + data.list.id + '` SET `latest_open`=NOW() WHERE id=?';
|
||||
connection.query(query, [data.subscription.id], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
let query = 'UPDATE campaigns SET opened = opened + 1 WHERE id=?';
|
||||
connection.query(query, [data.campaign.id], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.add = (url, campaignId, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let cid = shortid.generate();
|
||||
let query = 'INSERT INTO links (`cid`, `campaign`, `url`) VALUES (?,?,?)';
|
||||
connection.query(query, [cid, campaignId, url], (err, result) => {
|
||||
if (err && err.code !== 'ER_DUP_ENTRY') {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!err && result && result.insertId) {
|
||||
connection.release();
|
||||
return callback(null, result.insertId, cid);
|
||||
}
|
||||
|
||||
let query = 'SELECT id, cid FROM links WHERE `campaign`=? AND `url`=? LIMIT 1';
|
||||
connection.query(query, [campaignId, url], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (rows && rows.length) {
|
||||
return callback(null, rows[0].id, rows[0].cid);
|
||||
}
|
||||
|
||||
return callback(null, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.updateLinks = (campaign, list, subscription, serviceUrl, message, callback) => {
|
||||
if ((campaign.openTrackingDisabled && campaign.clickTrackingDisabled) || !message || !message.trim()) {
|
||||
// tracking is disabled, do not modify the message
|
||||
return setImmediate(() => callback(null, message));
|
||||
}
|
||||
|
||||
// insert tracking image
|
||||
if (!campaign.openTrackingDisabled) {
|
||||
let inserted = false;
|
||||
let imgUrl = urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid)));
|
||||
let img = '<img src="' + imgUrl + '" width="1" height="1" alt="mt">';
|
||||
message = message.replace(/<\/body\b/i, match => {
|
||||
inserted = true;
|
||||
return img + match;
|
||||
});
|
||||
if (!inserted) {
|
||||
message = message + img;
|
||||
}
|
||||
if (campaign.clickTrackingDisabled) {
|
||||
return callback(null, message);
|
||||
}
|
||||
}
|
||||
|
||||
if (!campaign.clickTrackingDisabled) {
|
||||
let re = /(<a[^>]* href\s*=[\s"']*)(http[^"'>\s]+)/gi;
|
||||
let urls = new Set();
|
||||
(message || '').replace(re, (match, prefix, url) => {
|
||||
urls.add(url);
|
||||
});
|
||||
|
||||
let map = new Map();
|
||||
let vals = urls.values();
|
||||
|
||||
let replaceUrls = () => {
|
||||
callback(null,
|
||||
message.replace(re, (match, prefix, url) =>
|
||||
prefix + (map.has(url) ? urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid), encodeURIComponent(map.get(url)))) : url)));
|
||||
};
|
||||
|
||||
let storeNext = () => {
|
||||
let urlItem = vals.next();
|
||||
if (urlItem.done) {
|
||||
return replaceUrls();
|
||||
}
|
||||
|
||||
module.exports.add(he.decode(urlItem.value, {
|
||||
isAttributeValue: true
|
||||
}), campaign.id, (err, linkId, cid) => {
|
||||
if (err) {
|
||||
log.error('Link', err);
|
||||
return storeNext();
|
||||
}
|
||||
map.set(urlItem.value, cid);
|
||||
return storeNext();
|
||||
});
|
||||
};
|
||||
|
||||
storeNext();
|
||||
}
|
||||
};
|
||||
|
||||
function getSubscriptionData(campaignCid, listCid, subscriptionCid, callback) {
|
||||
campaigns.getByCid(campaignCid, (err, campaign) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!campaign) {
|
||||
return callback(new Error(_('Campaign not found')));
|
||||
}
|
||||
|
||||
lists.getByCid(listCid, (err, list) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!list) {
|
||||
return callback(new Error(_('List not found')));
|
||||
}
|
||||
|
||||
subscriptions.get(list.id, subscriptionCid, (err, subscription) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!subscription) {
|
||||
return callback(new Error(_('Subscription not found')));
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
campaign,
|
||||
list,
|
||||
subscription
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,332 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let tools = require('../tools');
|
||||
let shortid = require('shortid');
|
||||
let segments = require('./segments');
|
||||
let subscriptions = require('./subscriptions');
|
||||
let _ = require('../translate')._;
|
||||
let tableHelpers = require('../table-helpers');
|
||||
const UnsubscriptionMode = require('../../shared/lists').UnsubscriptionMode;
|
||||
|
||||
module.exports.UnsubscriptionMode = UnsubscriptionMode;
|
||||
|
||||
let allowedKeys = ['description', 'default_form', 'public_subscribe', 'unsubscription_mode'];
|
||||
|
||||
module.exports.list = (start, limit, callback) => {
|
||||
tableHelpers.list('lists', ['*'], 'name', null, start, limit, callback);
|
||||
};
|
||||
|
||||
module.exports.filter = (request, parent, callback) => {
|
||||
tableHelpers.filter('lists', ['*'], request, ['#', 'name', 'cid', 'subscribers', 'description'], ['name'], 'name ASC', null, callback);
|
||||
};
|
||||
|
||||
module.exports.filterQuicklist = (request, callback) => {
|
||||
tableHelpers.filter('lists', ['id', 'name', 'subscribers'], request, ['#', 'name', 'subscribers'], ['name'], 'name ASC', null, callback);
|
||||
};
|
||||
|
||||
module.exports.quicklist = callback => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT id, name, subscribers FROM lists ORDER BY name LIMIT 1000', (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let lists = (rows || []).map(tools.convertKeys);
|
||||
|
||||
connection.query('SELECT id, list, name FROM segments ORDER BY list, name LIMIT 1000', (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let segments = (rows || []).map(tools.convertKeys);
|
||||
|
||||
lists.forEach(list => {
|
||||
list.segments = segments.filter(segment => segment.list === list.id);
|
||||
});
|
||||
|
||||
return callback(null, lists);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getListsWithEmail = (email, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT id, name FROM lists', (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let lists = (rows || []).map(tools.convertKeys);
|
||||
const results = [];
|
||||
lists.forEach((list, index, arr) => {
|
||||
subscriptions.getByEmail(list.id, email, (err, sub) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (sub) {
|
||||
results.push(list.id);
|
||||
}
|
||||
if (index === arr.length - 1) {
|
||||
return callback(null, lists.filter(list => results.includes(list.id)));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getByCid = (cid, callback) => {
|
||||
resolveCid(cid, (err, id) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!id) {
|
||||
return callback(null, false);
|
||||
}
|
||||
module.exports.get(id, callback);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM lists WHERE id=?', [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let list = tools.convertKeys(rows[0]);
|
||||
segments.list(list.id, (err, segmentList) => {
|
||||
if (err || !segmentList) {
|
||||
segmentList = [];
|
||||
}
|
||||
list.segments = segmentList;
|
||||
return callback(null, list);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
updates = updates || {};
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
const data = tools.convertKeys(updates);
|
||||
|
||||
const keys = [];
|
||||
const values = [];
|
||||
|
||||
// The update can be only partial when executed from forms/:list
|
||||
if (!data.customFormChangeOnly) {
|
||||
data.publicSubscribe = data.publicSubscribe ? 1 : 0;
|
||||
data.unsubscriptionMode = Number(data.unsubscriptionMode);
|
||||
|
||||
let name = (data.name || '').toString().trim();
|
||||
|
||||
if (!name) {
|
||||
return callback(new Error(_('List Name must be set')));
|
||||
}
|
||||
|
||||
keys.push('name');
|
||||
values.push(name);
|
||||
}
|
||||
|
||||
Object.keys(data).forEach(key => {
|
||||
let value = data[key].toString().trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
|
||||
connection.query('UPDATE lists SET ' + keys.map(key => key + '=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.create = (list, callback) => {
|
||||
|
||||
let data = tools.convertKeys(list);
|
||||
data.publicSubscribe = data.publicSubscribe ? 1 : 0;
|
||||
|
||||
let name = (data.name || '').toString().trim();
|
||||
|
||||
if (!data) {
|
||||
return callback(new Error(_('List Name must be set')));
|
||||
}
|
||||
|
||||
let keys = ['name'];
|
||||
let values = [name];
|
||||
|
||||
Object.keys(data).forEach(key => {
|
||||
let value = data[key].toString().trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
let cid = shortid.generate();
|
||||
keys.push('cid');
|
||||
values.push(cid);
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO lists (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(',') + ')';
|
||||
connection.query(query, values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let listId = result && result.insertId || false;
|
||||
if (!listId) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
createSubscriptionTable(listId, err => {
|
||||
if (err) {
|
||||
// FIXME: rollback
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, listId);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM lists WHERE id=? LIMIT 1', id, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let affected = result && result.affectedRows || 0;
|
||||
|
||||
removeSubscriptionTable(id, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, affected);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function resolveCid(cid, callback) {
|
||||
cid = (cid || '').toString().trim();
|
||||
if (!cid) {
|
||||
return callback(new Error(_('Missing List CID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT id FROM lists WHERE cid=?', [cid], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, rows && rows[0] && rows[0].id || false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createSubscriptionTable(id, callback) {
|
||||
let query = 'CREATE TABLE `subscription__' + id + '` LIKE subscription';
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query(query, err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeSubscriptionTable(id, callback) {
|
||||
let query = 'DROP TABLE IF EXISTS `subscription__' + id + '`';
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query(query, err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,688 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let tools = require('../tools');
|
||||
let db = require('../db');
|
||||
let fields = require('./fields');
|
||||
let util = require('util');
|
||||
let _ = require('../translate')._;
|
||||
|
||||
module.exports.defaultColumns = [{
|
||||
column: 'email',
|
||||
name: _('Email address'),
|
||||
type: 'string'
|
||||
}, {
|
||||
column: 'opt_in_country',
|
||||
name: _('Signup country'),
|
||||
type: 'string'
|
||||
}, {
|
||||
column: 'created',
|
||||
name: _('Sign up date'),
|
||||
type: 'date'
|
||||
}, {
|
||||
column: 'latest_open',
|
||||
name: _('Latest open'),
|
||||
type: 'date'
|
||||
}, {
|
||||
column: 'latest_click',
|
||||
name: _('Latest click'),
|
||||
type: 'date'
|
||||
}, {
|
||||
column: 'first_name',
|
||||
name: _('First name'),
|
||||
type: 'string'
|
||||
}, {
|
||||
column: 'last_name',
|
||||
name: _('Last name'),
|
||||
type: 'string'
|
||||
}];
|
||||
|
||||
module.exports.list = (listId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM segments WHERE list=? ORDER BY name';
|
||||
connection.query(query, [listId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let segments = (rows || []).map(tools.convertKeys);
|
||||
return callback(null, segments);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Segment ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM segments WHERE id=? LIMIT 1';
|
||||
connection.query(query, [id], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
if (!rows || !rows.length) {
|
||||
connection.release();
|
||||
return callback(new Error(_('Segment not found')));
|
||||
}
|
||||
|
||||
let segment = tools.convertKeys(rows[0]);
|
||||
|
||||
let query = 'SELECT * FROM segment_rules WHERE segment=? ORDER BY id ASC';
|
||||
connection.query(query, [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
fields.list(segment.list, (err, fieldList) => {
|
||||
if (err || !fieldList) {
|
||||
fieldList = [];
|
||||
}
|
||||
|
||||
segment.columns = [].concat(module.exports.defaultColumns);
|
||||
fieldList.forEach(field => {
|
||||
if (fields.genericTypes[field.type] === 'textarea') {
|
||||
return;
|
||||
}
|
||||
if (field.column) {
|
||||
segment.columns.push({
|
||||
column: field.column,
|
||||
name: field.name,
|
||||
type: fields.genericTypes[field.type] || 'string'
|
||||
});
|
||||
}
|
||||
if (field.options) {
|
||||
field.options.forEach(subField => {
|
||||
if (subField.column) {
|
||||
segment.columns.push({
|
||||
column: subField.column,
|
||||
name: field.name + ': ' + subField.name,
|
||||
type: fields.genericTypes[subField.type] || 'string'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
segment.rules = (rows || []).map(rule => {
|
||||
rule = tools.convertKeys(rule);
|
||||
if (rule.value) {
|
||||
try {
|
||||
rule.value = JSON.parse(rule.value);
|
||||
} catch (E) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (!rule.value) {
|
||||
rule.value = {};
|
||||
}
|
||||
rule.columnType = segment.columns.filter(column => rule.column === column.column).pop() || {};
|
||||
rule.name = rule.columnType.name || '';
|
||||
switch (rule.columnType.type) {
|
||||
case 'number':
|
||||
case 'date':
|
||||
case 'birthday':
|
||||
if (rule.value.relativeRange) {
|
||||
let startString = rule.value.startDirection ? util.format(_('%s days after today'), rule.value.start) : util.format(_('%s days before today'), rule.value.start);
|
||||
let endString = rule.value.endDirection ? util.format(_('%s days after today'), rule.value.end) : util.format(_('%s days before today'), rule.value.end);
|
||||
rule.formatted = (rule.value.start ? startString : _('today')) + ' … ' + (rule.value.end ? endString : _('today'));
|
||||
} else if (rule.value.range) {
|
||||
rule.formatted = (rule.value.start || '') + ' … ' + (rule.value.end || '');
|
||||
} else {
|
||||
rule.formatted = rule.value.value || '';
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
rule.formatted = rule.value.value ? _('Selected') : _('Not selected');
|
||||
break;
|
||||
default:
|
||||
rule.formatted = rule.value.value || '';
|
||||
}
|
||||
|
||||
return rule;
|
||||
});
|
||||
|
||||
return callback(null, segment);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.create = (listId, segment, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
segment = tools.convertKeys(segment);
|
||||
|
||||
segment.name = (segment.name || '').toString().trim();
|
||||
segment.type = Number(segment.type) || 0;
|
||||
|
||||
if (!segment.name) {
|
||||
return callback(new Error(_('Field Name must be set')));
|
||||
}
|
||||
|
||||
if (segment.type <= 0) {
|
||||
return callback(new Error(_('Invalid segment rule type')));
|
||||
}
|
||||
|
||||
let keys = ['list', 'name', 'type'];
|
||||
let values = [listId, segment.name, segment.type];
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO segments (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(',') + ')';
|
||||
connection.query(query, values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.insertId || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
updates = updates || {};
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Segment ID')));
|
||||
}
|
||||
|
||||
let segment = tools.convertKeys(updates);
|
||||
|
||||
segment.name = (segment.name || '').toString().trim();
|
||||
segment.type = Number(segment.type) || 0;
|
||||
|
||||
if (!segment.name) {
|
||||
return callback(new Error(_('Field Name must be set')));
|
||||
}
|
||||
|
||||
if (segment.type <= 0) {
|
||||
return callback(new Error(_('Invalid segment rule type')));
|
||||
}
|
||||
|
||||
let keys = ['name', 'type'];
|
||||
let values = [segment.name, segment.type];
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
|
||||
connection.query('UPDATE segments SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Segment ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM segments WHERE id=? LIMIT 1', [id], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.createRule = (segmentId, rule, callback) => {
|
||||
segmentId = Number(segmentId) || 0;
|
||||
|
||||
if (segmentId < 1) {
|
||||
return callback(new Error(_('Missing Segment ID')));
|
||||
}
|
||||
|
||||
rule = tools.convertKeys(rule);
|
||||
|
||||
module.exports.get(segmentId, (err, segment) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
return callback(new Error(_('Selected segment not found')));
|
||||
}
|
||||
|
||||
let column = segment.columns.filter(column => column.column === rule.column).pop();
|
||||
if (!column) {
|
||||
return callback(new Error(_('Invalid rule type')));
|
||||
}
|
||||
|
||||
let value;
|
||||
|
||||
switch (column.type) {
|
||||
case 'date':
|
||||
case 'birthday':
|
||||
case 'number':
|
||||
if (column.type === 'date' && rule.range === 'relative') {
|
||||
value = {
|
||||
relativeRange: true,
|
||||
start: Number(rule.startRelative) || 0,
|
||||
startDirection: Number(rule.startDirection) ? 1 : 0,
|
||||
end: Number(rule.endRelative) || 0,
|
||||
endDirection: Number(rule.endDirection) ? 1 : 0
|
||||
};
|
||||
} else if (rule.range === 'yes') {
|
||||
value = {
|
||||
range: true,
|
||||
start: rule.start,
|
||||
end: rule.end
|
||||
};
|
||||
} else {
|
||||
value = {
|
||||
value: rule.value
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
value = {
|
||||
value: rule.value ? 1 : 0
|
||||
};
|
||||
break;
|
||||
default:
|
||||
value = {
|
||||
value: rule.value
|
||||
};
|
||||
}
|
||||
|
||||
let keys = ['segment', 'column', 'value'];
|
||||
let values = [segment.id, rule.column, JSON.stringify(value)];
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO segment_rules (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(',') + ')';
|
||||
connection.query(query, values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.insertId || false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getRule = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Rule ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM segment_rules WHERE id=? LIMIT 1';
|
||||
connection.query(query, [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(new Error(_('Specified rule not found')));
|
||||
}
|
||||
|
||||
let rule = tools.convertKeys(rows[0]);
|
||||
|
||||
module.exports.get(rule.segment, (err, segment) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
return callback(new Error(_('Specified segment not found')));
|
||||
}
|
||||
|
||||
if (rule.value) {
|
||||
try {
|
||||
rule.value = JSON.parse(rule.value);
|
||||
} catch (E) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (!rule.value) {
|
||||
rule.value = {};
|
||||
}
|
||||
|
||||
rule.columnType = segment.columns.filter(column => rule.column === column.column).pop() || {};
|
||||
|
||||
rule.name = rule.columnType.name || '';
|
||||
switch (rule.columnType.type) {
|
||||
case 'number':
|
||||
case 'date':
|
||||
case 'birthday':
|
||||
if (rule.value.relativeRange) {
|
||||
|
||||
let startString = rule.value.startDirection ? util.format(_('%s days after today'), rule.value.start) : util.format(_('%s days before today'), rule.value.start);
|
||||
let endString = rule.value.endDirection ? util.format(_('%s days after today'), rule.value.end) : util.format(_('%s days before today'), rule.value.end);
|
||||
rule.formatted = (rule.value.start ? startString : _('today')) + ' … ' + (rule.value.end ? endString : _('today'));
|
||||
} else if (rule.value.range) {
|
||||
rule.formatted = (rule.value.start || '') + ' … ' + (rule.value.end || '');
|
||||
} else {
|
||||
rule.formatted = rule.value.value || '';
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
rule.formatted = rule.value.value ? _('Selected') : _('Not selected');
|
||||
break;
|
||||
default:
|
||||
rule.formatted = rule.value.value || '';
|
||||
}
|
||||
|
||||
return callback(null, rule);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.updateRule = (id, rule, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Rule ID')));
|
||||
}
|
||||
|
||||
rule = tools.convertKeys(rule);
|
||||
|
||||
module.exports.getRule(id, (err, existingRule) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!existingRule) {
|
||||
return callback(new Error(_('Selected rule not found')));
|
||||
}
|
||||
|
||||
module.exports.get(existingRule.segment, (err, segment) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
return callback(new Error(_('Selected segment not found')));
|
||||
}
|
||||
|
||||
let column = segment.columns.filter(column => column.column === existingRule.column).pop();
|
||||
if (!column) {
|
||||
return callback(new Error(_('Invalid rule type')));
|
||||
}
|
||||
|
||||
let value;
|
||||
switch (column.type) {
|
||||
case 'date':
|
||||
case 'birthday':
|
||||
case 'number':
|
||||
if (column.type === 'date' && rule.range === 'relative') {
|
||||
value = {
|
||||
relativeRange: true,
|
||||
start: Number(rule.startRelative) || 0,
|
||||
startDirection: Number(rule.startDirection) ? 1 : 0,
|
||||
end: Number(rule.endRelative) || 0,
|
||||
endDirection: Number(rule.endDirection) ? 1 : 0
|
||||
};
|
||||
} else if (rule.range === 'yes') {
|
||||
value = {
|
||||
range: true,
|
||||
start: rule.start,
|
||||
end: rule.end
|
||||
};
|
||||
} else {
|
||||
value = {
|
||||
value: rule.value
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
value = {
|
||||
value: rule.value ? 1 : 0
|
||||
};
|
||||
break;
|
||||
default:
|
||||
value = {
|
||||
value: rule.value
|
||||
};
|
||||
}
|
||||
|
||||
let keys = ['value'];
|
||||
let values = [JSON.stringify(value)];
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
|
||||
connection.query('UPDATE segment_rules SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.deleteRule = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Rule ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM segment_rules WHERE id=? LIMIT 1', [id], err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getQuery = (id, prefix, callback) => {
|
||||
module.exports.get(id, (err, segment) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!segment) {
|
||||
return callback(new Error(_('Segment not found')));
|
||||
}
|
||||
|
||||
prefix = prefix ? prefix + '.' : '';
|
||||
|
||||
let query = [];
|
||||
let values = [];
|
||||
|
||||
let getRelativeDate = (days, direction) => {
|
||||
let date = new Date(Date.now() + (direction ? 1 : -1) * days * 24 * 3600 * 1000);
|
||||
return date.toISOString().substr(0, 10);
|
||||
};
|
||||
|
||||
let getDate = (value, nextDay) => {
|
||||
let parts = value.trim().split(/\D/);
|
||||
let year = Number(parts.shift()) || 0;
|
||||
let month = Number(parts.shift()) || 0;
|
||||
let day = Number(parts.shift()) || 0;
|
||||
if (!year || !month || !day) {
|
||||
return false;
|
||||
}
|
||||
return new Date(Date.UTC(year, month - 1, day + (nextDay ? 1 : 0)));
|
||||
};
|
||||
|
||||
segment.rules.forEach(rule => {
|
||||
switch (rule.columnType.type) {
|
||||
case 'string':
|
||||
query.push(prefix + '`' + rule.columnType.column + '` LIKE ?');
|
||||
values.push(rule.value.value);
|
||||
break;
|
||||
case 'boolean':
|
||||
query.push(prefix + '`' + rule.columnType.column + '` = ?');
|
||||
values.push(rule.value.value);
|
||||
break;
|
||||
case 'number':
|
||||
if (rule.value.range) {
|
||||
let ruleval = '';
|
||||
if (rule.value.start) {
|
||||
ruleval = prefix + '`' + rule.columnType.column + '` >= ?';
|
||||
values.push(rule.value.start);
|
||||
}
|
||||
if (rule.value.end) {
|
||||
ruleval = (ruleval ? '(' + ruleval + ' AND ' : '') + prefix + '`' + rule.columnType.column + '` < ?' + (ruleval ? ')' : '');
|
||||
values.push(rule.value.end);
|
||||
}
|
||||
if (ruleval) {
|
||||
query.push(ruleval);
|
||||
}
|
||||
} else {
|
||||
query.push(prefix + '`' + rule.columnType.column + '` = ?');
|
||||
values.push(rule.value.value);
|
||||
}
|
||||
break;
|
||||
case 'birthday':
|
||||
if (rule.value.range) {
|
||||
let start = rule.value.start || '01-01';
|
||||
let end = rule.value.end || '12-31';
|
||||
query.push('(' + prefix + '`' + rule.columnType.column + '` >= ? AND ' + prefix + '`' + rule.columnType.column + '` < ?)');
|
||||
values.push(getDate('2000-' + start));
|
||||
values.push(getDate('2000-' + end, true));
|
||||
} else {
|
||||
query.push('(' + prefix + '`' + rule.columnType.column + '` >= ? AND ' + prefix + '`' + rule.columnType.column + '` < ?)');
|
||||
values.push(getDate('2000-' + rule.value.value));
|
||||
values.push(getDate('2000-' + rule.value.value, true));
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
if (rule.value.relativeRange) {
|
||||
query.push('(' + prefix + '`' + rule.columnType.column + '` >= ? AND ' + prefix + '`' + rule.columnType.column + '` < ?)');
|
||||
// start
|
||||
values.push(getDate(getRelativeDate(rule.value.start, rule.value.startDirection)));
|
||||
// end
|
||||
values.push(getDate(getRelativeDate(rule.value.end, rule.value.endDirection), true));
|
||||
} else if (rule.value.range) {
|
||||
let ruleval = '';
|
||||
if (rule.value.start) {
|
||||
ruleval = prefix + '`' + rule.columnType.column + '` >= ?';
|
||||
values.push(getDate(rule.value.start));
|
||||
}
|
||||
if (rule.value.end) {
|
||||
ruleval = (ruleval ? '(' + ruleval + ' AND ' : '') + prefix + '`' + rule.columnType.column + '` < ?' + (ruleval ? ')' : '');
|
||||
values.push(getDate(rule.value.end, true));
|
||||
}
|
||||
if (ruleval) {
|
||||
query.push(ruleval);
|
||||
}
|
||||
} else {
|
||||
query.push('(' + prefix + '`' + rule.columnType.column + '` >= ? AND ' + prefix + '`' + rule.columnType.column + '` < ?)');
|
||||
values.push(getDate(rule.value.value));
|
||||
values.push(getDate(rule.value.value, true));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return callback(null, {
|
||||
where: query.join(segment.type === 1 ? ' AND ' : ' OR ') || '1',
|
||||
values
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.subscribers = (id, onlySubscribed, callback) => {
|
||||
module.exports.get(id, (err, segment) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!segment) {
|
||||
return callback(new Error(_('Segment not found')));
|
||||
}
|
||||
module.exports.getQuery(id, false, (err, queryData) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query;
|
||||
if (!onlySubscribed) {
|
||||
query = 'SELECT COUNT(id) AS `count` FROM `subscription__' + segment.list + '` WHERE ' + queryData.where + ' LIMIT 1';
|
||||
} else {
|
||||
query = 'SELECT COUNT(id) AS `count` FROM `subscription__' + segment.list + '` WHERE `status`=1 ' + (queryData.where ? ' AND (' + queryData.where + ')' : '') + ' LIMIT 1';
|
||||
}
|
||||
|
||||
connection.query(query, queryData.values, (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let count = rows && rows[0] && rows[0].count || 0;
|
||||
return callback(null, count);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -1,926 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let shortid = require('shortid');
|
||||
let striptags = require('striptags');
|
||||
let tools = require('../tools');
|
||||
let helpers = require('../helpers');
|
||||
let fields = require('./fields');
|
||||
let segments = require('./segments');
|
||||
let _ = require('../translate')._;
|
||||
let tableHelpers = require('../table-helpers');
|
||||
|
||||
const Status = require('../../shared/lists').SubscriptionStatus;
|
||||
module.exports.Status = Status;
|
||||
|
||||
module.exports.list = (listId, start, limit, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
if (!listId) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
tableHelpers.list('subscription__' + listId, ['*'], 'email', null, start, limit, (err, rows, total) => {
|
||||
if (!err) {
|
||||
rows = rows.map(row => tools.convertKeys(row));
|
||||
}
|
||||
return callback(err, rows, total);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.listTestUsers = (listId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT id, cid, email, first_name, last_name FROM `subscription__' + listId + '` WHERE is_test=1 LIMIT 100', (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
let subscribers = rows.map(subscriber => {
|
||||
subscriber = tools.convertKeys(subscriber);
|
||||
let fullName = [].concat(subscriber.firstName || []).concat(subscriber.lastName || []).join(' ');
|
||||
if (fullName) {
|
||||
subscriber.displayName = fullName + ' <' + subscriber.email + '>';
|
||||
} else {
|
||||
subscriber.displayName = subscriber.email;
|
||||
}
|
||||
return subscriber;
|
||||
});
|
||||
return callback(null, subscribers);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.filter = (listId, request, columns, segmentId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
segmentId = Number(segmentId) || 0;
|
||||
|
||||
if (!listId) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
if (segmentId) {
|
||||
segments.getQuery(segmentId, false, (err, queryData) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
tableHelpers.filter('subscription__' + listId, ['*'], request, columns, ['email', 'first_name', 'last_name'], 'email ASC', queryData, callback);
|
||||
});
|
||||
} else {
|
||||
tableHelpers.filter('subscription__' + listId, ['*'], request, columns, ['email', 'first_name', 'last_name'], 'email ASC', null, callback);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
Adds a new subscription. Returns error if a subscription with the same email address is already present and is not unsubscribed.
|
||||
If it is unsubscribed, the existing subscription is changed based on the provided data.
|
||||
If meta.partial is true, it updates even an active subscription.
|
||||
*/
|
||||
module.exports.insert = (listId, meta, subscriptionData, callback) => {
|
||||
meta = tools.convertKeys(meta);
|
||||
subscriptionData = tools.convertKeys(subscriptionData);
|
||||
|
||||
meta.email = meta.email || subscriptionData.email;
|
||||
meta.cid = meta.cid || shortid.generate();
|
||||
|
||||
fields.list(listId, (err, fieldList) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let insertKeys = ['email', 'cid', 'opt_in_ip', 'opt_in_country', 'imported'];
|
||||
let insertValues = [meta.email, meta.cid, meta.optInIp || null, meta.optInCountry || null, meta.imported || null];
|
||||
let keys = [];
|
||||
let values = [];
|
||||
|
||||
let allowedKeys = ['first_name', 'last_name', 'tz', 'is_test'];
|
||||
Object.keys(subscriptionData).forEach(key => {
|
||||
let value = subscriptionData[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'tz') {
|
||||
value = (value || '').toString().toLowerCase().trim();
|
||||
}
|
||||
if (key === 'is_test') {
|
||||
value = value ? '1' : '0';
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
fields.getValues(fields.getRow(fieldList, subscriptionData, true, true, !!meta.partial), true).forEach(field => {
|
||||
keys.push(field.key);
|
||||
values.push(field.value);
|
||||
});
|
||||
|
||||
values = values.map(v => typeof v === 'string' ? striptags(v) : v);
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT `id`, `status`, `cid` FROM `subscription__' + listId + '` WHERE `email`=? OR `cid`=? LIMIT 1';
|
||||
connection.query(query, [meta.email, meta.cid], (err, rows) => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
let query;
|
||||
let queryArgs;
|
||||
let existing = rows && rows[0] || false;
|
||||
let entryId = existing ? existing.id : false;
|
||||
|
||||
meta.cid = existing ? rows[0].cid : meta.cid;
|
||||
|
||||
// meta.status may be 'undefined' or '0' when adding a subscription via API call or CSV import. In both cases meta.partial is 'true'.
|
||||
// This must either update an existing subscription without changing its status or insert a new subscription with status SUBSCRIBED.
|
||||
meta.status = meta.status || (existing ? existing.status : Status.SUBSCRIBED);
|
||||
|
||||
let statusChange = !existing || existing.status !== meta.status;
|
||||
let statusDirection;
|
||||
|
||||
if (existing && existing.status === Status.SUBSCRIBED && !meta.partial) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(new Error(_('Email address already registered'))));
|
||||
}
|
||||
|
||||
if (statusChange) {
|
||||
keys.push('status', 'status_change');
|
||||
values.push(meta.status, new Date());
|
||||
statusDirection = !existing ? (meta.status === Status.SUBSCRIBED ? '+' : false) : (existing.status === Status.SUBSCRIBED ? '-' : '+');
|
||||
}
|
||||
|
||||
if (!keys.length) {
|
||||
// nothing to update
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, {
|
||||
entryId,
|
||||
cid: meta.cid,
|
||||
inserted: !existing
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!existing) {
|
||||
// insert as new
|
||||
keys = insertKeys.concat(keys);
|
||||
queryArgs = values = insertValues.concat(values);
|
||||
query = 'INSERT INTO `subscription__' + listId + '` (`' + keys.join('`, `') + '`) VALUES (' + keys.map(() => '?').join(',') + ')';
|
||||
} else {
|
||||
// update existing
|
||||
queryArgs = values.concat(existing.id);
|
||||
query = 'UPDATE `subscription__' + listId + '` SET ' + keys.map(key => '`' + key + '`=?') + ' WHERE id=? LIMIT 1';
|
||||
}
|
||||
|
||||
connection.query(query, queryArgs, (err, result) => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
entryId = result.insertId || entryId;
|
||||
|
||||
if (statusChange && statusDirection) {
|
||||
connection.query('UPDATE lists SET `subscribers`=`subscribers`' + statusDirection + '1 WHERE id=?', [listId], err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, {
|
||||
entryId,
|
||||
cid: meta.cid,
|
||||
inserted: !existing
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, {
|
||||
entryId,
|
||||
cid: meta.cid,
|
||||
inserted: !existing
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (listId, cid, callback) => {
|
||||
cid = (cid || '').toString().trim();
|
||||
|
||||
if (!cid) {
|
||||
return callback(new Error(_('Missing Subbscription ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM `subscription__' + listId + '` WHERE cid=?', [cid], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let subscription = tools.convertKeys(rows[0]);
|
||||
// ensure list id in response
|
||||
subscription.list = subscription.list || listId;
|
||||
return callback(null, subscription);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getById = (listId, id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (!id) {
|
||||
return callback(new Error(_('Missing Subbscription ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM `subscription__' + listId + '` WHERE id=?', [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let subscription = tools.convertKeys(rows[0]);
|
||||
// ensure list id in response
|
||||
subscription.list = subscription.list || listId;
|
||||
return callback(null, subscription);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getByEmail = (listId, email, callback) => {
|
||||
if (!email) {
|
||||
return callback(new Error(_('Missing Subbscription email address')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM `subscription__' + listId + '` WHERE email=?', [email], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let subscription = tools.convertKeys(rows[0]);
|
||||
// ensure list id in response
|
||||
subscription.list = subscription.list || listId;
|
||||
return callback(null, subscription);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getWithMergeTags = (listId, cid, callback) => {
|
||||
module.exports.get(listId, cid, (err, subscription) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
fields.list(listId, (err, fieldList) => {
|
||||
if (err || !fieldList) {
|
||||
return fieldList = [];
|
||||
}
|
||||
|
||||
subscription.mergeTags = {
|
||||
EMAIL: subscription.email,
|
||||
FIRST_NAME: subscription.firstName,
|
||||
LAST_NAME: subscription.lastName,
|
||||
FULL_NAME: [].concat(subscription.firstName || []).concat(subscription.lastName || []).join(' '),
|
||||
TIMEZONE: subscription.tz || ''
|
||||
};
|
||||
|
||||
fields.getRow(fieldList, subscription, false, true).forEach(field => {
|
||||
if (field.mergeTag) {
|
||||
subscription.mergeTags[field.mergeTag] = field.mergeValue || '';
|
||||
}
|
||||
if (field.options) {
|
||||
field.options.forEach(subField => {
|
||||
if (subField.mergeTag) {
|
||||
subscription.mergeTags[subField.mergeTag] = subField.mergeValue || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return callback(null, subscription);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.update = (listId, cid, updates, allowEmail, callback) => {
|
||||
updates = tools.convertKeys(updates);
|
||||
listId = Number(listId) || 0;
|
||||
cid = (cid || '').toString().trim();
|
||||
|
||||
let keys = [];
|
||||
let values = [];
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
if (!cid) {
|
||||
return callback(new Error(_('Missing Subscription ID')));
|
||||
}
|
||||
|
||||
fields.list(listId, (err, fieldList) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let allowedKeys = ['first_name', 'last_name', 'tz', 'is_test'];
|
||||
|
||||
if (allowEmail) {
|
||||
allowedKeys.unshift('email');
|
||||
}
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
let value = updates[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'tz') {
|
||||
value = (value || '').toString().toLowerCase().trim();
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
fields.getValues(fields.getRow(fieldList, updates, true, true, true), true).forEach(field => {
|
||||
keys.push(field.key);
|
||||
values.push(field.value);
|
||||
});
|
||||
|
||||
if (!values.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
values = values.map(v => typeof v === 'string' ? striptags(v) : v);
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(cid);
|
||||
connection.query('UPDATE `subscription__' + listId + '` SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE `cid`=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.changeStatus = (listId, id, campaignId, status, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT `status` FROM `subscription__' + listId + '` WHERE id=? LIMIT 1', [id], (err, rows) => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(null, false));
|
||||
}
|
||||
|
||||
let oldStatus = rows[0].status;
|
||||
let statusChange = oldStatus !== status;
|
||||
let statusDirection;
|
||||
|
||||
if (!statusChange) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(null, true));
|
||||
}
|
||||
|
||||
if (statusChange && oldStatus === Status.SUBSCRIBED || status === Status.SUBSCRIBED) {
|
||||
statusDirection = status === Status.SUBSCRIBED ? '+' : '-';
|
||||
}
|
||||
|
||||
connection.query('UPDATE `subscription__' + listId + '` SET `status`=?, `status_change`=NOW() WHERE id=? LIMIT 1', [status, id], err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
if (!statusDirection) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
connection.query('UPDATE `lists` SET `subscribers`=`subscribers`' + statusDirection + '1 WHERE id=? LIMIT 1', [listId], err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
// status change is not related to a campaign or it marks message as bounced etc.
|
||||
if (!campaignId || status !== Status.SUBSCRIBED && status !== Status.UNSUBSCRIBED) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
connection.query('SELECT `id` FROM `campaigns` WHERE `cid`=? LIMIT 1', [campaignId], (err, rows) => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
let campaign = rows && rows[0] || false;
|
||||
|
||||
if (!campaign) {
|
||||
// should not happend
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
// we should see only unsubscribe events here but you never know
|
||||
connection.query('UPDATE `campaigns` SET `unsubscribed`=`unsubscribed`' + (status === Status.UNSUBSCRIBED ? '+' : '-') + '1 WHERE `cid`=? LIMIT 1', [campaignId], err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
let query = 'UPDATE `campaign__' + campaign.id + '` SET `status`=? WHERE `list`=? AND `subscription`=? LIMIT 1';
|
||||
let values = [status, listId, id];
|
||||
|
||||
// Updated tracker status
|
||||
connection.query(query, values, err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (listId, cid, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
cid = (cid || '').toString().trim();
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
if (!cid) {
|
||||
return callback(new Error(_('Missing subscription ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT id, email, status FROM `subscription__' + listId + '` WHERE cid=? LIMIT 1', [cid], (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let subscription = rows && rows[0];
|
||||
if (!subscription) {
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM `subscription__' + listId + '` WHERE cid=? LIMIT 1', [cid], err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
if (subscription.status !== Status.SUBSCRIBED) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, subscription.email);
|
||||
});
|
||||
}
|
||||
|
||||
connection.query('UPDATE lists SET subscribers=subscribers-1 WHERE id=? LIMIT 1', [listId], err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, subscription.email);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.createImport = (listId, type, path, size, delimiter, emailcheck, mapping, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
type = Number(type) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'INSERT INTO importer (`list`, `type`, `path`, `size`, `delimiter`, `emailcheck`, `mapping`) VALUES(?,?,?,?,?,?,?)';
|
||||
connection.query(query, [listId, type, path, size, delimiter, emailcheck, JSON.stringify(mapping)], (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.insertId || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.updateImport = (listId, importId, data, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
importId = Number(importId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
if (importId < 1) {
|
||||
return callback(new Error(_('Missing Import ID')));
|
||||
}
|
||||
|
||||
let keys = [];
|
||||
let values = [];
|
||||
|
||||
let allowedKeys = ['type', 'path', 'size', 'delimiter', 'status', 'error', 'processed', 'new', 'failed', 'mapping', 'finished'];
|
||||
Object.keys(data).forEach(key => {
|
||||
let value = data[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'UPDATE importer SET ' + keys.map(key => '`' + key + '`=?') + ' WHERE id=? AND list=? LIMIT 1';
|
||||
connection.query(query, values.concat([importId, listId]), (err, result) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let affected = result && result.affectedRows || false;
|
||||
|
||||
if (data.failed === 0) {
|
||||
// remove entries from import_failed table
|
||||
let query = 'DELETE FROM `import_failed` WHERE `import`=?';
|
||||
connection.query(query, [importId], () => {
|
||||
connection.release();
|
||||
return callback(null, affected);
|
||||
});
|
||||
} else {
|
||||
connection.release();
|
||||
return callback(null, affected);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getImport = (listId, importId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
importId = Number(importId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
if (importId < 1) {
|
||||
return callback(new Error(_('Missing Import ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'SELECT * FROM importer WHERE id=? AND list=? LIMIT 1';
|
||||
connection.query(query, [importId, listId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let importer = tools.convertKeys(rows[0]);
|
||||
try {
|
||||
importer.mapping = JSON.parse(importer.mapping);
|
||||
} catch (E) {
|
||||
importer.mapping = {
|
||||
columns: []
|
||||
};
|
||||
}
|
||||
|
||||
return callback(null, importer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getFailedImports = (importId, callback) => {
|
||||
importId = Number(importId) || 0;
|
||||
|
||||
if (importId < 1) {
|
||||
return callback(new Error(_('Missing Import ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'SELECT * FROM import_failed WHERE import=? LIMIT 1000';
|
||||
connection.query(query, [importId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, (rows || []).map(tools.convertKeys));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.listImports = (listId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'SELECT * FROM importer WHERE list=? AND status > 0 ORDER BY id DESC';
|
||||
connection.query(query, [listId], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
let imports = rows.map(row => {
|
||||
let importer = tools.convertKeys(row);
|
||||
try {
|
||||
importer.mapping = JSON.parse(importer.mapping);
|
||||
} catch (E) {
|
||||
importer.mapping = {
|
||||
columns: []
|
||||
};
|
||||
}
|
||||
return importer;
|
||||
});
|
||||
|
||||
return callback(null, imports);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
Performs checks before update of an address. This includes finding the existing subscriber, validating the new email
|
||||
and checking whether the new email does not conflict with other subscribers.
|
||||
*/
|
||||
module.exports.updateAddressCheck = (list, cid, emailNew, ip, callback) => {
|
||||
cid = (cid || '').toString().trim();
|
||||
|
||||
if (!list || !list.id) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
if (!cid) {
|
||||
return callback(new Error(_('Missing subscription ID')));
|
||||
}
|
||||
|
||||
tools.validateEmail(emailNew, false, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM `subscription__' + list.id + '` WHERE `cid`=? AND `status`=' + Status.SUBSCRIBED + ' LIMIT 1';
|
||||
let args = [cid];
|
||||
connection.query(query, args, (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
if (!rows || !rows.length) {
|
||||
connection.release();
|
||||
return callback(new Error(_('Unknown subscription ID')));
|
||||
}
|
||||
|
||||
if (rows[0].email === emailNew) {
|
||||
connection.release();
|
||||
return callback(new Error(_('Nothing seems to be changed')));
|
||||
}
|
||||
|
||||
let old = rows[0];
|
||||
|
||||
let query = 'SELECT `id` FROM `subscription__' + list.id + '` WHERE `email`=? AND `cid`<>? AND `status`=' + Status.SUBSCRIBED + ' LIMIT 1';
|
||||
let args = [emailNew, cid];
|
||||
connection.query(query, args, (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (rows && rows.length > 0) {
|
||||
return callback(null, old, false);
|
||||
} else {
|
||||
return callback(null, old, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
Updates address in subscription__xxx
|
||||
*/
|
||||
module.exports.updateAddress = (listId, subscriptionId, emailNew, callback) => {
|
||||
// update email address instead of adding new
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT `id` FROM `subscription__' + listId + '` WHERE `email`=? AND `id`<>? AND `status`=' + Status.SUBSCRIBED + ' LIMIT 1';
|
||||
let args = [emailNew, subscriptionId];
|
||||
connection.query(query, args, (err, rows) => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
if (rows && rows.length > 0) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(new Error(_('Email address already registered'))));
|
||||
}
|
||||
|
||||
let query = 'DELETE FROM `subscription__' + listId + '` WHERE `email`=? AND `id`<>?';
|
||||
let args = [emailNew, subscriptionId];
|
||||
connection.query(query, args, err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
let query = 'UPDATE `subscription__' + listId + '` SET `email`=? WHERE `id`=? AND `status`=' + Status.SUBSCRIBED + ' LIMIT 1';
|
||||
let args = [emailNew, subscriptionId];
|
||||
connection.query(query, args, (err, result) => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
if (!result || !result.affectedRows) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(new Error(_('Subscription not found in this list'))));
|
||||
}
|
||||
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
module.exports.getUnsubscriptionMode = (list, subscriptionId) => list.unsubscriptionMode; // eslint-disable-line no-unused-vars
|
||||
// TODO: Once the unsubscription mode is customizable per segment, then this will be a good place to process it.
|
|
@ -1,176 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let tools = require('../tools');
|
||||
let _ = require('../translate')._;
|
||||
let tableHelpers = require('../table-helpers');
|
||||
|
||||
let allowedKeys = ['description', 'editor_name', 'editor_data', 'html', 'text'];
|
||||
|
||||
module.exports.list = (start, limit, callback) => {
|
||||
tableHelpers.list('templates', ['*'], 'name', null, start, limit, callback);
|
||||
};
|
||||
|
||||
module.exports.filter = (request, parent, callback) => {
|
||||
tableHelpers.filter('templates', ['*'], request, ['#', 'name', 'description'], ['name'], 'name ASC', null, callback);
|
||||
};
|
||||
|
||||
module.exports.quicklist = callback => {
|
||||
tableHelpers.quicklist('templates', ['id', 'name'], 'name', callback);
|
||||
};
|
||||
|
||||
module.exports.get = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Template ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM templates WHERE id=?', [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let template = tools.convertKeys(rows[0]);
|
||||
return callback(null, template);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.create = (template, callback) => {
|
||||
|
||||
let data = tools.convertKeys(template);
|
||||
|
||||
if (!(data.name || '').toString().trim()) {
|
||||
return callback(new Error(_('Template Name must be set')));
|
||||
}
|
||||
|
||||
let name = (template.name || '').toString().trim();
|
||||
|
||||
let keys = ['name'];
|
||||
let values = [name];
|
||||
|
||||
Object.keys(template).forEach(key => {
|
||||
let value = template[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (!allowedKeys.includes(key)) {
|
||||
return;
|
||||
}
|
||||
value = value.trim();
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO templates (' + keys.join(', ') + ') VALUES (' + values.map(() => '?').join(',') + ')';
|
||||
connection.query(query, values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let templateId = result && result.insertId || false;
|
||||
return callback(null, templateId);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
updates = updates || {};
|
||||
id = Number(id) || 0;
|
||||
|
||||
let data = tools.convertKeys(updates);
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Template ID')));
|
||||
}
|
||||
|
||||
if (!(data.name || '').toString().trim()) {
|
||||
return callback(new Error(_('Template Name must be set')));
|
||||
}
|
||||
|
||||
let name = (updates.name || '').toString().trim();
|
||||
let keys = ['name'];
|
||||
let values = [name];
|
||||
|
||||
Object.keys(updates).forEach(key => {
|
||||
let value = updates[key].trim();
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'description') {
|
||||
value = tools.purifyHTML(value);
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
|
||||
connection.query('UPDATE templates SET ' + keys.map(key => key + '=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, result && result.affectedRows || false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.duplicate = (id, callback) => module.exports.get(id, (err, template) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!template) {
|
||||
return callback(new Error(_('Template does not exist')));
|
||||
}
|
||||
template.name = template.name + ' Copy';
|
||||
return module.exports.create(template, callback);
|
||||
});
|
||||
|
||||
module.exports.delete = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Template ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM templates WHERE id=? LIMIT 1', id, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let affected = result && result.affectedRows || 0;
|
||||
|
||||
return callback(null, affected);
|
||||
});
|
||||
});
|
||||
};
|
|
@ -1,384 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
let tools = require('../tools');
|
||||
let db = require('../db');
|
||||
let lists = require('./lists');
|
||||
let util = require('util');
|
||||
let _ = require('../translate')._;
|
||||
let tableHelpers = require('../table-helpers');
|
||||
|
||||
module.exports.defaultColumns = [{
|
||||
column: 'created',
|
||||
name: _('Sign up date'),
|
||||
type: 'date'
|
||||
}, {
|
||||
column: 'latest_open',
|
||||
name: _('Latest open'),
|
||||
type: 'date'
|
||||
}, {
|
||||
column: 'latest_click',
|
||||
name: _('Latest click'),
|
||||
type: 'date'
|
||||
}];
|
||||
|
||||
module.exports.defaultCampaignEvents = [{
|
||||
option: 'delivered',
|
||||
name: _('Delivered')
|
||||
}, {
|
||||
option: 'opened',
|
||||
name: _('Has Opened')
|
||||
}, {
|
||||
option: 'clicked',
|
||||
name: _('Has Clicked')
|
||||
}, {
|
||||
option: 'not_opened',
|
||||
name: _('Not Opened')
|
||||
}, {
|
||||
option: 'not_clicked',
|
||||
name: _('Not Clicked')
|
||||
}];
|
||||
|
||||
let defaultColumnMap = {};
|
||||
let defaultEventMap = {};
|
||||
module.exports.defaultColumns.forEach(col => defaultColumnMap[col.column] = col.name);
|
||||
module.exports.defaultCampaignEvents.forEach(evt => defaultEventMap[evt.option] = evt.name);
|
||||
|
||||
module.exports.list = callback => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let tableFields = [
|
||||
'`triggers`.`id` AS `id`',
|
||||
'`triggers`.`name` AS `name`',
|
||||
'`triggers`.`description` AS `description`',
|
||||
'`triggers`.`enabled` AS `enabled`',
|
||||
'`triggers`.`list` AS `list`',
|
||||
'`lists`.`name` AS `list_name`',
|
||||
'`source`.`id` AS `source_campaign`',
|
||||
'`source`.`name` AS `source_campaign_name`',
|
||||
'`dest`.`id` AS `dest_campaign`',
|
||||
'`dest`.`name` AS `dest_campaign_name`',
|
||||
'`triggers`.`count` AS `count`',
|
||||
'`custom_fields`.`id` AS `column_id`',
|
||||
'`triggers`.`column` AS `column`',
|
||||
'`custom_fields`.`name` AS `column_name`',
|
||||
'`triggers`.`rule` AS `rule`',
|
||||
'`triggers`.`seconds` AS `seconds`',
|
||||
'`triggers`.`created` AS `created`'
|
||||
];
|
||||
|
||||
let query = 'SELECT ' + tableFields.join(', ') + ' FROM `triggers` LEFT JOIN `campaigns` `source` ON `source`.`id`=`triggers`.`source_campaign` LEFT JOIN `campaigns` `dest` ON `dest`.`id`=`triggers`.`dest_campaign` LEFT JOIN `lists` ON `lists`.`id`=`triggers`.`list` LEFT JOIN `custom_fields` ON `custom_fields`.`list` = `triggers`.`list` AND `custom_fields`.`column`=`triggers`.`column` ORDER BY `triggers`.`name`';
|
||||
connection.query(query, (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let triggers = (rows || []).map(tools.convertKeys).map(row => {
|
||||
if (row.rule === 'subscription' && row.column && !row.columnName) {
|
||||
row.columnName = defaultColumnMap[row.column];
|
||||
}
|
||||
|
||||
let days = Math.round(row.seconds / (24 * 3600));
|
||||
row.formatted = util.format('%s days after %s', days, row.rule === 'subscription' ? row.columnName : (util.format('%s <a href="/campaigns/view/%s">%s</a>', defaultEventMap[row.column], row.sourceCampaign, row.sourceCampaignName)));
|
||||
|
||||
return row;
|
||||
});
|
||||
return callback(null, triggers);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.getQuery = (id, callback) => {
|
||||
module.exports.get(id, (err, trigger) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let limit = 300;
|
||||
|
||||
// time..NOW..time + 24h, 24 hour window after trigger target to detect it
|
||||
//We need a 24 hour window for triggers as the format for dates added via the API are stored as 00:00:00
|
||||
let treshold = 3600 * 24;
|
||||
|
||||
let intervalQuery = (column, seconds, treshold) => column + ' <= NOW() - INTERVAL ' + seconds + ' SECOND AND ' + column + ' >= NOW() - INTERVAL ' + (treshold + seconds) + ' SECOND';
|
||||
|
||||
let query = false;
|
||||
switch (trigger.rule) {
|
||||
case 'subscription':
|
||||
query = 'SELECT id FROM `subscription__' + trigger.list + '` subscription WHERE ' + intervalQuery('`' + trigger.column + '`', trigger.seconds, treshold) + ' AND id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
|
||||
break;
|
||||
case 'campaign':
|
||||
switch (trigger.column) {
|
||||
case 'delivered':
|
||||
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription WHERE campaign.status=1 AND ' + intervalQuery('`campaign`.`created`', trigger.seconds, treshold) + ' AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
|
||||
break;
|
||||
case 'not_clicked':
|
||||
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription LEFT JOIN `campaign_tracker__' + trigger.sourceCampaign + '` tracker ON tracker.list=campaign.list AND tracker.subscriber=subscription.id AND tracker.link=0 WHERE campaign.status=1 AND ' + intervalQuery('`campaign`.`created`', trigger.seconds, treshold) + ' AND tracker.created IS NULL AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
|
||||
break;
|
||||
case 'not_opened':
|
||||
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription LEFT JOIN `campaign_tracker__' + trigger.sourceCampaign + '` tracker ON tracker.list=campaign.list AND tracker.subscriber=subscription.id AND tracker.link=-1 WHERE campaign.status=1 AND ' + intervalQuery('`campaign`.`created`', trigger.seconds, treshold) + ' AND tracker.created IS NULL AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
|
||||
break;
|
||||
case 'clicked':
|
||||
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription LEFT JOIN `campaign_tracker__' + trigger.sourceCampaign + '` tracker ON tracker.list=campaign.list AND tracker.subscriber=subscription.id AND tracker.link=0 WHERE campaign.status=1 AND ' + intervalQuery('`tracker`.`created`', trigger.seconds, treshold) + ' AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
|
||||
break;
|
||||
case 'opened':
|
||||
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription LEFT JOIN `campaign_tracker__' + trigger.sourceCampaign + '` tracker ON tracker.list=campaign.list AND tracker.subscriber=subscription.id AND tracker.link=-1 WHERE campaign.status=1 AND ' + intervalQuery('`tracker`.`created`', trigger.seconds, treshold) + ' AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
callback(null, query);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.get = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error('Missing Trigger ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM triggers WHERE id=?', [id], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let trigger = tools.convertKeys(rows[0]);
|
||||
return callback(null, trigger);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.create = (trigger, callback) => {
|
||||
|
||||
trigger = tools.convertKeys(trigger);
|
||||
let name = (trigger.name || '').toString().trim();
|
||||
let description = (trigger.description || '').toString().trim();
|
||||
let listId = Number(trigger.list) || 0;
|
||||
let seconds = (Number(trigger.days) || 0) * 24 * 3600;
|
||||
let rule = (trigger.rule || '').toString().toLowerCase().trim();
|
||||
let destCampaign = Number(trigger.destCampaign) || 0;
|
||||
let sourceCampaign = null;
|
||||
let column;
|
||||
|
||||
if (!listId) {
|
||||
return callback(new Error(_('Missing or invalid list ID')));
|
||||
}
|
||||
|
||||
if (seconds < 0) {
|
||||
return callback(new Error(_('Days in the past are not allowed')));
|
||||
}
|
||||
|
||||
if (!rule || ['campaign', 'subscription'].indexOf(rule) < 0) {
|
||||
return callback(new Error(_('Missing or invalid trigger rule')));
|
||||
}
|
||||
|
||||
switch (rule) {
|
||||
case 'subscription':
|
||||
column = (trigger.column || '').toString().toLowerCase().trim();
|
||||
if (!column) {
|
||||
return callback(new Error(_('Invalid subscription configuration')));
|
||||
}
|
||||
break;
|
||||
case 'campaign':
|
||||
column = (trigger.campaignOption || '').toString().toLowerCase().trim();
|
||||
sourceCampaign = Number(trigger.sourceCampaign) || 0;
|
||||
if (!column || !sourceCampaign) {
|
||||
return callback(new Error(_('Invalid campaign configuration')));
|
||||
}
|
||||
if (sourceCampaign === destCampaign) {
|
||||
return callback(new Error(_('A campaing can not be a target for itself')));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return callback(new Error(_('Missing or invalid trigger rule')));
|
||||
}
|
||||
|
||||
lists.get(listId, (err, list) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!list) {
|
||||
return callback(new Error(_('Missing or invalid list ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let keys = ['name', 'description', 'list', 'source_campaign', 'rule', 'column', 'seconds', 'dest_campaign', 'last_check'];
|
||||
let values = [name, description, list.id, sourceCampaign, rule, column, seconds, destCampaign];
|
||||
|
||||
let query = 'INSERT INTO `triggers` (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(', ') + ', NOW())';
|
||||
|
||||
connection.query(query, values, (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let id = result && result.insertId;
|
||||
if (!id) {
|
||||
return callback(new Error(_('Could not store trigger row')));
|
||||
}
|
||||
|
||||
createTriggerTable(id, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, id);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.update = (id, trigger, callback) => {
|
||||
id = Number(id) || 0;
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing or invalid Trigger ID')));
|
||||
}
|
||||
|
||||
trigger = tools.convertKeys(trigger);
|
||||
let name = (trigger.name || '').toString().trim();
|
||||
let description = (trigger.description || '').toString().trim();
|
||||
let enabled = trigger.enabled ? 1 : 0;
|
||||
let seconds = (Number(trigger.days) || 0) * 24 * 3600;
|
||||
let rule = (trigger.rule || '').toString().toLowerCase().trim();
|
||||
let destCampaign = Number(trigger.destCampaign) || 0;
|
||||
let sourceCampaign = null;
|
||||
let column;
|
||||
|
||||
if (seconds < 0) {
|
||||
return callback(new Error(_('Days in the past are not allowed')));
|
||||
}
|
||||
|
||||
if (!rule || ['campaign', 'subscription'].indexOf(rule) < 0) {
|
||||
return callback(new Error(_('Missing or invalid trigger rule')));
|
||||
}
|
||||
|
||||
switch (rule) {
|
||||
case 'subscription':
|
||||
column = (trigger.column || '').toString().toLowerCase().trim();
|
||||
if (!column) {
|
||||
return callback(new Error(_('Invalid subscription configuration')));
|
||||
}
|
||||
break;
|
||||
case 'campaign':
|
||||
column = (trigger.campaignOption || '').toString().toLowerCase().trim();
|
||||
sourceCampaign = Number(trigger.sourceCampaign) || 0;
|
||||
if (!column || !sourceCampaign) {
|
||||
return callback(new Error(_('Invalid campaign configuration')));
|
||||
}
|
||||
if (sourceCampaign === destCampaign) {
|
||||
return callback(new Error(_('A campaing can not be a target for itself')));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return callback(new Error(_('Missing or invalid trigger rule')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let keys = ['name', 'description', 'enabled', 'source_campaign', 'rule', 'column', 'seconds', 'dest_campaign'];
|
||||
let values = [name, description, enabled, sourceCampaign, rule, column, seconds, destCampaign];
|
||||
|
||||
let query = 'UPDATE `triggers` SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE `id`=? LIMIT 1';
|
||||
|
||||
connection.query(query, values.concat(id), (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, result && result.affectedRows);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.delete = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing Trigger ID')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('DELETE FROM triggers WHERE id=? LIMIT 1', [id], (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let affected = result && result.affectedRows || 0;
|
||||
removeTriggerTable(id, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, affected);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.filterSubscribers = (trigger, request, columns, callback) => {
|
||||
let queryData = {
|
||||
where: 'trigger__' + trigger.id + '.list=?',
|
||||
values: [trigger.list]
|
||||
};
|
||||
|
||||
tableHelpers.filter('subscription__' + trigger.list + ' JOIN trigger__' + trigger.id + ' ON trigger__' + trigger.id + '.subscription=subscription__' + trigger.list + '.id', ['*'], request, columns, ['email', 'first_name', 'last_name'], 'email ASC', queryData, callback);
|
||||
};
|
||||
|
||||
function createTriggerTable(id, callback) {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'CREATE TABLE `trigger__' + id + '` LIKE `trigger`';
|
||||
connection.query(query, err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeTriggerTable(id, callback) {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'DROP TABLE IF EXISTS `trigger__' + id + '`';
|
||||
connection.query(query, err => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -8,7 +8,7 @@ const contextHelpers = require('../lib/context-helpers');
|
|||
let runningWorkersCount = 0;
|
||||
let maxWorkersCount = 1;
|
||||
|
||||
let workers = {};
|
||||
const workers = {};
|
||||
|
||||
function startWorker(report) {
|
||||
|
||||
|
|
|
@ -3,16 +3,13 @@
|
|||
const log = require('npmlog');
|
||||
const fields = require('../models/fields');
|
||||
const settings = require('../models/settings');
|
||||
const urllib = require('url');
|
||||
const helpers = require('./helpers');
|
||||
const {getTrustedUrl} = require('./urls');
|
||||
const _ = require('./translate')._;
|
||||
const util = require('util');
|
||||
const contextHelpers = require('./context-helpers');
|
||||
const {getFieldKey} = require('../shared/lists');
|
||||
const forms = require('../models/forms');
|
||||
const bluebird = require('bluebird');
|
||||
const sendMail = bluebird.promisify(require('./mailer').sendMail);
|
||||
|
||||
const mailers = require('./mailers');
|
||||
|
||||
module.exports = {
|
||||
sendAlreadySubscribed,
|
||||
|
@ -99,8 +96,6 @@ function getDisplayName(flds, subscription) {
|
|||
}
|
||||
|
||||
async function _sendMail(list, email, template, subject, relativeUrls, subscription) {
|
||||
console.log(subscription);
|
||||
|
||||
const flds = await fields.list(contextHelpers.getAdminContext(), list.id);
|
||||
|
||||
const encryptionKeys = [];
|
||||
|
@ -110,16 +105,16 @@ async function _sendMail(list, email, template, subject, relativeUrls, subscript
|
|||
}
|
||||
}
|
||||
|
||||
const configItems = await settings.get(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'serviceUrl']);
|
||||
const configItems = await settings.get(contextHelpers.getAdminContext(), ['defaultHomepage', 'adminEmail']);
|
||||
|
||||
const data = {
|
||||
title: list.name,
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
homepage: configItems.defaultHomepage || getTrustedUrl(),
|
||||
contactAddress: list.from_email || configItems.adminEmail,
|
||||
};
|
||||
|
||||
for (let relativeUrlKey in relativeUrls) {
|
||||
data[relativeUrlKey] = urllib.resolve(configItems.serviceUrl, relativeUrls[relativeUrlKey]);
|
||||
data[relativeUrlKey] = getTrustedUrl(relativeUrls[relativeUrlKey]);
|
||||
}
|
||||
|
||||
const fsTemplate = template.replace(/_/g, '-');
|
||||
|
@ -142,22 +137,27 @@ async function _sendMail(list, email, template, subject, relativeUrls, subscript
|
|||
}
|
||||
|
||||
try {
|
||||
await sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: getDisplayName(flds, subscription),
|
||||
address: email
|
||||
},
|
||||
subject: util.format(subject, list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
text,
|
||||
data
|
||||
});
|
||||
if (list.send_configuration) {
|
||||
const mailer = await mailers.getOrCreateMailer(list.send_configuration);
|
||||
await mailer.sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: getDisplayName(flds, subscription),
|
||||
address: email
|
||||
},
|
||||
subject: util.format(subject, list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
text,
|
||||
data
|
||||
});
|
||||
} else {
|
||||
log.warn('Subscription', `Not sending email for list id:${list.id} because not send configuration is set.`);
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Subscription', err);
|
||||
}
|
||||
|
|
|
@ -1,134 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const db = require('./db');
|
||||
const tools = require('./tools');
|
||||
|
||||
module.exports.list = (source, fields, orderBy, queryData, start, limit, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let limitQuery = '';
|
||||
let limitValues = [];
|
||||
if (limit) {
|
||||
limitQuery = ' LIMIT ?';
|
||||
limitValues.push(limit);
|
||||
|
||||
if (start) {
|
||||
limitQuery += ' OFFSET ?';
|
||||
limitValues.push(start);
|
||||
}
|
||||
}
|
||||
|
||||
let whereClause = '';
|
||||
let whereValues = [];
|
||||
|
||||
if (queryData) {
|
||||
whereClause = ' WHERE ' + queryData.where;
|
||||
whereValues = queryData.values;
|
||||
}
|
||||
|
||||
connection.query('SELECT SQL_CALC_FOUND_ROWS ' + fields.join(', ') + ' FROM ' + source + whereClause + ' ORDER BY ' + orderBy + ' DESC' + limitQuery, whereValues.concat(limitValues), (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT FOUND_ROWS() AS total', (err, total) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, rows, total && total[0] && total[0].total);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.quicklist = (source, fields, orderBy, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT ' + fields.join(', ') + ' FROM ' + source + ' ORDER BY ' + orderBy + ' LIMIT 1000', (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, (rows || []).map(tools.convertKeys));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.filter = (source, fields, request, columns, searchFields, defaultOrdering, queryData, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT COUNT(*) AS total FROM ' + source;
|
||||
let values = [];
|
||||
|
||||
if (queryData) {
|
||||
query += ' WHERE ' + queryData.where;
|
||||
values = values.concat(queryData.values || []);
|
||||
}
|
||||
|
||||
connection.query(query, values, (err, total) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
total = total && total[0] && total[0].total || 0;
|
||||
|
||||
let ordering = [];
|
||||
|
||||
if (request.order && request.order.length) {
|
||||
|
||||
request.order.forEach(order => {
|
||||
let orderField = columns[Number(order.column)];
|
||||
let orderDirection = (order.dir || '').toString().toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
||||
if (orderField) {
|
||||
ordering.push(orderField + ' ' + orderDirection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!ordering.length) {
|
||||
ordering.push(defaultOrdering);
|
||||
}
|
||||
|
||||
let searchWhere = '';
|
||||
let searchArgs = [];
|
||||
|
||||
if (request.search && request.search.value) {
|
||||
let searchVal = '%' + request.search.value.replace(/\\/g, '\\\\').replace(/([%_])/g, '\\$1') + '%';
|
||||
|
||||
searchWhere = searchFields.map(field => field + ' LIKE ?').join(' OR ');
|
||||
searchArgs = searchFields.map(() => searchVal);
|
||||
}
|
||||
|
||||
let query = 'SELECT SQL_CALC_FOUND_ROWS ' + fields.join(', ') + ' FROM ' + source +' WHERE ' + (searchWhere ? '(' + searchWhere + ')': '1') + (queryData ? ' AND (' + queryData.where + ')' : '') + ' ORDER BY ' + ordering.join(', ') + ' LIMIT ? OFFSET ?';
|
||||
let args = searchArgs.concat(queryData ? queryData.values : []).concat([Number(request.length) || 50, Number(request.start) || 0]);
|
||||
|
||||
connection.query(query, args, (err, rows) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('SELECT FOUND_ROWS() AS total', (err, filteredTotal) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
rows = rows.map(row => tools.convertKeys(row));
|
||||
|
||||
filteredTotal = filteredTotal && filteredTotal[0] && filteredTotal[0].total || 0;
|
||||
return callback(null, rows, total, filteredTotal);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -1,50 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const _ = require('./translate')._;
|
||||
const util = require('util');
|
||||
const isemail = require('isemail');
|
||||
|
||||
const bluebird = require('bluebird');
|
||||
const mergeTemplateIntoLayout = bluebird.promisify(require('./tools').mergeTemplateIntoLayout);
|
||||
|
||||
module.exports = {
|
||||
validateEmail,
|
||||
validateEmailGetMessage,
|
||||
mergeTemplateIntoLayout
|
||||
};
|
||||
|
||||
async function validateEmail(address, checkBlocked) {
|
||||
let user = (address || '').toString().split('@').shift().toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
|
||||
if (checkBlocked && blockedUsers.indexOf(user) >= 0) {
|
||||
throw new new Error(util.format(_('Blocked email address "%s"'), address));
|
||||
}
|
||||
|
||||
const result = await new Promise(resolve => {
|
||||
const result = isemail.validate(address, {
|
||||
checkDNS: true,
|
||||
errorLevel: 1
|
||||
}, resolve);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateEmailGetMessage(result, address) {
|
||||
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 message;
|
||||
}
|
||||
}
|
||||
|
388
lib/tools.js
388
lib/tools.js
|
@ -1,283 +1,155 @@
|
|||
'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');
|
||||
const _ = require('./translate')._;
|
||||
const util = require('util');
|
||||
const isemail = require('isemail');
|
||||
|
||||
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'];
|
||||
const bluebird = require('bluebird');
|
||||
|
||||
module.exports = {
|
||||
queryParams,
|
||||
createSlug,
|
||||
updateMenu,
|
||||
validateEmail,
|
||||
formatMessage,
|
||||
getMessageLinks,
|
||||
prepareHtml,
|
||||
purifyHTML,
|
||||
mergeTemplateIntoLayout,
|
||||
workers: new Set()
|
||||
};
|
||||
const hasher = require('node-object-hash')();
|
||||
const mjml = require('mjml');
|
||||
const hbs = require('hbs');
|
||||
const juice = require('juice');
|
||||
|
||||
function queryParams(obj) {
|
||||
return Object.keys(obj).
|
||||
filter(key => key !== '_csrf').
|
||||
map(key => encodeURIComponent(key) + '=' + encodeURIComponent(obj[key])).
|
||||
join('&');
|
||||
}
|
||||
const fsReadFile = bluebird.promisify(require('fs').readFile);
|
||||
const jsdomEnv = bluebird.promisify(require('jsdom').env);
|
||||
|
||||
function createSlug(table, name, callback) {
|
||||
|
||||
let baseSlug = slugify(name).trim().toLowerCase() || 'list';
|
||||
let counter = 0;
|
||||
|
||||
if (baseSlug.length > 80) {
|
||||
baseSlug = baseSlug.substr(0, 80);
|
||||
}
|
||||
const templates = new Map();
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME - remove once we fully manage the menu in the client
|
||||
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'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME - either remove of delegate to validateEmail in tools-async (or vice-versa)
|
||||
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, '<br/>')), {
|
||||
useNamedReferences: true,
|
||||
allowUnsafeSymbols: true
|
||||
}) : (containsHTML ? htmlToText.fromString(value) : value);
|
||||
}
|
||||
async function getTemplate(template) {
|
||||
if (!template) {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
return message.replace(/\[([a-z0-9_]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => {
|
||||
identifier = identifier.toUpperCase();
|
||||
let value = getValue(identifier);
|
||||
if (value === false) {
|
||||
return match;
|
||||
const key = (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);
|
||||
} else {
|
||||
source = await fsReadFile(path.join(__dirname, '..', 'views', template), 'utf-8');
|
||||
}
|
||||
|
||||
if (template.type === 'mjml') {
|
||||
const compiled = mjml.mjml2html(source);
|
||||
|
||||
if (compiled.errors.length) {
|
||||
throw new Error(compiled.errors[0].message || compiled.errors[0]);
|
||||
}
|
||||
value = (value || fallback || '').trim();
|
||||
return filter(value);
|
||||
});
|
||||
|
||||
source = compiled.html;
|
||||
}
|
||||
|
||||
const renderer = hbs.handlebars.compile(compiled.html);
|
||||
templates.set(key, renderer);
|
||||
|
||||
return renderer;
|
||||
}
|
||||
|
||||
function prepareHtml(html, callback) {
|
||||
if (!(html || '').toString().trim()) {
|
||||
return callback(null, false);
|
||||
|
||||
async function mergeTemplateIntoLayout(template, layout) {
|
||||
layout = layout || '{{{body}}}';
|
||||
|
||||
async function readFile(relPath) {
|
||||
return await fsReadFile(path.join(__dirname, '..', 'views', relPath), 'utf-8');
|
||||
}
|
||||
jsdom.env(false, false, {
|
||||
|
||||
// Please dont end your custom messages with .hbs ...
|
||||
if (layout.endsWith('.hbs')) {
|
||||
layout = await readFile(layout);
|
||||
}
|
||||
|
||||
if (template.endsWith('.hbs')) {
|
||||
template = readFile(template);
|
||||
}
|
||||
|
||||
const source = layout.replace(/\{\{\{body\}\}\}/g, template);
|
||||
return source;
|
||||
}
|
||||
|
||||
|
||||
async function validateEmail(address, checkBlocked) {
|
||||
let user = (address || '').toString().split('@').shift().toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
|
||||
if (checkBlocked && blockedUsers.indexOf(user) >= 0) {
|
||||
throw new new Error(util.format(_('Blocked email address "%s"'), address));
|
||||
}
|
||||
|
||||
const result = await new Promise(resolve => {
|
||||
const result = isemail.validate(address, {
|
||||
checkDNS: true,
|
||||
errorLevel: 1
|
||||
}, resolve);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function validateEmailGetMessage(result, address) {
|
||||
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 message;
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareHtml(html) {
|
||||
if (!(html || '').toString().trim()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const win = await jsdomEnv(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;
|
||||
}
|
||||
const head = win.document.querySelector('head');
|
||||
let hasCharsetTag = false;
|
||||
const 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 = '<!doctype html><html>' + win.document.documentElement.innerHTML + '</html>';
|
||||
|
||||
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);
|
||||
}
|
||||
if (!hasCharsetTag) {
|
||||
const charsetTag = win.document.createElement('meta');
|
||||
charsetTag.setAttribute('charset', 'utf-8');
|
||||
head.appendChild(charsetTag);
|
||||
}
|
||||
const preparedHtml = '<!doctype html><html>' + win.document.documentElement.innerHTML + '</html>';
|
||||
|
||||
return juice(preparedHtml);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
validateEmail,
|
||||
validateEmailGetMessage,
|
||||
mergeTemplateIntoLayout,
|
||||
getTemplate,
|
||||
prepareHtml
|
||||
};
|
||||
|
||||
|
|
10
lib/urls.js
10
lib/urls.js
|
@ -1,23 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const url = require('url');
|
||||
const urllib = require('url');
|
||||
|
||||
function getTrustedUrl(path) {
|
||||
return config.www.trustedUrlBase + (path || '');
|
||||
return urllib.resolve(config.www.trustedUrlBase, path || '');
|
||||
}
|
||||
|
||||
function getSandboxUrl(path) {
|
||||
return config.www.sandboxUrlBase + (path || '');
|
||||
return urllib.resolve(config.www.sandboxUrlBase, path || '');
|
||||
}
|
||||
|
||||
function getTrustedUrlBaseDir() {
|
||||
const mailtrainUrl = url.parse(getTrustedUrl());
|
||||
const mailtrainUrl = urllib.parse(getTrustedUrl());
|
||||
return mailtrainUrl.pathname;
|
||||
}
|
||||
|
||||
function getSandboxUrlBaseDir() {
|
||||
const mailtrainUrl = url.parse(getSandboxUrl());
|
||||
const mailtrainUrl = urllib.parse(getSandboxUrl());
|
||||
return mailtrainUrl.pathname;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue