Work in progress on subscriptions
This commit is contained in:
parent
eecb3cd067
commit
b22a87e712
18 changed files with 1729 additions and 884 deletions
176
lib/helpers.js
176
lib/helpers.js
|
|
@ -18,11 +18,6 @@ module.exports = {
|
|||
getDefaultMergeTags,
|
||||
getRSSMergeTags,
|
||||
getListMergeTags,
|
||||
captureFlashMessages,
|
||||
injectCustomFormData,
|
||||
injectCustomFormTemplates,
|
||||
filterCustomFields,
|
||||
getMjmlTemplate,
|
||||
rollbackAndReleaseConnection,
|
||||
filterObject,
|
||||
enforce
|
||||
|
|
@ -119,176 +114,7 @@ function getListMergeTags(listId, callback) {
|
|||
});
|
||||
}
|
||||
|
||||
function filterCustomFields(customFieldsIn = [], fieldIds = [], method = 'include') {
|
||||
let customFields = customFieldsIn.slice();
|
||||
fieldIds = typeof fieldIds === 'string' ? fieldIds.split(',') : fieldIds;
|
||||
|
||||
customFields.unshift({
|
||||
id: 'email',
|
||||
name: 'Email Address',
|
||||
type: 'Email',
|
||||
typeSubscriptionEmail: true
|
||||
}, {
|
||||
id: 'firstname',
|
||||
name: 'First Name',
|
||||
type: 'Text',
|
||||
typeFirstName: true
|
||||
}, {
|
||||
id: 'lastname',
|
||||
name: 'Last Name',
|
||||
type: 'Text',
|
||||
typeLastName: true
|
||||
});
|
||||
|
||||
let filtered = [];
|
||||
|
||||
if (method === 'include') {
|
||||
fieldIds.forEach(id => {
|
||||
let field = customFields.find(f => f.id.toString() === id);
|
||||
field && filtered.push(field);
|
||||
});
|
||||
} else {
|
||||
customFields.forEach(field => {
|
||||
!fieldIds.includes(field.id.toString()) && filtered.push(field);
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function injectCustomFormData(customFormId, viewPath, data, callback) {
|
||||
|
||||
let injectDefaultData = data => {
|
||||
data.customFields = filterCustomFields(data.customFields, [], 'exclude');
|
||||
data.formInputStyle = '@import url(/subscription/form-input-style.css);';
|
||||
return data;
|
||||
};
|
||||
|
||||
if (Number(customFormId) < 1) {
|
||||
return callback(null, injectDefaultData(data));
|
||||
}
|
||||
|
||||
forms.get(customFormId, (err, form) => {
|
||||
if (err) {
|
||||
return callback(null, injectDefaultData(data));
|
||||
}
|
||||
|
||||
let view = viewPath.split('/')[1];
|
||||
|
||||
if (view === 'web-subscribe') {
|
||||
data.customFields = form.fieldsShownOnSubscribe
|
||||
? filterCustomFields(data.customFields, form.fieldsShownOnSubscribe)
|
||||
: filterCustomFields(data.customFields, [], 'exclude');
|
||||
} else if (view === 'web-manage') {
|
||||
data.customFields = form.fieldsShownOnManage
|
||||
? filterCustomFields(data.customFields, form.fieldsShownOnManage)
|
||||
: filterCustomFields(data.customFields, [], 'exclude');
|
||||
}
|
||||
|
||||
let key = tools.fromDbKey(view);
|
||||
data.template.template = form[key] || data.template.template;
|
||||
data.template.layout = form.layout || data.template.layout;
|
||||
data.formInputStyle = form.formInputStyle || '@import url(/subscription/form-input-style.css);';
|
||||
|
||||
settings.list(['ua_code'], (err, configItems) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
data.uaCode = configItems.uaCode;
|
||||
data.customSubscriptionScripts = config.customsubscriptionscripts || [];
|
||||
callback(null, data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function injectCustomFormTemplates(customFormId, templates, callback) {
|
||||
if (Number(customFormId) < 1) {
|
||||
return callback(null, templates);
|
||||
}
|
||||
|
||||
forms.get(customFormId, (err, form) => {
|
||||
if (err) {
|
||||
return callback(null, templates);
|
||||
}
|
||||
|
||||
let lookUp = name => {
|
||||
let key = tools.fromDbKey(
|
||||
/subscription\/([^.]*)/.exec(name)[1]
|
||||
);
|
||||
return form[key] || name;
|
||||
};
|
||||
|
||||
Object.keys(templates).forEach(key => {
|
||||
let value = templates[key];
|
||||
|
||||
if (typeof value === 'string') {
|
||||
templates[key] = lookUp(value);
|
||||
}
|
||||
if (typeof value === 'object' && value.template) {
|
||||
templates[key].template = lookUp(value.template);
|
||||
}
|
||||
if (typeof value === 'object' && value.layout) {
|
||||
templates[key].layout = lookUp(value.layout);
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, templates);
|
||||
});
|
||||
}
|
||||
|
||||
function getMjmlTemplate(template, callback) {
|
||||
if (!template) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
let key = (typeof template === 'object') ? objectHash(template) : template;
|
||||
|
||||
if (mjmlTemplates.has(key)) {
|
||||
return callback(null, mjmlTemplates.get(key));
|
||||
}
|
||||
|
||||
let done = source => {
|
||||
let compiled;
|
||||
try {
|
||||
compiled = mjml.mjml2html(source);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (compiled.errors.length) {
|
||||
return callback(compiled.errors[0].message || compiled.errors[0]);
|
||||
}
|
||||
let renderer = hbs.handlebars.compile(compiled.html);
|
||||
mjmlTemplates.set(key, renderer);
|
||||
callback(null, renderer);
|
||||
};
|
||||
|
||||
if (typeof template === 'object') {
|
||||
tools.mergeTemplateIntoLayout(template.template, template.layout, (err, source) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
done(source);
|
||||
});
|
||||
} else {
|
||||
fs.readFile(path.join(__dirname, '..', 'views', template), 'utf-8', (err, source) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
done(source);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function captureFlashMessages(req, res, callback) {
|
||||
res.render('subscription/capture-flash-messages', { layout: null }, (err, flash) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, flash);
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME - remove once we get rid of non-async models
|
||||
function rollbackAndReleaseConnection(connection, callback) {
|
||||
connection.rollback(() => {
|
||||
connection.release();
|
||||
|
|
|
|||
|
|
@ -42,6 +42,38 @@ module.exports.loggedIn = (req, res, next) => {
|
|||
}
|
||||
};
|
||||
|
||||
module.exports.authByAccessToken = (req, res, next) => {
|
||||
nodeifyPromise((async () => {
|
||||
if (!req.query.access_token) {
|
||||
res.status(403);
|
||||
return res.json({
|
||||
error: 'Missing access_token',
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await users.getByAccessToken(req.query.access_token);
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (err) {
|
||||
if (err instanceof interoperableErrors.NotFoundError) {
|
||||
res.status(403);
|
||||
return res.json({
|
||||
error: 'Invalid or expired access_token',
|
||||
data: []
|
||||
});
|
||||
} else {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
}
|
||||
})(), next);
|
||||
};
|
||||
|
||||
module.exports.setup = app => {
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
'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');
|
||||
const fields = require('../models/fields');
|
||||
const settings = require('../models/settings');
|
||||
const urllib = require('url');
|
||||
const helpers = require('./helpers');
|
||||
const _ = require('./translate')._;
|
||||
const util = require('util');
|
||||
const contextHelpers = require('./context-helpers');
|
||||
const {getFieldKey} = require('../shared/lists');
|
||||
const forms = require('../models/forms');
|
||||
const bluebird = require('bluebird');
|
||||
const sendMail = bluebird.promisify(require('./mailer').sendMail);
|
||||
|
||||
|
||||
module.exports = {
|
||||
|
|
@ -19,16 +23,16 @@ module.exports = {
|
|||
sendUnsubscriptionConfirmed
|
||||
};
|
||||
|
||||
function sendSubscriptionConfirmed(list, email, subscription, callback) {
|
||||
async function sendSubscriptionConfirmed(list, email, subscription) {
|
||||
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);
|
||||
await sendMail(list, email, 'subscription_confirmed', _('%s: Subscription Confirmed'), relativeUrls, {}, subscription);
|
||||
}
|
||||
|
||||
function sendAlreadySubscribed(list, email, subscription, callback) {
|
||||
async function sendAlreadySubscribed(list, email, subscription) {
|
||||
const mailOpts = {
|
||||
ignoreDisableConfirmations: true
|
||||
};
|
||||
|
|
@ -36,120 +40,141 @@ function sendAlreadySubscribed(list, email, subscription, callback) {
|
|||
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);
|
||||
await sendMail(list, email, 'already_subscribed', _('%s: Email Address Already Registered'), relativeUrls, mailOpts, subscription);
|
||||
}
|
||||
|
||||
function sendConfirmAddressChange(list, email, cid, subscription, callback) {
|
||||
async function sendConfirmAddressChange(list, email, cid, subscription) {
|
||||
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);
|
||||
await sendMail(list, email, 'confirm_address_change', _('%s: Please Confirm Email Change in Subscription'), relativeUrls, mailOpts, subscription);
|
||||
}
|
||||
|
||||
function sendConfirmSubscription(list, email, cid, subscription, callback) {
|
||||
async function sendConfirmSubscription(list, email, cid, subscription) {
|
||||
const mailOpts = {
|
||||
ignoreDisableConfirmations: true
|
||||
};
|
||||
const relativeUrls = {
|
||||
confirmUrl: '/subscription/confirm/subscribe/' + cid
|
||||
};
|
||||
sendMail(list, email, 'confirm-subscription', _('%s: Please Confirm Subscription'), relativeUrls, mailOpts, subscription, callback);
|
||||
await sendMail(list, email, 'confirm_subscription', _('%s: Please Confirm Subscription'), relativeUrls, mailOpts, subscription);
|
||||
}
|
||||
|
||||
function sendConfirmUnsubscription(list, email, cid, subscription, callback) {
|
||||
async function sendConfirmUnsubscription(list, email, cid, subscription) {
|
||||
const mailOpts = {
|
||||
ignoreDisableConfirmations: true
|
||||
};
|
||||
const relativeUrls = {
|
||||
confirmUrl: '/subscription/confirm/unsubscribe/' + cid
|
||||
};
|
||||
sendMail(list, email, 'confirm-unsubscription', _('%s: Please Confirm Unsubscription'), relativeUrls, mailOpts, subscription, callback);
|
||||
await sendMail(list, email, 'confirm_unsubscription', _('%s: Please Confirm Unsubscription'), relativeUrls, mailOpts, subscription);
|
||||
}
|
||||
|
||||
function sendUnsubscriptionConfirmed(list, email, subscription, callback) {
|
||||
async function sendUnsubscriptionConfirmed(list, email, subscription) {
|
||||
const relativeUrls = {
|
||||
subscribeUrl: '/subscription/' + list.cid + '?cid=' + subscription.cid
|
||||
};
|
||||
sendMail(list, email, 'unsubscription-confirmed', _('%s: Unsubscription Confirmed'), relativeUrls, {}, subscription, callback);
|
||||
await sendMail(list, email, 'unsubscription_confirmed', _('%s: Unsubscription Confirmed'), relativeUrls, {}, subscription);
|
||||
}
|
||||
|
||||
function getDisplayName(flds, subscription) {
|
||||
let firstName, lastName, name;
|
||||
|
||||
function sendMail(list, email, template, subject, relativeUrls, mailOpts, subscription, callback) {
|
||||
fields.list(list.id, (err, fieldList) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
for (const fld of flds) {
|
||||
if (fld.key === 'FIRST_NAME') {
|
||||
firstName = subscription[fld.column];
|
||||
}
|
||||
|
||||
let encryptionKeys = [];
|
||||
fields.getRow(fieldList, subscription).forEach(field => {
|
||||
if (field.type === 'gpg' && field.value) {
|
||||
encryptionKeys.push(field.value.trim());
|
||||
}
|
||||
});
|
||||
if (fld.key === 'LAST_NAME') {
|
||||
lastName = subscription[fld.column];
|
||||
}
|
||||
|
||||
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl', 'disableConfirmations'], (err, configItems) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (fld.key === 'NAME') {
|
||||
name = subscription[fld.column];
|
||||
}
|
||||
}
|
||||
|
||||
if (!mailOpts.ignoreDisableConfirmations && configItems.disableConfirmations) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
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 && tmpl) {
|
||||
text = tmpl.text || text;
|
||||
html = tmpl.html || html;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
callback();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
if (name) {
|
||||
return name;
|
||||
} else if (firstName && lastName) {
|
||||
return firstName + ' ' + lastName;
|
||||
} else if (lastName) {
|
||||
return lastName;
|
||||
} else if (firstName) {
|
||||
return firstName;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMail(list, email, template, subject, relativeUrls, mailOpts, subscription) {
|
||||
console.log(subscription);
|
||||
|
||||
const flds = await fields.list(contextHelpers.getAdminContext(), list.id);
|
||||
|
||||
const encryptionKeys = [];
|
||||
for (const fld of flds) {
|
||||
if (fld.type === 'gpg' && field.value) {
|
||||
encryptionKeys.push(subscription[getFieldKey(fld)].value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
const configItems = await settings.get(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl', 'disableConfirmations']);
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
const fsTemplate = template.replace(/_/g, '-');
|
||||
const text = {
|
||||
template: 'subscription/mail-' + fsTemplate + '-text.hbs'
|
||||
};
|
||||
|
||||
const html = {
|
||||
template: 'subscription/mail-' + fsTemplate + '-html.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs',
|
||||
type: 'mjml'
|
||||
};
|
||||
|
||||
if (list.default_form !== null) {
|
||||
const form = await forms.getById(contextHelpers.getAdminContext(), list.default_form);
|
||||
|
||||
text.template = form['mail_' + template + '_text'] || text.template;
|
||||
html.template = form['mail_' + template + '_html'] || html.template;
|
||||
html.layout = form.layout || html.layout;
|
||||
}
|
||||
|
||||
try {
|
||||
await sendMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: getDisplayName(flds, subscription),
|
||||
address: email
|
||||
},
|
||||
subject: util.format(subject, list.name),
|
||||
encryptionKeys
|
||||
}, {
|
||||
html,
|
||||
text,
|
||||
data
|
||||
});
|
||||
} catch (err) {
|
||||
log.error('Subscription', err);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,12 @@ const _ = require('./translate')._;
|
|||
const util = require('util');
|
||||
const isemail = require('isemail');
|
||||
|
||||
const bluebird = require('bluebird');
|
||||
const mergeTemplateIntoLayout = bluebird.promisify(require('./tools').mergeTemplateIntoLayout);
|
||||
|
||||
module.exports = {
|
||||
validateEmail
|
||||
validateEmail,
|
||||
mergeTemplateIntoLayout
|
||||
};
|
||||
|
||||
async function validateEmail(address, checkBlocked) {
|
||||
|
|
@ -24,3 +28,4 @@ async function validateEmail(address, checkBlocked) {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
41
lib/tools.js
41
lib/tools.js
|
|
@ -18,9 +18,6 @@ let htmlToText = require('html-to-text');
|
|||
let blockedUsers = ['abuse', 'admin', 'billing', 'compliance', 'devnull', 'dns', 'ftp', 'hostmaster', 'inoc', 'ispfeedback', 'ispsupport', 'listrequest', 'list', 'maildaemon', 'noc', 'noreply', 'noreply', 'null', 'phish', 'phishing', 'postmaster', 'privacy', 'registrar', 'root', 'security', 'spam', 'support', 'sysadmin', 'tech', 'undisclosedrecipients', 'unsubscribe', 'usenet', 'uucp', 'webmaster', 'www'];
|
||||
|
||||
module.exports = {
|
||||
toDbKey,
|
||||
fromDbKey,
|
||||
convertKeys,
|
||||
queryParams,
|
||||
createSlug,
|
||||
updateMenu,
|
||||
|
|
@ -33,42 +30,6 @@ module.exports = {
|
|||
workers: new Set()
|
||||
};
|
||||
|
||||
function toDbKey(key) {
|
||||
return key.
|
||||
replace(/[^a-z0-9\-_]/gi, '').
|
||||
replace(/-+/g, '_').
|
||||
replace(/[A-Z]/g, c => '_' + c.toLowerCase()).
|
||||
replace(/^_+|_+$/g, '').
|
||||
replace(/_+/g, '_').
|
||||
trim();
|
||||
}
|
||||
|
||||
function fromDbKey(key) {
|
||||
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) {
|
||||
options = options || {};
|
||||
let response = {};
|
||||
Object.keys(obj || {}).forEach(key => {
|
||||
let lKey = fromDbKey(key);
|
||||
if (options.skip && options.skip.indexOf(lKey) >= 0) {
|
||||
return;
|
||||
}
|
||||
if (options.keep && options.keep.indexOf(lKey) < 0) {
|
||||
return;
|
||||
}
|
||||
response[lKey] = obj[key];
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
function queryParams(obj) {
|
||||
return Object.keys(obj).
|
||||
filter(key => key !== '_csrf').
|
||||
|
|
@ -116,6 +77,7 @@ function createSlug(table, name, callback) {
|
|||
});
|
||||
}
|
||||
|
||||
// FIXME - remove once we fully manage the menu in the client
|
||||
function updateMenu(res) {
|
||||
if (!res.locals.menu) {
|
||||
res.locals.menu = [];
|
||||
|
|
@ -148,6 +110,7 @@ function updateMenu(res) {
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME - either remove of delegate to validateEmail in tools-async (or vice-versa)
|
||||
function validateEmail(address, checkBlocked, callback) {
|
||||
let user = (address || '').toString().split('@').shift().toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
if (checkBlocked && blockedUsers.indexOf(user) >= 0) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue