Halfway through extending subscriptions by selectable unsubscription process. Also contains changes towards better handling of scenarios when address is already subscribed.
This commit is contained in:
parent
b0d51c7dad
commit
3783d7c2ce
27 changed files with 727 additions and 431 deletions
|
@ -14,22 +14,30 @@ 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'
|
||||
];
|
||||
|
||||
|
||||
module.exports.list = (listId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
|
|
|
@ -7,7 +7,16 @@ let segments = require('./segments');
|
|||
let _ = require('../translate')._;
|
||||
let tableHelpers = require('../table-helpers');
|
||||
|
||||
let allowedKeys = ['description', 'default_form', 'public_subscribe'];
|
||||
const UnsubscriptionMode = {
|
||||
ONE_STEP: 0,
|
||||
TWO_STEP: 1,
|
||||
MANUAL: 2,
|
||||
MAX: 3
|
||||
};
|
||||
|
||||
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 +108,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 +223,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;
|
||||
|
||||
|
|
|
@ -88,21 +88,16 @@ module.exports.filter = (listId, request, columns, segmentId, callback) => {
|
|||
|
||||
};
|
||||
|
||||
module.exports.addConfirmation = (list, email, optInIp, data, callback) => {
|
||||
module.exports.addConfirmation = (list, email, ip, 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) => {
|
||||
let query = 'INSERT INTO confirmations (cid, list, email, ip, data) VALUES (?,?,?,?,?)';
|
||||
connection.query(query, [cid, list.id, email, ip, JSON.stringify(data || {})], (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
@ -135,6 +130,13 @@ module.exports.addConfirmation = (list, email, optInIp, data, callback) => {
|
|||
return;
|
||||
}
|
||||
|
||||
// FIXME - move to router
|
||||
const mailOpts = {
|
||||
subject: _('%s: Please Confirm Subscription'),
|
||||
confirmUrlRoute: '/subscription/confirm/',
|
||||
templateType: 'subscription'
|
||||
};
|
||||
|
||||
let sendMail = (html, text) => {
|
||||
mailer.sendMail({
|
||||
from: {
|
||||
|
@ -145,7 +147,7 @@ module.exports.addConfirmation = (list, email, optInIp, data, callback) => {
|
|||
name: [].concat(data.firstName || []).concat(data.lastName || []).join(' '),
|
||||
address: email
|
||||
},
|
||||
subject: util.format(_('%s: Please Confirm Subscription'), list.name),
|
||||
subject: util.format(mailOpts.subject, list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
|
@ -154,7 +156,7 @@ module.exports.addConfirmation = (list, email, optInIp, data, callback) => {
|
|||
title: list.name,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
confirmUrl: urllib.resolve(configItems.serviceUrl, '/subscription/subscribe/' + cid)
|
||||
confirmUrl: urllib.resolve(configItems.serviceUrl, mailOpts.confirmUrlRoute + cid)
|
||||
}
|
||||
}, err => {
|
||||
if (err) {
|
||||
|
@ -164,11 +166,11 @@ module.exports.addConfirmation = (list, email, optInIp, data, callback) => {
|
|||
};
|
||||
|
||||
let text = {
|
||||
template: 'subscription/mail-confirm-text.hbs'
|
||||
template: 'subscription/mail-confirm-' + mailOpts.templateType + '-text.hbs'
|
||||
};
|
||||
|
||||
let html = {
|
||||
template: 'subscription/mail-confirm-html.mjml.hbs',
|
||||
template: 'subscription/mail-confirm-' + mailOpts.templateType + '-html.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs',
|
||||
type: 'mjml'
|
||||
};
|
||||
|
@ -186,10 +188,9 @@ module.exports.addConfirmation = (list, email, optInIp, data, callback) => {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.subscribe = (cid, optInIp, callback) => {
|
||||
module.exports.processConfirmation = (cid, ip, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
@ -215,7 +216,11 @@ module.exports.subscribe = (cid, optInIp, callback) => {
|
|||
subscription = {};
|
||||
}
|
||||
|
||||
if (subscription.action === 'update' && subscription.subscriber) {
|
||||
if (subscription._action === 'update') {
|
||||
if (!subscription.subscriber) { // Something went terribly wrong and we don't have data that we have originally provided
|
||||
return callback(new Error(_('Subscriber info corrupted or missing')));
|
||||
}
|
||||
|
||||
// update email address instead of adding new
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
|
@ -230,23 +235,28 @@ module.exports.subscribe = (cid, optInIp, callback) => {
|
|||
}
|
||||
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 module.exports.getById(listId, subscription.subscriber, (err, subscriptionData) => {
|
||||
return callback(err, subscriptionData, subscription._action);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (subscription._action === 'unsubscribe') {
|
||||
// TODO
|
||||
return;
|
||||
|
||||
} else if (subscription._action === 'subscribe') {
|
||||
subscription.cid = cid;
|
||||
subscription.list = listId;
|
||||
subscription.email = email;
|
||||
|
||||
let optInCountry = geoip.lookupCountry(optInIp) || null;
|
||||
let optInCountry = geoip.lookupCountry(ip) || null;
|
||||
module.exports.insert(listId, {
|
||||
email,
|
||||
cid,
|
||||
optInIp,
|
||||
optInIp: ip,
|
||||
optInCountry,
|
||||
status: 1
|
||||
}, subscription, (err, result) => {
|
||||
|
@ -265,11 +275,18 @@ module.exports.subscribe = (cid, optInIp, callback) => {
|
|||
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);
|
||||
return module.exports.getById(listId, result.entryId, (err, subscriptionData) => {
|
||||
return callback(err, subscriptionData, subscription._action);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
} else {
|
||||
return callback(new Error(util.format(_('Subscription request corrupted - action: %s'), subscription._action)));
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -307,6 +324,7 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
}
|
||||
});
|
||||
|
||||
// FIXME - see issue #218
|
||||
fields.getValues(fields.getRow(fieldList, subscription, true, true, !!meta.partial), true).forEach(field => {
|
||||
keys.push(field.key);
|
||||
values.push(field.value);
|
||||
|
@ -377,6 +395,8 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
queryArgs = values.concat(existing.id);
|
||||
query = 'UPDATE `subscription__' + listId + '` SET ' + keys.map(key => '`' + key + '`=?') + ' WHERE id=? LIMIT 1';
|
||||
}
|
||||
console.log(query);
|
||||
console.log(queryArgs);
|
||||
|
||||
connection.query(query, queryArgs, (err, result) => {
|
||||
if (err) {
|
||||
|
@ -1076,7 +1096,7 @@ module.exports.listImports = (listId, callback) => {
|
|||
};
|
||||
|
||||
|
||||
module.exports.updateAddress = (list, cid, updates, optInIp, callback) => {
|
||||
module.exports.updateAddress = (list, cid, updates, ip, callback) => {
|
||||
updates = tools.convertKeys(updates);
|
||||
cid = (cid || '').toString().trim();
|
||||
|
||||
|
@ -1128,11 +1148,13 @@ module.exports.updateAddress = (list, cid, updates, optInIp, callback) => {
|
|||
}
|
||||
|
||||
if (rows && rows[0] && rows[0].id) {
|
||||
|
||||
|
||||
return callback(new Error(_('This address is already registered by someone else')));
|
||||
}
|
||||
|
||||
module.exports.addConfirmation(list, emailNew, optInIp, {
|
||||
action: 'update',
|
||||
module.exports.addConfirmation(list, emailNew, ip, {
|
||||
_action: 'update',
|
||||
cid,
|
||||
subscriber: old.id,
|
||||
emailOld: old.email
|
||||
|
@ -1142,3 +1164,114 @@ module.exports.updateAddress = (list, cid, updates, optInIp, callback) => {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports.sendMail = (listId, email, template, subject, mailOpts, subscription, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
lists.get(listId, (err, list) => {
|
||||
if (!err && !list) {
|
||||
err = new Error(_('Selected list not found'));
|
||||
err.status = 404;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let encryptionKeys = [];
|
||||
fields.getRow(fieldList, subscription).forEach(field => {
|
||||
if (field.type === 'gpg' && field.value) {
|
||||
encryptionKeys.push(field.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl'], (err, configItems) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const data = {
|
||||
title: list.name,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
};
|
||||
|
||||
if (mailOpts.confirmUrlRoute) {
|
||||
data.confirmUrl = urllib.resolve(configItems.serviceUrl, mailOpts.confirmUrlRoute + cid)
|
||||
}
|
||||
|
||||
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(null, cid);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
FIXME
|
||||
function getUnsubscriptionMode = (listId, start, limit, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
if (!listId) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
tableHelpers.list('subscription__' + listId, ['*'], 'email', null, start, limit, (err, rows, total) => {
|
||||
if (!err) {
|
||||
rows = rows.map(row => tools.convertKeys(row));
|
||||
}
|
||||
return callback(err, rows, total);
|
||||
});
|
||||
};
|
||||
|
||||
*/
|
10
lib/tools.js
10
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];
|
||||
|
|
|
@ -93,6 +93,8 @@ router.post('/subscribe/:listId', (req, res) => {
|
|||
subscription.tz = (input.TIMEZONE || '').toString().trim();
|
||||
}
|
||||
|
||||
subscription._action = 'subscribe';
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err && !fieldList) {
|
||||
fieldList = [];
|
||||
|
|
|
@ -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,18 +227,43 @@ 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
|
||||
}]
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
@ -771,4 +775,28 @@ 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.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.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;
|
||||
|
|
|
@ -44,8 +44,8 @@ let corsOrCsrfProtection = (req, res, next) => {
|
|||
}
|
||||
};
|
||||
|
||||
router.get('/subscribe/:cid', (req, res, next) => {
|
||||
subscriptions.subscribe(req.params.cid, req.ip, (err, subscription) => {
|
||||
router.get('/confirm/:cid', (req, res, next) => {
|
||||
subscriptions.processConfirmation(req.params.cid, req.ip, (err, subscription, action) => {
|
||||
if (!err && !subscription) {
|
||||
err = new Error(_('Selected subscription not found'));
|
||||
err.status = 404;
|
||||
|
@ -70,40 +70,9 @@ router.get('/subscribe/:cid', (req, res, next) => {
|
|||
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'
|
||||
}
|
||||
};
|
||||
// FIXME - split decision based on action
|
||||
|
||||
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-subscribed', 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
res.redirect('/subscription/' + list.cid + '/subscribed-notice');
|
||||
|
||||
if (configItems.disableConfirmations) {
|
||||
return;
|
||||
|
@ -318,164 +287,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,6 +307,15 @@ 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
|
||||
// simple check should be replaced with an actual captcha
|
||||
|
@ -532,6 +352,7 @@ router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, r
|
|||
data._address = req.body.address;
|
||||
data._sub = req.body.sub;
|
||||
data._skip = !testsPass;
|
||||
data._action = 'subscribe';
|
||||
|
||||
subscriptions.addConfirmation(list, email, req.ip, data, (err, confirmCid) => {
|
||||
if (!err && !confirmCid) {
|
||||
|
@ -551,7 +372,8 @@ router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, r
|
|||
msg: _('Please Confirm Subscription')
|
||||
});
|
||||
}
|
||||
res.redirect('/subscription/' + req.params.cid + '/confirm-notice');
|
||||
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -732,7 +554,7 @@ router.post('/:lcid/manage-address', passport.parseForm, passport.csrfProtection
|
|||
return res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/manage-address/' + encodeURIComponent(req.body.cid) + '?' + tools.queryParams(req.body));
|
||||
}
|
||||
|
||||
req.flash('info', _('Email address updated, check your mailbox for verification instructions'));
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@ -822,7 +644,7 @@ router.post('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, (
|
|||
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');
|
||||
res.redirect('/subscription/' + req.params.lcid + '/unsubscribed-notice');
|
||||
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err) {
|
||||
|
@ -874,11 +696,11 @@ router.post('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, (
|
|||
};
|
||||
|
||||
let text = {
|
||||
template: 'subscription/mail-unsubscribe-confirmed-text.hbs'
|
||||
template: 'subscription/mail-unsubscription-confirmed-text.hbs'
|
||||
};
|
||||
|
||||
let html = {
|
||||
template: 'subscription/mail-unsubscribe-confirmed-html.mjml.hbs',
|
||||
template: 'subscription/mail-unsubscription-confirmed-html.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs',
|
||||
type: 'mjml'
|
||||
};
|
||||
|
@ -896,6 +718,26 @@ router.post('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, (
|
|||
});
|
||||
});
|
||||
|
||||
router.get('/:cid/confirm-subscription-notice', (req, res, next) => {
|
||||
notice('confirm-subscription', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/confirm-unsubscription-notice', (req, res, next) => {
|
||||
notice('confirm-unsubscription', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/subscribed-notice', (req, res, next) => {
|
||||
notice('subscribed', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/updated-notice', (req, res, next) => {
|
||||
notice('updated', req, res, next);
|
||||
});
|
||||
|
||||
router.get('/:cid/unsubscribed-notice', (req, res, next) => {
|
||||
notice('unsubscribed', req, res, next);
|
||||
});
|
||||
|
||||
router.post('/publickey', passport.parseForm, (req, res, next) => {
|
||||
settings.list(['pgpPassphrase', 'pgpPrivateKey'], (err, configItems) => {
|
||||
if (err) {
|
||||
|
@ -934,4 +776,59 @@ router.post('/publickey', passport.parseForm, (req, res, next) => {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
function notice(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'], (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-' + 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;
|
||||
|
|
24
setup/sql/upgrade-00028.sql
Normal file
24
setup/sql/upgrade-00028.sql
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Header section
|
||||
# Define incrementing schema version number
|
||||
SET @schema_version = '28';
|
||||
|
||||
# Add unsubscription mode field to lists
|
||||
ALTER TABLE `lists` ADD COLUMN `unsubscription_mode` int(11) unsigned DEFAULT 0 NOT NULL AFTER `public_subscribe`;
|
||||
|
||||
# Change the name of the column to better reflect that confirmations are also used for unsubscription and email address update
|
||||
ALTER TABLE `confirmations` CHANGE `opt_in_ip` `ip` varchar(100) DEFAULT NULL;
|
||||
|
||||
# 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;
|
||||
INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
|
||||
UNLOCK TABLES;
|
|
@ -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,11 +46,15 @@
|
|||
<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>
|
||||
{{#if testUsers}}
|
||||
|
|
||||
<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 subscribed{{/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 subscribed{{/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}}}
|
|
@ -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>
|
Loading…
Add table
Add a link
Reference in a new issue