Subscription/unsubscription seems to work.

This commit is contained in:
Tomas Bures 2018-01-27 16:37:14 +01:00
parent d8ee364a4b
commit e9165838dc
22 changed files with 14939 additions and 196 deletions

1
.gitignore vendored
View file

@ -3,7 +3,6 @@
node_modules node_modules
npm-debug.log npm-debug.log
package-lock.json
.DS_Store .DS_Store
config/development.* config/development.*
config/production.* config/production.*

6159
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -24,7 +24,6 @@
"immutable": "^3.8.1", "immutable": "^3.8.1",
"moment": "^2.18.1", "moment": "^2.18.1",
"moment-timezone": "^0.5.13", "moment-timezone": "^0.5.13",
"owasp-password-strength-test": "github:bures/owasp-password-strength-test",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"querystringify": "^1.0.0", "querystringify": "^1.0.0",
"react": "^15.6.1", "react": "^15.6.1",

View file

@ -56,7 +56,7 @@ export default class API extends Component {
return ( return (
<div> <div>
<Title>{t('Sign in')}</Title> <Title>{t('API')}</Title>
<div className="panel panel-default"> <div className="panel panel-default">

View file

@ -12,7 +12,7 @@ import {
Dropdown, Form, Dropdown, Form,
withForm withForm
} from '../../lib/form'; } from '../../lib/form';
import {Icon} from "../../lib/bootstrap-components"; import {Icon, Button} from "../../lib/bootstrap-components";
import axios from '../../lib/axios'; import axios from '../../lib/axios';
import {getFieldTypes, getSubscriptionStatusLabels} from './helpers'; import {getFieldTypes, getSubscriptionStatusLabels} from './helpers';
@ -154,6 +154,7 @@ export default class List extends Component {
return ( return (
<div> <div>
<Toolbar> <Toolbar>
<a href={`/subscription/${this.props.list.cid}`} className="btn-default"><Button label={t('Subscription Form')} className="btn-default"/></a>
<NavButton linkTo={`/lists/${this.props.list.id}/subscriptions/create`} className="btn-primary" icon="plus" label={t('Add Subscriber')}/> <NavButton linkTo={`/lists/${this.props.list.id}/subscriptions/create`} className="btn-primary" icon="plus" label={t('Add Subscriber')}/>
</Toolbar> </Toolbar>

View file

@ -25,7 +25,10 @@ export function getFieldTypes(t) {
const stringFieldType = long => ({ const stringFieldType = long => ({
form: groupedField => long ? <TextArea key={getFieldKey(groupedField)} id={getFieldKey(groupedField)} label={groupedField.name}/> : <InputField key={getFieldKey(groupedField)} id={getFieldKey(groupedField)} label={groupedField.name}/>, form: groupedField => long ? <TextArea key={getFieldKey(groupedField)} id={getFieldKey(groupedField)} label={groupedField.name}/> : <InputField key={getFieldKey(groupedField)} id={getFieldKey(groupedField)} label={groupedField.name}/>,
assignFormData: (groupedField, data) => {}, assignFormData: (groupedField, data) => {
const value = data[getFieldKey(groupedField)];
data[getFieldKey(groupedField)] = value || '';
},
initFormData: (groupedField, data) => { initFormData: (groupedField, data) => {
data[getFieldKey(groupedField)] = ''; data[getFieldKey(groupedField)] = '';
}, },
@ -109,7 +112,10 @@ export function getFieldTypes(t) {
const jsonFieldType = { const jsonFieldType = {
form: groupedField => <ACEEditor key={getFieldKey(groupedField)} id={getFieldKey(groupedField)} label={groupedField.name} mode="json" height="300px"/>, form: groupedField => <ACEEditor key={getFieldKey(groupedField)} id={getFieldKey(groupedField)} label={groupedField.name} mode="json" height="300px"/>,
assignFormData: (groupedField, data) => {}, assignFormData: (groupedField, data) => {
const value = data[getFieldKey(groupedField)];
data[getFieldKey(groupedField)] = value || '';
},
initFormData: (groupedField, data) => { initFormData: (groupedField, data) => {
data[getFieldKey(groupedField)] = ''; data[getFieldKey(groupedField)] = '';
}, },

View file

@ -10,8 +10,7 @@ function getRequestContext(req) {
return context; return context;
} }
function getAdminContext() { const adminContext = {
const context = {
user: { user: {
admin: true, admin: true,
id: 0, id: 0,
@ -21,7 +20,8 @@ function getAdminContext() {
} }
}; };
return context; function getAdminContext() {
return adminContext;
} }
module.exports = { module.exports = {

View file

@ -166,13 +166,13 @@ async function ajaxListWithPermissionsTx(tx, context, fetchSpecs, params, queryF
async function ajaxList(params, queryFun, columns, options) { async function ajaxList(params, queryFun, columns, options) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
return ajaxListTx(tx, params, queryFun, columns, options) return await ajaxListTx(tx, params, queryFun, columns, options)
}); });
} }
async function ajaxListWithPermissions(context, fetchSpecs, params, queryFun, columns, options) { async function ajaxListWithPermissions(context, fetchSpecs, params, queryFun, columns, options) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
return ajaxListWithPermissionsTx(tx, context, fetchSpecs, params, queryFun, columns, options) return await ajaxListWithPermissionsTx(tx, context, fetchSpecs, params, queryFun, columns, options)
}); });
} }

View file

@ -8,7 +8,7 @@ const knex = require('knex')({
migrations: { migrations: {
directory: __dirname + '/../setup/knex/migrations' directory: __dirname + '/../setup/knex/migrations'
} }
, debug: true //, debug: true
}); });
module.exports = knex; module.exports = knex;

View file

@ -117,7 +117,7 @@ async function _sendMail(list, email, template, subject, relativeUrls, mailOpts,
const encryptionKeys = []; const encryptionKeys = [];
for (const fld of flds) { for (const fld of flds) {
if (fld.type === 'gpg' && field.value) { if (fld.type === 'gpg' && fld.value) {
encryptionKeys.push(subscription[getFieldKey(fld)].value.trim()); encryptionKeys.push(subscription[getFieldKey(fld)].value.trim());
} }
} }

View file

@ -6,10 +6,13 @@ const isemail = require('isemail');
const bluebird = require('bluebird'); const bluebird = require('bluebird');
const mergeTemplateIntoLayout = bluebird.promisify(require('./tools').mergeTemplateIntoLayout); const mergeTemplateIntoLayout = bluebird.promisify(require('./tools').mergeTemplateIntoLayout);
const queryParams = require('./tools').queryParams;
module.exports = { module.exports = {
validateEmail, validateEmail,
mergeTemplateIntoLayout validateEmailGetMessage,
mergeTemplateIntoLayout,
queryParams
}; };
async function validateEmail(address, checkBlocked) { async function validateEmail(address, checkBlocked) {
@ -29,3 +32,21 @@ async function validateEmail(address, checkBlocked) {
return result; return result;
} }
function validateEmailGetMessage(result, address) {
if (result !== 0) {
let message = util.format(_('Invalid email address "%s".'), address);
switch (result) {
case 5:
message += ' ' + _('MX record not found for domain');
break;
case 6:
message += ' ' + _('Address domain not found');
break;
case 12:
message += ' ' + _('Address domain name is required');
break;
}
return message;
}
}

View file

@ -87,7 +87,7 @@ async function _getById(tx, id) {
async function getById(context, id) { async function getById(context, id) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'view'); await shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'view');
const entity = await _getById(tx, id); const entity = await _getById(tx, id);
entity.permissions = await shares.getPermissionsTx(tx, context, 'customForm', id); entity.permissions = await shares.getPermissionsTx(tx, context, 'customForm', id);
return entity; return entity;
@ -171,9 +171,9 @@ async function updateWithConsistencyCheck(context, entity) {
async function remove(context, id) { async function remove(context, id) {
await knex.transaction(async tx => { await knex.transaction(async tx => {
shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'delete'); await shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'delete');
lists.removeFormFromAllTx(tx, context, id); await lists.removeFormFromAllTx(tx, context, id);
await tx('custom_forms_data').where('form', id).del(); await tx('custom_forms_data').where('form', id).del();
await tx('custom_forms').where('id', id).del(); await tx('custom_forms').where('id', id).del();

View file

@ -33,21 +33,22 @@ async function listDTAjax(context, params) {
} }
async function _getByIdTx(tx, context, id) { async function _getByIdTx(tx, context, id) {
shares.enforceEntityPermissionTx(tx, context, 'list', id, 'view'); await shares.enforceEntityPermissionTx(tx, context, 'list', id, 'view');
const entity = await tx('lists').where('id', id).first(); const entity = await tx('lists').where('id', id).first();
entity.permissions = await shares.getPermissionsTx(tx, context, 'list', id);
return entity; return entity;
} }
async function getById(context, id) { async function getById(context, id) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
return _getByIdTx(tx, context, id); // note that permissions are not obtained here as this methods is used only with synthetic admin context
return await _getByIdTx(tx, context, id);
}); });
} }
async function getByIdWithListFields(context, id) { async function getByIdWithListFields(context, id) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
const entity = _getByIdTx(tx, context, id); const entity = await _getByIdTx(tx, context, id);
entity.permissions = await shares.getPermissionsTx(tx, context, 'list', id);
entity.listFields = await fields.listByOrderListTx(tx, id); entity.listFields = await fields.listByOrderListTx(tx, id);
return entity; return entity;
}); });
@ -60,8 +61,7 @@ async function getByCid(context, cid) {
shares.throwPermissionDenied(); shares.throwPermissionDenied();
} }
shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'view'); await shares.enforceEntityPermissionTx(tx, context, 'list', entity.id, 'view');
entity.permissions = await shares.getPermissionsTx(tx, context, 'list', entity.id);
return entity; return entity;
}); });
} }

