WiP on mailers

This commit is contained in:
Tomas Bures 2018-04-29 18:13:40 +02:00
parent e97415c237
commit a4ee1534cc
46 changed files with 1263 additions and 529 deletions

View file

@ -0,0 +1,86 @@
'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

View file

@ -0,0 +1,91 @@
'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);
});
});
});
});
});
};

View file

@ -0,0 +1,617 @@
'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);
}

View file

@ -0,0 +1,418 @@
'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;
}

View file

@ -0,0 +1,364 @@
'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
});
});
});
});
}

View file

@ -0,0 +1,332 @@
'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);
});
});
}

View file

@ -0,0 +1,688 @@
'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);
});
});
});
});
};

View file

@ -0,0 +1,926 @@
'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.

View file

@ -0,0 +1,176 @@
'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);
});
});
};

View file

@ -0,0 +1,384 @@
'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);
});
});
}