Before renaming imports to tasks

This commit is contained in:
Tomas Bures 2018-09-23 22:28:58 +02:00
parent a494dc6482
commit 86efa11994
18 changed files with 81 additions and 58 deletions

12
TODO.md
View file

@ -1,3 +1,6 @@
### Front page
- Some dashboard
### Deletion ### Deletion
- Delete button in Lists - Delete button in Lists
- Check/delete dependencies - Check/delete dependencies
@ -19,4 +22,13 @@
- Add X-Mailer header option in settings to override or disable it - 44fe8882b876bdfd9990110496d16f819dc64ac3 - Add X-Mailer header option in settings to override or disable it - 44fe8882b876bdfd9990110496d16f819dc64ac3
- Add custom unsubscribe option in a campaign - 68cb8384f7dfdbcaf2932293ec5a2f1ec0a1554e - Add custom unsubscribe option in a campaign - 68cb8384f7dfdbcaf2932293ec5a2f1ec0a1554e
### API
- Add API extensions - Add API extensions
### GDPR
- Removal of personal data upon unsubscribe (settable per list)
- Refuse editing subscriptions which have been anonymized
- Add field to subscriptions which says till when the consent has been given
- Provide a link (and merge tag) that will update the consent date to now
- Add campaign trigger that triggers if the consent for specific subscription field is about to expire (i.e. it is greater than now - seconds)
- Removal of personal data upon consent expiration (settable per list)

View file

@ -13,4 +13,4 @@ The migration should happen almost automatically. There are however the followin
4. Imports are not migrated. If you have any pending imports, complete them before migration to v2. 4. Imports are not migrated. If you have any pending imports, complete them before migration to v2.
5. Zone MTA configuration endpoint (webhooks/zone-mta/sender-config) has changed. The send-configuration CID has to be 5. Zone MTA configuration endpoint (webhooks/zone-mta/sender-config) has changed. The send-configuration CID has to be
part of the URL - e.g. webhooks/zone-mta/sender-config/default. part of the URL - e.g. webhooks/zone-mta/sender-config/system.

View file