View file

@ -31,11 +31,16 @@ function getOptionsMap(groupedField) {
return result; return result;
} }
fieldTypes.text = fieldTypes.website = fieldTypes.longtext = fieldTypes.gpg = fieldTypes.number = { fieldTypes.text = fieldTypes.website = fieldTypes.longtext = fieldTypes.gpg = {
afterJSON: (groupedField, entity) => {}, afterJSON: (groupedField, entity) => {},
listRender: (groupedField, value) => value listRender: (groupedField, value) => value
}; };
fieldTypes.number = {
afterJSON: (groupedField, entity) => {},
listRender: (groupedField, value) => Number(value)
};
fieldTypes.json = { fieldTypes.json = {
afterJSON: (groupedField, entity) => {}, afterJSON: (groupedField, entity) => {},
listRender: (groupedField, value) => value listRender: (groupedField, value) => value
@ -59,14 +64,20 @@ fieldTypes['radio-enum'] = fieldTypes['dropdown-enum'] = fieldTypes['radio-group
fieldTypes.date = { fieldTypes.date = {
afterJSON: (groupedField, entity) => { afterJSON: (groupedField, entity) => {
entity[getFieldKey(groupedField)] = moment(entity[getFieldKey(groupedField)]).toDate(); const key = getFieldKey(groupedField);
if (key in entity) {
entity[key] = entity[key] ? moment(entity[key]).toDate() : null;
}
}, },
listRender: (groupedField, value) => formatDate(groupedField.settings.dateFormat, value) listRender: (groupedField, value) => formatDate(groupedField.settings.dateFormat, value)
}; };
fieldTypes.birthday = { fieldTypes.birthday = {
afterJSON: (groupedField, entity) => { afterJSON: (groupedField, entity) => {
entity[getFieldKey(groupedField)] = moment(entity[getFieldKey(groupedField)]).toDate(); const key = getFieldKey(groupedField);
if (key in entity) {
entity[key] = entity[key] ? moment(entity[key]).toDate() : null;
}
}, },
listRender: (groupedField, value) => formatBirthday(groupedField.settings.dateFormat, value) listRender: (groupedField, value) => formatBirthday(groupedField.settings.dateFormat, value)
}; };
@ -153,7 +164,7 @@ function ungroupSubscription(groupedFieldsMap, entity) {
} }
} else { } else {
const values = entity[fldKey]; const values = entity[fldKey] || []; // The default (empty array) is here because create may be called with an entity that has some fields not filled in
for (const optionKey in fld.groupedOptions) { for (const optionKey in fld.groupedOptions) {
const option = fld.groupedOptions[optionKey]; const option = fld.groupedOptions[optionKey];
entity[option.column] = values.includes(option.column); entity[option.column] = values.includes(option.column);
@ -210,11 +221,6 @@ async function _getStatusBy(context, listId, key, value) {
}); });
} }
async function getStatusByCid(context, listId, cid) {
return await _getStatusBy(context, listId, 'cid', cid);
}
async function _getBy(context, listId, key, value, grouped) { async function _getBy(context, listId, key, value, grouped) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions'); await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
@ -387,8 +393,9 @@ async function serverValidate(context, listId, data) {
}); });
} }
async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, isCreate) { async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, meta, isCreate) {
enforce(entity.email, 'Email must be set'); enforce(entity.email, 'Email must be set');
enforce(entity.status > 0 && entity.status < SubscriptionStatus.MAX, 'Subscription status is invalid');
const existingWithKeyQuery = tx(getSubscriptionTableName(listId)).where('email', entity.email); const existingWithKeyQuery = tx(getSubscriptionTableName(listId)).where('email', entity.email);
@ -397,8 +404,13 @@ async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, isCr
} }
const existingWithKey = await existingWithKeyQuery.first(); const existingWithKey = await existingWithKeyQuery.first();
if (existingWithKey) { if (existingWithKey) {
if (meta && meta.replaceOfUnsubscribedAllowed && existingWithKey.status === SubscriptionStatus.UNSUBSCRIBED) {
meta.updateNeeded = true;
meta.existing = existingWithKey;
} else {
throw new interoperableErrors.DuplicitEmailError(); throw new interoperableErrors.DuplicitEmailError();
} }
}
enforce(entity.status >= 0 && entity.status < SubscriptionStatus.MAX, 'Invalid status'); enforce(entity.status >= 0 && entity.status < SubscriptionStatus.MAX, 'Invalid status');
@ -409,33 +421,68 @@ async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, isCr
} }
} }
async function create(context, listId, entity, meta = {}) { async function _update(tx, listId, existing, filteredEntity) {
if (existing.status !== filteredEntity.status) {
filteredEntity.status_change = new Date();
}
await tx(getSubscriptionTableName(listId)).where('id', existing.id).update(filteredEntity);
let countIncrement = 0;
if (existing.status === SubscriptionStatus.SUBSCRIBED && filteredEntity.status !== SubscriptionStatus.SUBSCRIBED) {
countIncrement = -1;
} else if (existing.status !== SubscriptionStatus.SUBSCRIBED && filteredEntity.status === SubscriptionStatus.SUBSCRIBED) {
countIncrement = 1;
}
if (countIncrement) {
await tx('lists').where('id', listId).increment('subscribers', countIncrement);
}
}
async function _create(tx, listId, filteredEntity) {
const ids = await tx(getSubscriptionTableName(listId)).insert(filteredEntity);
const id = ids[0];
if (filteredEntity.status === SubscriptionStatus.SUBSCRIBED) {
await tx('lists').where('id', listId).increment('subscribers', 1);
}
return id;
}
async function create(context, listId, entity, meta /* meta is provided when called from /confirm/subscribe/:cid */) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions'); await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId); const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
const allowedKeys = getAllowedKeys(groupedFieldsMap); const allowedKeys = getAllowedKeys(groupedFieldsMap);
await _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, true); await _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, meta, true);
const filteredEntity = filterObject(entity, allowedKeys); const filteredEntity = filterObject(entity, allowedKeys);
filteredEntity.cid = shortid.generate();
filteredEntity.status_change = new Date(); filteredEntity.status_change = new Date();
ungroupSubscription(groupedFieldsMap, filteredEntity); ungroupSubscription(groupedFieldsMap, filteredEntity);
filteredEntity.opt_in_ip = meta.ip; filteredEntity.opt_in_ip = meta && meta.ip;
filteredEntity.opt_in_country = meta.country; filteredEntity.opt_in_country = meta && meta.country;
filteredEntity.imported = meta.imported || false; filteredEntity.imported = meta && !!meta.imported;
const ids = await tx(getSubscriptionTableName(listId)).insert(filteredEntity); if (meta && meta.updateNeeded) {
const id = ids[0]; await _update(tx, listId, meta.existing, filteredEntity);
meta.cid = meta.existing.cid; // The cid is needed by /confirm/subscribe/:cid
return meta.existing.id;
} else {
filteredEntity.cid = shortid.generate();
if (entity.status === SubscriptionStatus.SUBSCRIBED) { if (meta) {
await tx('lists').where('id', listId).increment('subscribers', 1); meta.cid = filteredEntity.cid; // The cid is needed by /confirm/subscribe/:cid
} }
return id; return await _create(tx, listId, filteredEntity);
}
}); });
} }
@ -458,29 +505,13 @@ async function updateWithConsistencyCheck(context, listId, entity) {
throw new interoperableErrors.ChangedError(); throw new interoperableErrors.ChangedError();
} }
await _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, false); await _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, null, false);
const filteredEntity = filterObject(entity, allowedKeys); const filteredEntity = filterObject(entity, allowedKeys);
ungroupSubscription(groupedFieldsMap, filteredEntity); ungroupSubscription(groupedFieldsMap, filteredEntity);
if (existing.status !== entity.status) { await _update(tx, listId, existing, filteredEntity);
filteredEntity.status_change = new Date();
}
await tx(getSubscriptionTableName(listId)).where('id', entity.id).update(filteredEntity);
let countIncrement = 0;
if (existing.status === SubscriptionStatus.SUBSCRIBED && entity.status !== SubscriptionStatus.SUBSCRIBED) {
countIncrement = -1;
} else if (existing.status !== SubscriptionStatus.SUBSCRIBED && entity.status === SubscriptionStatus.SUBSCRIBED) {
countIncrement = 1;
}
if (countIncrement) {
await tx('lists').where('id', listId).increment('subscribers', countIncrement);
}
}); });
} }
@ -505,18 +536,16 @@ async function remove(context, listId, id) {
}); });
} }
async function unsubscribeByCidAndGet(context, listId, subscriptionCid, campaignCid) { async function _unsubscribeAndGetTx(tx, context, listId, existingSubscription, campaignCid) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions'); await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
const existing = await tx(getSubscriptionTableName(listId)).where('cid', subscriptionCid).first(); if (!(existingSubscription && existingSubscription.status === SubscriptionStatus.SUBSCRIBED)) {
if (!(existing && existing.status === SubscriptionStatus.SUBSCRIBED)) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
existing.status = SubscriptionStatus.UNSUBSCRIBED; existingSubscription.status = SubscriptionStatus.UNSUBSCRIBED;
await tx(getSubscriptionTableName(listId)).where('cid', subscriptionCid).update({ await tx(getSubscriptionTableName(listId)).where('id', existingSubscription.id).update({
status: SubscriptionStatus.UNSUBSCRIBED status: SubscriptionStatus.UNSUBSCRIBED
}); });
@ -524,7 +553,7 @@ async function unsubscribeByCidAndGet(context, listId, subscriptionCid, campaign
if (campaignCid) { if (campaignCid) {
const campaign = await tx('campaigns').where('cid', campaignCid); const campaign = await tx('campaigns').where('cid', campaignCid);
const subscriptionInCampaign = await tx(getCampaignTableName(campaign.id)).where({subscription: existing.id, list: listId}); const subscriptionInCampaign = await tx(getCampaignTableName(campaign.id)).where({subscription: existingSubscription.id, list: listId});
if (!subscriptionInCampaign) { if (!subscriptionInCampaign) {
throw new Error('Invalid campaign.') throw new Error('Invalid campaign.')
@ -532,13 +561,27 @@ async function unsubscribeByCidAndGet(context, listId, subscriptionCid, campaign
if (subscriptionInCampaign.status === SubscriptionStatus.SUBSCRIBED) { if (subscriptionInCampaign.status === SubscriptionStatus.SUBSCRIBED) {
await tx('campaigns').where('id', campaign.id).increment('unsubscribed', 1); await tx('campaigns').where('id', campaign.id).increment('unsubscribed', 1);
await tx(getCampaignTableName(campaign.id)).where({subscription: existing.id, list: listId}).update({ await tx(getCampaignTableName(campaign.id)).where({subscription: existingSubscription.id, list: listId}).update({
status: SubscriptionStatus.UNSUBSCRIBED status: SubscriptionStatus.UNSUBSCRIBED
}); });
} }
} }
return existing; return existingSubscription;
}
async function unsubscribeByIdAndGet(context, listId, subscriptionId) {
return await knex.transaction(async tx => {
const existing = await tx(getSubscriptionTableName(listId)).where('id', subscriptionId).first();
return _unsubscribeAndGetTx(tx, context, listId, existing);
});
}
async function unsubscribeByCidAndGet(context, listId, subscriptionCid, campaignCid) {
return await knex.transaction(async tx => {
const existing = await tx(getSubscriptionTableName(listId)).where('cid', subscriptionCid).first();
return _unsubscribeAndGetTx(tx, context, listId, existing, campaignCid);
}); });
} }
@ -564,8 +607,8 @@ async function updateManagedUngrouped(context, listId, entity) {
await knex.transaction(async tx => { await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions'); await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
const existing = await tx(getSubscriptionTableName(listId)).where('id', entity.id).first(); const existing = await tx(getSubscriptionTableName(listId)).where('cid', entity.cid).first();
if (!existing) { if (!existing || existing.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError(); throw new interoperableErrors.NotFoundError();
} }
@ -583,7 +626,7 @@ async function updateManagedUngrouped(context, listId, entity) {
} }
} }
await tx(getSubscriptionTableName(listId)).where('id', entity.id).update(update); await tx(getSubscriptionTableName(listId)).where('cid', entity.cid).update(update);
}); });
} }
@ -592,7 +635,6 @@ module.exports = {
getById, getById,
getByCid, getByCid,
getByEmail, getByEmail,
getStatusByCid,
list, list,
listDTAjax, listDTAjax,
serverValidate, serverValidate,
@ -600,6 +642,7 @@ module.exports = {
updateWithConsistencyCheck, updateWithConsistencyCheck,
remove, remove,
unsubscribeByCidAndGet, unsubscribeByCidAndGet,
unsubscribeByIdAndGet,
updateAddressAndGet, updateAddressAndGet,
updateManagedUngrouped updateManagedUngrouped
}; };

8447
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
{ {
"name": "mailtrain", "name": "mailtrain",
"private": true,
"version": "1.23.2", "version": "1.23.2",
"description": "Self hosted email newsletter app", "description": "Self hosted email newsletter app",
"main": "index.js", "main": "index.js",
@ -29,7 +28,6 @@
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^8.1.2", "babel-eslint": "^8.1.2",
"bluebird": "^3.5.0",
"chai": "^4.1.2", "chai": "^4.1.2",
"eslint-config-nodemailer": "^1.2.0", "eslint-config-nodemailer": "^1.2.0",
"grunt": "^1.0.1", "grunt": "^1.0.1",
@ -91,6 +89,7 @@
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"mjml": "3.3.5", "mjml": "3.3.5",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"moment": "^2.18.1",
"moment-timezone": "^0.5.13", "moment-timezone": "^0.5.13",
"morgan": "^1.8.2", "morgan": "^1.8.2",
"multer": "^1.3.0", "multer": "^1.3.0",
@ -105,7 +104,6 @@
"npmlog": "^4.1.2", "npmlog": "^4.1.2",
"object-hash": "^1.1.8", "object-hash": "^1.1.8",
"openpgp": "^2.6.1", "openpgp": "^2.6.1",
"owasp-password-strength-test": "github:bures/owasp-password-strength-test",
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"premailer-api": "^1.0.4", "premailer-api": "^1.0.4",

View file

@ -39,7 +39,7 @@ router.postAsync('/subscriptions-validate/:listId', passport.loggedIn, async (re
}); });
router.postAsync('/subscriptions-unsubscribe/:listId/:subscriptionId', passport.loggedIn, passport.csrfProtection, async (req, res) => { router.postAsync('/subscriptions-unsubscribe/:listId/:subscriptionId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
await subscriptions.unsubscribeAndGet(req.context, req.params.listId, req.params.subscriptionId); await subscriptions.unsubscribeByIdAndGet(req.context, req.params.listId, req.params.subscriptionId);
return res.json(); return res.json();
}); });

