Merge with upstream
This commit is contained in:
commit
25bb4afa80
60 changed files with 2177 additions and 1215 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,3 +1,6 @@
|
|||
/.idea
|
||||
/last-failed-e2e-test.*
|
||||
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
|
|
5
app.js
5
app.js
|
@ -184,8 +184,9 @@ app.use((req, res, next) => {
|
|||
res.locals.customScripts = config.customscripts || [];
|
||||
|
||||
let bodyClasses = [];
|
||||
app.get('env') === 'test' && bodyClasses.push('page--' + (req.path.substring(1).replace(/\//g, '--') || 'home'));
|
||||
req.user && bodyClasses.push('logged-in user-' + req.user.username);
|
||||
if (req.user) {
|
||||
bodyClasses.push('logged-in user-' + req.user.username);
|
||||
}
|
||||
res.locals.bodyClass = bodyClasses.join(' ');
|
||||
|
||||
settingsModel.list(['ua_code', 'shoutout'], (err, configItems) => {
|
||||
|
|
|
@ -22,7 +22,8 @@ module.exports = {
|
|||
injectCustomFormData,
|
||||
injectCustomFormTemplates,
|
||||
filterCustomFields,
|
||||
getMjmlTemplate
|
||||
getMjmlTemplate,
|
||||
rollbackAndReleaseConnection
|
||||
};
|
||||
|
||||
function getDefaultMergeTags(callback) {
|
||||
|
@ -124,7 +125,7 @@ function filterCustomFields(customFieldsIn = [], fieldIds = [], method = 'includ
|
|||
id: 'email',
|
||||
name: 'Email Address',
|
||||
type: 'Email',
|
||||
typeSubsciptionEmail: true
|
||||
typeSubscriptionEmail: true
|
||||
}, {
|
||||
id: 'firstname',
|
||||
name: 'First Name',
|
||||
|
@ -285,3 +286,10 @@ function captureFlashMessages(req, res, callback) {
|
|||
callback(null, flash);
|
||||
});
|
||||
}
|
||||
|
||||
function rollbackAndReleaseConnection(connection, callback) {
|
||||
connection.rollback(() => {
|
||||
connection.release();
|
||||
return callback();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1056,13 +1056,13 @@ module.exports.updateMessage = (message, status, updateSubscription, callback) =
|
|||
|
||||
let statusCode;
|
||||
if (status === 'unsubscribed') {
|
||||
statusCode = 2;
|
||||
}
|
||||
if (status === 'bounced') {
|
||||
statusCode = 3;
|
||||
}
|
||||
if (status === 'complained') {
|
||||
statusCode = 4;
|
||||
statusCode = subscriptions.Status.UNSUBSCRIBED;
|
||||
} else if (status === 'bounced') {
|
||||
statusCode = subscriptions.Status.BOUNCED;
|
||||
} else if (status === 'complained') {
|
||||
statusCode = subscriptions.Status.COMPLAINED;
|
||||
} else {
|
||||
return callback(new Error(_('Unrecognized message status')));
|
||||
}
|
||||
|
||||
let query = 'UPDATE `campaigns` SET `' + status + '`=`' + status + '`+1 WHERE id=? LIMIT 1';
|
||||
|
@ -1076,7 +1076,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 === subscriptions.Status.UNSUBSCRIBED ? message.campaign : false, statusCode, callback);
|
||||
} else {
|
||||
return callback(null, true);
|
||||
}
|
||||
|
|
91
lib/models/confirmations.js
Normal file
91
lib/models/confirmations.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -14,22 +14,31 @@ let allowedKeys = [
|
|||
'fields_shown_on_manage',
|
||||
'layout',
|
||||
'form_input_style',
|
||||
'mail_confirm_html',
|
||||
'mail_confirm_text',
|
||||
'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',
|
||||
'mail_unsubscribe_confirmed_html',
|
||||
'mail_unsubscribe_confirmed_text',
|
||||
'web_confirm_notice',
|
||||
'web_manage_address',
|
||||
'web_manage',
|
||||
'web_subscribe',
|
||||
'web_subscribed',
|
||||
'web_unsubscribe_notice',
|
||||
'web_manage_address',
|
||||
'web_updated_notice',
|
||||
'web_unsubscribe',
|
||||
'web_updated_notice'
|
||||
'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;
|
||||
|
||||
|
|
|
@ -7,7 +7,18 @@ let segments = require('./segments');
|
|||
let _ = require('../translate')._;
|
||||
let tableHelpers = require('../table-helpers');
|
||||
|
||||
let allowedKeys = ['description', 'default_form', 'public_subscribe'];
|
||||
const UnsubscriptionMode = {
|
||||
ONE_STEP: 0,
|
||||
ONE_STEP_WITH_FORM: 1,
|
||||
TWO_STEP: 2,
|
||||
TWO_STEP_WITH_FORM: 3,
|
||||
MANUAL: 4,
|
||||
MAX: 5
|
||||
};
|
||||
|
||||
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);
|
||||
|
@ -99,6 +110,63 @@ module.exports.get = (id, callback) => {
|
|||
});
|
||||
};
|
||||
|
||||
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);
|
||||
|
@ -157,54 +225,6 @@ module.exports.create = (list, callback) => {
|
|||
});
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
updates = updates || {};
|
||||
id = Number(id) || 0;
|
||||
|
||||
let data = tools.convertKeys(updates);
|
||||
data.publicSubscribe = data.publicSubscribe ? 1 : 0;
|
||||
|
||||
let name = (data.name || '').toString().trim();
|
||||
let keys = ['name'];
|
||||
let values = [name];
|
||||
|
||||
if (id < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return callback(new Error(_('List Name must be set')));
|
||||
}
|
||||
|
||||
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.delete = (id, callback) => {
|
||||
id = Number(id) || 0;
|
||||
|
||||
|
|
|
@ -13,7 +13,8 @@ const ReportState = {
|
|||
SCHEDULED: 0,
|
||||
PROCESSING: 1,
|
||||
FINISHED: 2,
|
||||
FAILED: 3
|
||||
FAILED: 3,
|
||||
MAX: 4
|
||||
};
|
||||
|
||||
module.exports.ReportState = ReportState;
|
||||
|
@ -245,8 +246,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) => {
|
||||
if (err) {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,8 @@ function listValues(filter, callback) {
|
|||
filter = false;
|
||||
}
|
||||
|
||||
// TODO: It would be good to cache the settings. It feels awkward to always go to DB to retrieve something what is essentially a constant
|
||||
|
||||
filter = [].concat(filter || []).map(key => tools.toDbKey(key));
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
|
|
|
@ -5,16 +5,20 @@ 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');
|
||||
|
||||
const Status = {
|
||||
SUBSCRIBED: 1,
|
||||
UNSUBSCRIBED: 2,
|
||||
BOUNCED: 3,
|
||||
COMPLAINED: 4,
|
||||
MAX: 5
|
||||
};
|
||||
|
||||
module.exports.Status = Status;
|
||||
|
||||
module.exports.list = (listId, start, limit, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
if (!listId) {
|
||||
|
@ -88,197 +92,17 @@ module.exports.filter = (listId, request, columns, segmentId, callback) => {
|
|||
|
||||
};
|
||||
|
||||
module.exports.addConfirmation = (list, email, optInIp, data, callback) => {
|
||||
let cid = shortid.generate();
|
||||
|
||||
tools.validateEmail(email, false, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO confirmations (cid, list, email, opt_in_ip, data) VALUES (?,?,?,?,?)';
|
||||
connection.query(query, [cid, list.id, email, optInIp, JSON.stringify(data || {})], (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!result || !result.affectedRows) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let encryptionKeys = [];
|
||||
fields.getRow(fieldList, data).forEach(field => {
|
||||
if (field.type === 'gpg' && field.value) {
|
||||
encryptionKeys.push(field.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl'], (err, configItems) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
setImmediate(() => {
|
||||
if (data._skip) {
|
||||
log.info('Subscription', 'Confirmation message for %s marked to be skipped (%s)', email, JSON.stringify(data));
|
||||
return;
|
||||
}
|
||||
|
||||
let sendMail = (html, text) => {
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: [].concat(data.firstName || []).concat(data.lastName || []).join(' '),
|
||||
address: email
|
||||
},
|
||||
subject: util.format(_('%s: Please Confirm Subscription'), list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
text,
|
||||
data: {
|
||||
title: list.name,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
confirmUrl: urllib.resolve(configItems.serviceUrl, '/subscription/subscribe/' + cid)
|
||||
}
|
||||
}, err => {
|
||||
if (err) {
|
||||
log.error('Subscription', err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let text = {
|
||||
template: 'subscription/mail-confirm-text.hbs'
|
||||
};
|
||||
|
||||
let html = {
|
||||
template: 'subscription/mail-confirm-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(null, cid);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.subscribe = (cid, optInIp, 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' && subscription.subscriber) {
|
||||
// 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();
|
||||
// reload full data from db in case it was an update, not insert
|
||||
return module.exports.getById(listId, subscription.subscriber, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
subscription.cid = cid;
|
||||
subscription.list = listId;
|
||||
subscription.email = email;
|
||||
|
||||
let optInCountry = geoip.lookupCountry(optInIp) || null;
|
||||
module.exports.insert(listId, {
|
||||
email,
|
||||
cid,
|
||||
optInIp,
|
||||
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, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
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) => {
|
||||
|
@ -292,8 +116,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();
|
||||
|
@ -307,7 +131,7 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
}
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
@ -326,10 +150,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;
|
||||
|
@ -338,25 +159,26 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
let entryId = existing ? existing.id : false;
|
||||
|
||||
meta.cid = existing ? rows[0].cid : meta.cid;
|
||||
meta.status = meta.status || (existing ? existing.status : 1);
|
||||
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 === 1 ? '+' : false) : (existing.status === 1 ? '-' : '+');
|
||||
statusDirection = !existing ? (meta.status === Status.SUBSCRIBED ? '+' : false) : (existing.status === Status.SUBSCRIBED ? '-' : '+');
|
||||
}
|
||||
|
||||
if (!keys.length) {
|
||||
// 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, {
|
||||
|
@ -380,10 +202,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;
|
||||
|
@ -391,17 +210,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, {
|
||||
|
@ -414,10 +227,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, {
|
||||
|
@ -575,7 +385,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) => {
|
||||
|
@ -627,46 +437,7 @@ module.exports.update = (listId, cid, updates, allowEmail, callback) => {
|
|||
});
|
||||
};
|
||||
|
||||
module.exports.unsubscribe = (listId, email, campaignId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
email = (email || '').toString().trim();
|
||||
|
||||
campaignId = (campaignId || '').toString().trim() || false;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error(_('Missing List ID')));
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
return callback(new Error(_('Missing 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 || 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);
|
||||
|
@ -679,17 +450,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;
|
||||
|
@ -697,31 +462,22 @@ 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) {
|
||||
statusDirection = status === 1 ? '+' : '-';
|
||||
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 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);
|
||||
|
@ -730,20 +486,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) {
|
||||
if (!campaignId || status !== Status.SUBSCRIBED) {
|
||||
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);
|
||||
|
@ -752,10 +502,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;
|
||||
|
@ -764,10 +511,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);
|
||||
|
@ -775,12 +519,9 @@ 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 => {
|
||||
connection.query('UPDATE `campaigns` SET `unsubscribed`=`unsubscribed`' + (status === Status.UNSUBSCRIBED ? '+' : '-') + '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';
|
||||
|
@ -789,18 +530,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);
|
||||
|
@ -852,19 +587,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) {
|
||||
if (subscription.status !== Status.SUBSCRIBED) {
|
||||
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);
|
||||
|
@ -873,17 +602,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);
|
||||
|
@ -963,11 +686,10 @@ module.exports.updateImport = (listId, importId, data, callback) => {
|
|||
connection.release();
|
||||
return callback(null, affected);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
connection.release();
|
||||
return callback(null, affected);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -1075,13 +797,13 @@ module.exports.listImports = (listId, callback) => {
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports.updateAddress = (list, cid, updates, optInIp, 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')));
|
||||
}
|
||||
|
@ -1100,7 +822,7 @@ module.exports.updateAddress = (list, cid, updates, optInIp, 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`=? AND `status`=' + Status.SUBSCRIBED + ' LIMIT 1';
|
||||
let args = [cid];
|
||||
connection.query(query, args, (err, rows) => {
|
||||
if (err) {
|
||||
|
@ -1119,7 +841,7 @@ module.exports.updateAddress = (list, cid, updates, optInIp, 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`=' + Status.SUBSCRIBED + ' LIMIT 1';
|
||||
let args = [emailNew, cid];
|
||||
connection.query(query, args, (err, rows) => {
|
||||
connection.release();
|
||||
|
@ -1127,18 +849,77 @@ module.exports.updateAddress = (list, cid, updates, optInIp, 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, optInIp, {
|
||||
action: 'update',
|
||||
cid,
|
||||
subscriber: old.id,
|
||||
emailOld: old.email
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
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.
|
||||
|
|
157
lib/subscription-mail-helpers.js
Normal file
157
lib/subscription-mail-helpers.js
Normal file
|
@ -0,0 +1,157 @@
|
|||
'use strict';
|
||||
|
||||
const log = require('npmlog');
|
||||
let fields = require('./models/fields');
|
||||
let settings = require('./models/settings');
|
||||
let mailer = require('./mailer');
|
||||
let urllib = require('url');
|
||||
let helpers = require('./helpers');
|
||||
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
|
||||
};
|
||||
|
||||
sendMail(list, email, 'subscription-confirmed', _('%s: Subscription Confirmed'), relativeUrls, {}, subscription, 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
|
||||
};
|
||||
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/change-address/' + cid
|
||||
};
|
||||
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/subscribe/' + cid
|
||||
};
|
||||
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/unsubscribe/' + cid
|
||||
};
|
||||
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
|
||||
};
|
||||
sendMail(list, email, 'unsubscription-confirmed', _('%s: Unsubscription Confirmed'), relativeUrls, {}, subscription, callback);
|
||||
}
|
||||
|
||||
|
||||
function sendMail(list, email, template, subject, relativeUrls, mailOpts, subscription, callback) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
}
|
13
lib/tools.js
13
lib/tools.js
|
@ -43,7 +43,13 @@ function toDbKey(key) {
|
|||
}
|
||||
|
||||
function fromDbKey(key) {
|
||||
return key.replace(/[_\-]([a-z])/g, (m, c) => c.toUpperCase());
|
||||
let prefix = '';
|
||||
if (key.startsWith('_')) {
|
||||
key = key.substring(1);
|
||||
prefix = '_';
|
||||
|
||||
}
|
||||
return prefix + key.replace(/[_\-]([a-z])/g, (m, c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function convertKeys(obj, options) {
|
||||
|
@ -54,7 +60,7 @@ function convertKeys(obj, options) {
|
|||
if (options.skip && options.skip.indexOf(lKey) >= 0) {
|
||||
return;
|
||||
}
|
||||
if (options.keep && options.skip.indexOf(lKey) < 0) {
|
||||
if (options.keep && options.keep.indexOf(lKey) < 0) {
|
||||
return;
|
||||
}
|
||||
response[lKey] = obj[key];
|
||||
|
@ -142,7 +148,6 @@ function updateMenu(res) {
|
|||
}
|
||||
|
||||
function validateEmail(address, checkBlocked, callback) {
|
||||
|
||||
let user = (address || '').toString().split('@').shift().toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
if (checkBlocked && blockedUsers.indexOf(user) >= 0) {
|
||||
return callback(new Error(util.format(_('Blocked email address "%s"'), address)));
|
||||
|
@ -175,7 +180,7 @@ function validateEmail(address, checkBlocked, callback) {
|
|||
|
||||
function getMessageLinks(serviceUrl, campaign, list, subscription) {
|
||||
return {
|
||||
LINK_UNSUBSCRIBE: urllib.resolve(serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid + '?auto=yes&c=' + campaign.cid),
|
||||
LINK_UNSUBSCRIBE: urllib.resolve(serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid + '?c=' + campaign.cid),
|
||||
LINK_PREFERENCES: urllib.resolve(serviceUrl, '/subscription/' + list.cid + '/manage/' + subscription.cid),
|
||||
LINK_BROWSER: urllib.resolve(serviceUrl, '/archive/' + campaign.cid + '/' + list.cid + '/' + subscription.cid),
|
||||
CAMPAIGN_ID: campaign.cid,
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"schemaVersion": 28
|
||||
"schemaVersion": 29
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
"sqldumptest": "NODE_ENV=test DUMP_NAME_SUFFIX=-test npm run sqldump",
|
||||
"sqlresettest": "NODE_ENV=test npm run sqldrop && NODE_ENV=test npm run sqlinit",
|
||||
"starttest": "NODE_ENV=test node index.js",
|
||||
"_e2e": "NODE_ENV=test mocha test/e2e/index.js",
|
||||
"_e2e": "NODE_ENV=test node test/e2e/index.js",
|
||||
"e2e": "npm run sqlresettest && npm run _e2e"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -41,7 +41,8 @@
|
|||
"jsxgettext-andris": "^0.9.0-patch.1",
|
||||
"mocha": "^3.3.0",
|
||||
"phantomjs": "^2.1.7",
|
||||
"selenium-webdriver": "^3.4.0"
|
||||
"selenium-webdriver": "^3.4.0",
|
||||
"url-pattern": "^1.0.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"posix": "^4.1.1"
|
||||
|
@ -69,6 +70,7 @@
|
|||
"faker": "^4.1.0",
|
||||
"feedparser": "^2.1.0",
|
||||
"file-type": "^4.1.0",
|
||||
"fs-extra": "^3.0.1",
|
||||
"geoip-ultralight": "^0.1.5",
|
||||
"gettext-parser": "^1.2.2",
|
||||
"gm": "^1.23.0",
|
||||
|
|
|
@ -5,10 +5,12 @@ let lists = require('../lib/models/lists');
|
|||
let fields = require('../lib/models/fields');
|
||||
let blacklist = require('../lib/models/blacklist');
|
||||
let subscriptions = require('../lib/models/subscriptions');
|
||||
let confirmations = require('../lib/models/confirmations');
|
||||
let tools = require('../lib/tools');
|
||||
let express = require('express');
|
||||
let log = require('npmlog');
|
||||
let router = new express.Router();
|
||||
let mailHelpers = require('../lib/subscription-mail-helpers');
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.query.access_token) {
|
||||
|
@ -123,7 +125,7 @@ router.post('/subscribe/:listId', (req, res) => {
|
|||
}
|
||||
|
||||
if (/^(yes|true|1)$/i.test(input.REQUIRE_CONFIRMATION)) {
|
||||
subscriptions.addConfirmation(list, input.EMAIL, req.ip, subscription, (err, cid) => {
|
||||
confirmations.addConfirmation(list.id, 'subscribe', req.ip, subscription, (err, confirmCid) => {
|
||||
if (err) {
|
||||
log.error('API', err);
|
||||
res.status(500);
|
||||
|
@ -132,13 +134,25 @@ router.post('/subscribe/:listId', (req, res) => {
|
|||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
mailHelpers.sendConfirmSubscription(list, input.EMAIL, confirmCid, subscription, (err) => {
|
||||
if (err) {
|
||||
log.error('API', err);
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
res.json({
|
||||
data: {
|
||||
id: cid
|
||||
id: confirmCid
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
subscriptions.insert(list.id, meta, subscription, (err, response) => {
|
||||
if (err) {
|
||||
|
@ -189,7 +203,25 @@ router.post('/unsubscribe/:listId', (req, res) => {
|
|||
data: []
|
||||
});
|
||||
}
|
||||
subscriptions.unsubscribe(list.id, input.EMAIL, false, (err, subscription) => {
|
||||
|
||||
subscriptions.getByEmail(list.id, input.EMAIL, (err, subscription) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
res.status(404);
|
||||
return res.json({
|
||||
error: 'Subscription with given email not found',
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
subscriptions.changeStatus(list.id, subscription.id, false, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
|
@ -207,6 +239,7 @@ router.post('/unsubscribe/:listId', (req, res) => {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.post('/delete/:listId', (req, res) => {
|
||||
let input = {};
|
||||
|
|
|
@ -161,22 +161,32 @@ router.get('/:list/edit/:form', passport.csrfProtection, (req, res) => {
|
|||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'web_confirm_notice',
|
||||
label: _('Web - Confirm Notice'),
|
||||
name: 'web_confirm_subscription_notice',
|
||||
label: _('Web - Confirm Subscription Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_confirm_html',
|
||||
name: 'mail_confirm_subscription_html',
|
||||
label: _('Mail - Confirm Subscription (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_confirm_text',
|
||||
name: 'mail_confirm_subscription_text',
|
||||
label: _('Mail - Confirm Subscription (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}, {
|
||||
name: 'web_subscribed',
|
||||
name: 'mail_already_subscribed_html',
|
||||
label: _('Mail - Already Subscribed (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_already_subscribed_text',
|
||||
label: _('Mail - Already Subscribed (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}, {
|
||||
name: 'web_subscribed_notice',
|
||||
label: _('Web - Subscribed Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
|
@ -217,20 +227,50 @@ router.get('/:list/edit/:form', passport.csrfProtection, (req, res) => {
|
|||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'web_unsubscribe_notice',
|
||||
label: _('Web - Unsubscribe Notice'),
|
||||
name: 'web_confirm_unsubscription_notice',
|
||||
label: _('Web - Confirm Unsubscription Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_unsubscribe_confirmed_html',
|
||||
label: _('Mail - Unsubscribe Confirmed (MJML)'),
|
||||
name: 'mail_confirm_unsubscription_html',
|
||||
label: _('Mail - Confirm Unsubscription (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_unsubscribe_confirmed_text',
|
||||
label: _('Mail - Unsubscribe Confirmed (Text)'),
|
||||
name: 'mail_confirm_unsubscription_text',
|
||||
label: _('Mail - Confirm Unsubscription (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}, {
|
||||
name: 'mail_confirm_address_change_html',
|
||||
label: _('Mail - Confirm Address Change (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_confirm_address_change_text',
|
||||
label: _('Mail - Confirm Address Change (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}, {
|
||||
name: 'web_unsubscribed_notice',
|
||||
label: _('Web - Unsubscribed Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_unsubscription_confirmed_html',
|
||||
label: _('Mail - Unsubscription Confirmed (MJML)'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}, {
|
||||
name: 'mail_unsubscription_confirmed_text',
|
||||
label: _('Mail - Unsubscription Confirmed (Text)'),
|
||||
type: 'text',
|
||||
help: helpEmailText
|
||||
}, {
|
||||
name: 'web_manual_unsubscribe_notice',
|
||||
label: _('Web - Manual Unsubscribe Notice'),
|
||||
type: 'mjml',
|
||||
help: helpMjmlGeneral
|
||||
}]
|
||||
}
|
||||
];
|
||||
|
|
|
@ -71,6 +71,8 @@ router.get('/create', passport.csrfProtection, (req, res) => {
|
|||
data.publicSubscribe = true;
|
||||
}
|
||||
|
||||
data.unsubscriptionModeOptions = getUnsubscriptionModeOptions(data.unsubscriptionMode || lists.UnsubscriptionMode.ONE_STEP);
|
||||
|
||||
res.render('lists/create', data);
|
||||
});
|
||||
|
||||
|
@ -103,6 +105,8 @@ router.get('/edit/:id', passport.csrfProtection, (req, res) => {
|
|||
return row;
|
||||
});
|
||||
|
||||
list.unsubscriptionModeOptions = getUnsubscriptionModeOptions(list.unsubscriptionMode);
|
||||
|
||||
list.csrfToken = req.csrfToken();
|
||||
res.render('lists/edit', list);
|
||||
});
|
||||
|
@ -447,7 +451,7 @@ router.post('/subscription/unsubscribe', passport.parseForm, passport.csrfProtec
|
|||
return res.redirect('/lists/view/' + list.id);
|
||||
}
|
||||
|
||||
subscriptions.unsubscribe(list.id, subscription.email, false, err => {
|
||||
subscriptions.changeStatus(list.id, subscription.id, false, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err || _('Could not unsubscribe user'));
|
||||
return res.redirect('/lists/subscription/' + list.id + '/edit/' + subscription.cid);
|
||||
|
@ -771,4 +775,40 @@ router.post('/quicklist/ajax', (req, res) => {
|
|||
});
|
||||
});
|
||||
|
||||
function getUnsubscriptionModeOptions(unsubscriptionMode) {
|
||||
const options = [];
|
||||
|
||||
options[lists.UnsubscriptionMode.ONE_STEP] = {
|
||||
value: lists.UnsubscriptionMode.ONE_STEP,
|
||||
selected: unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP,
|
||||
label: _('One-step (i.e. no email with confirmation link)')
|
||||
};
|
||||
|
||||
options[lists.UnsubscriptionMode.ONE_STEP_WITH_FORM] = {
|
||||
value: lists.UnsubscriptionMode.ONE_STEP_WITH_FORM,
|
||||
selected: unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM,
|
||||
label: _('One-step with unsubscription form (i.e. no email with confirmation link)')
|
||||
};
|
||||
|
||||
options[lists.UnsubscriptionMode.TWO_STEP] = {
|
||||
value: lists.UnsubscriptionMode.TWO_STEP,
|
||||
selected: unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP,
|
||||
label: _('Two-step (i.e. an email with confirmation link will be sent)')
|
||||
};
|
||||
|
||||
options[lists.UnsubscriptionMode.TWO_STEP_WITH_FORM] = {
|
||||
value: lists.UnsubscriptionMode.TWO_STEP_WITH_FORM,
|
||||
selected: unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM,
|
||||
label: _('Two-step with unsubscription form (i.e. an email with confirmation link will be sent)')
|
||||
};
|
||||
|
||||
options[lists.UnsubscriptionMode.MANUAL] = {
|
||||
value: lists.UnsubscriptionMode.MANUAL,
|
||||
selected: unsubscriptionMode === lists.UnsubscriptionMode.MANUAL,
|
||||
label: _('Manual (i.e. unsubscription has to be performed by the list administrator)')
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
@ -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 || [];
|
||||
|
||||
|
@ -44,10 +45,10 @@ let corsOrCsrfProtection = (req, res, next) => {
|
|||
}
|
||||
};
|
||||
|
||||
router.get('/subscribe/:cid', (req, res, next) => {
|
||||
subscriptions.subscribe(req.params.cid, req.ip, (err, subscription) => {
|
||||
if (!err && !subscription) {
|
||||
err = new Error(_('Selected subscription not found'));
|
||||
function checkAndExecuteConfirmation(req, action, errorMsg, next, exec) {
|
||||
confirmations.takeConfirmation(req.params.cid, (err, confirmation) => {
|
||||
if (!err && (!confirmation || confirmation.action !== action)) {
|
||||
err = new Error(_(errorMsg));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
|
@ -55,7 +56,7 @@ router.get('/subscribe/:cid', (req, res, next) => {
|
|||
return next(err);
|
||||
}
|
||||
|
||||
lists.get(subscription.list, (err, list) => {
|
||||
lists.get(confirmation.listId, (err, list) => {
|
||||
if (!err && !list) {
|
||||
err = new Error(_('Selected list not found'));
|
||||
err.status = 404;
|
||||
|
@ -65,109 +66,102 @@ router.get('/subscribe/:cid', (req, res, next) => {
|
|||
return next(err);
|
||||
}
|
||||
|
||||
settings.list(['defaultHomepage', 'serviceUrl', 'pgpPrivateKey', 'defaultAddress', 'defaultPostaddress', 'defaultFrom', 'disableConfirmations'], (err, configItems) => {
|
||||
exec(confirmation, list);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
router.get('/confirm/subscribe/:cid', (req, res, next) => {
|
||||
checkAndExecuteConfirmation(req, 'subscribe', 'Request invalid or already completed. If your subscription request is still pending, please subscribe again.', next, (confirmation, list) => {
|
||||
const data = confirmation.data;
|
||||
let optInCountry = geoip.lookupCountry(confirmation.ip) || null;
|
||||
|
||||
const meta = {
|
||||
email: data.email,
|
||||
optInIp: confirmation.ip,
|
||||
optInCountry,
|
||||
status: subscriptions.Status.SUBSCRIBED
|
||||
};
|
||||
|
||||
subscriptions.insert(list.id, meta, data.subscriptionData, (err, result) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
let data = {
|
||||
title: list.name,
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
preferences: '/subscription/' + list.cid + '/manage/' + subscription.cid,
|
||||
hasPubkey: !!configItems.pgpPrivateKey,
|
||||
defaultAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
template: {
|
||||
template: 'subscription/web-subscribed.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
if (!result.entryId) {
|
||||
return next(new Error(_('Could not save subscription')));
|
||||
}
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-subscribed', data, (err, data) => {
|
||||
subscriptions.getById(list.id, result.entryId, (err, subscription) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
mailHelpers.sendSubscriptionConfirmed(list, data.email, subscription, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
res.redirect('/subscription/' + list.cid + '/subscribed-notice');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/confirm/change-address/:cid', (req, res, next) => {
|
||||
checkAndExecuteConfirmation(req, 'change-address', 'Request invalid or already completed. If your address change request is still pending, please change the address again.', next, (confirmation, list) => {
|
||||
const data = confirmation.data;
|
||||
|
||||
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')));
|
||||
}
|
||||
|
||||
subscriptions.updateAddress(list.id, data.subscriptionId, data.emailNew, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (configItems.disableConfirmations) {
|
||||
return;
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
subscriptions.getById(list.id, data.subscriptionId, (err, subscription) => {
|
||||
if (err) {
|
||||
return log.error('Fields', err);
|
||||
return next(err);
|
||||
}
|
||||
|
||||
let encryptionKeys = [];
|
||||
fields.getRow(fieldList, subscription).forEach(field => {
|
||||
if (field.type === 'gpg' && field.value) {
|
||||
encryptionKeys.push(field.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
let sendMail = (html, text) => {
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: [].concat(subscription.firstName || []).concat(subscription.lastName || []).join(' '),
|
||||
address: subscription.email
|
||||
},
|
||||
subject: util.format(_('%s: Subscription Confirmed'), list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
text,
|
||||
data: {
|
||||
title: list.name,
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
preferencesUrl: urllib.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/manage/' + subscription.cid),
|
||||
unsubscribeUrl: urllib.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid)
|
||||
}
|
||||
}, err => {
|
||||
mailHelpers.sendSubscriptionConfirmed(list, data.emailNew, subscription, err => {
|
||||
if (err) {
|
||||
log.error('Subscription', err);
|
||||
return next(err);
|
||||
}
|
||||
|
||||
req.flash('info', _('Email address changed'));
|
||||
res.redirect('/subscription/' + list.cid + '/manage/' + subscription.cid);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
let text = {
|
||||
template: 'subscription/mail-subscription-confirmed-text.hbs'
|
||||
};
|
||||
router.get('/confirm/unsubscribe/:cid', (req, res, next) => {
|
||||
checkAndExecuteConfirmation(req, 'unsubscribe', 'Request invalid or already completed. If your unsubscription request is still pending, please unsubscribe again.', next, (confirmation, list) => {
|
||||
const data = confirmation.data;
|
||||
|
||||
let html = {
|
||||
template: 'subscription/mail-subscription-confirmed-html.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs',
|
||||
type: 'mjml'
|
||||
};
|
||||
|
||||
helpers.injectCustomFormTemplates(req.query.fid || list.defaultForm, { text, html }, (err, tmpl) => {
|
||||
subscriptions.changeStatus(list.id, confirmation.data.subscriptionId, confirmation.data.campaignId, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
||||
if (err) {
|
||||
return sendMail(html, text);
|
||||
return next(err);
|
||||
}
|
||||
|
||||
sendMail(tmpl.html, tmpl.text);
|
||||
});
|
||||
// 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/' + list.cid + '/unsubscribed-notice');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -190,6 +184,8 @@ router.get('/:cid', passport.csrfProtection, (req, res, next) => {
|
|||
return next(err);
|
||||
}
|
||||
|
||||
// TODO: process subscriber cid param for resubscription requests
|
||||
|
||||
let data = tools.convertKeys(req.query, {
|
||||
skip: ['layout']
|
||||
});
|
||||
|
@ -198,6 +194,8 @@ router.get('/:cid', passport.csrfProtection, (req, res, next) => {
|
|||
data.cid = list.cid;
|
||||
data.csrfToken = req.csrfToken();
|
||||
|
||||
|
||||
function nextStep() {
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err && !fieldList) {
|
||||
fieldList = [];
|
||||
|
@ -243,6 +241,27 @@ router.get('/:cid', passport.csrfProtection, (req, res, next) => {
|
|||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const ucid = req.query.cid;
|
||||
if (ucid) {
|
||||
subscriptions.get(list.id, ucid, (err, subscription) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
for (let key in subscription) {
|
||||
if (!(key in data)) {
|
||||
data[key] = subscription[key];
|
||||
}
|
||||
}
|
||||
|
||||
nextStep();
|
||||
});
|
||||
} else {
|
||||
nextStep();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -318,164 +337,6 @@ router.get('/:cid/widget', cors(corsOptions), (req, res, next) => {
|
|||
});
|
||||
});
|
||||
|
||||
router.get('/:cid/confirm-notice', (req, res, next) => {
|
||||
lists.getByCid(req.params.cid, (err, list) => {
|
||||
if (!err && !list) {
|
||||
err = new Error(_('Selected list not found'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
let data = {
|
||||
title: list.name,
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
defaultAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
template: {
|
||||
template: 'subscription/web-confirm-notice.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
}
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-confirm-notice', data, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.isConfirmNotice = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:cid/updated-notice', (req, res, next) => {
|
||||
lists.getByCid(req.params.cid, (err, list) => {
|
||||
if (!err && !list) {
|
||||
err = new Error(_('Selected list not found'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
let data = {
|
||||
title: list.name,
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
defaultAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
template: {
|
||||
template: 'subscription/web-updated-notice.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
}
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-updated-notice', data, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:cid/unsubscribe-notice', (req, res, next) => {
|
||||
lists.getByCid(req.params.cid, (err, list) => {
|
||||
if (!err && !list) {
|
||||
err = new Error(_('Selected list not found'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
let data = {
|
||||
title: list.name,
|
||||
layout: 'subscription/layout',
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
defaultAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
template: {
|
||||
template: 'subscription/web-unsubscribe-notice.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs'
|
||||
}
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-unsubscribe-notice', data, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.options('/:cid/subscribe', cors(corsOptions));
|
||||
|
||||
router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, res, next) => {
|
||||
|
@ -496,8 +357,17 @@ router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, r
|
|||
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
||||
}
|
||||
|
||||
tools.validateEmail(email, false, err => {
|
||||
if (err) {
|
||||
if (req.xhr) {
|
||||
return sendJsonError(err.message, 400);
|
||||
}
|
||||
req.flash('danger', err.message);
|
||||
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -520,24 +390,33 @@ 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(subscriptionData);
|
||||
|
||||
data = tools.convertKeys(data);
|
||||
|
||||
data._address = req.body.address;
|
||||
data._sub = req.body.sub;
|
||||
data._skip = !testsPass;
|
||||
|
||||
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) {
|
||||
return req.xhr ? sendJsonError(err) : next(err);
|
||||
}
|
||||
|
||||
if (subscription && subscription.status === subscriptions.Status.SUBSCRIBED) {
|
||||
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) {
|
||||
if (req.xhr) {
|
||||
return sendJsonError(err);
|
||||
|
@ -546,12 +425,29 @@ router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, r
|
|||
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-notice');
|
||||
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 {
|
||||
mailHelpers.sendConfirmSubscription(list, email, confirmCid, data, (err) => {
|
||||
if (err) {
|
||||
return req.xhr ? sendJsonError(err) : sendWebResponse(err);
|
||||
}
|
||||
sendWebResponse();
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -572,8 +468,8 @@ router.get('/:lcid/manage/:ucid', passport.csrfProtection, (req, res, next) => {
|
|||
return next(err);
|
||||
}
|
||||
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
|
||||
if (!err && !subscription) {
|
||||
err = new Error(_('Subscription not found from this list'));
|
||||
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
|
||||
err = new Error(_('Subscription not found in this list'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
|
@ -643,16 +539,25 @@ router.post('/:lcid/manage', passport.parseForm, passport.csrfProtection, (req,
|
|||
return next(err);
|
||||
}
|
||||
|
||||
subscriptions.update(list.id, req.body.cid, req.body, false, err => {
|
||||
subscriptions.get(list.id, req.body.cid, (err, subscription) => {
|
||||
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
|
||||
err = new Error(_('Subscription not found in this list'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
log.error('Subscription', err);
|
||||
return res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/manage/' + encodeURIComponent(req.body.cid) + '?' + tools.queryParams(req.body));
|
||||
return next(err);
|
||||
}
|
||||
|
||||
subscriptions.update(list.id, subscription.cid, req.body, false, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.redirect('/subscription/' + req.params.lcid + '/updated-notice');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:lcid/manage-address/:ucid', passport.csrfProtection, (req, res, next) => {
|
||||
lists.getByCid(req.params.lcid, (err, list) => {
|
||||
|
@ -671,8 +576,8 @@ router.get('/:lcid/manage-address/:ucid', passport.csrfProtection, (req, res, ne
|
|||
}
|
||||
|
||||
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
|
||||
if (!err && !subscription) {
|
||||
err = new Error(_('Subscription not found from this list'));
|
||||
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
|
||||
err = new Error(_('Subscription not found in this list'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
|
@ -725,16 +630,48 @@ router.post('/:lcid/manage-address', passport.parseForm, passport.csrfProtection
|
|||
return next(err);
|
||||
}
|
||||
|
||||
subscriptions.updateAddress(list, req.body.cid, req.body, req.ip, err => {
|
||||
let bodyData = tools.convertKeys(req.body); // This is here to convert "email-new" to "emailNew"
|
||||
const emailOld = (bodyData.email || '').toString().trim();
|
||||
const emailNew = (bodyData.emailNew || '').toString().trim();
|
||||
|
||||
if (emailOld === emailNew) {
|
||||
req.flash('info', _('Nothing seems to be changed'));
|
||||
res.redirect('/subscription/' + req.params.lcid + '/manage/' + req.body.cid);
|
||||
|
||||
} else {
|
||||
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));
|
||||
return next(err);
|
||||
}
|
||||
|
||||
req.flash('info', _('Email address updated, check your mailbox for verification instructions'));
|
||||
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, confirmCid) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
mailHelpers.sendConfirmAddressChange(list, emailNew, confirmCid, subscription, sendWebResponse);
|
||||
});
|
||||
|
||||
} else {
|
||||
mailHelpers.sendAlreadySubscribed(list, emailNew, subscription, sendWebResponse);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -755,8 +692,8 @@ 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'));
|
||||
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
|
||||
err = new Error(_('Subscription not found in this list'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
|
@ -764,10 +701,20 @@ router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next)
|
|||
return next(err);
|
||||
}
|
||||
|
||||
|
||||
const autoUnsubscribe = req.query.auto === 'yes';
|
||||
|
||||
if (autoUnsubscribe) {
|
||||
handleUnsubscribe(list, subscription, autoUnsubscribe, req.query.c, req.ip, res, next);
|
||||
|
||||
} else if (req.query.formTest ||
|
||||
list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
|
||||
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
||||
|
||||
subscription.lcid = req.params.lcid;
|
||||
subscription.ucid = req.params.ucid;
|
||||
subscription.title = list.name;
|
||||
subscription.csrfToken = req.csrfToken();
|
||||
subscription.autosubmit = !!req.query.auto;
|
||||
subscription.campaign = req.query.c;
|
||||
subscription.defaultAddress = configItems.defaultAddress;
|
||||
subscription.defaultPostaddress = configItems.defaultPostaddress;
|
||||
|
@ -798,6 +745,9 @@ router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next)
|
|||
});
|
||||
});
|
||||
});
|
||||
} else { // UnsubscriptionMode.ONE_STEP || UnsubscriptionMode.TWO_STEP || UnsubscriptionMode.MANUAL
|
||||
handleUnsubscribe(list, subscription, autoUnsubscribe, req.query.c, req.ip, res, next);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -814,86 +764,91 @@ router.post('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, (
|
|||
return next(err);
|
||||
}
|
||||
|
||||
let email = req.body.email;
|
||||
const campaignId = (req.body.campaign || '').toString().trim() || false;
|
||||
|
||||
subscriptions.get(list.id, req.body.ucid, (err, subscription) => {
|
||||
if (!err && (!subscription || subscription.status !== subscriptions.Status.SUBSCRIBED)) {
|
||||
err = new Error(_('Subscription not found in this list'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
subscriptions.unsubscribe(list.id, email, 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.cid) + '?' + tools.queryParams(req.body));
|
||||
}
|
||||
res.redirect('/subscription/' + req.params.lcid + '/unsubscribe-notice');
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err) {
|
||||
return log.error('Fields', err);
|
||||
return next(err);
|
||||
}
|
||||
|
||||
let encryptionKeys = [];
|
||||
fields.getRow(fieldList, subscription).forEach(field => {
|
||||
if (field.type === 'gpg' && field.value) {
|
||||
encryptionKeys.push(field.value.trim());
|
||||
}
|
||||
handleUnsubscribe(list, subscription, false, campaignId, req.ip, res, next);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl', 'disableConfirmations'], (err, configItems) => {
|
||||
function handleUnsubscribe(list, subscription, autoUnsubscribe, campaignId, ip, res, next) {
|
||||
if ((list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) ||
|
||||
(autoUnsubscribe && (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM)) ) {
|
||||
|
||||
subscriptions.changeStatus(list.id, subscription.id, campaignId, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
||||
if (err) {
|
||||
return log.error('Settings', err);
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (configItems.disableConfirmations) {
|
||||
return;
|
||||
// TODO: Shall we do anything with "found"?
|
||||
|
||||
mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
let sendMail = (html, text) => {
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: [].concat(subscription.firstName || []).concat(subscription.lastName || []).join(' '),
|
||||
address: subscription.email
|
||||
},
|
||||
subject: util.format(_('%s: Unsubscribe Confirmed'), list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
text,
|
||||
data: {
|
||||
title: list.name,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
subscribeUrl: urllib.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '?cid=' + subscription.cid)
|
||||
}
|
||||
}, err => {
|
||||
if (err) {
|
||||
log.error('Subscription', err);
|
||||
}
|
||||
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
|
||||
});
|
||||
});
|
||||
|
||||
} else if (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
|
||||
|
||||
const data = {
|
||||
subscriptionId: subscription.id,
|
||||
campaignId
|
||||
};
|
||||
|
||||
let text = {
|
||||
template: 'subscription/mail-unsubscribe-confirmed-text.hbs'
|
||||
};
|
||||
|
||||
let html = {
|
||||
template: 'subscription/mail-unsubscribe-confirmed-html.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs',
|
||||
type: 'mjml'
|
||||
};
|
||||
|
||||
helpers.injectCustomFormTemplates(req.query.fid || list.defaultForm, { text, html }, (err, tmpl) => {
|
||||
confirmations.addConfirmation(list.id, 'unsubscribe', ip, data, (err, confirmCid) => {
|
||||
if (err) {
|
||||
return sendMail(html, text);
|
||||
return next(err);
|
||||
}
|
||||
|
||||
sendMail(tmpl.html, tmpl.text);
|
||||
mailHelpers.sendConfirmUnsubscription(list, subscription.email, confirmCid, subscription, err => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.redirect('/subscription/' + list.cid + '/confirm-unsubscription-notice');
|
||||
});
|
||||
});
|
||||
|
||||
} else { // UnsubscriptionMode.MANUAL
|
||||
res.redirect('/subscription/' + list.cid + '/manual-unsubscribe-notice');
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/:cid/confirm-subscription-notice', (req, res, next) => {
|
||||
webNotice('confirm-subscription', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/confirm-unsubscription-notice', (req, res, next) => {
|
||||
webNotice('confirm-unsubscription', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/subscribed-notice', (req, res, next) => {
|
||||
webNotice('subscribed', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/updated-notice', (req, res, next) => {
|
||||
webNotice('updated', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/unsubscribed-notice', (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) => {
|
||||
|
@ -934,4 +889,59 @@ router.post('/publickey', passport.parseForm, (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'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail'], (err, configItems) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
let data = {
|
||||
title: list.name,
|
||||
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'
|
||||
}
|
||||
};
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-' + type + '-notice', data, (err, data) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
helpers.captureFlashMessages(req, res, (err, flash) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
data.isWeb = true;
|
||||
data.isConfirmNotice = true;
|
||||
data.flashMessages = flash;
|
||||
res.send(htmlRenderer(data));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
@ -419,7 +419,7 @@ function formatMessage(message, callback) {
|
|||
}
|
||||
},
|
||||
list: {
|
||||
unsubscribe: url.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + message.subscription.cid + '?auto=yes')
|
||||
unsubscribe: url.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + message.subscription.cid)
|
||||
},
|
||||
subject: tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, campaign.subject),
|
||||
html: renderedHtml,
|
||||
|
|
|
@ -2,10 +2,30 @@
|
|||
# Define incrementing schema version number
|
||||
SET @schema_version = '28';
|
||||
|
||||
# Rename column tracking_disabled
|
||||
ALTER TABLE `campaigns` ADD COLUMN `open_tracking_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL, ADD COLUMN `click_tracking_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL;
|
||||
UPDATE `campaigns` SET `open_tracking_disabled` = `tracking_disabled`, `click_tracking_disabled` = `tracking_disabled`;
|
||||
ALTER TABLE `campaigns` DROP COLUMN `tracking_disabled`;
|
||||
# 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";
|
||||
update custom_forms_data set data_key="mail_confirm_subscription_text" where data_key="mail_confirm_text";
|
||||
update custom_forms_data set data_key="mail_unsubscription_confirmed_html" where data_key="mail_unsubscribe_confirmed_html";
|
||||
update custom_forms_data set data_key="mail_unsubscription_confirmed_text" where data_key="mail_unsubscribe_confirmed_text";
|
||||
update custom_forms_data set data_key="web_confirm_subscription_notice" where data_key="web_confirm_notice";
|
||||
update custom_forms_data set data_key="web_subscribed_notice" where data_key="web_subscribed";
|
||||
update custom_forms_data set data_key="web_unsubscribed_notice" where data_key="web_unsubscribe_notice";
|
||||
|
||||
|
||||
# Footer section
|
||||
LOCK TABLES `settings` WRITE;
|
||||
|
|
13
setup/sql/upgrade-00029.sql
Normal file
13
setup/sql/upgrade-00029.sql
Normal file
|
@ -0,0 +1,13 @@
|
|||
# Header section
|
||||
# Define incrementing schema version number
|
||||
SET @schema_version = '29';
|
||||
|
||||
# Rename column tracking_disabled
|
||||
ALTER TABLE `campaigns` ADD COLUMN `open_tracking_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL, ADD COLUMN `click_tracking_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL;
|
||||
UPDATE `campaigns` SET `open_tracking_disabled` = `tracking_disabled`, `click_tracking_disabled` = `tracking_disabled`;
|
||||
ALTER TABLE `campaigns` DROP COLUMN `tracking_disabled`;
|
||||
|
||||
# Footer section
|
||||
LOCK TABLES `settings` WRITE;
|
||||
INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
|
||||
UNLOCK TABLES;
|
|
@ -1,15 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
const webdriver = require('selenium-webdriver');
|
||||
|
||||
const driver = new webdriver.Builder()
|
||||
.forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs')
|
||||
.build();
|
||||
|
||||
if (global.USE_SHARED_DRIVER === true) {
|
||||
driver.originalQuit = driver.quit;
|
||||
driver.quit = () => {};
|
||||
}
|
||||
|
||||
module.exports = driver;
|
|
@ -1,36 +1,30 @@
|
|||
'use strict';
|
||||
|
||||
require('./helpers/exit-unless-test');
|
||||
require('./lib/exit-unless-test');
|
||||
const { mocha, driver } = require('./lib/mocha-e2e');
|
||||
const path = require('path');
|
||||
|
||||
global.USE_SHARED_DRIVER = true;
|
||||
|
||||
const driver = require('./helpers/driver');
|
||||
const only = 'only';
|
||||
const skip = 'skip';
|
||||
|
||||
|
||||
|
||||
let tests = [
|
||||
['tests/login'],
|
||||
['tests/subscription']
|
||||
'login',
|
||||
'subscription'
|
||||
];
|
||||
|
||||
|
||||
|
||||
tests = tests.filter(t => t[1] !== skip);
|
||||
|
||||
if (tests.some(t => t[1] === only)) {
|
||||
tests = tests.filter(t => t[1] === only);
|
||||
tests = tests.map(testSpec => (testSpec.constructor === Array ? testSpec : [testSpec]));
|
||||
tests = tests.filter(testSpec => testSpec[1] !== skip);
|
||||
if (tests.some(testSpec => testSpec[1] === only)) {
|
||||
tests = tests.filter(testSpec => testSpec[1] === only);
|
||||
}
|
||||
|
||||
describe('e2e', function() {
|
||||
this.timeout(10000);
|
||||
for (const testSpec of tests) {
|
||||
const testPath = path.join(__dirname, 'tests', testSpec[0] + '.js');
|
||||
mocha.addFile(testPath);
|
||||
}
|
||||
|
||||
tests.forEach(t => {
|
||||
describe(t[0], () => {
|
||||
require('./' + t[0]); // eslint-disable-line global-require
|
||||
});
|
||||
});
|
||||
|
||||
after(() => driver.originalQuit());
|
||||
mocha.run(failures => {
|
||||
process.exit(failures); // exit with non-zero status if there were failures
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ const config = require('config');
|
|||
module.exports = {
|
||||
app: config,
|
||||
baseUrl: 'http://localhost:' + config.www.port,
|
||||
mailUrl: 'http://localhost:' + config.testserver.mailboxserverport,
|
||||
users: {
|
||||
admin: {
|
||||
username: 'admin',
|
19
test/e2e/lib/mail.js
Normal file
19
test/e2e/lib/mail.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
const driver = require('./mocha-e2e').driver;
|
||||
const page = require('./page');
|
||||
|
||||
module.exports = (...extras) => page({
|
||||
|
||||
async fetchMail(address) {
|
||||
await driver.sleep(1000);
|
||||
await driver.navigate().to(`${config.mailUrl}/${address}`);
|
||||
await this.waitUntilVisible();
|
||||
},
|
||||
|
||||
async ensureUrl(path) {
|
||||
throw new Error('Unsupported method.');
|
||||
},
|
||||
|
||||
}, ...extras);
|
217
test/e2e/lib/mocha-e2e.js
Normal file
217
test/e2e/lib/mocha-e2e.js
Normal file
|
@ -0,0 +1,217 @@
|
|||
'use strict';
|
||||
|
||||
const Mocha = require('mocha');
|
||||
const color = Mocha.reporters.Base.color;
|
||||
const Semaphore = require('./semaphore');
|
||||
const fs = require('fs-extra');
|
||||
const config = require('./config');
|
||||
const webdriver = require('selenium-webdriver');
|
||||
|
||||
const driver = new webdriver.Builder()
|
||||
.forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs')
|
||||
.build();
|
||||
|
||||
|
||||
const failHandlerRunning = new Semaphore();
|
||||
|
||||
|
||||
function UseCaseReporter(runner) {
|
||||
Mocha.reporters.Base.call(this, runner);
|
||||
|
||||
const self = this;
|
||||
let indents = 0;
|
||||
|
||||
function indent () {
|
||||
return Array(indents).join(' ');
|
||||
}
|
||||
|
||||
runner.on('start', function () {
|
||||
console.log();
|
||||
});
|
||||
|
||||
runner.on('suite', suite => {
|
||||
++indents;
|
||||
console.log(color('suite', '%s%s'), indent(), suite.title);
|
||||
});
|
||||
|
||||
runner.on('suite end', () => {
|
||||
--indents;
|
||||
if (indents === 1) {
|
||||
console.log();
|
||||
}
|
||||
});
|
||||
|
||||
runner.on('use-case', useCase => {
|
||||
++indents;
|
||||
console.log();
|
||||
console.log(color('suite', '%sUse case: %s'), indent(), useCase.title);
|
||||
});
|
||||
|
||||
runner.on('use-case end', () => {
|
||||
--indents;
|
||||
});
|
||||
|
||||
runner.on('steps', useCase => {
|
||||
++indents;
|
||||
console.log(color('pass', '%s%s'), indent(), useCase.title);
|
||||
});
|
||||
|
||||
runner.on('steps end', () => {
|
||||
--indents;
|
||||
});
|
||||
|
||||
runner.on('step pass', step => {
|
||||
console.log(indent() + color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) + color('pass', ' %s'), step.title);
|
||||
});
|
||||
|
||||
runner.on('step fail', step => {
|
||||
console.log(indent() + color('fail', ' %s'), step.title);
|
||||
});
|
||||
|
||||
runner.on('pending', test => {
|
||||
const fmt = indent() + color('pending', ' - %s');
|
||||
console.log(fmt, test.title);
|
||||
});
|
||||
|
||||
runner.on('pass', test => {
|
||||
let fmt;
|
||||
if (test.speed === 'fast') {
|
||||
fmt = indent() +
|
||||
color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) +
|
||||
color('pass', ' %s');
|
||||
console.log(fmt, test.title);
|
||||
} else {
|
||||
fmt = indent() +
|
||||
color('checkmark', ' ' + Mocha.reporters.Base.symbols.ok) +
|
||||
color('pass', ' %s') +
|
||||
color(test.speed, ' (%dms)');
|
||||
console.log(fmt, test.title, test.duration);
|
||||
}
|
||||
});
|
||||
|
||||
runner.on('fail', (test, err) => {
|
||||
failHandlerRunning.enter();
|
||||
(async () => {
|
||||
const currentUrl = await driver.getCurrentUrl();
|
||||
const info = `URL: ${currentUrl}`;
|
||||
await fs.writeFile('last-failed-e2e-test.info', info);
|
||||
await fs.writeFile('last-failed-e2e-test.html', await driver.getPageSource());
|
||||
await fs.writeFile('last-failed-e2e-test.png', new Buffer(await driver.takeScreenshot(), 'base64'));
|
||||
failHandlerRunning.exit();
|
||||
})();
|
||||
|
||||
console.log(indent() + color('fail', ' %s'), test.title);
|
||||
console.log();
|
||||
console.log(err);
|
||||
console.log();
|
||||
console.log(`Snaphot of and info about the current page are in last-failed-e2e-test.*`);
|
||||
});
|
||||
|
||||
runner.on('end', () => {
|
||||
const stats = self.stats;
|
||||
let fmt;
|
||||
|
||||
console.log();
|
||||
|
||||
// passes
|
||||
fmt = color('bright pass', ' ') + color('green', ' %d passing');
|
||||
console.log(fmt, stats.passes);
|
||||
|
||||
// pending
|
||||
if (stats.pending) {
|
||||
fmt = color('pending', ' ') + color('pending', ' %d pending');
|
||||
console.log(fmt, stats.pending);
|
||||
}
|
||||
|
||||
// failures
|
||||
if (stats.failures) {
|
||||
fmt = color('fail', ' %d failing');
|
||||
console.log(fmt, stats.failures);
|
||||
}
|
||||
|
||||
console.log();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const mocha = new Mocha()
|
||||
.timeout(120000)
|
||||
.reporter(UseCaseReporter)
|
||||
.ui('tdd');
|
||||
|
||||
mocha._originalRun = mocha.run;
|
||||
|
||||
|
||||
let runner;
|
||||
mocha.run = fn => {
|
||||
runner = mocha._originalRun(async () => {
|
||||
await failHandlerRunning.waitForEmpty();
|
||||
await driver.quit();
|
||||
|
||||
fn();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
async function useCaseExec(name, asyncFn) {
|
||||
runner.emit('use-case', {title: name});
|
||||
|
||||
try {
|
||||
await asyncFn();
|
||||
runner.emit('use-case end');
|
||||
} catch (err) {
|
||||
runner.emit('use-case end');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function useCase(name, asyncFn) {
|
||||
if (asyncFn) {
|
||||
return test('Use case: ' + name, () => useCaseExec(name, asyncFn));
|
||||
} else {
|
||||
// Pending test
|
||||
return test('Use case: ' + name);
|
||||
}
|
||||
}
|
||||
|
||||
useCase.only = (name, asyncFn) => {
|
||||
return test.only('Use case: ' + name, () => useCaseExec(name, asyncFn));
|
||||
};
|
||||
|
||||
useCase.skip = (name, asyncFn) => {
|
||||
return test.skip('Use case: ' + name, () => useCaseExec(name, asyncFn));
|
||||
};
|
||||
|
||||
async function step(name, asyncFn) {
|
||||
try {
|
||||
await asyncFn();
|
||||
runner.emit('step pass', {title: name});
|
||||
} catch (err) {
|
||||
runner.emit('step fail', {title: name});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function steps(name, asyncFn) {
|
||||
try {
|
||||
runner.emit('steps', {title: name});
|
||||
await asyncFn();
|
||||
runner.emit('steps end');
|
||||
} catch (err) {
|
||||
runner.emit('step end');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async function precondition(preConditionName, useCaseName, asyncFn) {
|
||||
await steps(`Including use case "${useCaseName}" to satisfy precondition "${preConditionName}"`, asyncFn);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mocha,
|
||||
useCase,
|
||||
step,
|
||||
steps,
|
||||
precondition,
|
||||
driver
|
||||
};
|
122
test/e2e/lib/page.js
Normal file
122
test/e2e/lib/page.js
Normal file
|
@ -0,0 +1,122 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
const webdriver = require('selenium-webdriver');
|
||||
const By = webdriver.By;
|
||||
const until = webdriver.until;
|
||||
const fs = require('fs-extra');
|
||||
const driver = require('./mocha-e2e').driver;
|
||||
const url = require('url');
|
||||
const UrlPattern = require('url-pattern');
|
||||
|
||||
const waitTimeout = 10000;
|
||||
|
||||
module.exports = (...extras) => Object.assign({
|
||||
elements: {},
|
||||
|
||||
async getElement(key) {
|
||||
return await driver.findElement(By.css(this.elements[key]));
|
||||
},
|
||||
|
||||
async getLinkParams(key) {
|
||||
const elem = await driver.findElement(By.css(this.elements[key]));
|
||||
|
||||
const linkUrl = await elem.getAttribute('href');
|
||||
const linkPath = url.parse(linkUrl).path;
|
||||
|
||||
const urlPattern = new UrlPattern(this.links[key]);
|
||||
|
||||
const params = urlPattern.match(linkPath);
|
||||
if (!params) {
|
||||
throw new Error(`Cannot match URL pattern ${this.links[key]}`);
|
||||
}
|
||||
return params;
|
||||
},
|
||||
|
||||
async waitUntilVisible(selector) {
|
||||
await driver.wait(until.elementLocated(By.css('body')), waitTimeout);
|
||||
|
||||
for (const elem of (this.elementsToWaitFor || [])) {
|
||||
const sel = this.elements[elem];
|
||||
if (!sel) {
|
||||
throw new Error(`Element "${elem}" not found.`);
|
||||
}
|
||||
await driver.wait(until.elementLocated(By.css(sel)), waitTimeout);
|
||||
}
|
||||
|
||||
for (const text of (this.textsToWaitFor || [])) {
|
||||
await driver.wait(new webdriver.Condition(`for text "${text}"`, async (driver) => {
|
||||
return await this.containsText(text);
|
||||
}), waitTimeout);
|
||||
}
|
||||
|
||||
if (this.url) {
|
||||
await this.ensureUrl();
|
||||
}
|
||||
|
||||
await driver.executeScript('document.mailTrainRefreshAcknowledged = true;');
|
||||
},
|
||||
|
||||
async waitUntilVisibleAfterRefresh(selector) {
|
||||
await driver.wait(new webdriver.Condition('for refresh', async (driver) => {
|
||||
const val = await driver.executeScript('return document.mailTrainRefreshAcknowledged;');
|
||||
return !val;
|
||||
}), waitTimeout);
|
||||
|
||||
await this.waitUntilVisible(selector);
|
||||
},
|
||||
|
||||
async click(key) {
|
||||
const elem = await this.getElement(key);
|
||||
await elem.click();
|
||||
},
|
||||
|
||||
async getHref(key) {
|
||||
const elem = await this.getElement(key);
|
||||
return await elem.getAttribute('href');
|
||||
},
|
||||
|
||||
async getText(key) {
|
||||
const elem = await this.getElement(key);
|
||||
return await elem.getText();
|
||||
},
|
||||
|
||||
async getValue(key) {
|
||||
const elem = await this.getElement(key);
|
||||
return await elem.getAttribute('value');
|
||||
},
|
||||
|
||||
async containsText(str) {
|
||||
return await driver.executeScript(`
|
||||
return (document.documentElement.innerText || document.documentElement.textContent).indexOf('${str}') > -1;
|
||||
`);
|
||||
},
|
||||
|
||||
async getSource() {
|
||||
return await driver.getPageSource();
|
||||
},
|
||||
|
||||
async saveSource(destPath) {
|
||||
const src = await this.getSource();
|
||||
await fs.writeFile(destPath, src);
|
||||
},
|
||||
|
||||
async saveScreenshot(destPath) {
|
||||
const pngData = await driver.takeScreenshot();
|
||||
const buf = new Buffer(pngData, 'base64');
|
||||
await fs.writeFile(destPath, buf);
|
||||
},
|
||||
|
||||
async saveSnapshot(destPathBase) {
|
||||
destPathBase = destPathBase || 'last-failed-e2e-test';
|
||||
const currentUrl = await driver.getCurrentUrl();
|
||||
const info = `URL: ${currentUrl}`;
|
||||
await fs.writeFile(destPathBase + '.info', info);
|
||||
await this.saveSource(destPathBase + '.html');
|
||||
await this.saveScreenshot(destPathBase + '.png');
|
||||
},
|
||||
|
||||
async sleep(ms) {
|
||||
await driver.sleep(ms);
|
||||
}
|
||||
}, ...extras);
|
35
test/e2e/lib/semaphore.js
Normal file
35
test/e2e/lib/semaphore.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
'use strict';
|
||||
|
||||
const Promise = require('bluebird');
|
||||
|
||||
class Semaphore {
|
||||
constructor() {
|
||||
this.counter = 0;
|
||||
}
|
||||
|
||||
enter() {
|
||||
this.counter++;
|
||||
}
|
||||
|
||||
exit() {
|
||||
this.counter--;
|
||||
}
|
||||
|
||||
async waitForEmpty() {
|
||||
const self = this;
|
||||
|
||||
function wait(resolve) {
|
||||
if (self.counter == 0) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(wait, 500, resolve);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
setTimeout(wait, 500, resolve);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Semaphore;
|
77
test/e2e/lib/web.js
Normal file
77
test/e2e/lib/web.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
const By = require('selenium-webdriver').By;
|
||||
const url = require('url');
|
||||
const UrlPattern = require('url-pattern');
|
||||
const driver = require('./mocha-e2e').driver;
|
||||
const page = require('./page');
|
||||
|
||||
module.exports = (...extras) => page({
|
||||
|
||||
async navigate(pathOrParams) {
|
||||
let path;
|
||||
if (typeof pathOrParams === 'string') {
|
||||
path = pathOrParams;
|
||||
} else {
|
||||
const urlPattern = new UrlPattern(this.requestUrl || this.url);
|
||||
path = urlPattern.stringify(pathOrParams)
|
||||
}
|
||||
|
||||
const parsedUrl = url.parse(path);
|
||||
let absolutePath;
|
||||
if (parsedUrl.host) {
|
||||
absolutePath = path;
|
||||
} else {
|
||||
absolutePath = config.baseUrl + path;
|
||||
}
|
||||
|
||||
await driver.navigate().to(absolutePath);
|
||||
await this.waitUntilVisible();
|
||||
},
|
||||
|
||||
async ensureUrl(path) {
|
||||
const desiredUrl = path || this.url;
|
||||
|
||||
if (desiredUrl) {
|
||||
const currentUrl = url.parse(await driver.getCurrentUrl());
|
||||
const urlPattern = new UrlPattern(desiredUrl);
|
||||
const params = urlPattern.match(currentUrl.pathname);
|
||||
if (!params || config.baseUrl !== `${currentUrl.protocol}//${currentUrl.host}`) {
|
||||
throw new Error(`Unexpected URL. Expecting ${config.baseUrl}${this.url} got ${currentUrl.protocol}//${currentUrl.host}/${currentUrl.pathname}`);
|
||||
}
|
||||
|
||||
this.params = params;
|
||||
}
|
||||
},
|
||||
|
||||
async submit() {
|
||||
const submitButton = await this.getElement('submitButton');
|
||||
await submitButton.click();
|
||||
},
|
||||
|
||||
async waitForFlash() {
|
||||
await this.waitUntilVisible('div.alert:not(.js-warning)');
|
||||
},
|
||||
|
||||
async getFlash() {
|
||||
const elem = await driver.findElement(By.css('div.alert:not(.js-warning)'));
|
||||
return await elem.getText();
|
||||
},
|
||||
|
||||
async clearFlash() {
|
||||
await driver.executeScript(`
|
||||
var elements = document.getElementsByClassName('alert');
|
||||
while(elements.length > 0){
|
||||
elements[0].parentNode.removeChild(elements[0]);
|
||||
}
|
||||
`);
|
||||
},
|
||||
|
||||
async setValue(key, value) {
|
||||
const elem = await this.getElement(key);
|
||||
await elem.clear();
|
||||
await elem.sendKeys(value);
|
||||
}
|
||||
|
||||
}, ...extras);
|
|
@ -1,21 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const page = require('./page');
|
||||
|
||||
module.exports = driver => Object.assign(page(driver), {
|
||||
elementToWaitFor: 'alert',
|
||||
elements: {
|
||||
alert: 'div.alert:not(.js-warning)'
|
||||
},
|
||||
getText() {
|
||||
return this.element('alert').getText();
|
||||
},
|
||||
clear() {
|
||||
return this.driver.executeScript(`
|
||||
var elements = document.getElementsByClassName('alert');
|
||||
while(elements.length > 0){
|
||||
elements[0].parentNode.removeChild(elements[0]);
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
|
@ -1,11 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const page = require('./page');
|
||||
const web = require('../lib/web');
|
||||
|
||||
module.exports = driver => Object.assign(page(driver), {
|
||||
url: '/',
|
||||
elementToWaitFor: 'body',
|
||||
elements: {
|
||||
body: 'body.page--home'
|
||||
}
|
||||
module.exports = web({
|
||||
url: '/'
|
||||
});
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../helpers/config');
|
||||
const webdriver = require('selenium-webdriver');
|
||||
const By = webdriver.By;
|
||||
const until = webdriver.until;
|
||||
|
||||
module.exports = driver => ({
|
||||
driver,
|
||||
elements: {},
|
||||
|
||||
element(key) {
|
||||
return this.driver.findElement(By.css(this.elements[key] || key));
|
||||
},
|
||||
|
||||
navigate(path) {
|
||||
this.driver.navigate().to(config.baseUrl + (path || this.url));
|
||||
return this.waitUntilVisible();
|
||||
},
|
||||
|
||||
waitUntilVisible() {
|
||||
let selector = this.elements[this.elementToWaitFor];
|
||||
if (!selector && this.url) {
|
||||
selector = 'body.page--' + (this.url.substring(1).replace(/\//g, '--') || 'home');
|
||||
}
|
||||
return selector ? this.driver.wait(until.elementLocated(By.css(selector))) : this.driver.sleep(1000);
|
||||
},
|
||||
|
||||
submit() {
|
||||
return this.element('submitButton').click();
|
||||
},
|
||||
|
||||
click(key) {
|
||||
return this.element(key).click();
|
||||
},
|
||||
|
||||
getText(key) {
|
||||
return this.element(key).getText();
|
||||
},
|
||||
|
||||
getValue(key) {
|
||||
return this.element(key).getAttribute('value');
|
||||
},
|
||||
|
||||
setValue(key, value) {
|
||||
return this.element(key).sendKeys(value);
|
||||
},
|
||||
|
||||
containsText(str) {
|
||||
// let text = await driver.findElement({ css: 'body' }).getText();
|
||||
return this.driver.executeScript(`
|
||||
return (document.documentElement.textContent || document.documentElement.innerText).indexOf('${str}') > -1;
|
||||
`);
|
||||
}
|
||||
});
|
|
@ -1,84 +1,132 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../helpers/config');
|
||||
const page = require('./page');
|
||||
const config = require('../lib/config');
|
||||
const web = require('../lib/web');
|
||||
const mail = require('../lib/mail');
|
||||
|
||||
const web = {
|
||||
enterEmail(value) {
|
||||
this.element('emailInput').clear();
|
||||
return this.element('emailInput').sendKeys(value);
|
||||
}
|
||||
};
|
||||
module.exports = list => ({
|
||||
|
||||
const mail = {
|
||||
navigate(address) {
|
||||
this.driver.sleep(100);
|
||||
this.driver.navigate().to(`http://localhost:${config.app.testserver.mailboxserverport}/${address}`);
|
||||
return this.waitUntilVisible();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = (driver, list) => ({
|
||||
|
||||
webSubscribe: Object.assign(page(driver), web, {
|
||||
webSubscribe: web({
|
||||
url: `/subscription/${list.cid}`,
|
||||
elementToWaitFor: 'form',
|
||||
elementsToWaitFor: ['form'],
|
||||
textsToWaitFor: ['Subscribe to list'],
|
||||
elements: {
|
||||
form: `form[action="/subscription/${list.cid}/subscribe"]`,
|
||||
emailInput: '#main-form input[name="email"]',
|
||||
firstNameInput: '#main-form input[name="first-name"]',
|
||||
lastNameInput: '#main-form input[name="last-name"]',
|
||||
submitButton: 'a[href="#submit"]'
|
||||
}
|
||||
}),
|
||||
|
||||
webConfirmSubscriptionNotice: Object.assign(page(driver), web, {
|
||||
url: `/subscription/${list.cid}/confirm-notice`,
|
||||
elementToWaitFor: 'homepageButton',
|
||||
webConfirmSubscriptionNotice: web({
|
||||
url: `/subscription/${list.cid}/confirm-subscription-notice`,
|
||||
textsToWaitFor: ['We need to confirm your email address']
|
||||
}),
|
||||
|
||||
mailConfirmSubscription: mail({
|
||||
elementsToWaitFor: ['confirmLink'],
|
||||
textsToWaitFor: ['Please Confirm Subscription'],
|
||||
elements: {
|
||||
homepageButton: `a[href="${config.settings['default-homepage']}"]`
|
||||
confirmLink: `a[href^="${config.settings['service-url']}subscription/confirm/subscribe/"]`
|
||||
}
|
||||
}),
|
||||
|
||||
mailConfirmSubscription: Object.assign(page(driver), mail, {
|
||||
elementToWaitFor: 'confirmLink',
|
||||
mailAlreadySubscribed: mail({
|
||||
elementsToWaitFor: ['unsubscribeLink'],
|
||||
textsToWaitFor: ['Email address already registered'],
|
||||
elements: {
|
||||
confirmLink: `a[href^="${config.settings['service-url']}subscription/subscribe/"]`
|
||||
unsubscribeLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}/unsubscribe/"]`,
|
||||
manageLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}/manage/"]`
|
||||
},
|
||||
links: {
|
||||
unsubscribeLink: `/subscription/${list.cid}/unsubscribe/:ucid`,
|
||||
manageLink: `/subscription/${list.cid}/manage/:ucid`
|
||||
}
|
||||
}),
|
||||
|
||||
webSubscribedNotice: Object.assign(page(driver), web, {
|
||||
elementToWaitFor: 'homepageButton',
|
||||
webSubscribedNotice: web({
|
||||
url: `/subscription/${list.cid}/subscribed-notice`,
|
||||
textsToWaitFor: ['Subscription Confirmed']
|
||||
}),
|
||||
|
||||
mailSubscriptionConfirmed: mail({
|
||||
elementsToWaitFor: ['unsubscribeLink'],
|
||||
textsToWaitFor: ['Subscription Confirmed'],
|
||||
elements: {
|
||||
homepageButton: 'a[href^="https://mailtrain.org"]'
|
||||
unsubscribeLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}/unsubscribe/"]`,
|
||||
manageLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}/manage/"]`
|
||||
},
|
||||
links: {
|
||||
unsubscribeLink: `/subscription/${list.cid}/unsubscribe/:ucid`,
|
||||
manageLink: `/subscription/${list.cid}/manage/:ucid`
|
||||
}
|
||||
}),
|
||||
|
||||
mailSubscriptionConfirmed: Object.assign(page(driver), mail, {
|
||||
elementToWaitFor: 'unsubscribeLink',
|
||||
webManage: web({
|
||||
url: `/subscription/${list.cid}/manage/:ucid`,
|
||||
elementsToWaitFor: ['form'],
|
||||
textsToWaitFor: ['Update Your Preferences'],
|
||||
elements: {
|
||||
unsubscribeLink: 'a[href*="/unsubscribe/"]',
|
||||
manageLink: 'a[href*="/manage/"]'
|
||||
form: `form[action="/subscription/${list.cid}/manage"]`,
|
||||
emailInput: '#main-form input[name="email"]',
|
||||
firstNameInput: '#main-form input[name="first-name"]',
|
||||
lastNameInput: '#main-form input[name="last-name"]',
|
||||
submitButton: 'a[href="#submit"]',
|
||||
manageAddressLink: `a[href^="/subscription/${list.cid}/manage-address/"]`
|
||||
},
|
||||
links: {
|
||||
manageAddressLink: `/subscription/${list.cid}/manage-address/:ucid`
|
||||
}
|
||||
}),
|
||||
|
||||
webUnsubscribe: Object.assign(page(driver), web, {
|
||||
elementToWaitFor: 'submitButton',
|
||||
webManageAddress: web({
|
||||
url: `/subscription/${list.cid}/manage-address/:ucid`,
|
||||
elementsToWaitFor: ['form'],
|
||||
textsToWaitFor: ['Update Your Email Address'],
|
||||
elements: {
|
||||
submitButton: 'a[href="#submit"]'
|
||||
form: `form[action="/subscription/${list.cid}/manage-address"]`,
|
||||
emailInput: '#main-form input[name="email"]',
|
||||
emailNewInput: '#main-form input[name="email-new"]',
|
||||
submitButton: 'a[href="#submit"]',
|
||||
}
|
||||
}),
|
||||
|
||||
webUnsubscribedNotice: Object.assign(page(driver), web, {
|
||||
elementToWaitFor: 'homepageButton',
|
||||
mailConfirmAddressChange: mail({
|
||||
elementsToWaitFor: ['confirmLink'],
|
||||
textsToWaitFor: ['Please Confirm Subscription Address Change'],
|
||||
elements: {
|
||||
homepageButton: 'a[href^="https://mailtrain.org"]'
|
||||
confirmLink: `a[href^="${config.settings['service-url']}subscription/confirm/change-address/"]`
|
||||
}
|
||||
}),
|
||||
|
||||
mailUnsubscriptionConfirmed: Object.assign(page(driver), mail, {
|
||||
elementToWaitFor: 'resubscribeLink',
|
||||
webUpdatedNotice: web({
|
||||
url: `/subscription/${list.cid}/updated-notice`,
|
||||
textsToWaitFor: ['Profile Updated'],
|
||||
}),
|
||||
|
||||
webUnsubscribedNotice: web({
|
||||
url: `/subscription/${list.cid}/unsubscribed-notice`,
|
||||
textsToWaitFor: ['Unsubscribe Successful'],
|
||||
}),
|
||||
|
||||
mailUnsubscriptionConfirmed: mail({
|
||||
elementsToWaitFor: ['resubscribeLink'],
|
||||
textsToWaitFor: ['You Are Now Unsubscribed'],
|
||||
elements: {
|
||||
resubscribeLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}"]`
|
||||
}
|
||||
})
|
||||
}),
|
||||
|
||||
/*
|
||||
webUnsubscribe: web({ // FIXME
|
||||
elementsToWaitFor: ['submitButton'],
|
||||
elements: {
|
||||
submitButton: 'a[href="#submit"]'
|
||||
}
|
||||
}),
|
||||
|
||||
*/
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -1,32 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
const page = require('./page');
|
||||
const web = require('../lib/web');
|
||||
|
||||
module.exports = driver => ({
|
||||
|
||||
login: Object.assign(page(driver), {
|
||||
module.exports = {
|
||||
login: web({
|
||||
url: '/users/login',
|
||||
elementToWaitFor: 'submitButton',
|
||||
elementsToWaitFor: ['submitButton'],
|
||||
elements: {
|
||||
usernameInput: 'form[action="/users/login"] input[name="username"]',
|
||||
passwordInput: 'form[action="/users/login"] input[name="password"]',
|
||||
submitButton: 'form[action="/users/login"] [type=submit]'
|
||||
},
|
||||
enterUsername(value) {
|
||||
// this.element('usernameInput').clear();
|
||||
return this.element('usernameInput').sendKeys(value);
|
||||
},
|
||||
enterPassword(value) {
|
||||
return this.element('passwordInput').sendKeys(value);
|
||||
}
|
||||
}),
|
||||
|
||||
account: Object.assign(page(driver), {
|
||||
logout: web({
|
||||
requestUrl: '/users/logout',
|
||||
url: '/'
|
||||
}),
|
||||
|
||||
account: web({
|
||||
url: '/users/account',
|
||||
elementToWaitFor: 'emailInput',
|
||||
elementsToWaitFor: ['form'],
|
||||
elements: {
|
||||
form: `form[action="/users/account"]`,
|
||||
emailInput: 'form[action="/users/account"] input[name="email"]'
|
||||
}
|
||||
})
|
||||
|
||||
});
|
||||
}),
|
||||
};
|
|
@ -1,57 +1,68 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../helpers/config');
|
||||
const config = require('../lib/config');
|
||||
const { useCase, step, driver } = require('../lib/mocha-e2e');
|
||||
const expect = require('chai').expect;
|
||||
const driver = require('../helpers/driver');
|
||||
const home = require('../page-objects/home')(driver);
|
||||
const flash = require('../page-objects/flash')(driver);
|
||||
const {
|
||||
login,
|
||||
account
|
||||
} = require('../page-objects/users')(driver);
|
||||
|
||||
describe('login', function() {
|
||||
this.timeout(10000);
|
||||
const page = require('../page-objects/user');
|
||||
const home = require('../page-objects/home');
|
||||
|
||||
suite('Login use-cases', function() {
|
||||
before(() => driver.manage().deleteAllCookies());
|
||||
|
||||
it('can access home page', async () => {
|
||||
test('User can access home page', async () => {
|
||||
await home.navigate();
|
||||
});
|
||||
|
||||
it('can not access restricted content', async () => {
|
||||
driver.navigate().to(config.baseUrl + '/settings');
|
||||
flash.waitUntilVisible();
|
||||
expect(await flash.getText()).to.contain('Need to be logged in to access restricted content');
|
||||
await flash.clear();
|
||||
test('Anonymous user cannot access restricted content', async () => {
|
||||
await driver.navigate().to(config.baseUrl + '/settings');
|
||||
await page.login.waitUntilVisible();
|
||||
await page.login.waitForFlash();
|
||||
expect(await page.login.getFlash()).to.contain('Need to be logged in to access restricted content');
|
||||
});
|
||||
|
||||
it('can not login with false credentials', async () => {
|
||||
login.enterUsername(config.users.admin.username);
|
||||
login.enterPassword('invalid');
|
||||
login.submit();
|
||||
flash.waitUntilVisible();
|
||||
expect(await flash.getText()).to.contain('Incorrect username or password');
|
||||
await flash.clear();
|
||||
useCase('Login (invalid credential)', async () => {
|
||||
await step('User navigates to the login page.', async () => {
|
||||
await page.login.navigate();
|
||||
});
|
||||
|
||||
it('can login as admin', async () => {
|
||||
login.enterUsername(config.users.admin.username);
|
||||
login.enterPassword(config.users.admin.password);
|
||||
login.submit();
|
||||
flash.waitUntilVisible();
|
||||
expect(await flash.getText()).to.contain('Logged in as admin');
|
||||
await step('User fills in the user name and incorrect password.', async () => {
|
||||
await page.login.setValue('usernameInput', config.users.admin.username);
|
||||
await page.login.setValue('passwordInput', 'invalid');
|
||||
await page.login.submit();
|
||||
});
|
||||
|
||||
it('can access account page as admin', async () => {
|
||||
await account.navigate();
|
||||
await step('System shows a flash notice that credentials are invalid.', async () => {
|
||||
await page.login.waitForFlash();
|
||||
expect(await page.login.getFlash()).to.contain('Incorrect username or password');
|
||||
});
|
||||
});
|
||||
|
||||
it('can logout', async () => {
|
||||
driver.navigate().to(config.baseUrl + '/users/logout');
|
||||
flash.waitUntilVisible();
|
||||
expect(await flash.getText()).to.contain('logged out');
|
||||
useCase('Login and logout', async () => {
|
||||
await step('User navigates to the login page.', async () => {
|
||||
await page.login.navigate();
|
||||
});
|
||||
|
||||
after(() => driver.quit());
|
||||
await step('User fills in the user name and password.', async () => {
|
||||
await page.login.setValue('usernameInput', config.users.admin.username);
|
||||
await page.login.setValue('passwordInput', config.users.admin.password);
|
||||
await page.login.submit();
|
||||
});
|
||||
|
||||
await step('System shows the home page and a flash notice that user has been logged in.', async () => {
|
||||
await home.waitUntilVisibleAfterRefresh();
|
||||
await home.waitForFlash();
|
||||
expect(await home.getFlash()).to.contain('Logged in as admin');
|
||||
});
|
||||
|
||||
await step('User navigates to its account.', async () => {
|
||||
await page.account.navigate();
|
||||
});
|
||||
|
||||
await step('User logs out.', async () => {
|
||||
await page.logout.navigate();
|
||||
await home.waitForFlash();
|
||||
expect(await home.getFlash()).to.contain('logged out');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,101 +1,248 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('../helpers/config');
|
||||
const config = require('../lib/config');
|
||||
const { useCase, step, precondition, driver } = require('../lib/mocha-e2e');
|
||||
const shortid = require('shortid');
|
||||
const expect = require('chai').expect;
|
||||
const driver = require('../helpers/driver');
|
||||
|
||||
const page = require('../page-objects/page')(driver);
|
||||
const flash = require('../page-objects/flash')(driver);
|
||||
const page = require('../page-objects/subscription')(config.lists.one);
|
||||
|
||||
const {
|
||||
webSubscribe,
|
||||
webConfirmSubscriptionNotice,
|
||||
mailConfirmSubscription,
|
||||
webSubscribedNotice,
|
||||
mailSubscriptionConfirmed,
|
||||
webUnsubscribe,
|
||||
webUnsubscribedNotice,
|
||||
mailUnsubscriptionConfirmed
|
||||
} = require('../page-objects/subscription')(driver, config.lists.one);
|
||||
function generateEmail() {
|
||||
return 'keep.' + shortid.generate() + '@mailtrain.org';
|
||||
}
|
||||
|
||||
const testuser = {
|
||||
email: 'keep.' + shortid.generate() + '@mailtrain.org'
|
||||
};
|
||||
async function subscribe(subscription) {
|
||||
await step('User navigates to list subscription page.', async () => {
|
||||
await page.webSubscribe.navigate();
|
||||
});
|
||||
|
||||
// console.log(testuser.email);
|
||||
await step('User submits a valid email and other subscription info.', async () => {
|
||||
await page.webSubscribe.setValue('emailInput', subscription.email);
|
||||
|
||||
describe('subscribe (list one)', function() {
|
||||
this.timeout(10000);
|
||||
if (subscription.firstName) {
|
||||
await page.webSubscribe.setValue('firstNameInput', subscription.firstName);
|
||||
}
|
||||
|
||||
if (subscription.lastName) {
|
||||
await page.webSubscribe.setValue('lastNameInput', subscription.lastName);
|
||||
}
|
||||
|
||||
await page.webSubscribe.submit();
|
||||
});
|
||||
|
||||
await step('System shows a notice that further instructions are in the email.', async () => {
|
||||
await page.webConfirmSubscriptionNotice.waitUntilVisibleAfterRefresh();
|
||||
});
|
||||
|
||||
await step('System sends an email with a link to confirm the subscription.', async () => {
|
||||
await page.mailConfirmSubscription.fetchMail(subscription.email);
|
||||
});
|
||||
|
||||
await step('User clicks confirm subscription in the email', async () => {
|
||||
await page.mailConfirmSubscription.click('confirmLink');
|
||||
});
|
||||
|
||||
await step('System shows a notice that subscription has been confirmed.', async () => {
|
||||
await page.webSubscribedNotice.waitUntilVisibleAfterRefresh();
|
||||
});
|
||||
|
||||
await step('System sends an email with subscription confirmation.', async () => {
|
||||
await page.mailSubscriptionConfirmed.fetchMail(subscription.email);
|
||||
subscription.unsubscribeLink = await page.mailSubscriptionConfirmed.getHref('unsubscribeLink');
|
||||
subscription.manageLink = await page.mailSubscriptionConfirmed.getHref('manageLink');
|
||||
|
||||
const unsubscribeParams = await page.mailSubscriptionConfirmed.getLinkParams('unsubscribeLink');
|
||||
const manageParams = await page.mailSubscriptionConfirmed.getLinkParams('manageLink');
|
||||
expect(unsubscribeParams.ucid).to.equal(manageParams.ucid);
|
||||
subscription.ucid = unsubscribeParams.ucid;
|
||||
});
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
async function subscriptionExistsPrecondition(subscription) {
|
||||
await precondition('Subscription exists', 'Subscription to a public list (main scenario)', async () => {
|
||||
await subscribe(subscription);
|
||||
});
|
||||
return subscription;
|
||||
}
|
||||
|
||||
suite('Subscription use-cases', function() {
|
||||
before(() => driver.manage().deleteAllCookies());
|
||||
|
||||
it('visits web-subscribe', async () => {
|
||||
await webSubscribe.navigate();
|
||||
});
|
||||
|
||||
it('submits invalid email (error)', async () => {
|
||||
webSubscribe.enterEmail('foo@bar.nope');
|
||||
webSubscribe.submit();
|
||||
flash.waitUntilVisible();
|
||||
expect(await flash.getText()).to.contain('Invalid email address');
|
||||
});
|
||||
|
||||
it('submits valid email', async () => {
|
||||
webSubscribe.enterEmail(testuser.email);
|
||||
await webSubscribe.submit();
|
||||
});
|
||||
|
||||
it('sees web-confirm-subscription-notice', async () => {
|
||||
webConfirmSubscriptionNotice.waitUntilVisible();
|
||||
expect(await page.containsText('Almost Finished')).to.be.true;
|
||||
});
|
||||
|
||||
it('receives mail-confirm-subscription', async () => {
|
||||
mailConfirmSubscription.navigate(testuser.email);
|
||||
expect(await page.containsText('Please Confirm Subscription')).to.be.true;
|
||||
});
|
||||
|
||||
it('clicks confirm subscription', async () => {
|
||||
await mailConfirmSubscription.click('confirmLink');
|
||||
});
|
||||
|
||||
it('sees web-subscribed-notice', async () => {
|
||||
webSubscribedNotice.waitUntilVisible();
|
||||
expect(await page.containsText('Subscription Confirmed')).to.be.true;
|
||||
});
|
||||
|
||||
it('receives mail-subscription-confirmed', async () => {
|
||||
mailSubscriptionConfirmed.navigate(testuser.email);
|
||||
expect(await page.containsText('Subscription Confirmed')).to.be.true;
|
||||
useCase('Subscription to a public list (main scenario)', async () => {
|
||||
await subscribe({
|
||||
email: generateEmail()
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsubscribe (list one)', function() {
|
||||
this.timeout(10000);
|
||||
|
||||
it('clicks unsubscribe', async () => {
|
||||
await mailSubscriptionConfirmed.click('unsubscribeLink');
|
||||
useCase('Subscription to a public list (invalid email)', async () => {
|
||||
await step('User navigates to list subscribe page', async () => {
|
||||
await page.webSubscribe.navigate();
|
||||
});
|
||||
|
||||
it('sees web-unsubscribe', async () => {
|
||||
webUnsubscribe.waitUntilVisible();
|
||||
expect(await page.containsText('Unsubscribe')).to.be.true;
|
||||
await step('User submits an invalid email.', async () => {
|
||||
await page.webSubscribe.setValue('emailInput', 'foo@bar.nope');
|
||||
await page.webSubscribe.submit();
|
||||
});
|
||||
|
||||
it('clicks confirm unsubscription', async () => {
|
||||
await webUnsubscribe.submit();
|
||||
await step('System shows a flash notice that email is invalid.', async () => {
|
||||
await page.webSubscribe.waitForFlash();
|
||||
expect(await page.webSubscribe.getFlash()).to.contain('Invalid email address');
|
||||
});
|
||||
});
|
||||
|
||||
it('sees web-unsubscribed-notice', async () => {
|
||||
webUnsubscribedNotice.waitUntilVisible();
|
||||
expect(await page.containsText('Unsubscribe Successful')).to.be.true;
|
||||
useCase('Subscription to a public list (email already registered)', async () => {
|
||||
const subscription = await subscriptionExistsPrecondition({
|
||||
email: generateEmail()
|
||||
});
|
||||
|
||||
it('receives mail-unsubscription-confirmed', async () => {
|
||||
mailUnsubscriptionConfirmed.navigate(testuser.email);
|
||||
expect(await page.containsText('You Are Now Unsubscribed')).to.be.true;
|
||||
await step('User navigates to list subscribe page', async () => {
|
||||
await page.webSubscribe.navigate();
|
||||
});
|
||||
|
||||
after(() => driver.quit());
|
||||
await step('User submits the email which has been already registered.', async () => {
|
||||
await page.webSubscribe.setValue('emailInput', subscription.email);
|
||||
await page.webSubscribe.submit();
|
||||
});
|
||||
|
||||
await step('System shows a notice that further instructions are in the email.', async () => {
|
||||
await page.webConfirmSubscriptionNotice.waitUntilVisibleAfterRefresh();
|
||||
});
|
||||
|
||||
await step('System sends an email informing that the address has been already registered.', async () => {
|
||||
await page.mailAlreadySubscribed.fetchMail(subscription.email);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
useCase('Subscription to a non-public list');
|
||||
|
||||
useCase('Change profile info', async () => {
|
||||
const subscription = await subscriptionExistsPrecondition({
|
||||
email: generateEmail(),
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
});
|
||||
|
||||
await step('User clicks the manage subscription button.', async () => {
|
||||
await page.mailSubscriptionConfirmed.click('manageLink');
|
||||
});
|
||||
|
||||
await step('Systems shows a form to change subscription details. The form contains data entered during subscription.', async () => {
|
||||
await page.webManage.waitUntilVisibleAfterRefresh();
|
||||
expect(await page.webManage.getValue('emailInput')).to.equal(subscription.email);
|
||||
expect(await page.webManage.getValue('firstNameInput')).to.equal(subscription.firstName);
|
||||
expect(await page.webManage.getValue('lastNameInput')).to.equal(subscription.lastName);
|
||||
});
|
||||
|
||||
await step('User enters another name and submits the form.', async () => {
|
||||
subscription.firstName = 'Adam';
|
||||
subscription.lastName = 'B';
|
||||
await page.webManage.setValue('firstNameInput', subscription.firstName);
|
||||
await page.webManage.setValue('lastNameInput', subscription.lastName);
|
||||
await page.webManage.submit();
|
||||
});
|
||||
|
||||
await step('Systems shows a notice that profile has been updated.', async () => {
|
||||
await page.webUpdatedNotice.waitUntilVisibleAfterRefresh();
|
||||
});
|
||||
|
||||
await step('User navigates to manage subscription again.', async () => {
|
||||
// await page.webManage.navigate(subscription.manageLink);
|
||||
await page.webManage.navigate({ ucid: subscription.ucid });
|
||||
});
|
||||
|
||||
await step('Systems shows a form with the changes made previously.', async () => {
|
||||
expect(await page.webManage.getValue('emailInput')).to.equal(subscription.email);
|
||||
expect(await page.webManage.getValue('firstNameInput')).to.equal(subscription.firstName);
|
||||
expect(await page.webManage.getValue('lastNameInput')).to.equal(subscription.lastName);
|
||||
});
|
||||
});
|
||||
|
||||
useCase('Change email', async () => {
|
||||
const subscription = await subscriptionExistsPrecondition({
|
||||
email: generateEmail(),
|
||||
firstName: 'John',
|
||||
lastName: 'Doe'
|
||||
});
|
||||
|
||||
await step('User clicks the manage subscription button.', async () => {
|
||||
await page.mailSubscriptionConfirmed.click('manageLink');
|
||||
});
|
||||
|
||||
await step('Systems shows a form to change subscription details. The form contains data entered during subscription.', async () => {
|
||||
await page.webManage.waitUntilVisibleAfterRefresh();
|
||||
expect(await page.webManage.getValue('emailInput')).to.equal(subscription.email);
|
||||
expect(await page.webManage.getValue('firstNameInput')).to.equal(subscription.firstName);
|
||||
expect(await page.webManage.getValue('lastNameInput')).to.equal(subscription.lastName);
|
||||
});
|
||||
|
||||
await step('User clicks the change address button.', async () => {
|
||||
await page.webManage.click('manageAddressLink');
|
||||
});
|
||||
|
||||
await step('Systems shows a form to change email.', async () => {
|
||||
await page.webManageAddress.waitUntilVisibleAfterRefresh();
|
||||
});
|
||||
|
||||
await step('User fills in a new email address and submits the form.', async () => {
|
||||
subscription.email = generateEmail();
|
||||
await page.webManageAddress.setValue('emailNewInput', subscription.email);
|
||||
await page.webManageAddress.submit();
|
||||
});
|
||||
|
||||
await step('System goes back to the profile form and shows a flash notice that further instructions are in the email.', async () => {
|
||||
await page.webManage.waitUntilVisibleAfterRefresh();
|
||||
await page.webManage.waitForFlash();
|
||||
expect(await page.webManage.getFlash()).to.contain('An email with further instructions has been sent to the provided address');
|
||||
});
|
||||
|
||||
await step('System sends an email with a link to confirm the address change.', async () => {
|
||||
await page.mailConfirmAddressChange.fetchMail(subscription.email);
|
||||
});
|
||||
|
||||
await step('User clicks confirm subscription in the email', async () => {
|
||||
await page.mailConfirmAddressChange.click('confirmLink');
|
||||
});
|
||||
|
||||
await step('System shows the profile form with a flash notice that address has been changed.', async () => {
|
||||
await page.webManage.waitUntilVisibleAfterRefresh();
|
||||
await page.webManage.waitForFlash();
|
||||
expect(await page.webManage.getFlash()).to.contain('Email address changed');
|
||||
expect(await page.webManage.getValue('emailInput')).to.equal(subscription.email);
|
||||
});
|
||||
|
||||
await step('System sends an email with subscription confirmation.', async () => {
|
||||
await page.mailSubscriptionConfirmed.fetchMail(subscription.email);
|
||||
});
|
||||
});
|
||||
|
||||
useCase('Unsubscription from list #1 (one-step, no form).', async () => {
|
||||
const subscription = await subscriptionExistsPrecondition({
|
||||
email: generateEmail()
|
||||
});
|
||||
|
||||
await step('User clicks the unsubscribe button.', async () => {
|
||||
await page.mailSubscriptionConfirmed.click('unsubscribeLink');
|
||||
});
|
||||
|
||||
await step('System shows a notice that confirms unsubscription.', async () => {
|
||||
await page.webUnsubscribedNotice.waitUntilVisibleAfterRefresh();
|
||||
});
|
||||
|
||||
await step('System sends an email that confirms unsubscription.', async () => {
|
||||
await page.mailUnsubscriptionConfirmed.fetchMail(subscription.email);
|
||||
});
|
||||
});
|
||||
|
||||
useCase('Unsubscription from list #2 (one-step, with form).');
|
||||
|
||||
useCase('Unsubscription from list #3 (two-step, no form).');
|
||||
|
||||
useCase('Unsubscription from list #4 (two-step, with form).');
|
||||
|
||||
useCase('Unsubscription from list #5 (manual unsubscribe).');
|
||||
|
||||
useCase('Resubscription.'); // This one is supposed to check that values pre-filled in resubscription (i.e. the re-subscribe link in unsubscription confirmation) are the same as the ones used before.
|
||||
});
|
||||
|
|
|
@ -26,13 +26,29 @@
|
|||
|
||||
<hr />
|
||||
|
||||
<div class="col-sm-offset-2">
|
||||
<div class="form-group">
|
||||
<label for="default_form" class="col-sm-2 control-label">{{#translate}}Subscription{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="public_subscribe" value="1" {{#if publicSubscribe}} checked {{/if}}> {{#translate}}Allow public users to subscribe themselves{{/translate}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="default_form" class="col-sm-2 control-label">{{#translate}}Unsubscription{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="unsubscription_mode" name="unsubscription_mode">
|
||||
{{#each unsubscriptionModeOptions}}
|
||||
<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<span class="help-block">{{#translate}}Select how an unsuscription request by subscriber is handled.{{/translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
|
|
|
@ -56,13 +56,29 @@
|
|||
|
||||
<hr />
|
||||
|
||||
<div class="col-sm-offset-2">
|
||||
<div class="form-group">
|
||||
<label for="default_form" class="col-sm-2 control-label">{{#translate}}Subscription{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="public_subscribe" value="1" {{#if publicSubscribe}} checked {{/if}}> {{#translate}}Allow public users to subscribe themselves{{/translate}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label for="default_form" class="col-sm-2 control-label">{{#translate}}Unsubscription{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<select class="form-control" id="unsubscription_mode" name="unsubscription_mode">
|
||||
{{#each unsubscriptionModeOptions}}
|
||||
<option value="{{value}}"{{#if selected}} selected{{/if}}>{{label}}</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<span class="help-block">{{#translate}}Select how an unsuscription request by subscriber is handled.{{/translate}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
|
|
|
@ -46,12 +46,20 @@
|
|||
<p>
|
||||
<a href="/subscription/{{list.cid}}?fid={{form.id}}" target="_blank">{{#translate}}Subscribe{{/translate}}</a>
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/confirm-notice?fid={{form.id}}" target="_blank">{{#translate}}Confirm Notice{{/translate}}</a>
|
||||
<a href="/subscription/{{list.cid}}/confirm-subscription-notice?fid={{form.id}}" target="_blank">{{#translate}}Confirm Subscription Notice{{/translate}}</a>
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/confirm-unsubscription-notice?fid={{form.id}}" target="_blank">{{#translate}}Confirm Unsubscription Notice{{/translate}}</a>
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/subscribed-notice?fid={{form.id}}" target="_blank">{{#translate}}Subscribed Notice{{/translate}}</a>
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/updated-notice?fid={{form.id}}" target="_blank">{{#translate}}Updated Notice{{/translate}}</a>
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/unsubscribe-notice?fid={{form.id}}" target="_blank">{{#translate}}Unsubscribed Notice{{/translate}}</a>
|
||||
<a href="/subscription/{{list.cid}}/unsubscribed-notice?fid={{form.id}}" target="_blank">{{#translate}}Unsubscribed Notice{{/translate}}</a>
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/manual-unsubscribe-notice?fid={{form.id}}" target="_blank">{{#translate}}Manual Unsubscribe Notice{{/translate}}</a>
|
||||
{{#if testUsers}}
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/unsubscribe/{{testUsers.0.cid}}?fid={{form.id}}&formTest=1" target="_blank">{{#translate}}Unsubscribe{{/translate}}</a>
|
||||
|
|
||||
<a href="/subscription/{{list.cid}}/manage/{{testUsers.0.cid}}?fid={{form.id}}" target="_blank">{{#translate}}Manage{{/translate}}</a>
|
||||
|
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
<form class="form-inline" method="post" action="/lists/edit?next=%2Fforms%2F{{list.id}}">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="id" value="{{list.id}}" />
|
||||
<input type="hidden" name="name" value="{{list.name}}" />
|
||||
<input type="hidden" name="customFormChangeOnly" value="1" />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="default_form" class="control-label" style="color: #666; font-weight: normal;">{{#translate}}The default form for this list is:{{/translate}}</label>
|
||||
|
|
24
views/subscription/mail-already-subscribed-html.mjml.hbs
Normal file
24
views/subscription/mail-already-subscribed-html.mjml.hbs
Normal file
|
@ -0,0 +1,24 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#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}}.
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}If you received this email by mistake, simply delete it. Your existing subscription won't be affected.{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}If you want to modify your subscription then you can {{/translate}}
|
||||
<a href="{{preferencesUrl}}">{{#translate}}manage your preferences{{/translate}}</a> {{#translate}}or{{/translate}} <a href="{{unsubscribeUrl}}">{{#translate}}unsubscribe here{{/translate}}</a>.
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{homepage}}">
|
||||
{{#translate}}Return to our website{{/translate}}
|
||||
</mj-button>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}For questions about this list, please contact:{{/translate}}
|
||||
<br/><a href="mailto:{{contactAddress}}">{{contactAddress}}</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
18
views/subscription/mail-already-subscribed-text.hbs
Normal file
18
views/subscription/mail-already-subscribed-text.hbs
Normal file
|
@ -0,0 +1,18 @@
|
|||
{{{title}}}
|
||||
{{#translate}}Email address already registered{{/translate}}
|
||||
================================
|
||||
|
||||
{{#translate}}We have received a subscription request. Your email address is however already registered.{{/translate}}
|
||||
|
||||
{{#translate}}If you received this email by mistake, simply delete it. Your existing subscription won't be affected.{{/translate}}
|
||||
|
||||
{{#translate}}If you want to modify your subscription then you can:{{/translate}}
|
||||
|
||||
{{#translate}}manage your preferences{{/translate}}: {{preferencesUrl}}
|
||||
|
||||
- {{#translate}}or{{/translate}} -
|
||||
|
||||
{{#translate}}unsubscribe here{{/translate}}: {{unsubscribeUrl}}
|
||||
|
||||
{{#translate}}For questions about this list, please contact:{{/translate}}
|
||||
{{{contactAddress}}}
|
17
views/subscription/mail-confirm-address-change-html.mjml.hbs
Normal file
17
views/subscription/mail-confirm-address-change-html.mjml.hbs
Normal file
|
@ -0,0 +1,17 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Please Confirm Subscription Address Change{{/translate}}
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{confirmUrl}}">
|
||||
{{#translate}}Yes, subscribe this email address to the list{{/translate}}
|
||||
</mj-button>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}If you received this email by mistake, simply delete it. You won't be subscribed if you don't click the confirmation link above.{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}For questions about this list, please contact:{{/translate}}
|
||||
<br/><a href="mailto:{{contactAddress}}">{{contactAddress}}</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
10
views/subscription/mail-confirm-address-change-text.hbs
Normal file
10
views/subscription/mail-confirm-address-change-text.hbs
Normal file
|
@ -0,0 +1,10 @@
|
|||
{{{title}}}
|
||||
{{#translate}}Please Confirm Subscription Address Change{{/translate}}
|
||||
==========================================
|
||||
|
||||
{{#translate}}Yes, subscribe this email address to the list{{/translate}}: {{{confirmUrl}}}
|
||||
|
||||
{{#translate}}If you received this email by mistake, simply delete it. You won't be subscribed unless you click the confirmation link above.{{/translate}}
|
||||
|
||||
{{#translate}}For questions about this list, please contact:{{/translate}}
|
||||
{{{contactAddress}}}
|
17
views/subscription/mail-confirm-unsubscription-html.mjml.hbs
Normal file
17
views/subscription/mail-confirm-unsubscription-html.mjml.hbs
Normal file
|
@ -0,0 +1,17 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Please Confirm Unsubscription{{/translate}}
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{confirmUrl}}">
|
||||
{{#translate}}Yes, unsubscribe me from this list{{/translate}}
|
||||
</mj-button>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}If you received this email by mistake, simply delete it. You won't be unsubscribed if you don't click the confirmation link above.{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}For questions about this list, please contact:{{/translate}}
|
||||
<br/><a href="mailto:{{contactAddress}}">{{contactAddress}}</a>
|
||||
</mj-text>
|
||||
</mj-column>
|
||||
</mj-section>
|
10
views/subscription/mail-confirm-unsubscription-text.hbs
Normal file
10
views/subscription/mail-confirm-unsubscription-text.hbs
Normal file
|
@ -0,0 +1,10 @@
|
|||
{{{title}}}
|
||||
{{#translate}}Please Confirm Subscription{{/translate}}
|
||||
===========================
|
||||
|
||||
{{#translate}}Yes, unsubscribe me from this list{{/translate}}: {{{confirmUrl}}}
|
||||
|
||||
{{#translate}}If you received this email by mistake, simply delete it. You won't be unsubscribed unless you click the confirmation link above.{{/translate}}
|
||||
|
||||
{{#translate}}For questions about this list, please contact:{{/translate}}
|
||||
{{{contactAddress}}}
|
|
@ -1,6 +1,6 @@
|
|||
{{#each customFields}}
|
||||
|
||||
{{#if typeSubsciptionEmail}}
|
||||
{{#if typeSubscriptionEmail}}
|
||||
<div class="form-group email">
|
||||
<label for="email">{{#translate}}Email Address{{/translate}}</label>
|
||||
{{#if ../isManagePreferences}}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<input type="hidden" class="tz-detect" name="tz" id="tz" value="{{tz}}">
|
||||
<input type="hidden" name="address" value="">
|
||||
<input type="hidden" name="sub" id="sub" value="">
|
||||
<input type="hidden" name="ucid" value="{{ucid}}">
|
||||
|
||||
{{> subscription_custom_fields}}
|
||||
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
<form method="post" id="main-form" action="/subscription/{{lcid}}/unsubscribe">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="campaign" value="{{campaign}}">
|
||||
<input type="hidden" name="cid" value="{{cid}}">
|
||||
<input type="hidden" name="ucid" value="{{ucid}}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">{{#translate}}Email address{{/translate}}</label>
|
||||
<input type="email" name="email" id="email" placeholder="" value="{{email}}" autofocus required>
|
||||
<input type="email" name="email" id="email" placeholder="" value="{{email}}" readonly>
|
||||
</div>
|
||||
|
||||
<button type="submit" style="position: absolute; top: -9999px; left: -9999px;">{{#translate}}Unsubscribe{{/translate}}</button>
|
||||
</form>
|
||||
|
||||
{{#if email}}
|
||||
{{#if autosubmit}}
|
||||
<script>
|
||||
document.getElementById('main-form').submit();
|
||||
</script>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<mj-section>
|
||||
<mj-column>
|
||||
<mj-text mj-class="h3">
|
||||
{{#translate}}Almost Finished{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}We need to confirm your email address. To complete the unsubscription process, please click the link in the email we just sent you.{{/translate}}
|
||||
</mj-text>
|
||||
<mj-button mj-class="button" href="{{homepage}}">
|
||||
{{#translate}}Return to our website{{/translate}}
|
||||
</mj-button>
|
||||
</mj-column>
|
||||
</mj-section>
|
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>
|
|
@ -3,9 +3,6 @@
|
|||
<mj-text mj-class="h3">
|
||||
{{#translate}}Unsubscribe{{/translate}}
|
||||
</mj-text>
|
||||
<mj-text mj-class="p">
|
||||
{{#translate}}Enter your email address to unsubscribe from:{{/translate}} {{title}}
|
||||
</mj-text>
|
||||
<mj-text>
|
||||
{{> subscription_unsubscribe_form}}
|
||||
</mj-text>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue