diff --git a/UPGRADE.md b/UPGRADE.md index 72bbc870..2abf71ce 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -13,4 +13,9 @@ 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. 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/system. \ No newline at end of file + part of the URL - e.g. webhooks/zone-mta/sender-config/system. + +6. If there are lists that contain birthday or date fields that were created before + commit `bc73a0df0cab9943d726bd12fc1c6f2ff1279aa7` (on Jan 3, 2018), they still have TIMESTAMP data type in DB instead + of DATETIME. The problem was that that commit did not introduce migration from TIMESTAMP to DATETIME. + Mailtrain v2 does this migration, however in some corner cases, this may shift the date by a day back or forth. diff --git a/client/src/campaigns/Status.js b/client/src/campaigns/Status.js index 94a7b053..ab8401b2 100644 --- a/client/src/campaigns/Status.js +++ b/client/src/campaigns/Status.js @@ -190,6 +190,8 @@ class SendControls extends Component { date.minute(time.minute()); date.second(0); date.millisecond(0); + date.utcOffset(0, true); // TODO, process offset from user settings + await this.postAndMaskStateError(`rest/campaign-start-at/${this.props.entity.id}/${date.valueOf()}`); @@ -245,6 +247,7 @@ class SendControls extends Component {
+ {/* TODO: Timezone selector */}
} diff --git a/client/src/campaigns/triggers/List.js b/client/src/campaigns/triggers/List.js index 9e77c4ac..91632009 100644 --- a/client/src/campaigns/triggers/List.js +++ b/client/src/campaigns/triggers/List.js @@ -65,7 +65,7 @@ export default class List extends Component { }); } - if (perms.includes('manageTriggers')) { + if (this.props.campaign.permissions.includes('manageTriggers')) { tableDeleteDialogAddDeleteButton(actions, this, null, data[0], data[1]); } diff --git a/client/src/lists/segments/helpers.js b/client/src/lists/segments/helpers.js index 589ad014..ab2827ee 100644 --- a/client/src/lists/segments/helpers.js +++ b/client/src/lists/segments/helpers.js @@ -95,14 +95,13 @@ export function getRuleHelpers(t, fields) { } }; - // TODO: This generates strings that cannot be statically detected. It will require dynamic discovery of translatable strings. function getRelativeDateTreeLabel(rule, textFragment) { if (rule.value === 0) { - return t('dateInColumnColName' + textFragment + ' the current date', {colName: ruleHelpers.getColumnName(rule.column)}) + return t(/*ignore*/'Date in column ' + textFragment + ' the current date', {colName: ruleHelpers.getColumnName(rule.column)}) } else if (rule.value > 0) { - return t('dateInColumnColName' + textFragment + ' {{value}}-th day after the current date', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}); + return t(/*ignore*/'Date in column ' + textFragment + ' {{value}}-th day after the current date', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}); } else { - return t('dateInColumnColName' + textFragment + ' {{value}}-th day before the current date', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}); + return t(/*ignore*/'Date in column ' + textFragment + ' {{value}}-th day before the current date', {colName: ruleHelpers.getColumnName(rule.column), value: rule.value}); } } @@ -129,22 +128,47 @@ export function getRuleHelpers(t, fields) { }, eqTodayPlusDays: { dropdownLabel: t('onXthDayBeforeafterCurrentDate'), + /* + tMark('Date in column is the current date') + tMark('Date in column is {{value}}-th day after the current date') + tMark('Date in column is {{value}}-th day before the current date') + */ treeLabel: rule => getRelativeDateTreeLabel(rule, 'is'), }, ltTodayPlusDays: { dropdownLabel: t('beforeXthDayBeforeafterCurrentDate'), + /* + tMark('Date in column is before the current date') + tMark('Date in column is before {{value}}-th day after the current date') + tMark('Date in column is before {{value}}-th day before the current date') + */ treeLabel: rule => getRelativeDateTreeLabel(rule, 'is before'), }, leTodayPlusDays: { dropdownLabel: t('beforeOrOnXthDayBeforeafterCurrentDate'), + /* + tMark('Date in column is before or on the current date') + tMark('Date in column is before or on {{value}}-th day after the current date') + tMark('Date in column is before or on {{value}}-th day before the current date') + */ treeLabel: rule => getRelativeDateTreeLabel(rule, 'is before or on'), }, gtTodayPlusDays: { dropdownLabel: t('afterXthDayBeforeafterCurrentDate'), + /* + tMark('Date in column is after the current date') + tMark('Date in column is after {{value}}-th day after the current date') + tMark('Date in column is after {{value}}-th day after the current date') + */ treeLabel: rule => getRelativeDateTreeLabel(rule, 'is after'), }, geTodayPlusDays: { dropdownLabel: t('afterOrOnXthDayBeforeafterCurrentDate'), + /* + tMark('Date in column is after or on the current date') + tMark('Date in column is after or on {{value}}-th day after the current date') + tMark('Date in column is after or on {{value}}-th day after the current date') + */ treeLabel: rule => getRelativeDateTreeLabel(rule, 'is after or on'), } }; diff --git a/client/src/reports/templates/CUD.js b/client/src/reports/templates/CUD.js index 1495338b..82684a36 100644 --- a/client/src/reports/templates/CUD.js +++ b/client/src/reports/templates/CUD.js @@ -87,10 +87,10 @@ export default class CUD extends Component { ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' {{#if results}}\n' + @@ -146,16 +146,16 @@ export default class CUD extends Component { '
\n' + - ' {{#translate}}Email{{/translate}}\n' + + ' Email\n' + ' \n' + - ' {{#translate}}Tracker Count{{/translate}}\n' + + ' Tracker Count\n' + '
\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' {{#if results}}\n' + diff --git a/docs/README.md b/docs/README.md index 855177f3..ba5f69f3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -314,11 +314,11 @@ let str2 = util.format( _('My name is "%s"'), 'Mailtrain'); ### Translating Handlebars Files -Enclose translatable strings to `{{#translate}}` tags +Enclose translatable strings to `` tags ```handlebars

- Mailtrain – {{#translate}}the best newsletter app{{/translate}} + Mailtrain – the best newsletter app

``` diff --git a/locales/extract.js b/locales/extract.js index 3f25e442..0134071c 100644 --- a/locales/extract.js +++ b/locales/extract.js @@ -137,6 +137,8 @@ function parseSpec(specStr) { const elems = entry.match(entryMatcher); if (elems) { spec[elems[1]] = elems[2]; + } else { + spec[entry] = true; } } } @@ -224,6 +226,10 @@ function parseT(fragment) { const originalKey = match[5]; const spec = parseSpec(match[3]); + if (spec.ignore) { + return null; + } + // console.log(`${file}: ${line}`); // console.log(` |${match[1]}|${match[2]}|${match[4]}|${match[5]}|${match[6]}| - ${JSON.stringify(spec)}`); @@ -254,27 +260,30 @@ function processFile(file) { function update(fragments, parseFun) { if (fragments) { for (const fragment of fragments) { - const {key, originalKey, value, originalValue, replacement} = parseFun(fragment); - // console.log(`${key} <- ${originalKey} | ${value} <- ${originalValue} | ${fragment} -> ${replacement}`); + const parseStruct = parseFun(fragment); + if (parseStruct) { + const {key, originalKey, value, originalValue, replacement} = parseStruct; + // console.log(`${key} <- ${originalKey} | ${value} <- ${originalValue} | ${fragment} -> ${replacement}`); - source = source.split(fragment).join(replacement); - setInDict(resDict, key, value); - - const variants = originalKey ? findAllVariantsByPrefixInDict(originalResDict, originalKey + '_') : []; - for (const variant of variants) { - setInDict(resDict, key + '_' + variant, findInDict(originalResDict, originalKey + '_' + variant)); - } - - if (originalKey !== key) { - renamedKeys.set(originalKey, key); + source = source.split(fragment).join(replacement); + setInDict(resDict, key, value); + const variants = originalKey ? findAllVariantsByPrefixInDict(originalResDict, originalKey + '_') : []; for (const variant of variants) { - renamedKeys.set(originalKey + '_' + variant, key + '_' + variant); + setInDict(resDict, key + '_' + variant, findInDict(originalResDict, originalKey + '_' + variant)); } - } - if (originalKey !== key || originalValue !== value) { - anyUpdates = true; + if (originalKey !== key) { + renamedKeys.set(originalKey, key); + + for (const variant of variants) { + renamedKeys.set(originalKey + '_' + variant, key + '_' + variant); + } + } + + if (originalKey !== key || originalValue !== value) { + anyUpdates = true; + } } } } diff --git a/server/config/default.yaml b/server/config/default.yaml index 75de813d..ade58697 100644 --- a/server/config/default.yaml +++ b/server/config/default.yaml @@ -89,6 +89,7 @@ mysql: port: 3306 charset: utf8mb4 # The timezone configured on the MySQL server. This can be 'local', 'Z', or an offset in the form +HH:MM or -HH:MM + # If the MySQL server runs on the same server as Mailtrain, use 'local' timezone: local verp: diff --git a/server/dbtest.js b/server/dbtest.js new file mode 100644 index 00000000..ec3ae585 --- /dev/null +++ b/server/dbtest.js @@ -0,0 +1,32 @@ +'use strict'; + +const config = require('config'); +const knex = require('./lib/knex'); +const moment = require('moment'); +const shortid = require('shortid'); + +async function run() { +// const info = await knex('subscription__1').columnInfo(); +// console.log(info); + +// const ts = moment().toDate(); + const ts = new Date(Date.now()); + console.log(ts); + + const cid = shortid.generate(); + + await knex('subscription__1') + .insert({ + email: cid, + cid, + custom_date_mmddyy_rjkeojrzz: ts + }); + + + const row = await knex('subscription__1').select(['id', 'created', 'custom_date_mmddyy_rjkeojrzz']).where('cid', cid).first(); + +// const row = await knex('subscription__1').where('id', 2).first(); + console.log(row); +} + +run(); \ No newline at end of file diff --git a/server/lib/campaign-sender.js b/server/lib/campaign-sender.js index 87fb9aac..1bf5a261 100644 --- a/server/lib/campaign-sender.js +++ b/server/lib/campaign-sender.js @@ -279,14 +279,25 @@ class CampaignSender { return await this._getMessage(campaign, list, subscriptionGrouped, mergeTags, false); } - async sendMessage(listId, email) { + async sendMessageByEmail(listId, email) { + const subscriptionGrouped = await subscriptions.getByEmail(contextHelpers.getAdminContext(), listId, email); + await this._sendMessage(listId, subscriptionGrouped); + } + + async sendMessageBySubscriptionId(listId, subscriptionId) { + const subscriptionGrouped = await subscriptions.getById(contextHelpers.getAdminContext(), listId, subscriptionId); + await this._sendMessage(listId, subscriptionGrouped); + } + + async _sendMessage(listId, subscriptionGrouped) { + const email = subscriptionGrouped.email; + if (await blacklist.isBlacklisted(email)) { return; } const list = this.listsById.get(listId); - const subscriptionGrouped = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, email); - const flds = this.listsFieldsGrouped.get(listId); + const flds = this.listsFieldsGrouped.get(list.id); const campaign = this.campaign; const mergeTags = fields.getMergeTags(flds, subscriptionGrouped, this._getExtraTags(campaign)); @@ -391,16 +402,28 @@ class CampaignSender { const responseId = response.split(/\s+/).pop(); const now = new Date(); - await knex('campaign_messages').insert({ - campaign: this.campaign.id, - list: listId, - subscription: subscriptionGrouped.id, - send_configuration: sendConfiguration.id, - status, - response, - response_id: responseId, - updated: now - }); + + if (campaign.type === CampaignType.REGULAR || campaign.type === CampaignType.RSS_ENTRY) { + await knex('campaign_messages').insert({ + campaign: this.campaign.id, + list: list.id, + subscription: subscriptionGrouped.id, + send_configuration: sendConfiguration.id, + status, + response, + response_id: responseId, + updated: now + }); + + } else if (campaign.type = CampaignType.TRIGGERED) { + await knex('queued') + .where({ + campaign: this.campaign.id, + list: list.id, + subscription: subscriptionGrouped.id + }) + .del(); + } } } diff --git a/server/lib/knex.js b/server/lib/knex.js index 327c55ac..62119161 100644 --- a/server/lib/knex.js +++ b/server/lib/knex.js @@ -1,14 +1,26 @@ 'use strict'; const config = require('config'); +const moment = require('moment'); const knex = require('knex')({ client: 'mysql2', - connection: config.mysql, + connection: { + ...config.mysql, + + // DATE and DATETIME types contain no timezone info. The MySQL driver tries to interpret them w.r.t. to local time, which + // does not work well with assigning these values in UTC and handling them as if in UTC + dateStrings: [ + 'DATE', + 'DATETIME' + ] + }, migrations: { directory: __dirname + '/../setup/knex/migrations' } - //, debug: true +//, debug: true }); + + module.exports = knex; diff --git a/server/lib/translate.js b/server/lib/translate.js index 57b25ae4..fe2fd65c 100644 --- a/server/lib/translate.js +++ b/server/lib/translate.js @@ -52,7 +52,7 @@ function tUI(key, lang, args) { args = {}; } - return i18n.t(key, { ...args, defaultValue, lng: lang }); + return i18n.t(key, { ...args, lng: lang }); } function tMark(key) { diff --git a/server/models/segments.js b/server/models/segments.js index 1d4b5299..e6cfadf5 100644 --- a/server/models/segments.js +++ b/server/models/segments.js @@ -11,8 +11,6 @@ const fields = require('./fields'); const subscriptions = require('./subscriptions'); const dependencyHelpers = require('../lib/dependency-helpers'); -const { parseDate, parseBirthday, DateFormat } = require('../../shared/date'); - const allowedKeys = new Set(['name', 'settings']); diff --git a/server/models/subscriptions.js b/server/models/subscriptions.js index e6e3a48e..77d3c7b0 100644 --- a/server/models/subscriptions.js +++ b/server/models/subscriptions.js @@ -59,7 +59,7 @@ fieldTypes.date = { afterJSON: (groupedField, entity) => { const key = getFieldColumn(groupedField); if (key in entity) { - entity[key] = entity[key] ? moment(entity[key]).toDate() : null; + entity[key] = entity[key] ? moment(entity[key]).toISOString() : null; } }, listRender: (groupedField, value) => formatDate(groupedField.settings.dateFormat, value) @@ -69,7 +69,7 @@ fieldTypes.birthday = { afterJSON: (groupedField, entity) => { const key = getFieldColumn(groupedField); if (key in entity) { - entity[key] = entity[key] ? moment(entity[key]).toDate() : null; + entity[key] = entity[key] ? moment(entity[key]).toISOString() : null; } }, listRender: (groupedField, value) => formatBirthday(groupedField.settings.dateFormat, value) @@ -539,6 +539,7 @@ async function _update(tx, listId, existing, filteredEntity) { } } + console.log(filteredEntity); await tx(getSubscriptionTableName(listId)).where('id', existing.id).update(filteredEntity); if ('status' in filteredEntity) { diff --git a/server/models/triggers.js b/server/models/triggers.js index 1d6f7da2..5a02de7b 100644 --- a/server/models/triggers.js +++ b/server/models/triggers.js @@ -77,6 +77,7 @@ async function create(context, campaignId, entity) { const filteredEntity = filterObject(entity, allowedKeys); filteredEntity.campaign = campaignId; + filteredEntity.last_check = new Date(); // This is to prevent processing subscriptions that predate this trigger. const ids = await tx('triggers').insert(filteredEntity); const id = ids[0]; diff --git a/server/routes/subscription.js b/server/routes/subscription.js index 0c14f3c1..8d09c40b 100644 --- a/server/routes/subscription.js +++ b/server/routes/subscription.js @@ -235,6 +235,7 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as shares.throwPermissionDenied(); } + // TODO: Validate date/birthady formats here. Now if the value is wrong, it gets simply ignored const subscriptionData = await fields.fromPost(contextHelpers.getAdminContext(), list.id, req.body); const email = cleanupFromPost(req.body.EMAIL); diff --git a/server/services/sender-master.js b/server/services/sender-master.js index 65b186cd..621fa2e9 100644 --- a/server/services/sender-master.js +++ b/server/services/sender-master.js @@ -15,6 +15,7 @@ const workerProcesses = new Map(); const idleWorkers = []; let campaignSchedulerRunning = false; +let queuedSchedulerRunning = false; let workerSchedulerRunning = false; const campaignsCheckPeriod = 5 * 1000; @@ -27,6 +28,7 @@ const messageQueueCont = new Map(); // campaignId -> next batch callback const workAssignment = new Map(); // workerId -> { campaignId, subscribers: [{listId, email}] } let workerSchedulerCont = null; +let queuedLastId = 0; function messagesProcessed(workerId) { @@ -151,7 +153,6 @@ async function processCampaign(campaignId) { messageQueueCont.set(campaignId, resolve); }); - // noinspection JSIgnoredPromiseFromCall setImmediate(scheduleWorkers); await nextBatchNeeded; @@ -175,36 +176,86 @@ async function scheduleCampaigns() { campaignSchedulerRunning = true; - while (true) { - let campaignId = 0; + try { + while (true) { + let campaignId = 0; - await knex.transaction(async tx => { - const scheduledCampaign = await tx('campaigns') - .whereIn('campaigns.type', [CampaignType.REGULAR, CampaignType.RSS_ENTRY]) - .where('campaigns.status', CampaignStatus.SCHEDULED) - .where(qry => qry.whereNull('campaigns.scheduled').orWhere('campaigns.scheduled', '<=', new Date())) - .select(['id']) - .first(); + await knex.transaction(async tx => { + const scheduledCampaign = await tx('campaigns') + .whereIn('campaigns.type', [CampaignType.REGULAR, CampaignType.RSS_ENTRY]) + .where('campaigns.status', CampaignStatus.SCHEDULED) + .where(qry => qry.whereNull('campaigns.scheduled').orWhere('campaigns.scheduled', '<=', new Date())) + .select(['id']) + .first(); - if (scheduledCampaign) { - await tx('campaigns').where('id', scheduledCampaign.id).update({status: CampaignStatus.SENDING}); - campaignId = scheduledCampaign.id; + if (scheduledCampaign) { + await tx('campaigns').where('id', scheduledCampaign.id).update({status: CampaignStatus.SENDING}); + campaignId = scheduledCampaign.id; + } + }); + + if (campaignId) { + // noinspection JSIgnoredPromiseFromCall + processCampaign(campaignId); + + } else { + break; } - }); - - if (campaignId) { - // noinspection JSIgnoredPromiseFromCall - processCampaign(campaignId); - - } else { - break; } + } catch (err) { + log.error('Senders', `Scheduling campaigns failed with error: ${err.message}`) + log.verbose(err); } + campaignSchedulerRunning = false; } +async function processQueued() { + if (queuedSchedulerRunning) { + return; + } + + queuedSchedulerRunning = true; + + try { + while (true) { + const rows = await knex('queued') + .orderBy('id', 'asc') + .where('id', '>', queuedLastId) + .limit(retrieveBatchSize); + + if (rows.length === 0) { + break; + } + + for (const row of rows) { + let msgQueue = messageQueue.get(row.campaign); + if (!msgQueue) { + msgQueue = []; + messageQueue.set(row.campaign, msgQueue); + } + + msgQueue.push({ + listId: row.list, + subscriptionId: row.subscription + }); + } + + queuedLastId = rows[rows.length - 1].id; + + setImmediate(scheduleWorkers); + } + } catch (err) { + log.error('Senders', `Processing queued messages failed with error: ${err.message}`) + log.verbose(err); + } + + queuedSchedulerRunning = false; +} + + async function spawnWorker(workerId) { return await new Promise((resolve, reject) => { log.verbose('Senders', `Spawning worker process ${workerId}`); @@ -251,6 +302,9 @@ function periodicCampaignsCheck() { // noinspection JSIgnoredPromiseFromCall scheduleCampaigns(); + // noinspection JSIgnoredPromiseFromCall + processQueued(); + setTimeout(periodicCampaignsCheck, campaignsCheckPeriod); } @@ -286,5 +340,6 @@ async function init() { periodicCampaignsCheck(); } +// noinspection JSIgnoredPromiseFromCall init(); diff --git a/server/services/sender-worker.js b/server/services/sender-worker.js index 96636a6b..61358187 100644 --- a/server/services/sender-worker.js +++ b/server/services/sender-worker.js @@ -4,6 +4,7 @@ const config = require('config'); const log = require('../lib/log'); const mailers = require('../lib/mailers'); const CampaignSender = require('../lib/campaign-sender'); +const {enforce} = require('../lib/helpers'); const workerId = Number.parseInt(process.argv[2]); let running = false; @@ -21,8 +22,17 @@ async function processMessages(campaignId, subscribers) { for (const subData of subscribers) { try { - await cs.sendMessage(subData.listId, subData.email); - log.verbose('Senders', 'Message sent and status updated for %s:%s', subData.listId, subData.email); + if (subData.email) { + await cs.sendMessageByEmail(subData.listId, subData.email); + + } else if (subData.subscriptionId) { + await cs.sendMessageBySubscriptionId(subData.listId, subData.subscriptionId); + + } else { + enforce(false); + } + + log.verbose('Senders', 'Message sent and status updated for %s:%s', subData.listId, subData.email || subData.subscriptionId); } catch (err) { log.error('Senders', `Sending message to ${subData.listId}:${subData.email} failed with error: ${err.message}`) log.verbose(err); diff --git a/server/services/triggers.js b/server/services/triggers.js index 537e5e4d..706e28b1 100644 --- a/server/services/triggers.js +++ b/server/services/triggers.js @@ -15,17 +15,21 @@ const triggerCheckPeriod = 15 * 1000; const triggerFirePeriod = 60 * 1000; -async function start() { +async function run() { while (true) { const fired = await knex.transaction(async tx => { - const currentTs = new Date(); + const currentTs = Date.now(); - const trigger = await tx('triggers').where('enabled', true).andWhere('last_check', '<', currentTs - triggerFirePeriod).orderBy('last_check', 'asc').first(); + const trigger = await tx('triggers') + .where('enabled', true) + .where(qry => qry.whereNull('last_check').orWhere('last_check', '<', new Date(currentTs - triggerFirePeriod))) + .orderBy('last_check', 'asc') + .first(); if (!trigger) { return false; } - const campaign = campaigns.getByIdTx(tx, contextHelpers.getAdminContext(), trigger.campaign, false); + const campaign = await campaigns.getByIdTx(tx, contextHelpers.getAdminContext(), trigger.campaign, false); for (const cpgList of campaign.lists) { const addSegmentQuery = cpgList.segment ? await segments.getQueryGeneratorTx(tx, cpgList.list, cpgList.segment) : () => { @@ -36,8 +40,10 @@ async function start() { .leftJoin( function () { return this.from('trigger_messages') - .where('trigger_messages.campaign', campaign.id) + .innerJoin('triggers', 'trigger_messages.trigger', 'triggers.id') + .where('triggers.campaign', campaign.id) .where('trigger_messages.list', cpgList.list) + .select(['id', 'subscription']) .as('related_trigger_messages'); }, 'related_trigger_messages.subscription', subsTable + '.id' @@ -45,7 +51,7 @@ async function start() { .where(function () { addSegmentQuery(this); }) - .whereNotNull('related_trigger_messages.id') // This means only those where the trigger has not fired yet somewhen in the past + .whereNull('related_trigger_messages.id') // This means only those where the trigger has not fired yet somewhen in the past .select(subsTable + '.id'); let column; @@ -129,20 +135,18 @@ async function start() { column = 'campaign_messages.created'; } - - - sqlQry = sqlQry.where(column, '<=', currentTs - trigger.seconds); - - if (trigger.last_check !== null) { - sqlQry = sqlQry.where(column, '>', trigger.last_check); - } } + sqlQry = sqlQry.where(column, '<=', new Date(currentTs - trigger.seconds)); + + if (trigger.last_check !== null) { + sqlQry = sqlQry.where(column, '>', trigger.last_check); + } const subscribers = await sqlQry; for (const subscriber of subscribers) { await tx('trigger_messages').insert({ - campaign: campaign.id, + trigger: trigger.id, list: cpgList.list, subscription: subscriber.id }); @@ -161,7 +165,7 @@ async function start() { } - await tx('triggers').update('last_check', currentTs).where('id', trigger.id); + await tx('triggers').update('last_check', new Date(currentTs)).where('id', trigger.id); return true; }); @@ -177,4 +181,11 @@ async function start() { } } +function start() { + log.info('Triggers', 'Starting trigger check service'); + run().catch(err => { + log.error('Triggers', err); + }); +} + module.exports.start = start; diff --git a/server/setup/knex/migrations/20170506102634_v1_to_v2.js b/server/setup/knex/migrations/20170506102634_v1_to_v2.js index 64126c4d..17e1e262 100644 --- a/server/setup/knex/migrations/20170506102634_v1_to_v2.js +++ b/server/setup/knex/migrations/20170506102634_v1_to_v2.js @@ -246,10 +246,20 @@ async function migrateSubscriptions(knex) { const fields = await knex('custom_fields').where('list', list.id); + const info = await knex('subscription__' + list.id).columnInfo(); for (const field of fields) { if (field.column != null) { await knex.schema.raw('ALTER TABLE `subscription__' + list.id + '` ADD `source_' + field.column +'` int(11) DEFAULT NULL'); } + + if (field.type === 'date' || field.type === 'birthday') { + // Fix the problem that commit bc73a0df0cab9943d726bd12fc1c6f2ff1279aa7 did not introduce migration that would convert TIMESTAMP columns to DATE + if (info[field.column].type === 'timestamp') { + await knex.schema.table('subscription__' + list.id, table => { + table.dateTime(field.column).alter(); + }); + } + } } let lastId = 0; @@ -644,6 +654,7 @@ async function migrateReports(knex) { await knex.schema.table('reports', table => { table.dropForeign('report_template', 'report_template_ibfk_1'); table.foreign('report_template').references('report_templates.id'); + table.timestamp('last_run').nullable().defaultTo(null).alter(); }); } @@ -1114,6 +1125,11 @@ async function migrateAttachments(knex) { async function migrateTriggers(knex) { await knex.schema.table('queued', table => { + table.dropPrimary(); + }); + + await knex.schema.table('queued', table => { + table.increments('id').first().primary(); table.renameColumn('subscriber', 'subscription'); table.renameColumn('source', 'trigger'); }); @@ -1174,7 +1190,7 @@ async function migrateImporter(knex) { table.text('settings', 'longtext'); table.integer('mapping_type').unsigned().notNullable(); table.text('mapping', 'longtext'); - table.dateTime('last_run'); + table.timestamp('last_run').nullable().defaultTo(null); table.text('error'); table.timestamp('created').defaultTo(knex.fn.now()); }); @@ -1190,7 +1206,7 @@ async function migrateImporter(knex) { table.integer('processed').defaultTo(0); table.text('error'); table.timestamp('created').defaultTo(knex.fn.now()); - table.dateTime('finished'); + table.timestamp('finished').nullable().defaultTo(null); }); await knex.schema.createTable('import_failed', table => { diff --git a/server/views/ckeditor/layout.hbs b/server/views/ckeditor/layout.hbs index 08c2bcc4..d7dc71c0 100644 --- a/server/views/ckeditor/layout.hbs +++ b/server/views/ckeditor/layout.hbs @@ -6,7 +6,7 @@ - + diff --git a/server/views/codeeditor/layout.hbs b/server/views/codeeditor/layout.hbs index 08c2bcc4..d7dc71c0 100644 --- a/server/views/codeeditor/layout.hbs +++ b/server/views/codeeditor/layout.hbs @@ -6,7 +6,7 @@ - + diff --git a/server/views/grapesjs/layout.hbs b/server/views/grapesjs/layout.hbs index d73461ec..d0375943 100644 --- a/server/views/grapesjs/layout.hbs +++ b/server/views/grapesjs/layout.hbs @@ -6,7 +6,7 @@ - + diff --git a/server/views/layout.hbs b/server/views/layout.hbs index 14e13420..7d735d22 100644 --- a/server/views/layout.hbs +++ b/server/views/layout.hbs @@ -6,7 +6,7 @@ - + diff --git a/server/views/mosaico/layout.hbs b/server/views/mosaico/layout.hbs index 203bd3f2..dd5a1d59 100644 --- a/server/views/mosaico/layout.hbs +++ b/server/views/mosaico/layout.hbs @@ -6,7 +6,7 @@ - + diff --git a/server/views/subscription/mail-already-subscribed-html.mjml.hbs b/server/views/subscription/mail-already-subscribed-html.mjml.hbs index 83e7eb1a..d551c00a 100644 --- a/server/views/subscription/mail-already-subscribed-html.mjml.hbs +++ b/server/views/subscription/mail-already-subscribed-html.mjml.hbs @@ -1,23 +1,23 @@ - {{#translate}}Email address already registered{{/translate}} + Email address already registered - {{#translate}}We have received a subscription request. Your email address is however already registered.{{/translate}}. + We have received a subscription request. Your email address is however already registered.. - {{#translate}}If you received this email by mistake, simply delete it. Your existing subscription won't be affected.{{/translate}} + If you received this email by mistake, simply delete it. Your existing subscription won't be affected. - {{#translate}}If you want to modify your subscription then you can {{/translate}} - {{#translate}}manage your preferences{{/translate}} {{#translate}}or{{/translate}} {{#translate}}unsubscribe here{{/translate}}. + If you want to modify your subscription then you can + manage your preferences or unsubscribe here. - {{#translate}}Return to our website{{/translate}} + Return to our website - {{#translate}}For questions about this list, please contact:{{/translate}} + For questions about this list, please contact:
{{contactAddress}}
diff --git a/server/views/subscription/mail-already-subscribed-text.hbs b/server/views/subscription/mail-already-subscribed-text.hbs index 5c273dfe..40600959 100644 --- a/server/views/subscription/mail-already-subscribed-text.hbs +++ b/server/views/subscription/mail-already-subscribed-text.hbs @@ -1,18 +1,18 @@ {{{title}}} -{{#translate}}Email address already registered{{/translate}} +Email address already registered ================================ -{{#translate}}We have received a subscription request. Your email address is however already registered.{{/translate}} +We have received a subscription request. Your email address is however already registered. -{{#translate}}If you received this email by mistake, simply delete it. Your existing subscription won't be affected.{{/translate}} +If you received this email by mistake, simply delete it. Your existing subscription won't be affected. -{{#translate}}If you want to modify your subscription then you can:{{/translate}} +If you want to modify your subscription then you can: -{{#translate}}manage your preferences{{/translate}}: {{preferencesUrl}} +manage your preferences: {{preferencesUrl}} -- {{#translate}}or{{/translate}} - +- or - -{{#translate}}unsubscribe here{{/translate}}: {{unsubscribeUrl}} +unsubscribe here: {{unsubscribeUrl}} -{{#translate}}For questions about this list, please contact:{{/translate}} +For questions about this list, please contact: {{{contactAddress}}} diff --git a/server/views/subscription/mail-confirm-address-change-html.mjml.hbs b/server/views/subscription/mail-confirm-address-change-html.mjml.hbs index a144fe33..7b52a4b2 100644 --- a/server/views/subscription/mail-confirm-address-change-html.mjml.hbs +++ b/server/views/subscription/mail-confirm-address-change-html.mjml.hbs @@ -1,16 +1,16 @@ - {{#translate}}Please Confirm Subscription Address Change{{/translate}} + Please Confirm Subscription Address Change - {{#translate}}Yes, subscribe this email address to the list{{/translate}} + Yes, subscribe this email address to the list - {{#translate}}If you received this email by mistake, simply delete it. You won't be subscribed if you don't click the confirmation link above.{{/translate}} + If you received this email by mistake, simply delete it. You won't be subscribed if you don't click the confirmation link above. - {{#translate}}For questions about this list, please contact:{{/translate}} + For questions about this list, please contact:
{{contactAddress}}
diff --git a/server/views/subscription/mail-confirm-address-change-text.hbs b/server/views/subscription/mail-confirm-address-change-text.hbs index baca9209..fa33d80e 100644 --- a/server/views/subscription/mail-confirm-address-change-text.hbs +++ b/server/views/subscription/mail-confirm-address-change-text.hbs @@ -1,10 +1,10 @@ {{{title}}} -{{#translate}}Please Confirm Subscription Address Change{{/translate}} +Please Confirm Subscription Address Change ========================================== -{{#translate}}Yes, subscribe this email address to the list{{/translate}}: {{{confirmUrl}}} +Yes, subscribe this email address to the list: {{{confirmUrl}}} -{{#translate}}If you received this email by mistake, simply delete it. You won't be subscribed unless you click the confirmation link above.{{/translate}} +If you received this email by mistake, simply delete it. You won't be subscribed unless you click the confirmation link above. -{{#translate}}For questions about this list, please contact:{{/translate}} +For questions about this list, please contact: {{{contactAddress}}} diff --git a/server/views/subscription/mail-confirm-subscription-html.mjml.hbs b/server/views/subscription/mail-confirm-subscription-html.mjml.hbs index d179d96a..96775ec3 100644 --- a/server/views/subscription/mail-confirm-subscription-html.mjml.hbs +++ b/server/views/subscription/mail-confirm-subscription-html.mjml.hbs @@ -1,16 +1,16 @@ - {{#translate}}Please Confirm Subscription{{/translate}} + Please Confirm Subscription - {{#translate}}Yes, subscribe me to this list{{/translate}} + Yes, subscribe me to this list - {{#translate}}If you received this email by mistake, simply delete it. You won't be subscribed if you don't click the confirmation link above.{{/translate}} + If you received this email by mistake, simply delete it. You won't be subscribed if you don't click the confirmation link above. - {{#translate}}For questions about this list, please contact:{{/translate}} + For questions about this list, please contact:
{{contactAddress}}
diff --git a/server/views/subscription/mail-confirm-subscription-text.hbs b/server/views/subscription/mail-confirm-subscription-text.hbs index 75b5a70a..74a4e69d 100644 --- a/server/views/subscription/mail-confirm-subscription-text.hbs +++ b/server/views/subscription/mail-confirm-subscription-text.hbs @@ -1,10 +1,10 @@ {{{title}}} -{{#translate}}Please Confirm Subscription{{/translate}} +Please Confirm Subscription =========================== -{{#translate}}Yes, subscribe me to this list{{/translate}}: {{{confirmUrl}}} +Yes, subscribe me to this list: {{{confirmUrl}}} -{{#translate}}If you received this email by mistake, simply delete it. You won't be subscribed unless you click the confirmation link above.{{/translate}} +If you received this email by mistake, simply delete it. You won't be subscribed unless you click the confirmation link above. -{{#translate}}For questions about this list, please contact:{{/translate}} +For questions about this list, please contact: {{{contactAddress}}} diff --git a/server/views/subscription/mail-confirm-unsubscription-html.mjml.hbs b/server/views/subscription/mail-confirm-unsubscription-html.mjml.hbs index 89f6bbce..fde17676 100644 --- a/server/views/subscription/mail-confirm-unsubscription-html.mjml.hbs +++ b/server/views/subscription/mail-confirm-unsubscription-html.mjml.hbs @@ -1,16 +1,16 @@ - {{#translate}}Please Confirm Unsubscription{{/translate}} + Please Confirm Unsubscription - {{#translate}}Yes, unsubscribe me from this list{{/translate}} + Yes, unsubscribe me from this list - {{#translate}}If you received this email by mistake, simply delete it. You won't be unsubscribed if you don't click the confirmation link above.{{/translate}} + If you received this email by mistake, simply delete it. You won't be unsubscribed if you don't click the confirmation link above. - {{#translate}}For questions about this list, please contact:{{/translate}} + For questions about this list, please contact:
{{contactAddress}}
diff --git a/server/views/subscription/mail-confirm-unsubscription-text.hbs b/server/views/subscription/mail-confirm-unsubscription-text.hbs index 2dd2dd4f..381873f1 100644 --- a/server/views/subscription/mail-confirm-unsubscription-text.hbs +++ b/server/views/subscription/mail-confirm-unsubscription-text.hbs @@ -1,10 +1,10 @@ {{{title}}} -{{#translate}}Please Confirm Subscription{{/translate}} +Please Confirm Subscription =========================== -{{#translate}}Yes, unsubscribe me from this list{{/translate}}: {{{confirmUrl}}} +Yes, unsubscribe me from this list: {{{confirmUrl}}} -{{#translate}}If you received this email by mistake, simply delete it. You won't be unsubscribed unless you click the confirmation link above.{{/translate}} +If you received this email by mistake, simply delete it. You won't be unsubscribed unless you click the confirmation link above. -{{#translate}}For questions about this list, please contact:{{/translate}} +For questions about this list, please contact: {{{contactAddress}}} diff --git a/server/views/subscription/mail-subscription-confirmed-html.mjml.hbs b/server/views/subscription/mail-subscription-confirmed-html.mjml.hbs index a2aeb4e4..e781994d 100644 --- a/server/views/subscription/mail-subscription-confirmed-html.mjml.hbs +++ b/server/views/subscription/mail-subscription-confirmed-html.mjml.hbs @@ -1,17 +1,17 @@ - {{#translate}}Subscription Confirmed{{/translate}} + Subscription Confirmed - {{#translate}}Your subscription to our list has been confirmed{{/translate}}. {{#translate}}If you want to modify your subscription then you can {{/translate}} - {{#translate}}manage your preferences{{/translate}} {{#translate}}or{{/translate}} {{#translate}}unsubscribe here{{/translate}}. + Your subscription to our list has been confirmed. If you want to modify your subscription then you can + manage your preferences or unsubscribe here. - {{#translate}}Return to our website{{/translate}} + Return to our website - {{#translate}}For questions about this list, please contact:{{/translate}} + For questions about this list, please contact:
{{contactAddress}}
diff --git a/server/views/subscription/mail-subscription-confirmed-text.hbs b/server/views/subscription/mail-subscription-confirmed-text.hbs index bc500178..07d4e614 100644 --- a/server/views/subscription/mail-subscription-confirmed-text.hbs +++ b/server/views/subscription/mail-subscription-confirmed-text.hbs @@ -1,16 +1,16 @@ {{{title}}} -{{#translate}}Subscription Confirmed{{/translate}} +Subscription Confirmed ====================== -{{#translate}}Your subscription to our list has been confirmed.{{/translate}} +Your subscription to our list has been confirmed. -{{#translate}}If you want to modify your subscription then you can:{{/translate}} +If you want to modify your subscription then you can: -{{#translate}}manage your preferences{{/translate}}: {{preferencesUrl}} +manage your preferences: {{preferencesUrl}} -- {{#translate}}or{{/translate}} - +- or - -{{#translate}}unsubscribe here{{/translate}}: {{unsubscribeUrl}} +unsubscribe here: {{unsubscribeUrl}} -{{#translate}}For questions about this list, please contact:{{/translate}} +For questions about this list, please contact: {{{contactAddress}}} diff --git a/server/views/subscription/mail-unsubscription-confirmed-html.mjml.hbs b/server/views/subscription/mail-unsubscription-confirmed-html.mjml.hbs index 48a3782d..748b7006 100644 --- a/server/views/subscription/mail-unsubscription-confirmed-html.mjml.hbs +++ b/server/views/subscription/mail-unsubscription-confirmed-html.mjml.hbs @@ -1,16 +1,16 @@ - {{#translate}}You Are Now Unsubscribed{{/translate}} + You Are Now Unsubscribed - {{#translate}}We have removed your email address from our list{{/translate}}. {{#translate}}If you unsubscribed by mistake, you can re-subscribe at:{{/translate}} + We have removed your email address from our list. If you unsubscribed by mistake, you can re-subscribe at: - {{#translate}}Subscribe{{/translate}} + Subscribe - {{#translate}}For questions about this list, please contact:{{/translate}} + For questions about this list, please contact:
{{contactAddress}}
diff --git a/server/views/subscription/mail-unsubscription-confirmed-text.hbs b/server/views/subscription/mail-unsubscription-confirmed-text.hbs index 0afbe1d7..f7745b12 100644 --- a/server/views/subscription/mail-unsubscription-confirmed-text.hbs +++ b/server/views/subscription/mail-unsubscription-confirmed-text.hbs @@ -1,12 +1,12 @@ {{{title}}} -{{#translate}}You Are Now Unsubscribed{{/translate}} +You Are Now Unsubscribed ======================== -{{#translate}}We have removed your email address from our list.{{/translate}} +We have removed your email address from our list. -{{#translate}}If you unsubscribed by mistake, you can re-subscribe at:{{/translate}} +If you unsubscribed by mistake, you can re-subscribe at: -{{#translate}}Subscribe{{/translate}}: {{subscribeUrl}} +Subscribe: {{subscribeUrl}} -{{#translate}}For questions about this list, please contact:{{/translate}} +For questions about this list, please contact: {{{contactAddress}}} diff --git a/server/views/subscription/partials/subscription-custom-fields.hbs b/server/views/subscription/partials/subscription-custom-fields.hbs index d61c4a74..81951307 100644 --- a/server/views/subscription/partials/subscription-custom-fields.hbs +++ b/server/views/subscription/partials/subscription-custom-fields.hbs @@ -2,11 +2,11 @@ {{#if typeSubscriptionEmail}}
\n' + - ' {{#translate}}Country{{/translate}}\n' + + ' Country\n' + ' \n' + - ' {{#translate}}Opened{{/translate}}\n' + + ' Opened\n' + ' \n' + - ' {{#translate}}All{{/translate}}\n' + + ' All\n' + ' \n' + - ' {{#translate}}Percentage{{/translate}}\n' + + ' Percentage\n' + '