View file

@ -7,6 +7,7 @@ const confirmations = require('../models/confirmations');
const subscriptions = require('../models/subscriptions'); const subscriptions = require('../models/subscriptions');
const lists = require('../models/lists'); const lists = require('../models/lists');
const fields = require('../models/fields'); const fields = require('../models/fields');
const shares = require('../models/shares');
const settings = require('../models/settings'); const settings = require('../models/settings');
const _ = require('../lib/translate')._; const _ = require('../lib/translate')._;
const contextHelpers = require('../lib/context-helpers'); const contextHelpers = require('../lib/context-helpers');
@ -127,32 +128,40 @@ async function getMjmlTemplate(template) {
return renderer; return renderer;
} }
function captureFlashMessages(req, res) { async function captureFlashMessages(res) {
return new Promise((resolve, reject) => { const renderAsync = bluebird.promisify(res.render.bind(res));
res.render('subscription/capture-flash-messages', { layout: null }, (err, flash) => { return await renderAsync('subscription/capture-flash-messages', { layout: null });
reject(err);
resolve(flash);
});
})
} }
router.getAsync('/confirm/subscribe/:cid', async (req, res) => { router.getAsync('/confirm/subscribe/:cid', async (req, res) => {
const confirmation = await takeConfirmationAndValidate(req, 'subscribe', () => new interoperableErrors.InvalidConfirmationForSubscriptionError('Request invalid or already completed. If your subscription request is still pending, please subscribe again.')); const confirmation = await takeConfirmationAndValidate(req, 'subscribe', () => new interoperableErrors.InvalidConfirmationForSubscriptionError('Request invalid or already completed. If your subscription request is still pending, please subscribe again.'));
const subscription = confirmation.data; const data = confirmation.data;
const meta = { const meta = {
cid: req.params.cid,
ip: confirmation.ip, ip: confirmation.ip,
country: geoip.lookupCountry(confirmation.ip) || null country: geoip.lookupCountry(confirmation.ip) || null,
replaceOfUnsubscribedAllowed: true
}; };
const subscription = data.subscriptionData;
subscription.email = data.email;
subscription.status = SubscriptionStatus.SUBSCRIBED; subscription.status = SubscriptionStatus.SUBSCRIBED;
try {
await subscriptions.create(contextHelpers.getAdminContext(), confirmation.list, subscription, meta); await subscriptions.create(contextHelpers.getAdminContext(), confirmation.list, subscription, meta);
} catch (err) {
if (err instanceof interoperableErrors.DuplicitEmailError) {
throw new interoperableErrors.DuplicitEmailError('Subscription already present'); // This is here to provide some meaningful error message.
} else {
throw err;
}
}
console.log(subscription);
const list = await lists.getById(contextHelpers.getAdminContext(), confirmation.list); const list = await lists.getById(contextHelpers.getAdminContext(), confirmation.list);
subscription.cid = meta.cid;
await mailHelpers.sendSubscriptionConfirmed(list, subscription.email, subscription); await mailHelpers.sendSubscriptionConfirmed(list, subscription.email, subscription);
res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/subscribed-notice'); res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/subscribed-notice');
@ -187,15 +196,15 @@ router.getAsync('/confirm/unsubscribe/:cid', async (req, res) => {
router.getAsync('/:cid', passport.csrfProtection, async (req, res) => { router.getAsync('/:cid', passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.cid); const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
if (!list.publicSubscribe) { if (!list.public_subscribe) {
throw new interoperableErrors.SubscriptionNotAllowedError('The list does not allow public subscriptions.'); shares.throwPermissionDenied();
} }
const ucid = req.query.cid; const ucid = req.query.cid;
const data = {}; const data = req.query;
data.layout = 'subscription/layout'; data.layout = 'subscription/layout';
data.title = list.name; data.title = list.name;
data.cid = list.cid; data.cid = list.cid;
@ -203,10 +212,14 @@ router.getAsync('/:cid', passport.csrfProtection, async (req, res) => {
let subscription; let subscription;
if (ucid) { if (ucid) {
subscription = await subscriptions.getById(contextHelpers.getAdminContext(), list.id, ucid, false); subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, ucid, false);
if (subscription) {
data.email = subscription.email;
}
} }
data.customFields = fields.getRow(contextHelpers.getAdminContext(), list.id, subscription); data.customFields = await fields.getRow(contextHelpers.getAdminContext(), list.id, subscription);
data.useEditor = true; data.useEditor = true;
const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress']); const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress']);
@ -227,7 +240,8 @@ router.getAsync('/:cid', passport.csrfProtection, async (req, res) => {
data.needsJsWarning = true; data.needsJsWarning = true;
data.flashMessages = await captureFlashMessages(res); data.flashMessages = await captureFlashMessages(res);
res.send(htmlRenderer(data)); const result = htmlRenderer(data);
res.send(result);
}); });
@ -241,7 +255,7 @@ router.getAsync('/:cid/widget', cors(corsOptions), async (req, res) => {
return res.status(200).json(cached); return res.status(200).json(cached);
} }
const list = await lists.getByCid(req.params.cid); const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
const configItems = await settings.get(['serviceUrl', 'pgpPrivateKey']); const configItems = await settings.get(['serviceUrl', 'pgpPrivateKey']);
@ -250,7 +264,7 @@ router.getAsync('/:cid/widget', cors(corsOptions), async (req, res) => {
cid: list.cid, cid: list.cid,
serviceUrl: configItems.serviceUrl, serviceUrl: configItems.serviceUrl,
hasPubkey: !!configItems.pgpPrivateKey, hasPubkey: !!configItems.pgpPrivateKey,
customFields: fields.getRow(contextHelpers.getAdminContext(), list.id), customFields: await fields.getRow(contextHelpers.getAdminContext(), list.id),
template: {}, template: {},
layout: null, layout: null,
}; };
@ -293,11 +307,13 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
const emailErr = await tools.validateEmail(email); const emailErr = await tools.validateEmail(email);
if (emailErr) { if (emailErr) {
const errMsg = tools.validateEmailGetMessage(emailErr, email);
if (req.xhr) { if (req.xhr) {
throw new Error(emailErr.message); throw new Error(errMsg);
} }
req.flash('danger', emailErr.message); req.flash('danger', errMsg);
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body)); return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
} }
@ -310,20 +326,20 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
let addressTest = !req.body.address; let addressTest = !req.body.address;
let testsPass = subTimeTest && addressTest; let testsPass = subTimeTest && addressTest;
const list = await lists.getByCid(req.params.cid); const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
if (!list.publicSubscribe) { if (!list.public_subscribe) {
throw new interoperableErrors.SubscriptionNotAllowedError('The list does not allow public subscriptions.'); shares.throwPermissionDenied();
} }
let subscriptionData = {}; const subscriptionData = {};
Object.keys(req.body).forEach(key => { Object.keys(req.body).forEach(key => {
if (key !== 'email' && key.charAt(0) !== '_') { if (key !== 'email' && key.charAt(0) !== '_') {
subscriptionData[key] = (req.body[key] || '').toString().trim(); subscriptionData[key] = (req.body[key] || '').toString().trim();
} }
}); });
const subscription = subscriptions.getByEmail(list.id, email, false) const subscription = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, email, false);
if (subscription && subscription.status === SubscriptionStatus.SUBSCRIBED) { if (subscription && subscription.status === SubscriptionStatus.SUBSCRIBED) {
await mailHelpers.sendAlreadySubscribed(list, email, subscription); await mailHelpers.sendAlreadySubscribed(list, email, subscription);
@ -353,33 +369,36 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
}); });
router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res) => { router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid); const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false); const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) { if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list'); throw new interoperableErrors.NotFoundError('Subscription not found in this list');
} }
subscription.lcid = req.params.lcid; const data = {};
subscription.title = list.name; data.email = subscription.email;
subscription.csrfToken = req.csrfToken(); data.cid = subscription.cid;
subscription.layout = 'subscription/layout'; data.lcid = req.params.lcid;
data.title = list.name;
data.csrfToken = req.csrfToken();
data.layout = 'data/layout';
subscription.customFields = await fields.getRow(contextHelpers.getAdminContext(), list.id, subscription); data.customFields = await fields.getRow(contextHelpers.getAdminContext(), list.id, subscription);
subscription.useEditor = true; data.useEditor = true;
const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress']); const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress']);
subscription.hasPubkey = !!configItems.pgpPrivateKey; data.hasPubkey = !!configItems.pgpPrivateKey;
subscription.defaultAddress = configItems.defaultAddress; data.defaultAddress = configItems.defaultAddress;
subscription.defaultPostaddress = configItems.defaultPostaddress; data.defaultPostaddress = configItems.defaultPostaddress;
subscription.template = { data.template = {
template: 'subscription/web-manage.mjml.hbs', template: 'subscription/web-manage.mjml.hbs',
layout: 'subscription/layout.mjml.hbs' layout: 'subscription/layout.mjml.hbs'
}; };
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-manage', subscription); await injectCustomFormData(req.query.fid || list.default_form, 'data/web-manage', data);
const htmlRenderer = await getMjmlTemplate(data.template); const htmlRenderer = await getMjmlTemplate(data.template);
@ -392,20 +411,23 @@ router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res)
}); });
router.postAsync('/:lcid/manage', passport.parseForm, passport.csrfProtection, async (req, res) => { router.postAsync('/:lcid/manage', passport.parseForm, passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid); const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
const status = await subscriptions.getStatusByCid(contextHelpers.getAdminContext(), list.id, req.body.cid);
if (status !== SubscriptionStatus.SUBSCRIBED) { try {
await subscriptions.updateManagedUngrouped(contextHelpers.getAdminContext(), list.id, req.body);
} catch (err) {
if (err instanceof interoperableErrors.NotFoundError) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list'); throw new interoperableErrors.NotFoundError('Subscription not found in this list');
} else {
throw err;
}
} }
await subscriptions.updateManagedUngrouped(contextHelpers.getAdminContext(), list.id, req.body)
res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/updated-notice'); res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/updated-notice');
}); });
router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (req, res) => { router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid); const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false); const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) { if (!subscription || subscription.status !== SubscriptionStatus.SUBSCRIBED) {
@ -414,18 +436,21 @@ router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (r
const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']); const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']);
subscription.lcid = req.params.lcid; const data = {};
subscription.title = list.name; data.email = subscription.email;
subscription.csrfToken = req.csrfToken(); data.cid = subscription.cid;
subscription.defaultAddress = configItems.defaultAddress; data.lcid = req.params.lcid;
subscription.defaultPostaddress = configItems.defaultPostaddress; data.title = list.name;
data.csrfToken = req.csrfToken();
data.defaultAddress = configItems.defaultAddress;
data.defaultPostaddress = configItems.defaultPostaddress;
subscription.template = { data.template = {
template: 'subscription/web-manage-address.mjml.hbs', template: 'subscription/web-manage-address.mjml.hbs',
layout: 'subscription/layout.mjml.hbs' layout: 'subscription/layout.mjml.hbs'
}; };
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-manage-address', subscription); await injectCustomFormData(req.query.fid || list.default_form, 'data/web-manage-address', subscription);
const htmlRenderer = await getMjmlTemplate(data.template); const htmlRenderer = await getMjmlTemplate(data.template);
@ -439,13 +464,13 @@ router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (r
router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProtection, async (req, res) => { router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid); const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
const emailNew = (req.body['email-new'] || '').toString().trim(); const emailNew = (req.body['email-new'] || '').toString().trim();
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.body.cid, false); const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.body.cid, false);
if (status !== SubscriptionStatus.SUBSCRIBED) { if (subscription.status !== SubscriptionStatus.SUBSCRIBED) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list'); throw new interoperableErrors.NotFoundError('Subscription not found in this list');
} }
@ -455,7 +480,9 @@ router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProte
} else { } else {
const emailErr = await tools.validateEmail(emailNew); const emailErr = await tools.validateEmail(emailNew);
if (emailErr) { if (emailErr) {
req.flash('danger', emailErr.message); const errMsg = tools.validateEmailGetMessage(emailErr, email);
req.flash('danger', errMsg);
} else { } else {
const newSubscription = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, emailNew, false); const newSubscription = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, emailNew, false);
@ -463,8 +490,13 @@ router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProte
if (newSubscription && newSubscription.status === SubscriptionStatus.SUBSCRIBED) { if (newSubscription && newSubscription.status === SubscriptionStatus.SUBSCRIBED) {
await mailHelpers.sendAlreadySubscribed(list, emailNew, subscription); await mailHelpers.sendAlreadySubscribed(list, emailNew, subscription);
} else { } else {
const data = {
subscriptionId: subscription.id,
emailNew
};
const confirmCid = await confirmations.addConfirmation(list.id, 'change-address', req.ip, data); const confirmCid = await confirmations.addConfirmation(list.id, 'change-address', req.ip, data);
await mailHelpers.sendConfirmAddressChange(list, emailNew, confirmCid, subscription, sendWebResponse); await mailHelpers.sendConfirmAddressChange(list, emailNew, confirmCid, subscription);
} }
req.flash('info', _('An email with further instructions has been sent to the provided address')); req.flash('info', _('An email with further instructions has been sent to the provided address'));
@ -476,7 +508,7 @@ router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProte
router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req, res) => { router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid); const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']); const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']);
@ -486,8 +518,8 @@ router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req,
handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, res, next); handleUnsubscribe(list, req.params.ucid, autoUnsubscribe, req.query.c, req.ip, res, next);
} else if (req.query.formTest || } else if (req.query.formTest ||
list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM || list.unsubscription_mode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) { list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false); const subscription = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, req.params.ucid, false);
@ -495,20 +527,22 @@ router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req,
throw new interoperableErrors.NotFoundError('Subscription not found in this list'); throw new interoperableErrors.NotFoundError('Subscription not found in this list');
} }
subscription.lcid = req.params.lcid; const data = {};
subscription.ucid = req.params.ucid; data.email = subscription.email;
subscription.title = list.name; data.lcid = req.params.lcid;
subscription.csrfToken = req.csrfToken(); data.ucid = req.params.ucid;
subscription.campaign = req.query.c; data.title = list.name;
subscription.defaultAddress = configItems.defaultAddress; data.csrfToken = req.csrfToken();
subscription.defaultPostaddress = configItems.defaultPostaddress; data.campaign = req.query.c;
data.defaultAddress = configItems.defaultAddress;
data.defaultPostaddress = configItems.defaultPostaddress;
subscription.template = { data.template = {
template: 'subscription/web-unsubscribe.mjml.hbs', template: 'subscription/web-unsubscribe.mjml.hbs',
layout: 'subscription/layout.mjml.hbs' layout: 'subscription/layout.mjml.hbs'
}; };
await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-unsubscribe', subscription); await injectCustomFormData(req.query.fid || list.default_form, 'subscription/web-unsubscribe', data);
const htmlRenderer = await getMjmlTemplate(data.template); const htmlRenderer = await getMjmlTemplate(data.template);
@ -526,7 +560,7 @@ router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req,
router.postAsync('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, async (req, res) => { router.postAsync('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, async (req, res) => {
const list = await lists.getByCid(req.params.lcid); const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
const campaignCid = (req.body.campaign || '').toString().trim() || false; const campaignCid = (req.body.campaign || '').toString().trim() || false;
@ -535,8 +569,8 @@ router.postAsync('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtecti
async function handleUnsubscribe(list, subscriptionCid, autoUnsubscribe, campaignCid, ip, res) { async function handleUnsubscribe(list, subscriptionCid, autoUnsubscribe, campaignCid, ip, res) {
if ((list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) || if ((list.unsubscription_mode === lists.UnsubscriptionMode.ONE_STEP || list.unsubscription_mode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) ||
(autoUnsubscribe && (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM)) ) { (autoUnsubscribe && (list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM)) ) {
try { try {
const subscription = await subscriptions.unsubscribeByCidAndGet(contextHelpers.getAdminContext(), list.id, subscriptionCid, campaignCid); const subscription = await subscriptions.unsubscribeByCidAndGet(contextHelpers.getAdminContext(), list.id, subscriptionCid, campaignCid);
@ -558,7 +592,7 @@ async function handleUnsubscribe(list, subscriptionCid, autoUnsubscribe, campaig
throw new interoperableErrors.NotFoundError('Subscription not found in this list'); throw new interoperableErrors.NotFoundError('Subscription not found in this list');
} }
if (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) { if (list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP || list.unsubscription_mode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
const data = { const data = {
subscriptionCid, subscriptionCid,
@ -638,7 +672,7 @@ router.postAsync('/publickey', passport.parseForm, async (req, res) => {
async function webNotice(type, req, res) { async function webNotice(type, req, res) {
const list = await lists.getByCid(req.params.cid); const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
const configItems = await settings.get(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail']); const configItems = await settings.get(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail']);

View file

@ -105,13 +105,6 @@ class InvalidConfirmationForUnsubscriptionError extends InteroperableError {
} }
} }
class SubscriptionNotAllowedError extends InteroperableError {
constructor(msg, data) {
super('SubscriptionNotAllowedError', msg, data);
this.status = 403;
}
}
const errorTypes = { const errorTypes = {
InteroperableError, InteroperableError,
NotLoggedInError, NotLoggedInError,
@ -129,8 +122,7 @@ const errorTypes = {
PermissionDeniedError, PermissionDeniedError,
InvalidConfirmationForSubscriptionError, InvalidConfirmationForSubscriptionError,
InvalidConfirmationForAddressChangeError, InvalidConfirmationForAddressChangeError,
InvalidConfirmationForUnsubscriptionError, InvalidConfirmationForUnsubscriptionError
SubscriptionNotAllowedError
}; };
function deserialize(errorObj) { function deserialize(errorObj) {

24
shared/package-lock.json generated Normal file
View file

@ -0,0 +1,24 @@
{
"name": "mailtrain-shared",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"moment": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz",
"integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg=="
},
"moment-timezone": {
"version": "0.5.14",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.14.tgz",
"integrity": "sha1-TrOP+VOLgBCLpGekWPPtQmjM/LE=",
"requires": {
"moment": "2.20.1"
}
},
"owasp-password-strength-test": {
"version": "github:bures/owasp-password-strength-test#50bfcf0035b1468b9d03a00eaf561d4fed4973eb"
}
}
}

20
shared/package.json Normal file
View file

@ -0,0 +1,20 @@
{
"name": "mailtrain-shared",
"version": "1.0.0",
"description": "Self hosted email newsletter app - shared lib",
"repository": {
"type": "git",
"url": "git://github.com/Mailtrain-org/mailtrain.git"
},
"author": "",
"license": "GPL-3.0",
"homepage": "https://mailtrain.org/",
"engines": {
"node": ">=5.0.0"
},
"dependencies": {
"moment": "^2.18.1",
"moment-timezone": "^0.5.13",
"owasp-password-strength-test": "github:bures/owasp-password-strength-test"
}
}

View file

@ -181,7 +181,7 @@
<footer class="footer"> <footer class="footer">
<div class="container-fluid"> <div class="container-fluid">
<p class="text-muted">&copy; 2016 Kreata OÜ <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{{#translate}}Source on GitHub{{/translate}}</a></p> <p class="text-muted">&copy; 2018 <a href="https://mailtrain.org">Mailtrain.org</a>, <a href="mailto:info@mailtrain.org">info@mailtrain.org</a>. <a href="https://github.com/Mailtrain-org/mailtrain">{{#translate}}Source on GitHub{{/translate}}</a></p>
</div> </div>
</footer> </footer>