@ -141,7 +141,7 @@ function createApp(appType) {
app.disable('x-powered-by'); app.disable('x-powered-by');
app.use(compression()); app.use(compression());
app.use(favicon(path.join(__dirname, 'client', 'public', 'favicon.ico'))); app.use(favicon(path.join(__dirname, 'client', 'static', 'favicon.ico')));
app.use(logger(config.www.log, { app.use(logger(config.www.log, {
stream: { stream: {
@ -182,7 +182,7 @@ function createApp(appType) {
app.use(passport.tryAuthByRestrictedAccessToken); app.use(passport.tryAuthByRestrictedAccessToken);
} }
useWith404Fallback('/public', express.static(path.join(__dirname, 'client', 'public'))); useWith404Fallback('/static', express.static(path.join(__dirname, 'client', 'static')));
useWith404Fallback('/mailtrain', express.static(path.join(__dirname, 'client', 'dist'))); useWith404Fallback('/mailtrain', express.static(path.join(__dirname, 'client', 'dist')));
useWith404Fallback('/locales', express.static(path.join(__dirname, 'client', 'locales'))); useWith404Fallback('/locales', express.static(path.join(__dirname, 'client', 'locales')));

View file

@ -559,10 +559,11 @@ export default class CUD extends Component {
const sendConfigurationsColumns = [ const sendConfigurationsColumns = [
{ data: 1, title: t('Name') }, { data: 1, title: t('Name') },
{ data: 2, title: t('Description') }, { data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Type'), render: data => this.mailerTypes[data].typeName }, { data: 3, title: t('Description') },
{ data: 4, title: t('Created'), render: data => moment(data).fromNow() }, { data: 4, title: t('Type'), render: data => this.mailerTypes[data].typeName },
{ data: 5, title: t('Namespace') } { data: 5, title: t('Created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('Namespace') }
]; ];
let sendSettings; let sendSettings;

View file

@ -84,7 +84,7 @@ export default class CUD extends Component {
componentDidMount() { componentDidMount() {
if (this.props.entity) { if (this.props.entity) {
this.getFormValuesFromEntity(this.props.entity, data => { this.getFormValuesFromEntity(this.props.entity, data => {
data.daysAfter = (Math.round(data.seconds_after / (3600 * 24))).toString(); data.daysAfter = (Math.round(data.seconds / (3600 * 24))).toString();
if (data.entity === Entity.SUBSCRIPTION) { if (data.entity === Entity.SUBSCRIPTION) {
data.subscriptionEvent = data.event; data.subscriptionEvent = data.event;
@ -157,7 +157,7 @@ export default class CUD extends Component {
this.setFormStatusMessage('info', t('Saving ...')); this.setFormStatusMessage('info', t('Saving ...'));
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => { const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
data.seconds_after = Number.parseInt(data.daysAfter) * 3600 * 24; data.seconds = Number.parseInt(data.daysAfter) * 3600 * 24;
if (data.entity === Entity.SUBSCRIPTION) { if (data.entity === Entity.SUBSCRIPTION) {
data.event = data.subscriptionEvent; data.event = data.subscriptionEvent;

View file

@ -156,9 +156,10 @@ export default class CUD extends Component {
const sendConfigurationsColumns = [ const sendConfigurationsColumns = [
{ data: 1, title: t('Name') }, { data: 1, title: t('Name') },
{ data: 2, title: t('Description') }, { data: 2, title: t('ID'), render: data => <code>{data}</code> },
{ data: 3, title: t('Type'), render: data => this.mailerTypes[data].typeName }, { data: 3, title: t('Description') },
{ data: 5, title: t('Namespace') } { data: 4, title: t('Type'), render: data => this.mailerTypes[data].typeName },
{ data: 6, title: t('Namespace') }
]; ];
return ( return (

View file

@ -96,7 +96,7 @@ export default class List extends Component {
if (perms.includes('viewImports')) { if (perms.includes('viewImports')) {
actions.push({ actions.push({
label: <Icon icon="sort" title={t('Imports')}/>, label: <Icon icon="sort" title={t('Imports & Tasks')}/>,
link: `/lists/${data[0]}/imports` link: `/lists/${data[0]}/imports`
}); });
} }

View file

@ -386,7 +386,7 @@ export default class CUD extends Component {
let saveButtonLabel; let saveButtonLabel;
if (!isEdit) { if (!isEdit) {
saveButtonLabel = t('Save and edit mapping'); saveButtonLabel = t('Save and edit settings');
} else { } else {
saveButtonLabel = t('Save'); saveButtonLabel = t('Save');
} }
@ -404,7 +404,7 @@ export default class CUD extends Component {
deletedMsg={t('Field deleted')}/> deletedMsg={t('Field deleted')}/>
} }
<Title>{isEdit ? t('Edit Import') : t('Create Import')}</Title> <Title>{isEdit ? t('Edit Import/Task') : t('Create Import/Task')}</Title>
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}> <Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
<InputField id="name" label={t('Name')}/> <InputField id="name" label={t('Name')}/>

View file

@ -81,11 +81,11 @@ export default class List extends Component {
<div> <div>
{mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports') && {mailtrainConfig.globalPermissions.includes('setupAutomation') && this.props.list.permissions.includes('manageImports') &&
<Toolbar> <Toolbar>
<NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('Create Import')}/> <NavButton linkTo={`/lists/${this.props.list.id}/imports/create`} className="btn-primary" icon="plus" label={t('Create Import/Task')}/>
</Toolbar> </Toolbar>
} }
<Title>{t('Imports')}</Title> <Title>{t('Imports & Tasks')}</Title>
<Table withHeader dataUrl={`rest/imports-table/${this.props.list.id}`} columns={columns} /> <Table withHeader dataUrl={`rest/imports-table/${this.props.list.id}`} columns={columns} />
</div> </div>

View file

@ -132,7 +132,7 @@ function getMenus(t) {
} }
}, },
imports: { imports: {
title: t('Imports'), title: t('Imports & Tasks'),
link: params => `/lists/${params.listId}/imports/`, link: params => `/lists/${params.listId}/imports/`,
visible: resolved => resolved.list.permissions.includes('viewImports'), visible: resolved => resolved.list.permissions.includes('viewImports'),
panelRender: props => <ImportsList list={props.resolved.list} />, panelRender: props => <ImportsList list={props.resolved.list} />,

View file

@ -18,6 +18,7 @@ const executor = require('./lib/executor');
const privilegeHelpers = require('./lib/privilege-helpers'); const privilegeHelpers = require('./lib/privilege-helpers');
const knex = require('./lib/knex'); const knex = require('./lib/knex');
const shares = require('./models/shares'); const shares = require('./models/shares');
const { AppType } = require('./shared/app');
const trustedPort = config.www.trustedPort; const trustedPort = config.www.trustedPort;
const sandboxPort = config.www.sandboxPort; const sandboxPort = config.www.sandboxPort;
@ -31,7 +32,7 @@ if (config.title) {
log.level = config.log.level; log.level = config.log.level;
function startHTTPServer(appType, port, callback) { function startHTTPServer(appType, appName, port, callback) {
const app = appBuilder.createApp(appType); const app = appBuilder.createApp(appType);
app.set('port', port); app.set('port', port);
@ -60,7 +61,7 @@ function startHTTPServer(appType, port, callback) {
server.on('listening', () => { server.on('listening', () => {
const addr = server.address(); const addr = server.address();
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port; const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
log.info('Express', 'WWW server listening on %s', bind); log.info('Express', 'WWW server [%s] listening on %s', appName, bind);
}); });
server.listen({port, host}, callback); server.listen({port, host}, callback);
@ -85,9 +86,9 @@ dbcheck(err => { // Check if database needs upgrading before starting the server
executor.spawn(() => { executor.spawn(() => {
testServer(() => { testServer(() => {
verpServer(() => { verpServer(() => {
startHTTPServer(AppType.TRUSTED, trustedPort, () => { startHTTPServer(AppType.TRUSTED, 'trusted', trustedPort, () => {
startHTTPServer(AppType.SANDBOXED, sandboxPort, () => { startHTTPServer(AppType.SANDBOXED, 'sandbox', sandboxPort, () => {
startHTTPServer(AppType.PUBLIC, publicPort, () => { startHTTPServer(AppType.PUBLIC, 'public', publicPort, () => {
privilegeHelpers.dropRootPrivileges(); privilegeHelpers.dropRootPrivileges();
tzupdate.start(); tzupdate.start();

View file

@ -172,6 +172,20 @@ async function listTestUsersDTAjax(context, campaignId, params) {
}); });
} }
async function getTrackingSettingsByCidTx(tx, cid) {
const entity = await tx('campaigns').where('campaigns.cid', cid)
.select([
'campaigns.id', 'campaigns.click_tracking_disabled', 'campaigns.open_tracking_disabled'
])
.first();
if (!entity) {
throw new interoperableErrors.NotFoundError();
}
return entity;
}
async function rawGetByTx(tx, key, id) { async function rawGetByTx(tx, key, id) {
const entity = await tx('campaigns').where('campaigns.' + key, id) const entity = await tx('campaigns').where('campaigns.' + key, id)
.leftJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign') .leftJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
@ -709,8 +723,6 @@ module.exports.listOthersWhoseListsAreIncludedDTAjax = listOthersWhoseListsAreIn
module.exports.listTestUsersDTAjax = listTestUsersDTAjax; module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
module.exports.getByIdTx = getByIdTx; module.exports.getByIdTx = getByIdTx;
module.exports.getById = getById; module.exports.getById = getById;
module.exports.getByCidTx = getByCidTx;
module.exports.getByCid = getByCid;
module.exports.create = create; module.exports.create = create;
module.exports.createRssTx = createRssTx; module.exports.createRssTx = createRssTx;
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck; module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
@ -731,4 +743,5 @@ module.exports.start = start;
module.exports.stop = stop; module.exports.stop = stop;
module.exports.reset = reset; module.exports.reset = reset;
module.exports.rawGetBy = rawGetBy; module.exports.rawGetByTx = rawGetByTx;
module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx;

View file

@ -4,7 +4,6 @@ const log = require('npmlog');
const knex = require('../lib/knex'); const knex = require('../lib/knex');
const dtHelpers = require('../lib/dt-helpers'); const dtHelpers = require('../lib/dt-helpers');
const shares = require('./shares'); const shares = require('./shares');
const tools = require('../lib/tools');
const campaigns = require('./campaigns'); const campaigns = require('./campaigns');
const lists = require('./lists'); const lists = require('./lists');
const subscriptions = require('./subscriptions'); const subscriptions = require('./subscriptions');
@ -28,7 +27,7 @@ async function resolve(linkCid) {
async function countLink(remoteIp, userAgent, campaignCid, listCid, subscriptionCid, linkId) { async function countLink(remoteIp, userAgent, campaignCid, listCid, subscriptionCid, linkId) {
await knex.transaction(async tx => { await knex.transaction(async tx => {
const list = await lists.getByCidTx(tx, contextHelpers.getAdminContext(), listCid); const list = await lists.getByCidTx(tx, contextHelpers.getAdminContext(), listCid);
const campaign = await campaigns.getByCidTx(tx, contextHelpers.getAdminContext(), campaignCid); const campaign = await campaigns.getTrackingSettingsByCidTx(tx, campaignCid);
const subscription = await subscriptions.getByCidTx(tx, contextHelpers.getAdminContext(), subscriptionCid); const subscription = await subscriptions.getByCidTx(tx, contextHelpers.getAdminContext(), subscriptionCid);
const country = geoip.lookupCountry(remoteIp) || null; const country = geoip.lookupCountry(remoteIp) || null;

View file

@ -9,7 +9,7 @@ const shares = require('./shares');
const {EntityVals, EventVals, Entity} = require('../shared/triggers'); const {EntityVals, EventVals, Entity} = require('../shared/triggers');
const campaigns = require('./campaigns'); const campaigns = require('./campaigns');
const allowedKeys = new Set(['name', 'description', 'entity', 'event', 'seconds_after', 'enabled', 'source_campaign']); const allowedKeys = new Set(['name', 'description', 'entity', 'event', 'seconds', 'enabled', 'source_campaign']);
function hash(entity) { function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys)); return hasher.hash(filterObject(entity, allowedKeys));
@ -36,7 +36,7 @@ async function listByCampaignDTAjax(context, campaignId, params) {
.from('triggers') .from('triggers')
.innerJoin('campaigns', 'campaigns.id', 'triggers.campaign') .innerJoin('campaigns', 'campaigns.id', 'triggers.campaign')
.where('triggers.campaign', campaignId), .where('triggers.campaign', campaignId),
[ 'triggers.id', 'triggers.name', 'triggers.description', 'triggers.entity', 'triggers.event', 'triggers.seconds_after', 'triggers.enabled' ] [ 'triggers.id', 'triggers.name', 'triggers.description', 'triggers.entity', 'triggers.event', 'triggers.seconds', 'triggers.enabled' ]
); );
}); });
} }
@ -51,13 +51,13 @@ async function listByListDTAjax(context, listId, params) {
.innerJoin('campaigns', 'campaigns.id', 'triggers.campaign') .innerJoin('campaigns', 'campaigns.id', 'triggers.campaign')
.innerJoin('campaign_lists', 'campaign_lists.campaign', 'campaigns.id') .innerJoin('campaign_lists', 'campaign_lists.campaign', 'campaigns.id')
.where('campaign_lists.list', listId), .where('campaign_lists.list', listId),
[ 'triggers.id', 'triggers.name', 'triggers.description', 'campaigns.name', 'triggers.entity', 'triggers.event', 'triggers.seconds_after', 'triggers.enabled', 'triggers.campaign' ] [ 'triggers.id', 'triggers.name', 'triggers.description', 'campaigns.name', 'triggers.entity', 'triggers.event', 'triggers.seconds', 'triggers.enabled', 'triggers.campaign' ]
); );
} }
async function _validateAndPreprocess(tx, context, campaignId, entity) { async function _validateAndPreprocess(tx, context, campaignId, entity) {
enforce(Number.isInteger(entity.seconds_after)); enforce(Number.isInteger(entity.seconds));
enforce(entity.seconds_after >= 0, 'Seconds after must not be negative'); enforce(entity.seconds >= 0, 'Seconds must not be negative');
enforce(entity.entity in EntityVals, 'Invalid entity'); enforce(entity.entity in EntityVals, 'Invalid entity');
enforce(entity.event in EventVals[entity.entity], 'Invalid event'); enforce(entity.event in EventVals[entity.entity], 'Invalid event');

View file

@ -154,25 +154,23 @@ router.postAsync('/mailgun', uploads.any(), async (req, res) => {
const evt = req.body; const evt = req.body;
const message = await campaigns.getMessageByCid([].concat(evt && evt.campaign_id || []).shift()); const message = await campaigns.getMessageByCid([].concat(evt && evt.campaign_id || []).shift());
if (!message) { if (message) {
continue; switch (evt.event) {
} case 'bounced':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('Mailgun', 'Marked message %s as bounced', evt.campaign_id);
break;
switch (evt.event) { case 'complained':
case 'bounced': await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.COMPLAINED, true);
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true); log.verbose('Mailgun', 'Marked message %s as complaint', evt.campaign_id);
log.verbose('Mailgun', 'Marked message %s as bounced', evt.campaign_id); break;
break;
case 'complained': case 'unsubscribed':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.COMPLAINED, true); await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.UNSUBSCRIBED, true);
log.verbose('Mailgun', 'Marked message %s as complaint', evt.campaign_id); log.verbose('Mailgun', 'Marked message %s as unsubscribed', evt.campaign_id);
break; break;
}
case 'unsubscribed':
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.UNSUBSCRIBED, true);
log.verbose('Mailgun', 'Marked message %s as unsubscribed', evt.campaign_id);
break;
} }
return res.json({ return res.json({
@ -188,12 +186,11 @@ router.postAsync('/zone-mta', async (req, res) => {
if (req.body.id) { if (req.body.id) {
const message = await campaigns.getMessageByCid(req.body.id); const message = await campaigns.getMessageByCid(req.body.id);
if (!message) {
continue;
}
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true); if (message) {
log.verbose('ZoneMTA', 'Marked message %s as bounced', req.body.id); await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('ZoneMTA', 'Marked message %s as bounced', req.body.id);
}
} }
res.json({ res.json({

View file

@ -131,7 +131,7 @@ async function start() {
} }
sqlQry = sqlQry.where(column, '<=', currentTs - trigger.seconds_after); sqlQry = sqlQry.where(column, '<=', currentTs - trigger.seconds);
if (trigger.last_check !== null) { if (trigger.last_check !== null) {
sqlQry = sqlQry.where(column, '>', trigger.last_check); sqlQry = sqlQry.where(column, '>', trigger.last_check);

View file

@ -1065,7 +1065,6 @@ async function migrateTriggers(knex) {
table.renameColumn('rule', 'entity'); table.renameColumn('rule', 'entity');
table.renameColumn('column', 'event'); table.renameColumn('column', 'event');
table.renameColumn('dest_campaign', 'campaign'); table.renameColumn('dest_campaign', 'campaign');
table.renameColumn('seconds', 'seconds_after');
}); });
const triggers = await knex('triggers'); const triggers = await knex('triggers');

View file

@ -11,7 +11,7 @@ function getSystemSendConfigurationId() {
} }
function getSystemSendConfigurationCid() { function getSystemSendConfigurationCid() {
return 'default'; return 'system';
} }
module.exports = { module.exports = {