More or less all the functionality for selectable unsubscription process. Not tested yet!
Sending emails moved completely to controller. It felt strange to have some emails sent from the controller and some of them from the model. Confirmations refactored to an independent model that can be potentially used also for other actions that need an email confirmation.
This commit is contained in:
parent
32e2e61789
commit
bd4961366f
13 changed files with 672 additions and 488 deletions
|
@ -21,7 +21,8 @@ module.exports = {
|
|||
injectCustomFormData,
|
||||
injectCustomFormTemplates,
|
||||
filterCustomFields,
|
||||
getMjmlTemplate
|
||||
getMjmlTemplate,
|
||||
rollbackAndReleaseConnection
|
||||
};
|
||||
|
||||
function getDefaultMergeTags(callback) {
|
||||
|
@ -258,3 +259,10 @@ function captureFlashMessages(req, res, callback) {
|
|||
callback(null, flash);
|
||||
});
|
||||
}
|
||||
|
||||
function rollbackAndReleaseConnection(connection, callback) {
|
||||
connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1074,7 +1074,7 @@ module.exports.updateMessage = (message, status, updateSubscription, callback) =
|
|||
}
|
||||
|
||||
if (updateSubscription) {
|
||||
subscriptions.changeStatus(message.subscription, message.list, statusCode === 2 ? message.campaign : false, statusCode, callback);
|
||||
subscriptions.changeStatus(message.list, message.subscription, statusCode === 2 ? message.campaign : false, statusCode, callback);
|
||||
} else {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
|
90
lib/models/confirmations.js
Normal file
90
lib/models/confirmations.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
'use strict';
|
||||
|
||||
let db = require('../db');
|
||||
let shortid = require('shortid');
|
||||
let helpers = require('../helpers');
|
||||
|
||||
/*
|
||||
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, list, action, ip, JSON.stringify(data || {})], (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!result || !result.affectedRows) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -34,7 +34,8 @@ let allowedKeys = [
|
|||
'mail_confirm_address_change_text',
|
||||
'web_unsubscribed_notice',
|
||||
'mail_unsubscription_confirmed_html',
|
||||
'mail_unsubscription_confirmed_text'
|
||||
'mail_unsubscription_confirmed_text',
|
||||
'web_manual_unsubscribe_notice'
|
||||
];
|
||||
|
||||
|
||||
|
|
|
@ -245,8 +245,8 @@ module.exports.getCampaignResults = (campaign, select, clause, callback) => {
|
|||
const query = 'SELECT ' + selFields.join(', ') + ' FROM `subscription__' + campaign.list + '` subscribers INNER JOIN `campaign__' + campaign.id + '` campaign on subscribers.id=campaign.subscription LEFT JOIN `campaign_tracker__' + campaign.id + '` tracker on subscribers.id=tracker.subscriber ' + clause;
|
||||
|
||||
connection.query(query, (err, results) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,9 @@ let shortid = require('shortid');
|
|||
let tools = require('../tools');
|
||||
let helpers = require('../helpers');
|
||||
let fields = require('./fields');
|
||||
let geoip = require('geoip-ultralight');
|
||||
let segments = require('./segments');
|
||||
let settings = require('./settings');
|
||||
let mailer = require('../mailer');
|
||||
let urllib = require('url');
|
||||
let log = require('npmlog');
|
||||
let _ = require('../translate')._;
|
||||
let util = require('util');
|
||||
let tableHelpers = require('../table-helpers');
|
||||
|
@ -88,150 +85,17 @@ module.exports.filter = (listId, request, columns, segmentId, callback) => {
|
|||
|
||||
};
|
||||
|
||||
module.exports.addConfirmation = (list, email, ip, data, callback) => {
|
||||
let cid = shortid.generate();
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO confirmations (cid, list, email, ip, data) VALUES (?,?,?,?,?)';
|
||||
connection.query(query, [cid, list.id, email, ip, JSON.stringify(data || {})], (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!result || !result.affectedRows) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
if (data._skip) {
|
||||
log.info('Subscription', 'Confirmation message for %s marked to be skipped (%s)', email, JSON.stringify(data));
|
||||
return callback(null, cid);
|
||||
}
|
||||
|
||||
// FIXME - customize from the router
|
||||
const mailOpts = {
|
||||
ignoreDisableConfirmations: true
|
||||
};
|
||||
const relativeUrls = {
|
||||
confirmUrl: '/subscription/confirm/' + cid
|
||||
};
|
||||
module.exports.sendMail(list, email, 'confirm-subscription', _('%s: Please Confirm Subscription'), relativeUrls, mailOpts, data, (err) => {
|
||||
return callback(err, cid);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.processConfirmation = (cid, ip, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM confirmations WHERE cid=? LIMIT 1';
|
||||
connection.query(query, [cid], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let subscription;
|
||||
let listId = rows[0].list;
|
||||
let email = rows[0].email;
|
||||
try {
|
||||
subscription = JSON.parse(rows[0].data);
|
||||
} catch (E) {
|
||||
subscription = {};
|
||||
}
|
||||
|
||||
if (subscription._action === 'update') {
|
||||
if (!subscription.subscriber) { // Something went terribly wrong and we don't have data that we have originally provided
|
||||
return callback(new Error(_('Subscriber info corrupted or missing')));
|
||||
}
|
||||
|
||||
// update email address instead of adding new
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
let query = 'UPDATE `subscription__' + listId + '` SET `email`=? WHERE `id`=? LIMIT 1';
|
||||
let args = [email, subscription.subscriber];
|
||||
connection.query(query, args, err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('DELETE FROM confirmations WHERE `cid`=? LIMIT 1', [cid], () => {
|
||||
connection.release();
|
||||
return module.exports.getById(listId, subscription.subscriber, (err, subscriptionData) => {
|
||||
return callback(err, subscriptionData, subscription._action);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return;
|
||||
|
||||
} else if (subscription._action === 'unsubscribe') {
|
||||
// TODO
|
||||
return;
|
||||
|
||||
} else if (subscription._action === 'subscribe') {
|
||||
subscription.cid = cid;
|
||||
subscription.list = listId;
|
||||
subscription.email = email;
|
||||
|
||||
let optInCountry = geoip.lookupCountry(ip) || null;
|
||||
module.exports.insert(listId, {
|
||||
email,
|
||||
cid,
|
||||
optInIp: ip,
|
||||
optInCountry,
|
||||
status: 1
|
||||
}, subscription, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!result.entryId) {
|
||||
return callback(new Error(_('Could not save subscription')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
connection.query('DELETE FROM confirmations WHERE `cid`=? LIMIT 1', [cid], () => {
|
||||
connection.release();
|
||||
// reload full data from db in case it was an update, not insert
|
||||
return module.exports.getById(listId, result.entryId, (err, subscriptionData) => {
|
||||
return callback(err, subscriptionData, subscription._action);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
return callback(new Error(util.format(_('Subscription request corrupted - action: %s'), subscription._action)));
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.insert = (listId, meta, subscription, 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);
|
||||
subscription = tools.convertKeys(subscription);
|
||||
subscriptionData = tools.convertKeys(subscriptionData);
|
||||
|
||||
meta.email = meta.email || subscription.email;
|
||||
meta.email = meta.email || subscriptionData.email;
|
||||
meta.cid = meta.cid || shortid.generate();
|
||||
|
||||
fields.list(listId, (err, fieldList) => {
|
||||
|
@ -245,8 +109,8 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
let values = [];
|
||||
|
||||
let allowedKeys = ['first_name', 'last_name', 'tz', 'is_test'];
|
||||
Object.keys(subscription).forEach(key => {
|
||||
let value = subscription[key];
|
||||
Object.keys(subscriptionData).forEach(key => {
|
||||
let value = subscriptionData[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'tz') {
|
||||
value = (value || '').toString().toLowerCase().trim();
|
||||
|
@ -260,8 +124,7 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
}
|
||||
});
|
||||
|
||||
// FIXME - see issue #218
|
||||
fields.getValues(fields.getRow(fieldList, subscription, true, true, !!meta.partial), true).forEach(field => {
|
||||
fields.getValues(fields.getRow(fieldList, subscriptionData, true, true, !!meta.partial), true).forEach(field => {
|
||||
keys.push(field.key);
|
||||
values.push(field.value);
|
||||
});
|
||||
|
@ -280,10 +143,7 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
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 connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
let query;
|
||||
|
@ -297,6 +157,10 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
let statusChange = !existing || existing.status !== meta.status;
|
||||
let statusDirection;
|
||||
|
||||
if (existing && !meta.partial) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, new Error(_('Email address already registered')), callback);
|
||||
}
|
||||
|
||||
if (statusChange) {
|
||||
keys.push('status', 'status_change');
|
||||
values.push(meta.status, new Date());
|
||||
|
@ -307,10 +171,7 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
// nothing to update
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, {
|
||||
|
@ -334,10 +195,7 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
|
||||
connection.query(query, queryArgs, (err, result) => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
entryId = result.insertId || entryId;
|
||||
|
@ -345,17 +203,11 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
if (statusChange && statusDirection) {
|
||||
connection.query('UPDATE lists SET `subscribers`=`subscribers`' + statusDirection + '1 WHERE id=?', [listId], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, {
|
||||
|
@ -368,10 +220,7 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
} else {
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, {
|
||||
|
@ -529,7 +378,7 @@ module.exports.update = (listId, cid, updates, allowEmail, callback) => {
|
|||
}
|
||||
|
||||
if (!cid) {
|
||||
return callback(new Error(_('Missing subscription ID')));
|
||||
return callback(new Error(_('Missing Subscription ID')));
|
||||
}
|
||||
|
||||
fields.list(listId, (err, fieldList) => {
|
||||
|
@ -581,45 +430,7 @@ module.exports.update = (listId, cid, updates, allowEmail, callback) => {
|
|||
});
|
||||
};
|
||||
|
||||
module.exports.unsubscribe = (listId, subscriberCid, campaignId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
campaignId = (campaignId || '').toString().trim() || false;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
if (!subscriberCid) {
|
||||
return callback(new Error(_('Missing subscriber cid')));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT * FROM `subscription__' + listId + '` WHERE `cid`=?', [subscriberCid], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!rows || !rows.length || rows[0].status !== 1) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let subscription = tools.convertKeys(rows[0]);
|
||||
module.exports.changeStatus(subscription.id, listId, campaignId, 2, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, subscription);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.changeStatus = (id, listId, campaignId, status, callback) => {
|
||||
module.exports.changeStatus = (listId, id, campaignId, status, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
@ -632,17 +443,11 @@ module.exports.changeStatus = (id, listId, campaignId, status, callback) => {
|
|||
|
||||
connection.query('SELECT `status` FROM `subscription__' + listId + '` WHERE id=? LIMIT 1', [id], (err, rows) => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(null, false);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(null, false));
|
||||
}
|
||||
|
||||
let oldStatus = rows[0].status;
|
||||
|
@ -650,10 +455,7 @@ module.exports.changeStatus = (id, listId, campaignId, status, callback) => {
|
|||
let statusDirection;
|
||||
|
||||
if (!statusChange) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(null, true));
|
||||
}
|
||||
|
||||
if (statusChange && oldStatus === 1 || status === 1) {
|
||||
|
@ -662,19 +464,13 @@ module.exports.changeStatus = (id, listId, campaignId, status, callback) => {
|
|||
|
||||
connection.query('UPDATE `subscription__' + listId + '` SET `status`=?, `status_change`=NOW() WHERE id=? LIMIT 1', [status, id], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
if (!statusDirection) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
|
@ -683,20 +479,14 @@ module.exports.changeStatus = (id, listId, campaignId, status, callback) => {
|
|||
|
||||
connection.query('UPDATE `lists` SET `subscribers`=`subscribers`' + statusDirection + '1 WHERE id=? LIMIT 1', [listId], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(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 > 2) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
|
@ -705,10 +495,7 @@ module.exports.changeStatus = (id, listId, campaignId, status, callback) => {
|
|||
|
||||
connection.query('SELECT `id` FROM `campaigns` WHERE `cid`=? LIMIT 1', [campaignId], (err, rows) => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
let campaign = rows && rows[0] || false;
|
||||
|
@ -717,10 +504,7 @@ module.exports.changeStatus = (id, listId, campaignId, status, callback) => {
|
|||
// should not happend
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
|
@ -730,10 +514,7 @@ module.exports.changeStatus = (id, listId, campaignId, status, callback) => {
|
|||
// we should see only unsubscribe events here but you never know
|
||||
connection.query('UPDATE `campaigns` SET `unsubscribed`=`unsubscribed`' + (status === 2 ? '+' : '-') + '1 WHERE `cid`=? LIMIT 1', [campaignId], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
let query = 'UPDATE `campaign__' + campaign.id + '` SET `status`=? WHERE `list`=? AND `subscription`=? LIMIT 1';
|
||||
|
@ -742,18 +523,12 @@ module.exports.changeStatus = (id, listId, campaignId, status, callback) => {
|
|||
// Updated tracker status
|
||||
connection.query(query, values, err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, true);
|
||||
|
@ -805,19 +580,13 @@ module.exports.delete = (listId, cid, callback) => {
|
|||
|
||||
connection.query('DELETE FROM `subscription__' + listId + '` WHERE cid=? LIMIT 1', [cid], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
if (subscription.status !== 1) {
|
||||
return connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, subscription.email);
|
||||
|
@ -826,17 +595,11 @@ module.exports.delete = (listId, cid, callback) => {
|
|||
|
||||
connection.query('UPDATE lists SET subscribers=subscribers-1 WHERE id=? LIMIT 1', [listId], err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.commit(err => {
|
||||
if (err) {
|
||||
return connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
});
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
connection.release();
|
||||
return callback(null, subscription.email);
|
||||
|
@ -1028,13 +791,13 @@ module.exports.listImports = (listId, callback) => {
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports.updateAddress = (list, cid, updates, ip, callback) => {
|
||||
updates = tools.convertKeys(updates);
|
||||
/*
|
||||
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();
|
||||
|
||||
let emailNew = (updates.emailNew || '').toString().trim();
|
||||
|
||||
if (!list || !list.id) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
@ -1053,7 +816,7 @@ module.exports.updateAddress = (list, cid, updates, ip, callback) => {
|
|||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT `id`, `email` FROM `subscription__' + list.id + '` WHERE `cid`=? LIMIT 1';
|
||||
let query = 'SELECT * FROM `subscription__' + list.id + '` WHERE `cid`=? LIMIT 1';
|
||||
let args = [cid];
|
||||
connection.query(query, args, (err, rows) => {
|
||||
if (err) {
|
||||
|
@ -1072,7 +835,7 @@ module.exports.updateAddress = (list, cid, updates, ip, callback) => {
|
|||
|
||||
let old = rows[0];
|
||||
|
||||
let query = 'SELECT `id` FROM `subscription__' + list.id + '` WHERE `email`=? AND `cid`<>? LIMIT 1';
|
||||
let query = 'SELECT `id` FROM `subscription__' + list.id + '` WHERE `email`=? AND `cid`<>? AND `status`=1 LIMIT 1';
|
||||
let args = [emailNew, cid];
|
||||
connection.query(query, args, (err, rows) => {
|
||||
connection.release();
|
||||
|
@ -1080,18 +843,11 @@ module.exports.updateAddress = (list, cid, updates, ip, callback) => {
|
|||
return callback(err);
|
||||
}
|
||||
|
||||
if (rows && rows[0] && rows[0].id) {
|
||||
|
||||
|
||||
return callback(new Error(_('This address is already registered by someone else')));
|
||||
if (rows && rows.length > 0) {
|
||||
return callback(null, old, false);
|
||||
} else {
|
||||
return callback(null, old, true);
|
||||
}
|
||||
|
||||
module.exports.addConfirmation(list, emailNew, ip, {
|
||||
_action: 'update',
|
||||
cid,
|
||||
subscriber: old.id,
|
||||
emailOld: old.email
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1099,106 +855,64 @@ module.exports.updateAddress = (list, cid, updates, ip, callback) => {
|
|||
};
|
||||
|
||||
|
||||
module.exports.sendMail = (list, email, template, subject, relativeUrls, mailOpts, subscription, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let encryptionKeys = [];
|
||||
fields.getRow(fieldList, subscription).forEach(field => {
|
||||
if (field.type === 'gpg' && field.value) {
|
||||
encryptionKeys.push(field.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl', 'disableConfirmations'], (err, configItems) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!mailOpts.ignoreDisableConfirmations && configItems.disableConfirmations) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
title: list.name,
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
};
|
||||
|
||||
for (let relativeUrlKey in relativeUrls) {
|
||||
data[relativeUrlKey] = urllib.resolve(configItems.serviceUrl, relativeUrls[relativeUrlKey]);
|
||||
}
|
||||
|
||||
function sendMail(html, text) {
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: [].concat(subscription.firstName || []).concat(subscription.lastName || []).join(' '),
|
||||
address: email
|
||||
},
|
||||
subject: util.format(subject, list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
text,
|
||||
data
|
||||
}, err => {
|
||||
if (err) {
|
||||
log.error('Subscription', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let text = {
|
||||
template: 'subscription/mail-' + template + '-text.hbs'
|
||||
};
|
||||
|
||||
let html = {
|
||||
template: 'subscription/mail-' + template + '-html.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs',
|
||||
type: 'mjml'
|
||||
};
|
||||
|
||||
helpers.injectCustomFormTemplates(list.defaultForm, { text, html }, (err, tmpl) => {
|
||||
if (err) {
|
||||
return sendMail(html, text);
|
||||
}
|
||||
|
||||
sendMail(tmpl.html, tmpl.text);
|
||||
});
|
||||
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
FIXME
|
||||
function getUnsubscriptionMode = (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));
|
||||
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);
|
||||
}
|
||||
return callback(err, rows, total);
|
||||
connection.beginTransaction(err => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'SELECT `id` FROM `subscription__' + listId + '` WHERE `email`=? AND `id`<>? AND `status`=1 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, new Error(_('Email address already registered')), callback);
|
||||
}
|
||||
|
||||
let query = 'DELETE FROM `subscription__' + listId + '` WHERE `email`=? AND `id`<>?';
|
||||
let args = [emailNew, subscriptionId];
|
||||
connection.query(query, args, (err, rows) => {
|
||||
if (err) {
|
||||
return helpers.rollbackAndReleaseConnection(connection, () => callback(err));
|
||||
}
|
||||
|
||||
let query = 'UPDATE `subscription__' + listId + '` SET `email`=? WHERE `id`=? LIMIT 1';
|
||||
let args = [emailNew, subscriptionId];
|
||||
connection.query(query, args, 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
module.exports.getUnsubscriptionMode = (list, subscriptionId) => {
|
||||
// TODO: Once the unsubscription mode is customizable per segment, then this will be a good place to process it.
|
||||
return list.unsubscriptionMode;
|
||||
};
|
||||
|
||||
*/
|
166
lib/subscription-mail-helpers.js
Normal file
166
lib/subscription-mail-helpers.js
Normal file
|
@ -0,0 +1,166 @@
|
|||
'use strict';
|
||||
|
||||
const log = require('npmlog');
|
||||
const config = require('config');
|
||||
let db = require('./db');
|
||||
let fields = require('./models/fields');
|
||||
let settings = require('./models/settings');
|
||||
let mailer = require('./mailer');
|
||||
let urllib = require('url');
|
||||
let helpers = require('./helpers');
|
||||
let tools = require('./tools');
|
||||
let _ = require('./translate')._;
|
||||
let util = require('util');
|
||||
|
||||
|
||||
module.exports = {
|
||||
sendAlreadySubscribed,
|
||||
sendConfirmAddressChange,
|
||||
sendConfirmSubscription,
|
||||
sendConfirmUnsubscription,
|
||||
sendSubscriptionConfirmed,
|
||||
sendUnsubscriptionConfirmed
|
||||
};
|
||||
|
||||
function sendSubscriptionConfirmed(list, email, subscription, callback) {
|
||||
const relativeUrls = {
|
||||
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
|
||||
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
|
||||
};
|
||||
|
||||
subscriptions.sendMail(list, email, 'subscription-confirmed', _('%s: Subscription Confirmed'), relativeUrls, {}, data.subscriptionData, callback);
|
||||
}
|
||||
|
||||
function sendAlreadySubscribed(list, email, subscription, callback) {
|
||||
const mailOpts = {
|
||||
ignoreDisableConfirmations: true
|
||||
};
|
||||
const relativeUrls = {
|
||||
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
|
||||
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
|
||||
};
|
||||
module.exports.sendMail(list, email, 'already-subscribed', _('%s: Email Address Already Registered'), relativeUrls, mailOpts, subscription, callback);
|
||||
}
|
||||
|
||||
function sendConfirmAddressChange(list, email, cid, subscription, callback) {
|
||||
const mailOpts = {
|
||||
ignoreDisableConfirmations: true
|
||||
};
|
||||
const relativeUrls = {
|
||||
confirmUrl: '/subscription/confirm/' + cid
|
||||
};
|
||||
module.exports.sendMail(list, email, 'confirm-address-change', _('%s: Please Confirm Email Change in Subscription'), relativeUrls, mailOpts, subscription, callback);
|
||||
}
|
||||
|
||||
function sendConfirmSubscription(list, email, cid, subscription, callback) {
|
||||
const mailOpts = {
|
||||
ignoreDisableConfirmations: true
|
||||
};
|
||||
const relativeUrls = {
|
||||
confirmUrl: '/subscription/confirm/' + cid
|
||||
};
|
||||
module.exports.sendMail(list, email, 'confirm-subscription', _('%s: Please Confirm Subscription'), relativeUrls, mailOpts, subscription, callback);
|
||||
}
|
||||
|
||||
function sendConfirmUnsubscription(list, email, cid, subscription, callback) {
|
||||
const mailOpts = {
|
||||
ignoreDisableConfirmations: true
|
||||
};
|
||||
const relativeUrls = {
|
||||
confirmUrl: '/subscription/confirm/' + cid
|
||||
};
|
||||
module.exports.sendMail(list, email, 'confirm-unsubscription', _('%s: Please Confirm Unsubscription'), relativeUrls, mailOpts, subscription, callback);
|
||||
}
|
||||
|
||||
function sendUnsubscriptionConfirmed(list, email, subscription, callback) {
|
||||
const relativeUrls = {
|
||||
subscribeUrl: '/subscription/' + list.cid + '?cid=' + subscription.cid
|
||||
};
|
||||
subscriptions.sendMail(list, email, 'unsubscription-confirmed', _('%s: Unsubscribe Confirmed'), relativeUrls, {}, subscription, callback);
|
||||
}
|
||||
|
||||
|
||||
function sendMail(list, email, template, subject, relativeUrls, mailOpts, subscription, callback) {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let encryptionKeys = [];
|
||||
fields.getRow(fieldList, subscription).forEach(field => {
|
||||
if (field.type === 'gpg' && field.value) {
|
||||
encryptionKeys.push(field.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl', 'disableConfirmations'], (err, configItems) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!mailOpts.ignoreDisableConfirmations && configItems.disableConfirmations) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
title: list.name,
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
};
|
||||
|
||||
for (let relativeUrlKey in relativeUrls) {
|
||||
data[relativeUrlKey] = urllib.resolve(configItems.serviceUrl, relativeUrls[relativeUrlKey]);
|
||||
}
|
||||
|
||||
function sendMail(html, text) {
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: [].concat(subscription.firstName || []).concat(subscription.lastName || []).join(' '),
|
||||
address: email
|
||||
},
|
||||
subject: util.format(subject, list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
text,
|
||||
data
|
||||
}, err => {
|
||||
if (err) {
|
||||
log.error('Subscription', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let text = {
|
||||
template: 'subscription/mail-' + template + '-text.hbs'
|
||||
};
|
||||
|
||||
let html = {
|
||||
template: 'subscription/mail-' + template + '-html.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs',
|
||||
type: 'mjml'
|
||||
};
|
||||
|
||||
helpers.injectCustomFormTemplates(list.defaultForm, { text, html }, (err, tmpl) => {
|
||||
if (err) {
|
||||
return sendMail(html, text);
|
||||
}
|
||||
|
||||
sendMail(tmpl.html, tmpl.text);
|
||||
});
|
||||
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -266,6 +266,11 @@ router.get('/:list/edit/:form', passport.csrfProtection, (req, res) => {
|
|||
label: _('Mail - Unsubscription Confirmed (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}, {
|
||||
name: 'web_manual_unsubscribe_notice',
|
||||
label: _('Web - Manual Unsubscribe Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}]
|
||||
}
|
||||
];
|
||||
|
|
|
@ -4,10 +4,8 @@ let log = require('npmlog');
|
|||
let config = require('config');
|
||||
let tools = require('../lib/tools');
|
||||
let helpers = require('../lib/helpers');
|
||||
let mailer = require('../lib/mailer');
|
||||
let passport = require('../lib/passport');
|
||||
let express = require('express');
|
||||
let urllib = require('url');
|
||||
let router = new express.Router();
|
||||
let lists = require('../lib/models/lists');
|
||||
let fields = require('../lib/models/fields');
|
||||
|
@ -18,6 +16,9 @@ let _ = require('../lib/translate')._;
|
|||
let util = require('util');
|
||||
let cors = require('cors');
|
||||
let cache = require('memory-cache');
|
||||
let geoip = require('geoip-ultralight');
|
||||
let confirmations = require('../lib/models/confirmations');
|
||||
let mailHelpers = require('../lib/subscription-mail-helpers');
|
||||
|
||||
let originWhitelist = config.cors && config.cors.origins || [];
|
||||
|
||||
|
@ -45,7 +46,7 @@ let corsOrCsrfProtection = (req, res, next) => {
|
|||
};
|
||||
|
||||
router.get('/confirm/:cid', (req, res, next) => {
|
||||
subscriptions.processConfirmation(req.params.cid, req.ip, (err, subscription, action) => {
|
||||
confirmations.takeConfirmation(req.params.cid, (err, confirmation) => {
|
||||
if (!err && !subscription) {
|
||||
err = new Error(_('Selected subscription not found'));
|
||||
err.status = 404;
|
||||
|
@ -55,7 +56,9 @@ router.get('/confirm/:cid', (req, res, next) => {
|
|||
return next(err);
|
||||
}
|
||||
|
||||
lists.get(subscription.list, (err, list) => {
|
||||
const data = confirmation.data;
|
||||
|
||||
lists.get(confirmation.listId, (err, list) => {
|
||||
if (!err && !list) {
|
||||
err = new Error(_('Selected list not found'));
|
||||
err.status = 404;
|
||||
|
@ -65,23 +68,89 @@ router.get('/confirm/:cid', (req, res, next) => {
|
|||
return next(err);
|
||||
}
|
||||
|
||||
// FIXME - differentiate email based on action
|
||||
|
||||
const relativeUrls = {
|
||||
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
|
||||
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
|
||||
};
|
||||
|
||||
subscriptions.sendMail(list, subscription.email, 'subscription-confirmed', _('%s: Subscription Confirmed'), relativeUrls, {}, subscription, (err) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
log.error('Subscription', err);
|
||||
return res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/unsubscribe/' + encodeURIComponent(req.body.cid) + '?' + tools.queryParams(req.body));
|
||||
if (confirmation.action === 'change-address') {
|
||||
if (!data.subscriptionId) { // Something went terribly wrong and we don't have data that we have originally provided
|
||||
return next(new Error(_('Subscriber info corrupted or missing')));
|
||||
}
|
||||
|
||||
res.redirect('/subscription/' + list.cid + '/subscribed-notice');
|
||||
});
|
||||
subscriptions.updateAddress(list.id, data.subscriptionId, data.emailNew, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
subscriptions.getById(list.id, subscriptionId, (err, subscription) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
mailHelpers.sendSubscriptionConfirmed(list, data.emailNew, subscription, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.redirect('/subscription/' + list.cid + '/manage-address/' + subscription.cid);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} else if (confirmation.action === 'subscribe') {
|
||||
let optInCountry = geoip.lookupCountry(confirmation.ip) || null;
|
||||
|
||||
const meta = {
|
||||
email: data.email,
|
||||
optInIp: confirmation.ip,
|
||||
optInCountry,
|
||||
status: 1
|
||||
};
|
||||
|
||||
subscriptions.insert(list.id, meta, data.subscriptionData, (err, result) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!result.entryId) {
|
||||
return next(new Error(_('Could not save subscription')));
|
||||
}
|
||||
|
||||
subscriptions.getById(list.id, result.entryId, (err, subscription) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
mailHelpers.sendSubscriptionConfirmed(list, data.email, subscription, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.redirect('/subscription/' + list.cid + '/subscribed-notice');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} else if (confirmation.action === 'unsubscribe') {
|
||||
subscriptions.changeStatus(list.id, confirmation.data.subscriptionId, confirmation.data.campaignId, 2, (err, found) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// TODO: Shall we do anything with "found"?
|
||||
|
||||
subscriptions.getById(list.id, confirmation.data.subscriptionId, (err, subscription) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.redirect('/subscription/' + req.params.lcid + '/unsubscribed-notice');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -262,7 +331,7 @@ router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, r
|
|||
}
|
||||
|
||||
// Check if the subscriber seems legit. This is a really simple check, the only requirement is that
|
||||
// the subsciber has JavaScript turned on and thats it. If Mailtrain gets more targeted then this
|
||||
// the subscriber has JavaScript turned on and thats it. If Mailtrain gets more targeted then this
|
||||
// simple check should be replaced with an actual captcha
|
||||
let subTime = Number(req.body.sub) || 0;
|
||||
// allow clock skew 24h in the past and 24h to the future
|
||||
|
@ -285,39 +354,67 @@ router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, r
|
|||
return req.xhr ? sendJsonError(err) : next(err);
|
||||
}
|
||||
|
||||
let data = {};
|
||||
let subscriptionData = {};
|
||||
Object.keys(req.body).forEach(key => {
|
||||
if (key !== 'email' && key.charAt(0) !== '_') {
|
||||
data[key] = (req.body[key] || '').toString().trim();
|
||||
subscriptionData[key] = (req.body[key] || '').toString().trim();
|
||||
}
|
||||
});
|
||||
subscriptionData = tools.convertKeys(data);
|
||||
|
||||
data = tools.convertKeys(data);
|
||||
|
||||
data._address = req.body.address;
|
||||
data._sub = req.body.sub;
|
||||
data._skip = !testsPass;
|
||||
data._action = 'subscribe';
|
||||
|
||||
subscriptions.addConfirmation(list, email, req.ip, data, (err, confirmCid) => {
|
||||
if (!err && !confirmCid) {
|
||||
err = new Error(_('Could not store confirmation data'));
|
||||
}
|
||||
|
||||
subscriptions.getByEmail(list.id, email, (err, subscription) => {
|
||||
if (err) {
|
||||
if (req.xhr) {
|
||||
return sendJsonError(err);
|
||||
}
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
||||
return req.xhr ? sendJsonError(err) : next(err);
|
||||
}
|
||||
|
||||
if (req.xhr) {
|
||||
return res.status(200).json({
|
||||
msg: _('Please Confirm Subscription')
|
||||
if (subscription) {
|
||||
mailHelpers.sendAlreadySubscribed(list, email, subscription, (err) => {
|
||||
if (err) {
|
||||
return req.xhr ? sendJsonError(err) : next(err);
|
||||
}
|
||||
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
|
||||
});
|
||||
} else {
|
||||
const data = {
|
||||
email,
|
||||
subscriptionData
|
||||
};
|
||||
|
||||
confirmations.addConfirmation(list.id, 'subscribe', req.ip, data, (err, confirmCid) => {
|
||||
if (!err && !confirmCid) {
|
||||
err = new Error(_('Could not store confirmation data'));
|
||||
}
|
||||
|
||||
if (err) {
|
||||
if (req.xhr) {
|
||||
return sendJsonError(err);
|
||||
}
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
||||
}
|
||||
|
||||
function sendWebResponse() {
|
||||
if (req.xhr) {
|
||||
return res.status(200).json({
|
||||
msg: _('Please Confirm Subscription')
|
||||
});
|
||||
}
|
||||
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
|
||||
}
|
||||
|
||||
if (!testsPass) {
|
||||
log.info('Subscription', 'Confirmation message for %s marked to be skipped (%s)', email, JSON.stringify(data));
|
||||
sendWebResponse();
|
||||
} else {
|
||||
sendConfirmSubscription(list, email, confirmCid, data, (err) => {
|
||||
if (err) {
|
||||
return req.xhr ? sendJsonError(err) : sendWebResponse(err);
|
||||
}
|
||||
sendWebResponse();
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -492,15 +589,41 @@ router.post('/:lcid/manage-address', passport.parseForm, passport.csrfProtection
|
|||
return next(err);
|
||||
}
|
||||
|
||||
subscriptions.updateAddress(list, req.body.cid, req.body, req.ip, err => {
|
||||
const emailNew = (req.body.emailNew || '').toString().trim();
|
||||
|
||||
subscriptions.updateAddressCheck(list, req.body.cid, emailNew, req.ip, (err, subscription, newEmailAvailable) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
log.error('Subscription', err);
|
||||
return res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/manage-address/' + encodeURIComponent(req.body.cid) + '?' + tools.queryParams(req.body));
|
||||
}
|
||||
|
||||
req.flash('info', _('An email with further instructions has been sent to the provided address'));
|
||||
res.redirect('/subscription/' + req.params.lcid + '/manage/' + req.body.cid);
|
||||
function sendWebResponse(err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
req.flash('info', _('An email with further instructions has been sent to the provided address'));
|
||||
res.redirect('/subscription/' + req.params.lcid + '/manage/' + req.body.cid);
|
||||
}
|
||||
|
||||
if (newEmailAvailable) {
|
||||
const data = {
|
||||
subscriptionId: subscription.id,
|
||||
emailNew
|
||||
};
|
||||
|
||||
confirmations.addConfirmation(list.id, 'change-address', req.ip, data, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
mailHelpers.sendConfirmAddressChange(list, emailNew, subscription, sendWebResponse);
|
||||
});
|
||||
|
||||
} else {
|
||||
mailHelpers.sendAlreadySubscribed(list, emailNew, subscription, sendWebResponse);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -525,7 +648,7 @@ router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next)
|
|||
|
||||
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
|
||||
if (!err && !subscription) {
|
||||
err = new Error(_('Subscription not found from this list'));
|
||||
err = new Error(_('Subscription not found in this list'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
|
@ -533,40 +656,47 @@ router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next)
|
|||
return next(err);
|
||||
}
|
||||
|
||||
subscription.lcid = req.params.lcid;
|
||||
subscription.ucid = req.params.ucid;
|
||||
subscription.title = list.name;
|
||||
subscription.csrfToken = req.csrfToken();
|
||||
subscription.campaign = req.query.c;
|
||||
subscription.defaultAddress = configItems.defaultAddress;
|
||||
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
||||
|
||||
subscription.template = {
|
||||
template: 'subscription/web-unsubscribe.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
};
|
||||
if (list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
|
||||
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-unsubscribe', subscription, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
subscription.lcid = req.params.lcid;
|
||||
subscription.ucid = req.params.ucid;
|
||||
subscription.title = list.name;
|
||||
subscription.csrfToken = req.csrfToken();
|
||||
subscription.campaign = req.query.c;
|
||||
subscription.defaultAddress = configItems.defaultAddress;
|
||||
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
subscription.template = {
|
||||
template: 'subscription/web-unsubscribe.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-unsubscribe', subscription, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} else { // UnsubscriptionMode.ONE_STEP || UnsubscriptionMode.TWO_STEP || UnsubscriptionMode.MANUAL
|
||||
handleUnsubscribe(list, subscription, res);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -583,47 +713,95 @@ router.post('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, (
|
|||
return next(err);
|
||||
}
|
||||
|
||||
subscriptions.unsubscribe(list.id, req.body.ucid, req.body.campaign, (err, subscription) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
log.error('Subscription', err);
|
||||
return res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/unsubscribe/' + encodeURIComponent(req.body.ucid) + '?' + tools.queryParams(req.body));
|
||||
const campaignId = (req.body.campaign || '').toString().trim() || false;
|
||||
|
||||
subscriptions.get(list.id, req.body.ucid, (err, subscription) => {
|
||||
if (!err && !subscription) {
|
||||
err = new Error(_('Subscription not found in this list'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
const relativeUrls = {
|
||||
subscribeUrl: '/subscription/' + list.cid + '?cid=' + subscription.cid
|
||||
};
|
||||
subscriptions.sendMail(list, subscription.email, 'unsubscription-confirmed', _('%s: Unsubscribe Confirmed'), relativeUrls, {}, subscription, (err) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
log.error('Subscription', err);
|
||||
return res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/unsubscribe/' + encodeURIComponent(subscription.cid) + '?' + tools.queryParams(req.body));
|
||||
}
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.redirect('/subscription/' + req.params.lcid + '/unsubscribed-notice');
|
||||
});
|
||||
handleUnsubscribe(list, subscription, res);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function handleUnsubscribe(list, subscription, res) {
|
||||
if (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP ||
|
||||
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
||||
|
||||
const data = {
|
||||
subscriptionId: subscription.id,
|
||||
campaignId
|
||||
};
|
||||
|
||||
confirmations.addConfirmation(list.id, 'unsubscribe', req.ip, data, (err, confirmCid) => {
|
||||
if (!err && !confirmCid) {
|
||||
err = new Error(_('Could not store confirmation data'));
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
mailHelpers.sendConfirmUnsubscription(list, subscription.email, subscription, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
|
||||
});
|
||||
});
|
||||
|
||||
} else if (list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP ||
|
||||
list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) {
|
||||
|
||||
subscriptions.changeStatus(subscription.id, list.id, campaignId, 2, (err, found) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// TODO: Shall we do anything with "found"?
|
||||
|
||||
mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
|
||||
});
|
||||
});
|
||||
} else { // UnsubscriptionMode.MANUAL
|
||||
res.redirect('/subscription/' + list.cid + '/manual-unsubscribe-notice');
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/:cid/confirm-subscription-notice', (req, res, next) => {
|
||||
notice('confirm-subscription', req, res, next);
|
||||
webNotice('confirm-subscription', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/confirm-unsubscription-notice', (req, res, next) => {
|
||||
notice('confirm-unsubscription', req, res, next);
|
||||
webNotice('confirm-unsubscription', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/subscribed-notice', (req, res, next) => {
|
||||
notice('subscribed', req, res, next);
|
||||
webNotice('subscribed', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/updated-notice', (req, res, next) => {
|
||||
notice('updated', req, res, next);
|
||||
webNotice('updated', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/unsubscribed-notice', (req, res, next) => {
|
||||
notice('unsubscribed', req, res, next);
|
||||
webNotice('unsubscribed', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/manual-unsubscribe-notice', (req, res, next) => {
|
||||
webNotice('manual-unsubscribe', req, res, next);
|
||||
});
|
||||
|
||||
router.post('/publickey', passport.parseForm, (req, res, next) => {
|
||||
|
@ -665,7 +843,7 @@ router.post('/publickey', passport.parseForm, (req, res, next) => {
|
|||
});
|
||||
|
||||
|
||||
function notice(type, req, res, next) {
|
||||
function webNotice(type, req, res, next) {
|
||||
lists.getByCid(req.params.cid, (err, list) => {
|
||||
if (!err && !list) {
|
||||
err = new Error(_('Selected list not found'));
|
||||
|
@ -676,7 +854,7 @@ function notice(type, req, res, next) {
|
|||
return next(err);
|
||||
}
|
||||
|
||||
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
@ -686,6 +864,7 @@ function notice(type, req, res, next) {
|
|||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
defaultAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
template: {
|
||||
template: 'subscription/web-' + type + '-notice.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
|
@ -718,5 +897,4 @@ function notice(type, req, res, next) {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
@ -5,8 +5,17 @@ SET @schema_version = '28';
|
|||
# Add unsubscription mode field to lists
|
||||
ALTER TABLE `lists` ADD COLUMN `unsubscription_mode` int(11) unsigned DEFAULT 0 NOT NULL AFTER `public_subscribe`;
|
||||
|
||||
# Delete all confirmations as we use different structure in "data".
|
||||
DELETE FROM `confirmations`;
|
||||
|
||||
# Change the name of the column to better reflect that confirmations are also used for unsubscription and email address update
|
||||
# Drop email field as this does not have a clear semantics in change address. Since email is not used to search in the table,
|
||||
# it can be stored in data
|
||||
# Create field action to distinguish between different confirmation types (subscribe, unsubscribe, change-address)
|
||||
ALTER TABLE `confirmations` CHANGE `opt_in_ip` `ip` varchar(100) DEFAULT NULL;
|
||||
ALTER TABLE `confirmations` DROP `email`;
|
||||
ALTER TABLE `confirmations` ADD COLUMN `action` varchar(100) NOT NULL AFTER `list`;
|
||||
|
||||
|
||||
# Rename affected forms in custom_forms_data
|
||||
update custom_forms_data set data_key="mail_confirm_subscription_html" where data_key="mail_confirm_html";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Email address already subscribed{{/translate}}
|
||||
{{#translate}}Email address already registered{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}We have received a subscription request. Your email address is however already registered.{{/translate}}.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{{{title}}}
|
||||
{{#translate}}Email address already subscribed{{/translate}}
|
||||
{{#translate}}Email address already registered{{/translate}}
|
||||
================================
|
||||
|
||||
{{#translate}}We have received a subscription request. Your email address is however already registered.{{/translate}}
|
||||
|
|
13
views/subscription/web-manual-unsubscribe-notice.mjml.hbs
Normal file
13
views/subscription/web-manual-unsubscribe-notice.mjml.hbs
Normal file
|
@ -0,0 +1,13 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Online Unsubscription Is Not Possible{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}Please contact us at{{/translate}} <a href="mailto:{{contactAddress}}">{{contactAddress}}</a> {{#translate}}to get removed from the list{{/translate}}.
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{homepage}}">
|
||||
{{#translate}}Return to our website{{/translate}}
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
Loading…
Reference in a new issue