- Refactoring of the mail sending part. Mail queue (table 'queued') is now used also for all test emails.
- More options how to send test emails. - Fixed problems with pausing a campaign (#593) - Started rework of transactional sender of templates (#606), however this contains functionality regression at the moment because it does not interpret templates as HBS. It needs HBS option for templates as described in https://github.com/Mailtrain-org/mailtrain/issues/611#issuecomment-502345227 TODO: - detect sending errors connected to not able to contact the mailer and pause/retry campaing and queued sending - don't mark the recipients as BOUNCED - add FAILED campaign state and fall into it if sending to campaign consistently fails (i.e. the error with sending is not temporary) - if the same happends for queued email, delete the message
This commit is contained in:
parent
ff66a6c39e
commit
30b361290b
42 changed files with 1366 additions and 786 deletions
|
@ -260,9 +260,9 @@ roles:
|
|||
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createCampaign, manageUsers]
|
||||
children:
|
||||
sendConfiguration: [viewPublic, viewPrivate, edit, delete, share, sendWithoutOverrides, sendWithAllowedOverrides, sendWithAnyOverrides]
|
||||
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports]
|
||||
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
|
||||
customForm: [view, edit, delete, share]
|
||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, viewStats, fetchRss]
|
||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
|
||||
template: [view, edit, delete, share, viewFiles, manageFiles]
|
||||
report: [view, edit, delete, share, execute, viewContent, viewOutput]
|
||||
reportTemplate: [view, edit, delete, share, execute]
|
||||
|
@ -275,9 +275,9 @@ roles:
|
|||
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createTemplate, createMosaicoTemplate, createCampaign]
|
||||
children:
|
||||
sendConfiguration: [viewPublic, sendWithoutOverrides, sendWithAllowedOverrides]
|
||||
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports]
|
||||
list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
|
||||
customForm: [view, edit, delete, share]
|
||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, viewStats, fetchRss]
|
||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
|
||||
template: [view, edit, delete, share, viewFiles, manageFiles]
|
||||
report: [view, edit, delete, share, execute, viewContent, viewOutput]
|
||||
reportTemplate: [view, share, execute]
|
||||
|
@ -286,11 +286,11 @@ roles:
|
|||
|
||||
campaignsCreator:
|
||||
name: Campaigns Creator
|
||||
description: In the respective namespace, the user has all permissions for templates and campaigns.
|
||||
description: In the respective namespace, the user has all permissions to create and manage templates and campaigns.
|
||||
permissions: [view, createTemplate, createCampaign]
|
||||
children:
|
||||
sendConfiguration: [viewPublic]
|
||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, fetchRss]
|
||||
campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, sendToTestUsers, fetchRss]
|
||||
template: [view, edit, delete, share, viewFiles, manageFiles]
|
||||
|
||||
sendConfiguration:
|
||||
|
@ -307,11 +307,11 @@ roles:
|
|||
master:
|
||||
name: Master
|
||||
description: All permissions
|
||||
permissions: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports]
|
||||
permissions: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
|
||||
campaignsCreator:
|
||||
name: Campaigns Creator
|
||||
description: The user can only use the list in setting up a campaign. However, this gives no permission to view subscriptions or to send to the list.
|
||||
permissions: [view, viewFields, viewSegments]
|
||||
description: The user can only use the list in setting up a campaign and to send email to test users. This gives no permission to view subscriptions or to send to the whole list.
|
||||
permissions: [view, viewFields, viewSegments, sendToTestUsers]
|
||||
|
||||
customForm:
|
||||
master:
|
||||
|
@ -323,11 +323,11 @@ roles:
|
|||
master:
|
||||
name: Master
|
||||
description: All permissions
|
||||
permissions: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, viewStats, manageMessages, fetchRss]
|
||||
permissions: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, manageMessages, fetchRss]
|
||||
campaignsCreator:
|
||||
name: Campaigns Creator
|
||||
description: The user can setup the campaign but cannot send it.
|
||||
permissions: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, fetchRss]
|
||||
permissions: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, sendToTestUsers, fetchRss]
|
||||
rssTrigger:
|
||||
name: RSS Campaign Trigger
|
||||
description: Allows triggering a fetch of an RSS campaign
|
||||
|
|
|
@ -21,215 +21,163 @@ const htmlToText = require('html-to-text');
|
|||
const {getPublicUrl} = require('./urls');
|
||||
const blacklist = require('../models/blacklist');
|
||||
const libmime = require('libmime');
|
||||
const shares = require('../models/shares');
|
||||
const { enforce } = require('./helpers');
|
||||
|
||||
const MessageType = {
|
||||
REGULAR: 0,
|
||||
TRIGGERED: 1,
|
||||
TEST: 2
|
||||
};
|
||||
|
||||
class CampaignSender {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
static async testSend(context, listCid, subscriptionCid, campaignId, sendConfigurationId, html, text) {
|
||||
let sendConfiguration, list, fieldsGrouped, campaign, subscriptionGrouped, useVerp, useVerpSenderHeader, mergeTags, attachments;
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), sendConfigurationId, false, true);
|
||||
list = await lists.getByCidTx(tx, context, listCid);
|
||||
fieldsGrouped = await fields.listGroupedTx(tx, list.id);
|
||||
|
||||
useVerp = config.verp.enabled && sendConfiguration.verp_hostname;
|
||||
useVerpSenderHeader = useVerp && !sendConfiguration.verp_disable_sender_header;
|
||||
|
||||
subscriptionGrouped = await subscriptions.getByCid(context, list.id, subscriptionCid);
|
||||
mergeTags = fields.getMergeTags(fieldsGrouped, subscriptionGrouped);
|
||||
|
||||
if (campaignId) {
|
||||
campaign = await campaigns.getByIdTx(tx, context, campaignId, false, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
|
||||
await campaigns.enforceSendPermissionTx(tx, context, campaign);
|
||||
} else {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', sendConfigurationId, 'sendWithoutOverrides');
|
||||
|
||||
// This is to fake the campaign for getMessageLinks, which is called inside formatMessage
|
||||
campaign = {
|
||||
cid: '[CAMPAIGN_ID]'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const encryptionKeys = [];
|
||||
for (const fld of fieldsGrouped) {
|
||||
if (fld.type === 'gpg' && mergeTags[fld.key]) {
|
||||
encryptionKeys.push(mergeTags[fld.key].trim());
|
||||
}
|
||||
}
|
||||
|
||||
attachments = [];
|
||||
// replace data: images with embedded attachments
|
||||
html = html.replace(/(<img\b[^>]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
|
||||
const cid = shortid.generate() + '-attachments';
|
||||
attachments.push({
|
||||
path: dataUri,
|
||||
cid
|
||||
});
|
||||
return prefix + 'cid:' + cid;
|
||||
});
|
||||
|
||||
html = tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, html, true);
|
||||
|
||||
text = (text || '').trim()
|
||||
? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, text)
|
||||
: htmlToText.fromString(html, {wordwrap: 130});
|
||||
|
||||
|
||||
const mailer = await mailers.getOrCreateMailer(sendConfiguration.id);
|
||||
|
||||
const getOverridable = key => {
|
||||
return sendConfiguration[key];
|
||||
};
|
||||
|
||||
const campaignAddress = [campaign.cid, list.cid, subscriptionGrouped.cid].join('.');
|
||||
|
||||
const mail = {
|
||||
from: {
|
||||
name: getOverridable('from_name'),
|
||||
address: getOverridable('from_email')
|
||||
},
|
||||
replyTo: getOverridable('reply_to'),
|
||||
xMailer: sendConfiguration.x_mailer ? sendConfiguration.x_mailer : false,
|
||||
to: {
|
||||
name: list.to_name === null ? undefined : tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, list.to_name, false),
|
||||
address: subscriptionGrouped.email
|
||||
},
|
||||
sender: useVerpSenderHeader ? campaignAddress + '@' + sendConfiguration.verp_hostname : false,
|
||||
|
||||
envelope: useVerp ? {
|
||||
from: campaignAddress + '@' + sendConfiguration.verp_hostname,
|
||||
to: subscriptionGrouped.email
|
||||
} : false,
|
||||
|
||||
headers: {
|
||||
'x-fbl': campaignAddress,
|
||||
// custom header for SparkPost
|
||||
'x-msys-api': JSON.stringify({
|
||||
campaign_id: campaignAddress
|
||||
}),
|
||||
// custom header for SendGrid
|
||||
'x-smtpapi': JSON.stringify({
|
||||
unique_args: {
|
||||
campaign_id: campaignAddress
|
||||
}
|
||||
}),
|
||||
// custom header for Mailgun
|
||||
'x-mailgun-variables': JSON.stringify({
|
||||
campaign_id: campaignAddress
|
||||
}),
|
||||
'List-ID': {
|
||||
prepared: true,
|
||||
value: libmime.encodeWords(list.name) + ' <' + list.cid + '.' + getPublicUrl() + '>'
|
||||
}
|
||||
},
|
||||
list: {
|
||||
unsubscribe: null
|
||||
},
|
||||
subject: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, getOverridable('subject'), false),
|
||||
html,
|
||||
text,
|
||||
|
||||
attachments,
|
||||
encryptionKeys
|
||||
};
|
||||
|
||||
|
||||
let response;
|
||||
try {
|
||||
const info = await mailer.sendMassMail(mail);
|
||||
response = info.response || info.messageId;
|
||||
} catch (err) {
|
||||
response = err.response || err.message;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
async init(settings) {
|
||||
/*
|
||||
settings is one of:
|
||||
- campaignCid / campaignId
|
||||
or
|
||||
- sendConfiguration, listId, attachments, html, text, subject
|
||||
*/
|
||||
async _init(settings) {
|
||||
this.listsById = new Map(); // listId -> list
|
||||
this.listsByCid = new Map(); // listCid -> list
|
||||
this.listsFieldsGrouped = new Map(); // listId -> fieldsGrouped
|
||||
this.attachments = [];
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
if (settings.campaignCid) {
|
||||
this.campaign = await campaigns.rawGetByTx(tx, 'cid', settings.campaignCid);
|
||||
} else {
|
||||
|
||||
} else if (settings.campaignId) {
|
||||
this.campaign = await campaigns.rawGetByTx(tx, 'id', settings.campaignId);
|
||||
|
||||
} else {
|
||||
// We are not within scope of a campaign (i.e. templates in MessageType.TEST message)
|
||||
// This is to fake the campaign for getMessageLinks, which is called inside formatMessage
|
||||
this.campaign = {
|
||||
cid: '[CAMPAIGN_ID]',
|
||||
from_name_override: null,
|
||||
from_email_override: null,
|
||||
reply_to_override: null
|
||||
};
|
||||
}
|
||||
|
||||
const campaign = this.campaign;
|
||||
|
||||
this.sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), campaign.send_configuration, false, true);
|
||||
|
||||
for (const listSpec of campaign.lists) {
|
||||
const list = await lists.getByIdTx(tx, contextHelpers.getAdminContext(), listSpec.list);
|
||||
this.listsById.set(list.id, list);
|
||||
this.listsByCid.set(list.cid, list);
|
||||
this.listsFieldsGrouped.set(list.id, await fields.listGroupedTx(tx, list.id));
|
||||
}
|
||||
|
||||
if (campaign.source === CampaignSource.TEMPLATE) {
|
||||
this.template = await templates.getByIdTx(tx, contextHelpers.getAdminContext(), campaign.data.sourceTemplate, false);
|
||||
}
|
||||
|
||||
const attachments = await files.listTx(tx, contextHelpers.getAdminContext(), 'campaign', 'attachment', campaign.id);
|
||||
for (const attachment of attachments) {
|
||||
this.attachments.push({
|
||||
filename: attachment.originalname,
|
||||
path: files.getFilePath('campaign', 'attachment', campaign.id, attachment.filename)
|
||||
});
|
||||
if (settings.sendConfigurationId) {
|
||||
this.sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), settings.sendConfigurationId, false, true);
|
||||
} else if (this.campaign.send_configuration) {
|
||||
this.sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), this.campaign.send_configuration, false, true);
|
||||
} else {
|
||||
enforce(false);
|
||||
}
|
||||
|
||||
this.useVerp = config.verp.enabled && this.sendConfiguration.verp_hostname;
|
||||
this.useVerpSenderHeader = this.useVerp && !this.sendConfiguration.verp_disable_sender_header;
|
||||
|
||||
if (settings.listId) {
|
||||
const list = await lists.getByIdTx(tx, contextHelpers.getAdminContext(), settings.listId);
|
||||
this.listsById.set(list.id, list);
|
||||
this.listsByCid.set(list.cid, list);
|
||||
this.listsFieldsGrouped.set(list.id, await fields.listGroupedTx(tx, list.id));
|
||||
|
||||
} else if (this.campaign.lists) {
|
||||
for (const listSpec of this.campaign.lists) {
|
||||
const list = await lists.getByIdTx(tx, contextHelpers.getAdminContext(), listSpec.list);
|
||||
this.listsById.set(list.id, list);
|
||||
this.listsByCid.set(list.cid, list);
|
||||
this.listsFieldsGrouped.set(list.id, await fields.listGroupedTx(tx, list.id));
|
||||
}
|
||||
|
||||
} else {
|
||||
enforce(false);
|
||||
}
|
||||
|
||||
if (settings.attachments) {
|
||||
this.attachments = settings.attachments;
|
||||
|
||||
} else if (this.campaign.id) {
|
||||
const attachments = await files.listTx(tx, contextHelpers.getAdminContext(), 'campaign', 'attachment', this.campaign.id);
|
||||
|
||||
this.attachments = [];
|
||||
for (const attachment of attachments) {
|
||||
this.attachments.push({
|
||||
filename: attachment.originalname,
|
||||
path: files.getFilePath('campaign', 'attachment', this.campaign.id, attachment.filename)
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
this.attachments = [];
|
||||
}
|
||||
|
||||
if (settings.html !== undefined) {
|
||||
this.html = settings.html;
|
||||
this.text = settings.text;
|
||||
} else if (this.campaign.source === CampaignSource.TEMPLATE) {
|
||||
this.template = await templates.getByIdTx(tx, contextHelpers.getAdminContext(), this.campaign.data.sourceTemplate, false);
|
||||
}
|
||||
|
||||
if (settings.subject !== undefined) {
|
||||
this.subject = settings.subject;
|
||||
} else if (this.campaign.subject !== undefined) {
|
||||
this.subject = this.campaign.subject;
|
||||
} else {
|
||||
enforce(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async _getMessage(campaign, list, subscriptionGrouped, mergeTags, replaceDataImgs) {
|
||||
async _getMessage(list, subscriptionGrouped, mergeTags, replaceDataImgs) {
|
||||
let html = '';
|
||||
let text = '';
|
||||
let renderTags = false;
|
||||
const campaign = this.campaign;
|
||||
|
||||
if (campaign.source === CampaignSource.URL) {
|
||||
const form = tools.getMessageLinks(campaign, list, subscriptionGrouped);
|
||||
for (const key in mergeTags) {
|
||||
form[key] = mergeTags[key];
|
||||
}
|
||||
|
||||
const response = await request.post({
|
||||
uri: campaign.sourceUrl,
|
||||
form,
|
||||
resolveWithFullResponse: true
|
||||
});
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(`Received status code ${httpResponse.statusCode} from ${campaign.sourceUrl}`);
|
||||
}
|
||||
|
||||
html = response.body;
|
||||
text = '';
|
||||
if (this.renderedHtml !== undefined) {
|
||||
html = this.renderedHtml;
|
||||
text = this.renderedText;
|
||||
renderTags = false;
|
||||
|
||||
} else if (campaign.source === CampaignSource.CUSTOM || campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN || campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||
html = campaign.data.sourceCustom.html;
|
||||
text = campaign.data.sourceCustom.text;
|
||||
} else if (this.html !== undefined) {
|
||||
html = this.html;
|
||||
text = this.text;
|
||||
renderTags = true;
|
||||
|
||||
} else if (campaign.source === CampaignSource.TEMPLATE) {
|
||||
const template = this.template;
|
||||
html = template.html;
|
||||
text = template.text;
|
||||
renderTags = true;
|
||||
} else {
|
||||
if (campaign.source === CampaignSource.URL) {
|
||||
const form = tools.getMessageLinks(campaign, list, subscriptionGrouped);
|
||||
for (const key in mergeTags) {
|
||||
form[key] = mergeTags[key];
|
||||
}
|
||||
|
||||
const response = await request.post({
|
||||
uri: campaign.sourceUrl,
|
||||
form,
|
||||
resolveWithFullResponse: true
|
||||
});
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(`Received status code ${httpResponse.statusCode} from ${campaign.sourceUrl}`);
|
||||
}
|
||||
|
||||
html = response.body;
|
||||
text = '';
|
||||
renderTags = false;
|
||||
|
||||
} else if (campaign.source === CampaignSource.CUSTOM || campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN || campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
|
||||
html = campaign.data.sourceCustom.html;
|
||||
text = campaign.data.sourceCustom.text;
|
||||
renderTags = true;
|
||||
|
||||
} else if (campaign.source === CampaignSource.TEMPLATE) {
|
||||
const template = this.template;
|
||||
html = template.html;
|
||||
text = template.text;
|
||||
renderTags = true;
|
||||
}
|
||||
|
||||
html = await links.updateLinks(campaign, list, subscriptionGrouped, mergeTags, html);
|
||||
}
|
||||
|
||||
html = await links.updateLinks(campaign, list, subscriptionGrouped, mergeTags, html);
|
||||
|
||||
const attachments = this.attachments.slice();
|
||||
if (replaceDataImgs) {
|
||||
// replace data: images with embedded attachments
|
||||
|
@ -273,6 +221,14 @@ class CampaignSender {
|
|||
return tags;
|
||||
}
|
||||
|
||||
async initByCampaignCid(campaignCid) {
|
||||
await this._init({campaignCid});
|
||||
}
|
||||
|
||||
async initByCampaignId(campaignId) {
|
||||
await this._init({campaignId});
|
||||
}
|
||||
|
||||
async getMessage(listCid, subscriptionCid) {
|
||||
const list = this.listsByCid.get(listCid);
|
||||
const subscriptionGrouped = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid);
|
||||
|
@ -280,20 +236,36 @@ class CampaignSender {
|
|||
const campaign = this.campaign;
|
||||
const mergeTags = fields.getMergeTags(flds, subscriptionGrouped, this._getExtraTags(campaign));
|
||||
|
||||
return await this._getMessage(campaign, list, subscriptionGrouped, mergeTags, false);
|
||||
return await this._getMessage(list, subscriptionGrouped, mergeTags, false);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
/*
|
||||
subData is one of:
|
||||
- queuedMessage
|
||||
or
|
||||
- email, listId
|
||||
*/
|
||||
async _sendMessage(subData) {
|
||||
let msgType;
|
||||
let subscriptionGrouped;
|
||||
let listId;
|
||||
|
||||
if (subData.queuedMessage) {
|
||||
const queuedMessage = subData.queuedMessage;
|
||||
msgType = queuedMessage.type;
|
||||
listId = queuedMessage.list;
|
||||
subscriptionGrouped = await subscriptions.getById(contextHelpers.getAdminContext(), listId, queuedMessage.subscription);
|
||||
|
||||
} else {
|
||||
enforce(subData.email);
|
||||
enforce(subData.listId);
|
||||
|
||||
msgType = MessageType.REGULAR;
|
||||
listId = subData.listId;
|
||||
subscriptionGrouped = await subscriptions.getByEmail(contextHelpers.getAdminContext(), listId, subData.email);
|
||||
}
|
||||
|
||||
async _sendMessage(listId, subscriptionGrouped) {
|
||||
const email = subscriptionGrouped.email;
|
||||
|
||||
if (await blacklist.isBlacklisted(email)) {
|
||||
|
@ -315,7 +287,7 @@ class CampaignSender {
|
|||
|
||||
const sendConfiguration = this.sendConfiguration;
|
||||
|
||||
const {html, text, attachments} = await this._getMessage(campaign, list, subscriptionGrouped, mergeTags, true);
|
||||
const {html, text, attachments} = await this._getMessage(list, subscriptionGrouped, mergeTags, true);
|
||||
|
||||
const campaignAddress = [campaign.cid, list.cid, subscriptionGrouped.cid].join('.');
|
||||
|
||||
|
@ -380,7 +352,7 @@ class CampaignSender {
|
|||
list: {
|
||||
unsubscribe: listUnsubscribe
|
||||
},
|
||||
subject: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, getOverridable('subject'), false),
|
||||
subject: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, this.subject, false),
|
||||
html,
|
||||
text,
|
||||
|
||||
|
@ -435,17 +407,47 @@ class CampaignSender {
|
|||
}
|
||||
|
||||
|
||||
await knex('campaigns').where('id', campaign.id).increment('delivered');
|
||||
if (msgType === MessageType.REGULAR || msgType === MessageType.TRIGGERED) {
|
||||
await knex('campaigns').where('id', campaign.id).increment('delivered');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
||||
/*
|
||||
{ Error: connect ECONNREFUSED 127.0.0.1:55871
|
||||
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1097:14)
|
||||
cause:
|
||||
{ Error: connect ECONNREFUSED 127.0.0.1:55871
|
||||
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1097:14)
|
||||
stack:
|
||||
'Error: connect ECONNREFUSED 127.0.0.1:55871\n at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1097:14)',
|
||||
errno: 'ECONNREFUSED',
|
||||
code: 'ECONNECTION',
|
||||
syscall: 'connect',
|
||||
address: '127.0.0.1',
|
||||
port: 55871,
|
||||
command: 'CONN' },
|
||||
isOperational: true,
|
||||
errno: 'ECONNREFUSED',
|
||||
code: 'ECONNECTION',
|
||||
syscall: 'connect',
|
||||
address: '127.0.0.1',
|
||||
port: 55871,
|
||||
command: 'CONN' }
|
||||
|
||||
*/
|
||||
|
||||
status = SubscriptionStatus.BOUNCED;
|
||||
response = err.response || err.message;
|
||||
await knex('campaigns').where('id', campaign.id).increment('delivered').increment('bounced');
|
||||
if (msgType === MessageType.REGULAR || msgType === MessageType.TRIGGERED) {
|
||||
await knex('campaigns').where('id', campaign.id).increment('delivered').increment('bounced');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const now = new Date();
|
||||
|
||||
if (campaign.type === CampaignType.REGULAR || campaign.type === CampaignType.RSS_ENTRY) {
|
||||
if (msgType === MessageType.REGULAR) {
|
||||
await knex('campaign_messages').insert({
|
||||
campaign: this.campaign.id,
|
||||
list: list.id,
|
||||
|
@ -457,16 +459,64 @@ class CampaignSender {
|
|||
updated: now
|
||||
});
|
||||
|
||||
} else if (campaign.type = CampaignType.TRIGGERED) {
|
||||
} else if (msgType === MessageType.TRIGGERED || msgType === MessageType.TEST) {
|
||||
if (subData.queuedMessage.data.attachments) {
|
||||
for (const attachment of subData.queuedMessage.data.attachments) {
|
||||
try {
|
||||
// We ignore any errors here because we already sent the message. Thus we have to mark it as completed to avoid sending it again.
|
||||
await knex.transaction(async tx => {
|
||||
await files.unlockTx(tx, 'campaign', 'attachment', attachment.id);
|
||||
});
|
||||
} catch (err) {
|
||||
log.error('CampaignSender', `Error when unlocking attachment ${attachment.id} for ${listId}:${subscriptionGrouped.email} (queuedId: ${subData.queuedMessage.id})`);
|
||||
log.verbose(err.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await knex('queued')
|
||||
.where({
|
||||
campaign: this.campaign.id,
|
||||
list: list.id,
|
||||
subscription: subscriptionGrouped.id
|
||||
})
|
||||
.where({id: subData.queuedMessage.id})
|
||||
.del();
|
||||
}
|
||||
}
|
||||
|
||||
async sendRegularMessage(listId, email) {
|
||||
await this._sendMessage({listId, email});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CampaignSender;
|
||||
CampaignSender.sendQueuedMessage = async (queuedMessage) => {
|
||||
const msgData = queuedMessage.data;
|
||||
|
||||
const cs = new CampaignSender();
|
||||
await cs._init({
|
||||
campaignId: msgData.campaignId,
|
||||
listId: queuedMessage.list,
|
||||
sendConfigurationId: queuedMessage.send_configuration,
|
||||
attachments: msgData.attachments,
|
||||
html: msgData.html,
|
||||
text: msgData.text,
|
||||
subject: msgData.subject
|
||||
});
|
||||
|
||||
await cs._sendMessage({queuedMessage});
|
||||
};
|
||||
|
||||
CampaignSender.queueMessageTx = async (tx, sendConfigurationId, listId, subscriptionId, messageType, messageData) => {
|
||||
if (messageData.attachments) {
|
||||
for (const attachment of messageData.attachments) {
|
||||
await files.lockTx(tx,'campaign', 'attachment', attachment.id);
|
||||
}
|
||||
}
|
||||
|
||||
await tx('queued').insert({
|
||||
send_configuration: sendConfigurationId,
|
||||
list: listId,
|
||||
subscription: subscriptionId,
|
||||
type: messageType,
|
||||
data: JSON.stringify(messageData)
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.CampaignSender = CampaignSender;
|
||||
module.exports.MessageType = MessageType;
|
|
@ -4,7 +4,6 @@ const knex = require('./knex');
|
|||
const interoperableErrors = require('../../shared/interoperable-errors');
|
||||
const entitySettings = require('./entity-settings');
|
||||
const shares = require('../models/shares');
|
||||
const { enforce } = require('./helpers');
|
||||
|
||||
const defaultNoOfDependenciesReported = 20;
|
||||
|
||||
|
@ -21,7 +20,7 @@ async function ensureNoDependencies(tx, context, id, depSpecs) {
|
|||
if (depSpec.query) {
|
||||
rows = await depSpec.query(tx).limit(defaultNoOfDependenciesReported + 1);
|
||||
} else if (depSpec.column) {
|
||||
rows = await tx(entityType.entitiesTable).where(depSpec.column, id).select(['id', 'name']).limit(defaultNoOfDependenciesReported + 1);
|
||||
rows = await tx(entityType.entitiesTable).where(depSpec.column, id).forShare().select(['id', 'name']).limit(defaultNoOfDependenciesReported + 1);
|
||||
} else if (depSpec.rows) {
|
||||
rows = await depSpec.rows(tx, defaultNoOfDependenciesReported + 1)
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ const entityTypes = {
|
|||
},
|
||||
attachment: {
|
||||
table: 'files_campaign_attachment',
|
||||
inUseTable: 'files_campaign_attachment_usage',
|
||||
permissions: {
|
||||
view: 'viewAttachments',
|
||||
manage: 'manageAttachments'
|
||||
|
|
|
@ -28,11 +28,11 @@ function filterObject(obj, allowedKeys) {
|
|||
return result;
|
||||
}
|
||||
|
||||
function castToInteger(id) {
|
||||
function castToInteger(id, msg) {
|
||||
const val = parseInt(id);
|
||||
|
||||
if (!Number.isInteger(val)) {
|
||||
throw new Error('Invalid id');
|
||||
throw new Error(msg || 'Invalid id');
|
||||
}
|
||||
|
||||
return val;
|
||||
|
|
|
@ -94,14 +94,18 @@ async function _sendMail(transport, mail, template) {
|
|||
return await trySendAsync();
|
||||
}
|
||||
|
||||
async function _sendTransactionalMail(transport, mail, template) {
|
||||
const sendConfiguration = transport.mailer.sendConfiguration;
|
||||
|
||||
async function _sendTransactionalMail(transport, mail) {
|
||||
if (!mail.headers) {
|
||||
mail.headers = {};
|
||||
}
|
||||
mail.headers['X-Sending-Zone'] = 'transactional';
|
||||
|
||||
return await _sendMail(transport, mail);
|
||||
}
|
||||
|
||||
async function _sendTransactionalMailBasedOnTemplate(transport, mail, template) {
|
||||
const sendConfiguration = transport.mailer.sendConfiguration;
|
||||
|
||||
mail.from = {
|
||||
name: sendConfiguration.from_name,
|
||||
address: sendConfiguration.from_email
|
||||
|
@ -129,7 +133,7 @@ async function _sendTransactionalMail(transport, mail, template) {
|
|||
});
|
||||
}
|
||||
|
||||
return await _sendMail(transport, mail);
|
||||
return await _sendTransactionalMail(transport, mail);
|
||||
}
|
||||
|
||||
async function _createTransport(sendConfiguration) {
|
||||
|
@ -263,7 +267,8 @@ async function _createTransport(sendConfiguration) {
|
|||
transport.mailer = {
|
||||
sendConfiguration,
|
||||
throttleWait: bluebird.promisify(throttleWait),
|
||||
sendTransactionalMail: async (mail, template) => await _sendTransactionalMail(transport, mail, template),
|
||||
sendTransactionalMail: async (mail, template) => await _sendTransactionalMail(transport, mail),
|
||||
sendTransactionalMailBasedOnTemplate: async (mail, template) => await _sendTransactionalMailBasedOnTemplate(transport, mail, template),
|
||||
sendMassMail: async (mail, template) => await _sendMail(transport, mail)
|
||||
};
|
||||
|
||||
|
|
|
@ -139,7 +139,7 @@ async function _sendMail(list, email, template, locale, subjectKey, relativeUrls
|
|||
try {
|
||||
if (list.send_configuration) {
|
||||
const mailer = await mailers.getOrCreateMailer(list.send_configuration);
|
||||
await mailer.sendTransactionalMail({
|
||||
await mailer.sendTransactionalMailBasedOnTemplate({
|
||||
to: {
|
||||
name: getDisplayName(flds, subscription),
|
||||
address: email
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const mailers = require('./mailers');
|
||||
const tools = require('./tools');
|
||||
const templates = require('../models/templates');
|
||||
const { getMergeTagsForBases } = require('../../shared/templates');
|
||||
const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
|
||||
|
||||
class TemplateSender {
|
||||
constructor(options) {
|
||||
this.defaultOptions = {
|
||||
maxMails: 100,
|
||||
...options
|
||||
};
|
||||
}
|
||||
|
||||
async send(params) {
|
||||
const options = { ...this.defaultOptions, ...params };
|
||||
this._validateMailOptions(options);
|
||||
|
||||
const [mailer, template] = await Promise.all([
|
||||
mailers.getOrCreateMailer(
|
||||
options.sendConfigurationId
|
||||
),
|
||||
templates.getById(
|
||||
options.context,
|
||||
options.templateId,
|
||||
false
|
||||
)
|
||||
]);
|
||||
|
||||
const variables = {
|
||||
EMAIL: options.email,
|
||||
...getMergeTagsForBases(getTrustedUrl(), getSandboxUrl(), getPublicUrl()),
|
||||
...options.variables
|
||||
};
|
||||
|
||||
const html = tools.formatTemplate(
|
||||
template.html,
|
||||
null,
|
||||
variables,
|
||||
true
|
||||
);
|
||||
const subject = tools.formatTemplate(
|
||||
options.subject || template.description || template.name,
|
||||
variables
|
||||
);
|
||||
return mailer.sendTransactionalMail(
|
||||
{
|
||||
to: options.email,
|
||||
subject
|
||||
},
|
||||
{
|
||||
html: { template: html },
|
||||
data: options.data,
|
||||
locale: options.locale
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
_validateMailOptions(options) {
|
||||
let { context, email, locale, templateId } = options;
|
||||
|
||||
if (!templateId) {
|
||||
throw new Error('Missing templateId');
|
||||
}
|
||||
if (!context) {
|
||||
throw new Error('Missing context');
|
||||
}
|
||||
if (!email || email.length === 0) {
|
||||
throw new Error('Missing email');
|
||||
}
|
||||
if (typeof email === 'string') {
|
||||
email = email.split(',');
|
||||
}
|
||||
if (email.length > options.maxMails) {
|
||||
throw new Error(
|
||||
`Cannot send more than ${options.maxMails} emails at once`
|
||||
);
|
||||
}
|
||||
if (!locale) {
|
||||
throw new Error('Missing locale');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TemplateSender;
|
|
@ -41,16 +41,17 @@ async function search(context, offset, limit, search) {
|
|||
async function add(context, email) {
|
||||
enforce(email, 'Email has to be set');
|
||||
|
||||
return await knex.transaction(async tx => {
|
||||
shares.enforceGlobalPermission(context, 'manageBlacklist');
|
||||
|
||||
const existing = await tx('blacklist').where('email', email).first();
|
||||
if (!existing) {
|
||||
await tx('blacklist').insert({email});
|
||||
}
|
||||
shares.enforceGlobalPermission(context, 'manageBlacklist');
|
||||
|
||||
try {
|
||||
await knex('blacklist').insert({email});
|
||||
await activityLog.logBlacklistActivity(BlacklistActivityType.ADD, email);
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(context, email) {
|
||||
|
|
|
@ -21,12 +21,14 @@ const {LinkId} = require('./links');
|
|||
const feedcheck = require('../lib/feedcheck');
|
||||
const contextHelpers = require('../lib/context-helpers');
|
||||
const {convertFileURLs} = require('../lib/campaign-content');
|
||||
const {CampaignSender, MessageType} = require('../lib/campaign-sender');
|
||||
const lists = require('./lists');
|
||||
|
||||
const {EntityActivityType, CampaignActivityType} = require('../../shared/activity-log');
|
||||
const activityLog = require('../lib/activity-log');
|
||||
|
||||
const allowedKeysCommon = ['name', 'description', 'segment', 'namespace',
|
||||
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
|
||||
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
|
||||
|
||||
const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]);
|
||||
const allowedKeysCreateRssEntry = new Set(['type', 'source', 'parent', ...allowedKeysCommon]);
|
||||
|
@ -168,7 +170,7 @@ async function listTestUsersDTAjax(context, campaignId, params) {
|
|||
let subsQry;
|
||||
|
||||
if (subsQrys.length === 1) {
|
||||
const subsUnionSql = '(' + subsQrys[0].sql + ') as `test_subscriptions`'
|
||||
const subsUnionSql = '(' + subsQrys[0].sql + ') as `test_subscriptions`';
|
||||
subsQry = knex.raw(subsUnionSql, subsQrys[0].bindings);
|
||||
|
||||
} else {
|
||||
|
@ -342,7 +344,7 @@ async function rawGetByTx(tx, key, id) {
|
|||
.groupBy('campaigns.id')
|
||||
.select([
|
||||
'campaigns.id', 'campaigns.cid', 'campaigns.name', 'campaigns.description', 'campaigns.namespace', 'campaigns.status', 'campaigns.type', 'campaigns.source',
|
||||
'campaigns.send_configuration', 'campaigns.from_name_override', 'campaigns.from_email_override', 'campaigns.reply_to_override', 'campaigns.subject_override',
|
||||
'campaigns.send_configuration', 'campaigns.from_name_override', 'campaigns.from_email_override', 'campaigns.reply_to_override', 'campaigns.subject',
|
||||
'campaigns.data', 'campaigns.click_tracking_disabled', 'campaigns.open_tracking_disabled', 'campaigns.unsubscribe_url', 'campaigns.scheduled',
|
||||
'campaigns.delivered', 'campaigns.unsubscribed', 'campaigns.bounced', 'campaigns.complained', 'campaigns.blacklisted', 'campaigns.opened', 'campaigns.clicks',
|
||||
knex.raw(`GROUP_CONCAT(CONCAT_WS(\':\', campaign_lists.list, campaign_lists.segment) ORDER BY campaign_lists.id SEPARATOR \';\') as lists`)
|
||||
|
@ -632,21 +634,32 @@ async function remove(context, id) {
|
|||
});
|
||||
}
|
||||
|
||||
async function enforceSendPermissionTx(tx, context, campaignId) {
|
||||
async function enforceSendPermissionTx(tx, context, campaignOrCampaignId, isToTestUsers, listId) {
|
||||
let campaign;
|
||||
|
||||
if (typeof campaignId === 'object') {
|
||||
campaign = campaignId;
|
||||
if (typeof campaignOrCampaignId === 'object') {
|
||||
campaign = campaignOrCampaignId;
|
||||
} else {
|
||||
campaign = await getByIdTx(tx, context, campaignId, false);
|
||||
campaign = await getByIdTx(tx, context, campaignOrCampaignId, false);
|
||||
}
|
||||
|
||||
const sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), campaign.send_configuration, false, false);
|
||||
|
||||
const requiredPermission = getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration);
|
||||
const requiredSendConfigurationPermission = getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration);
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', campaign.send_configuration, requiredSendConfigurationPermission);
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', campaign.send_configuration, requiredPermission);
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaign.id, 'send');
|
||||
const requiredListAndCampaignPermission = isToTestUsers ? 'sendToTestUsers' : 'send';
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaign.id, requiredListAndCampaignPermission);
|
||||
|
||||
if (listId) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, requiredListAndCampaignPermission);
|
||||
|
||||
} else {
|
||||
for (const listIds of campaign.lists) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listIds.list, requiredListAndCampaignPermission);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -845,12 +858,9 @@ async function getSubscribersQueryGeneratorTx(tx, campaignId) {
|
|||
|
||||
async function _changeStatus(context, campaignId, permittedCurrentStates, newState, invalidStateMessage, scheduled = null) {
|
||||
await knex.transaction(async tx => {
|
||||
const entity = await tx('campaigns').where('id', campaignId).first();
|
||||
if (!entity) {
|
||||
throw new interoperableErrors.NotFoundError();
|
||||
}
|
||||
const entity = await getByIdTx(tx, context, campaignId, false);
|
||||
|
||||
await enforceSendPermissionTx(tx, context, entity);
|
||||
await enforceSendPermissionTx(tx, context, entity, false);
|
||||
|
||||
if (!permittedCurrentStates.includes(entity.status)) {
|
||||
throw new interoperableErrors.InvalidStateError(invalidStateMessage);
|
||||
|
@ -869,11 +879,11 @@ async function _changeStatus(context, campaignId, permittedCurrentStates, newSta
|
|||
|
||||
|
||||
async function start(context, campaignId, startAt) {
|
||||
await _changeStatus(context, campaignId, [CampaignStatus.IDLE, CampaignStatus.SCHEDULED, CampaignStatus.PAUSED, CampaignStatus.FINISHED], CampaignStatus.SCHEDULED, 'Cannot start campaign until it is in IDLE or PAUSED state', startAt);
|
||||
await _changeStatus(context, campaignId, [CampaignStatus.IDLE, CampaignStatus.PAUSED, CampaignStatus.FINISHED], CampaignStatus.SCHEDULED, 'Cannot start campaign until it is in IDLE, PAUSED, or FINISHED state', startAt);
|
||||
}
|
||||
|
||||
async function stop(context, campaignId) {
|
||||
await _changeStatus(context, campaignId, [CampaignStatus.SCHEDULED], CampaignStatus.PAUSED, 'Cannot stop campaign until it is in SCHEDULED state');
|
||||
await _changeStatus(context, campaignId, [CampaignStatus.SCHEDULED, CampaignStatus.SENDING], CampaignStatus.PAUSING, 'Cannot stop campaign until it is in SCHEDULED or SENDING state');
|
||||
}
|
||||
|
||||
async function reset(context, campaignId) {
|
||||
|
@ -944,6 +954,103 @@ async function fetchRssCampaign(context, cid) {
|
|||
});
|
||||
}
|
||||
|
||||
async function testSend(context, data) {
|
||||
// Though it's a bit counterintuitive, this handles also test sends of a template (i.e. without any campaign id)
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
const processSubscriber = async (sendConfigurationId, listId, subscriptionId, messageData) => {
|
||||
await CampaignSender.queueMessageTx(tx, sendConfigurationId, listId, subscriptionId, MessageType.TEST, messageData);
|
||||
|
||||
await activityLog.logEntityActivity('campaign', CampaignActivityType.TEST_SEND, campaignId, {list: listId, subscription: subscriptionId});
|
||||
};
|
||||
|
||||
const campaignId = data.campaignId;
|
||||
|
||||
if (campaignId) { // This means we are sending a campaign
|
||||
/*
|
||||
Data coming from the client:
|
||||
- html, text
|
||||
- subjectPrepend, subjectAppend
|
||||
- listCid, subscriptionCid
|
||||
- listId, segmentId
|
||||
*/
|
||||
|
||||
const campaign = await getByIdTx(tx, context, campaignId, false);
|
||||
const sendConfigurationId = campaign.send_configuration;
|
||||
|
||||
const messageData = {
|
||||
campaignId: campaignId,
|
||||
subject: data.subjectPrepend + campaign.subject + data.subjectAppend,
|
||||
html: data.html, // The html and text may be undefined
|
||||
text: data.text,
|
||||
attachments: []
|
||||
};
|
||||
|
||||
const attachments = await files.listTx(tx, contextHelpers.getAdminContext(), 'campaign', 'attachment', campaignId);
|
||||
for (const attachment of attachments) {
|
||||
messageData.attachments.push({
|
||||
filename: attachment.originalname,
|
||||
path: files.getFilePath('campaign', 'attachment', campaign.id, attachment.filename),
|
||||
id: attachment.id
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
let listId = data.listId;
|
||||
if (!listId && data.listCid) {
|
||||
const list = await lists.getByCidTx(tx, context, data.listCid);
|
||||
listId = list.id;
|
||||
}
|
||||
|
||||
const segmentId = data.segmentId;
|
||||
|
||||
if (listId) {
|
||||
await enforceSendPermissionTx(tx, context, campaign, true, listId);
|
||||
|
||||
if (data.subscriptionCid) {
|
||||
const subscriber = await subscriptions.getByCidTx(tx, context, listId, data.subscriptionCid);
|
||||
await processSubscriber(sendConfigurationId, listId, subscriber.id, messageData);
|
||||
|
||||
} else {
|
||||
const subscribers = await subscriptions.listTestUsersTx(tx, context, listId, segmentId);
|
||||
for (const subscriber of subscribers) {
|
||||
await processSubscriber(sendConfigurationId, listId, subscriber.id, messageData);
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
for (const lstSeg of campaign.lists) {
|
||||
await enforceSendPermissionTx(tx, context, campaign, true, lstSeg.list);
|
||||
|
||||
const subscribers = await subscriptions.listTestUsersTx(tx, context, lstSeg.list, segmentId);
|
||||
for (const subscriber of subscribers) {
|
||||
await processSubscriber(sendConfigurationId, lstSeg.list, subscriber.id, messageData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else { // This means we are sending a template
|
||||
/*
|
||||
Data coming from the client:
|
||||
- html, text
|
||||
- listCid, subscriptionCid, sendConfigurationId
|
||||
*/
|
||||
|
||||
const messageData = {
|
||||
subject: 'Test',
|
||||
html: data.html,
|
||||
text: data.text
|
||||
};
|
||||
|
||||
const list = await lists.getByCidTx(tx, context, data.listCid);
|
||||
const subscriber = await subscriptions.getByCidTx(tx, context, list.id, data.subscriptionCid);
|
||||
await processSubscriber(data.sendConfigurationId, list.id, subscriber.id, messageData);
|
||||
}
|
||||
});
|
||||
|
||||
senders.scheduleCheck();
|
||||
}
|
||||
|
||||
module.exports.Content = Content;
|
||||
module.exports.hash = hash;
|
||||
|
||||
|
@ -986,4 +1093,6 @@ module.exports.rawGetByTx = rawGetByTx;
|
|||
module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx;
|
||||
module.exports.getStatisticsOpened = getStatisticsOpened;
|
||||
|
||||
module.exports.fetchRssCampaign = fetchRssCampaign;
|
||||
module.exports.fetchRssCampaign = fetchRssCampaign;
|
||||
|
||||
module.exports.testSend = testSend;
|
|
@ -45,7 +45,7 @@ async function listDTAjax(context, type, subType, entityId, params) {
|
|||
await shares.enforceEntityPermission(context, type, entityId, getFilesPermission(type, subType, 'view'));
|
||||
return await dtHelpers.ajaxList(
|
||||
params,
|
||||
builder => builder.from(getFilesTable(type, subType)).where({entity: entityId}),
|
||||
builder => builder.from(getFilesTable(type, subType)).where({entity: entityId, delete_pending: false}),
|
||||
['id', 'originalname', 'filename', 'size', 'created']
|
||||
);
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ async function listDTAjax(context, type, subType, entityId, params) {
|
|||
async function listTx(tx, context, type, subType, entityId) {
|
||||
enforceTypePermitted(type, subType);
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'view'));
|
||||
return await tx(getFilesTable(type, subType)).where({entity: entityId}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
|
||||
return await tx(getFilesTable(type, subType)).where({entity: entityId, delete_pending: false}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
|
||||
}
|
||||
|
||||
async function list(context, type, subType, entityId) {
|
||||
|
@ -65,7 +65,7 @@ async function list(context, type, subType, entityId) {
|
|||
async function getFileById(context, type, subType, id) {
|
||||
enforceTypePermitted(type, subType);
|
||||
const file = await knex.transaction(async tx => {
|
||||
const file = await tx(getFilesTable(type, subType)).where('id', id).first();
|
||||
const file = await tx(getFilesTable(type, subType)).where({id: id, delete_pending: false}).first();
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, getFilesPermission(type, subType, 'view'));
|
||||
return file;
|
||||
});
|
||||
|
@ -85,7 +85,7 @@ async function _getFileBy(context, type, subType, entityId, key, value) {
|
|||
enforceTypePermitted(type, subType);
|
||||
const file = await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'view'));
|
||||
const file = await tx(getFilesTable(type, subType)).where({entity: entityId, [key]: value}).first();
|
||||
const file = await tx(getFilesTable(type, subType)).where({entity: entityId, delete_pending: false, [key]: value}).first();
|
||||
return file;
|
||||
});
|
||||
|
||||
|
@ -155,7 +155,7 @@ async function createFiles(context, type, subType, entityId, files, replacementB
|
|||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'manage'));
|
||||
|
||||
const existingNamesRows = await tx(getFilesTable(type, subType)).where('entity', entityId).select(['id', 'filename', 'originalname']);
|
||||
const existingNamesRows = await tx(getFilesTable(type, subType)).where({entity: entityId, delete_pending: false}).select(['id', 'filename', 'originalname']);
|
||||
|
||||
const existingNameSet = new Set();
|
||||
for (const row of existingNamesRows) {
|
||||
|
@ -275,18 +275,49 @@ async function createFiles(context, type, subType, entityId, files, replacementB
|
|||
}
|
||||
}
|
||||
|
||||
async function lockTx(tx, type, subType, id) {
|
||||
enforceTypePermitted(type, subType);
|
||||
const filesTableName = getFilesTable(type, subType);
|
||||
await tx(filesTableName).where('id', id).increment('lock_count');
|
||||
}
|
||||
|
||||
async function unlockTx(tx, type, subType, id) {
|
||||
enforceTypePermitted(type, subType);
|
||||
|
||||
const filesTableName = getFilesTable(type, subType);
|
||||
const file = await tx(filesTableName).where('id', id).first();
|
||||
|
||||
enforce(file, `File ${id} not found`);
|
||||
enforce(file.lock_count > 0, `Corrupted lock count at file ${id}`);
|
||||
|
||||
if (file.lock_count === 1 && file.delete_pending) {
|
||||
await tx(filesTableName).where('id', id).del();
|
||||
|
||||
const filePath = getFilePath(type, subType, file.entity, file.filename);
|
||||
await fs.removeAsync(filePath);
|
||||
|
||||
} else {
|
||||
await tx(filesTableName).where('id', id).update({lock_count: file.lock_count - 1});
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFile(context, type, subType, id) {
|
||||
enforceTypePermitted(type, subType);
|
||||
|
||||
const file = await knex.transaction(async tx => {
|
||||
const file = await tx(getFilesTable(type, subType)).where('id', id).select('entity', 'filename').first();
|
||||
await knex.transaction(async tx => {
|
||||
const filesTableName = getFilesTable(type, subType);
|
||||
const file = await tx(filesTableName).where('id', id).first();
|
||||
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, getFilesPermission(type, subType, 'manage'));
|
||||
await tx(getFilesTable(type, subType)).where('id', id).del();
|
||||
return {filename: file.filename, entity: file.entity};
|
||||
});
|
||||
|
||||
const filePath = getFilePath(type, subType, file.entity, file.filename);
|
||||
await fs.removeAsync(filePath);
|
||||
if (!file.lock_count) {
|
||||
await tx(filesTableName).where('id', file.id).del();
|
||||
|
||||
const filePath = getFilePath(type, subType, file.entity, file.filename);
|
||||
await fs.removeAsync(filePath);
|
||||
} else {
|
||||
await tx(filesTableName).where('id', file.id).update({delete_pending: true});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function copyAllTx(tx, context, fromType, fromSubType, fromEntityId, toType, toSubType, toEntityId) {
|
||||
|
@ -296,7 +327,7 @@ async function copyAllTx(tx, context, fromType, fromSubType, fromEntityId, toTyp
|
|||
enforceTypePermitted(toType, toSubType);
|
||||
await shares.enforceEntityPermissionTx(tx, context, toType, toEntityId, getFilesPermission(toType, toSubType, 'manage'));
|
||||
|
||||
const rows = await tx(getFilesTable(fromType, fromSubType)).where({entity: fromEntityId});
|
||||
const rows = await tx(getFilesTable(fromType, fromSubType)).where({entity: fromEntityId, delete_pending: false});
|
||||
for (const row of rows) {
|
||||
const fromFilePath = getFilePath(fromType, fromSubType, fromEntityId, row.filename);
|
||||
const toFilePath = getFilePath(toType, toSubType, toEntityId, row.filename);
|
||||
|
@ -339,4 +370,6 @@ module.exports.getFileUrl = getFileUrl;
|
|||
module.exports.getFilePath = getFilePath;
|
||||
module.exports.copyAllTx = copyAllTx;
|
||||
module.exports.removeAllTx = removeAllTx;
|
||||
module.exports.lockTx = lockTx;
|
||||
module.exports.unlockTx = unlockTx;
|
||||
module.exports.ReplacementBehavior = ReplacementBehavior;
|
||||
|
|
|
@ -11,6 +11,7 @@ const he = require('he');
|
|||
const { getPublicUrl } = require('../lib/urls');
|
||||
const tools = require('../lib/tools');
|
||||
const shortid = require('shortid');
|
||||
const {enforce} = require('../lib/helpers');
|
||||
|
||||
const LinkId = {
|
||||
OPEN: -1,
|
||||
|
@ -103,16 +104,16 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
|
|||
}
|
||||
|
||||
async function addOrGet(campaignId, url) {
|
||||
return await knex.transaction(async tx => {
|
||||
const link = await tx('links').select(['id', 'cid']).where({
|
||||
campaign: campaignId,
|
||||
url
|
||||
}).first();
|
||||
const link = await knex('links').select(['id', 'cid']).where({
|
||||
campaign: campaignId,
|
||||
url
|
||||
}).first();
|
||||
|
||||
if (!link) {
|
||||
let cid = shortid.generate();
|
||||
if (!link) {
|
||||
let cid = shortid.generate();
|
||||
|
||||
const ids = await tx('links').insert({
|
||||
try {
|
||||
const ids = await knex('links').insert({
|
||||
campaign: campaignId,
|
||||
cid,
|
||||
url
|
||||
|
@ -122,10 +123,21 @@ async function addOrGet(campaignId, url) {
|
|||
id: ids[0],
|
||||
cid
|
||||
};
|
||||
} else {
|
||||
return link;
|
||||
} catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') {
|
||||
const link = await knex('links').select(['id', 'cid']).where({
|
||||
campaign: campaignId,
|
||||
url
|
||||
}).first();
|
||||
|
||||
enforce(link);
|
||||
return link;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateLinks(campaign, list, subscription, mergeTags, message) {
|
||||
|
|
|
@ -14,7 +14,7 @@ const mailers = require('../lib/mailers');
|
|||
const senders = require('../lib/senders');
|
||||
const dependencyHelpers = require('../lib/dependency-helpers');
|
||||
|
||||
const allowedKeys = new Set(['name', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'reply_to', 'reply_to_overridable', 'subject', 'subject_overridable', 'x_mailer', 'verp_hostname', 'verp_disable_sender_header', 'mailer_type', 'mailer_settings', 'namespace']);
|
||||
const allowedKeys = new Set(['name', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'reply_to', 'reply_to_overridable', 'x_mailer', 'verp_hostname', 'verp_disable_sender_header', 'mailer_type', 'mailer_settings', 'namespace']);
|
||||
|
||||
const allowedMailerTypes = new Set(Object.values(MailerType));
|
||||
|
||||
|
@ -75,7 +75,7 @@ async function _getByTx(tx, context, key, id, withPermissions, withPrivateData)
|
|||
entity.mailer_settings = JSON.parse(entity.mailer_settings);
|
||||
} else {
|
||||
entity = await tx('send_configurations').where(key, id).select(
|
||||
['id', 'name', 'cid', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'reply_to', 'reply_to_overridable', 'subject', 'subject_overridable']
|
||||
['id', 'name', 'cid', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'reply_to', 'reply_to_overridable']
|
||||
).first();
|
||||
|
||||
if (!entity) {
|
||||
|
|
|
@ -19,6 +19,8 @@ const lists = require('./lists');
|
|||
|
||||
const allowedKeysBase = new Set(['email', 'tz', 'is_test', 'status']);
|
||||
|
||||
const TEST_USERS_LIST_LIMIT = 1000;
|
||||
|
||||
const fieldTypes = {};
|
||||
|
||||
const Cardinality = {
|
||||
|
@ -409,6 +411,32 @@ async function list(context, listId, grouped, offset, limit) {
|
|||
});
|
||||
}
|
||||
|
||||
async function listTestUsersTx(tx, context, listId, segmentId, grouped) {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
||||
|
||||
let entitiesQry = tx(getSubscriptionTableName(listId)).orderBy('id', 'asc').where('is_test', true).limit(TEST_USERS_LIST_LIMIT);
|
||||
|
||||
if (segmentId) {
|
||||
const addSegmentQuery = await segments.getQueryGeneratorTx(tx, listId, segmentId);
|
||||
|
||||
entitiesQry = entitiesQry.where(function() {
|
||||
addSegmentQuery(this);
|
||||
});
|
||||
}
|
||||
|
||||
const entities = await entitiesQry;
|
||||
|
||||
if (grouped) {
|
||||
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||
|
||||
for (const entity of entities) {
|
||||
groupSubscription(groupedFieldsMap, entity);
|
||||
}
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
// Note that this does not do all the work in the transaction. Thus it is prone to fail if the list is deleted in during the run of the function
|
||||
async function* listIterator(context, listId, segmentId, grouped = true) {
|
||||
let groupedFieldsMap;
|
||||
|
@ -855,6 +883,7 @@ module.exports.getByEmail = getByEmail;
|
|||
module.exports.list = list;
|
||||
module.exports.listIterator = listIterator;
|
||||
module.exports.listDTAjax = listDTAjax;
|
||||
module.exports.listTestUsersTx = listTestUsersTx;
|
||||
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
|
||||
module.exports.serverValidate = serverValidate;
|
||||
module.exports.create = create;
|
||||
|
|
|
@ -7,11 +7,17 @@ const dtHelpers = require('../lib/dt-helpers');
|
|||
const interoperableErrors = require('../../shared/interoperable-errors');
|
||||
const namespaceHelpers = require('../lib/namespace-helpers');
|
||||
const shares = require('./shares');
|
||||
const reports = require('./reports');
|
||||
const files = require('./files');
|
||||
const dependencyHelpers = require('../lib/dependency-helpers');
|
||||
const {convertFileURLs} = require('../lib/campaign-content');
|
||||
|
||||
const mailers = require('../lib/mailers');
|
||||
const tools = require('../lib/tools');
|
||||
const sendConfigurations = require('./send-configurations');
|
||||
const { getMergeTagsForBases } = require('../../shared/templates');
|
||||
const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
|
||||
const htmlToText = require('html-to-text');
|
||||
|
||||
const allowedKeys = new Set(['name', 'description', 'type', 'data', 'html', 'text', 'namespace']);
|
||||
|
||||
function hash(entity) {
|
||||
|
@ -145,6 +151,65 @@ async function remove(context, id) {
|
|||
});
|
||||
}
|
||||
|
||||
const MAX_EMAIL_COUNT = 100;
|
||||
async function sendAsTransactionalEmail(context, templateId, sendConfigurationId, emails, subject, mergeTags) {
|
||||
// TODO - Update this to use CampaignSender.queueMessageTx (with renderedHtml and renderedText)
|
||||
|
||||
if (emails.length > MAX_EMAIL_COUNT) {
|
||||
throw new Error(`Cannot send more than ${MAX_EMAIL_COUNT} emails at once`);
|
||||
}
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
const template = await getByIdTx(tx, context, templateId,false);
|
||||
const sendConfiguration = await sendConfigurations.getByIdTx(tx, context, sendConfigurationId, false, false);
|
||||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', sendConfigurationId, 'sendWithoutOverrides');
|
||||
|
||||
const mailer = await mailers.getOrCreateMailer(sendConfigurationId);
|
||||
|
||||
const variablesSkeleton = {
|
||||
...getMergeTagsForBases(getTrustedUrl(), getSandboxUrl(), getPublicUrl()),
|
||||
...mergeTags
|
||||
};
|
||||
|
||||
for (const email of emails) {
|
||||
const variables = {
|
||||
...variablesSkeleton,
|
||||
EMAIL: email
|
||||
};
|
||||
|
||||
const html = tools.formatTemplate(
|
||||
template.html,
|
||||
null,
|
||||
variables,
|
||||
true
|
||||
);
|
||||
|
||||
const text = (template.text || '').trim()
|
||||
? tools.formatTemplate(
|
||||
template.text,
|
||||
null,
|
||||
variables,
|
||||
false
|
||||
) : htmlToText.fromString(html, {wordwrap: 130});
|
||||
|
||||
return mailer.sendTransactionalMail(
|
||||
{
|
||||
to: email,
|
||||
subject,
|
||||
from: {
|
||||
name: sendConfiguration.from_name,
|
||||
address: sendConfiguration.from_email
|
||||
},
|
||||
html,
|
||||
text
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports.hash = hash;
|
||||
module.exports.getByIdTx = getByIdTx;
|
||||
module.exports.getById = getById;
|
||||
|
@ -153,3 +218,4 @@ module.exports.listByNamespaceDTAjax = listByNamespaceDTAjax;
|
|||
module.exports.create = create;
|
||||
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
|
||||
module.exports.remove = remove;
|
||||
module.exports.sendAsTransactionalEmail = sendAsTransactionalEmail;
|
||||
|
|
|
@ -65,7 +65,7 @@ async function _validateAndPreprocess(tx, context, campaignId, entity) {
|
|||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.source_campaign, 'view');
|
||||
}
|
||||
|
||||
await campaigns.enforceSendPermissionTx(tx, context, campaignId);
|
||||
await campaigns.enforceSendPermissionTx(tx, context, campaignId, false);
|
||||
}
|
||||
|
||||
async function create(context, campaignId, entity) {
|
||||
|
|
|
@ -311,7 +311,7 @@ async function sendPasswordReset(locale, usernameOrEmail) {
|
|||
const { adminEmail } = await settings.get(contextHelpers.getAdminContext(), ['adminEmail']);
|
||||
|
||||
const mailer = await mailers.getOrCreateMailer();
|
||||
await mailer.sendTransactionalMail({
|
||||
await mailer.sendTransactionalMailBasedOnTemplate({
|
||||
to: {
|
||||
address: user.email
|
||||
},
|
||||
|
|
|
@ -16,8 +16,10 @@ const contextHelpers = require('../lib/context-helpers');
|
|||
const shares = require('../models/shares');
|
||||
const slugify = require('slugify');
|
||||
const passport = require('../lib/passport');
|
||||
const TemplateSender = require('../lib/template-sender');
|
||||
const templates = require('../models/templates');
|
||||
const campaigns = require('../models/campaigns');
|
||||
const {castToInteger} = require('../lib/helpers');
|
||||
const {getSystemSendConfigurationId} = require('../../shared/send-configurations');
|
||||
|
||||
class APIError extends Error {
|
||||
constructor(msg, status) {
|
||||
|
@ -236,7 +238,7 @@ router.postAsync('/blacklist/add', passport.loggedIn, async (req, res) => {
|
|||
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
||||
});
|
||||
if (!(input.EMAIL) || (input.EMAIL === '')) {
|
||||
throw new Error('EMAIL argument is required');
|
||||
throw new APIError('EMAIL argument is required', 400);
|
||||
}
|
||||
|
||||
await blacklist.add(req.context, input.EMAIL);
|
||||
|
@ -253,7 +255,7 @@ router.postAsync('/blacklist/delete', passport.loggedIn, async (req, res) => {
|
|||
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
||||
});
|
||||
if (!(input.EMAIL) || (input.EMAIL === '')) {
|
||||
throw new Error('EMAIL argument is required');
|
||||
throw new APIError('EMAIL argument is required', 400);
|
||||
}
|
||||
|
||||
await blacklist.remove(req.oontext, input.EMAIL);
|
||||
|
@ -288,32 +290,30 @@ router.getAsync('/rss/fetch/:campaignCid', passport.loggedIn, async (req, res) =
|
|||
|
||||
router.postAsync('/templates/:templateId/send', async (req, res) => {
|
||||
const input = {};
|
||||
Object.keys(req.body).forEach(key => {
|
||||
input[
|
||||
(key || '')
|
||||
.toString()
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
] = req.body[key] || '';
|
||||
});
|
||||
|
||||
try {
|
||||
const templateSender = new TemplateSender({
|
||||
context: req.context,
|
||||
locale: req.locale,
|
||||
templateId: req.params.templateId
|
||||
});
|
||||
const info = await templateSender.send({
|
||||
data: input.DATA,
|
||||
email: input.EMAIL,
|
||||
sendConfigurationId: input.SEND_CONFIGURATION_ID,
|
||||
subject: input.SUBJECT,
|
||||
variables: input.VARIABLES
|
||||
});
|
||||
res.status(200).json({ data: info });
|
||||
} catch (e) {
|
||||
throw new APIError(e.message, 400);
|
||||
for (const key in req.body) {
|
||||
const sanitizedKey = key.toString().trim().toUpperCase();
|
||||
input[sanitizedKey] = req.body[key] || '';
|
||||
}
|
||||
|
||||
const templateId = castToInteger(req.params.templateId, 'Invalid template ID');
|
||||
|
||||
let sendConfigurationId;
|
||||
if (!('SEND_CONFIGURATION_ID' in input)) {
|
||||
sendConfigurationId = getSystemSendConfigurationId();
|
||||
} else {
|
||||
sendConfigurationId = castToInteger(input.SEND_CONFIGURATION_ID, 'Invalid send configuration ID');
|
||||
}
|
||||
|
||||
if (!input.EMAIL || input.EMAIL === 0) {
|
||||
throw new APIError('Missing email(s)', 400);
|
||||
}
|
||||
|
||||
const emails = input.EMAIL.split(',');
|
||||
|
||||
const info = await templates.sendAsTransactionalEmail(req.context, templateId, sendConfigurationId, emails, input.SUBJECT, input.VARIABLES);
|
||||
|
||||
res.json({ data: info });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
const CampaignSender = require('../lib/campaign-sender');
|
||||
const {CampaignSender} = require('../lib/campaign-sender');
|
||||
|
||||
|
||||
router.get('/:campaign/:list/:subscription', (req, res, next) => {
|
||||
const cs = new CampaignSender();
|
||||
cs.init({campaignCid: req.params.campaign})
|
||||
cs.initByCampaignCid(req.params.campaign)
|
||||
.then(() => cs.getMessage(req.params.list, req.params.subscription))
|
||||
.then(result => {
|
||||
const {html} = result;
|
||||
|
|
|
@ -114,6 +114,12 @@ router.postAsync('/campaigns-link-clicks-table/:campaignId', passport.loggedIn,
|
|||
return res.json(await campaigns.listLinkClicksDTAjax(req.context, castToInteger(req.params.campaignId), req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/campaign-test-send', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const data = req.body;
|
||||
const result = await campaigns.testSend(req.context, data);
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -5,7 +5,6 @@ const templates = require('../../models/templates');
|
|||
|
||||
const router = require('../../lib/router-async').create();
|
||||
const {castToInteger} = require('../../lib/helpers');
|
||||
const CampaignSender = require('../../lib/campaign-sender');
|
||||
|
||||
|
||||
router.getAsync('/templates/:templateId', passport.loggedIn, async (req, res) => {
|
||||
|
@ -39,10 +38,4 @@ router.postAsync('/templates-by-namespace-table/:namespaceId', passport.loggedIn
|
|||
return res.json(await templates.listByNamespaceDTAjax(req.context, castToInteger(req.params.namespaceId), req.body));
|
||||
});
|
||||
|
||||
router.postAsync('/template-test-send', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const data = req.body;
|
||||
const result = await CampaignSender.testSend(req.context, data.listCid, data.subscriptionCid, data.campaignId, data.sendConfigurationId, data.html, data.text);
|
||||
return res.json(result);
|
||||
});
|
||||
|
||||
module.exports = router;
|
|
@ -112,7 +112,7 @@ async function run() {
|
|||
from_name_override: rssCampaign.from_name_override,
|
||||
from_email_override: rssCampaign.from_email_override,
|
||||
reply_to_override: rssCampaign.reply_to_override,
|
||||
subject_override: rssCampaign.subject_override,
|
||||
subject: rssCampaign.subject,
|
||||
data: campaignData,
|
||||
|
||||
click_tracking_disabled: rssCampaign.click_tracking_disabled,
|
||||
|
|
|
@ -6,35 +6,63 @@ const log = require('../lib/log');
|
|||
const path = require('path');
|
||||
const knex = require('../lib/knex');
|
||||
const {CampaignStatus, CampaignType} = require('../../shared/campaigns');
|
||||
const { enforce } = require('../lib/helpers');
|
||||
const campaigns = require('../models/campaigns');
|
||||
const builtinZoneMta = require('../lib/builtin-zone-mta');
|
||||
const {CampaignActivityType} = require('../../shared/activity-log');
|
||||
const activityLog = require('../lib/activity-log');
|
||||
const {MessageType} = require('../lib/campaign-sender')
|
||||
require('../lib/fork');
|
||||
|
||||
class Notifications {
|
||||
constructor() {
|
||||
this.conts = new Map();
|
||||
}
|
||||
|
||||
notify(id) {
|
||||
const cont = this.conts.get(id);
|
||||
if (cont) {
|
||||
for (const cb of cont) {
|
||||
setImmediate(cb);
|
||||
}
|
||||
this.conts.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
async waitFor(id) {
|
||||
let cont = this.conts.get(id);
|
||||
if (!cont) {
|
||||
cont = [];
|
||||
}
|
||||
|
||||
const notified = new Promise(resolve => {
|
||||
cont.push(resolve);
|
||||
});
|
||||
|
||||
this.conts.set(id, cont);
|
||||
|
||||
await notified;
|
||||
}
|
||||
}
|
||||
|
||||
const notifier = new Notifications();
|
||||
|
||||
let messageTid = 0;
|
||||
const workerProcesses = new Map();
|
||||
|
||||
const workersCount = config.queue.processes;
|
||||
const idleWorkers = [];
|
||||
|
||||
let campaignSchedulerRunning = false;
|
||||
let queuedSchedulerRunning = false;
|
||||
let workerSchedulerRunning = false;
|
||||
|
||||
const campaignsCheckPeriod = 5 * 1000;
|
||||
const campaignsCheckPeriod = 30 * 1000;
|
||||
const retrieveBatchSize = 1000;
|
||||
const workerBatchSize = 100;
|
||||
const workerBatchSize = 10;
|
||||
|
||||
const messageQueue = new Map(); // campaignId -> [{listId, email}]
|
||||
const messageQueueCont = new Map(); // campaignId -> next batch callback
|
||||
const campaignFinishCont = new Map(); // campaignId -> worker finished callback
|
||||
const sendConfigurationMessageQueue = new Map(); // sendConfigurationId -> [{queuedMessage}]
|
||||
const campaignMessageQueue = new Map(); // campaignId -> [{listId, email}]
|
||||
|
||||
const workAssignment = new Map(); // workerId -> { campaignId, subscribers: [{listId, email}] }
|
||||
|
||||
let workerSchedulerCont = null;
|
||||
let queuedLastId = 0;
|
||||
const workAssignment = new Map(); // workerId -> { campaignId, messages: [{listId, email} } / { sendConfigurationId, messages: [{queuedMessage}] }
|
||||
|
||||
|
||||
function messagesProcessed(workerId) {
|
||||
|
@ -43,108 +71,151 @@ function messagesProcessed(workerId) {
|
|||
workAssignment.delete(workerId);
|
||||
idleWorkers.push(workerId);
|
||||
|
||||
if (workerSchedulerCont) {
|
||||
const cont = workerSchedulerCont;
|
||||
setImmediate(workerSchedulerCont);
|
||||
workerSchedulerCont = null;
|
||||
}
|
||||
|
||||
if (campaignFinishCont.has(wa.campaignId)) {
|
||||
setImmediate(campaignFinishCont.get(wa.campaignId));
|
||||
campaignFinishCont.delete(wa.campaignId);
|
||||
}
|
||||
notifier.notify('workerFinished');
|
||||
}
|
||||
|
||||
async function scheduleWorkers() {
|
||||
async function workersLoop() {
|
||||
const reservedWorkersForTestCount = workersCount > 1 ? 1 : 0;
|
||||
|
||||
async function getAvailableWorker() {
|
||||
if (idleWorkers.length > 0) {
|
||||
return idleWorkers.shift();
|
||||
|
||||
} else {
|
||||
const workerAvailable = new Promise(resolve => {
|
||||
workerSchedulerCont = resolve;
|
||||
});
|
||||
|
||||
await workerAvailable;
|
||||
return idleWorkers.shift();
|
||||
while (idleWorkers.length === 0) {
|
||||
await notifier.waitFor('workerFinished');
|
||||
}
|
||||
|
||||
return idleWorkers.shift();
|
||||
}
|
||||
|
||||
function assignCampaignTaskToWorker(workerId, task) {
|
||||
const campaignId = task.campaignId;
|
||||
const queue = task.queue;
|
||||
|
||||
if (workerSchedulerRunning) {
|
||||
return;
|
||||
const messages = queue.splice(0, workerBatchSize);
|
||||
workAssignment.set(workerId, {campaignId, messages});
|
||||
|
||||
if (queue.length === 0) {
|
||||
notifier.notify(`campaignMessageQueueEmpty:${campaignId}`);
|
||||
}
|
||||
|
||||
sendToWorker(workerId, 'process-campaign-messages', {
|
||||
campaignId,
|
||||
messages
|
||||
});
|
||||
}
|
||||
|
||||
workerSchedulerRunning = true;
|
||||
let workerId = await getAvailableWorker();
|
||||
function assignSendConfigurationTaskToWorker(workerId, task) {
|
||||
const sendConfigurationId = task.sendConfigurationId;
|
||||
const queue = task.queue;
|
||||
|
||||
let keepLooping = true;
|
||||
const messages = queue.splice(0, workerBatchSize);
|
||||
workAssignment.set(workerId, {sendConfigurationId, messages});
|
||||
|
||||
while (keepLooping) {
|
||||
keepLooping = false;
|
||||
if (queue.length === 0) {
|
||||
notifier.notify(`sendConfigurationMessageQueueEmpty:${sendConfigurationId}`);
|
||||
}
|
||||
|
||||
for (const campaignId of messageQueue.keys()) {
|
||||
const queue = messageQueue.get(campaignId);
|
||||
sendToWorker(workerId, 'process-queued-messages', {
|
||||
sendConfigurationId,
|
||||
messages
|
||||
});
|
||||
}
|
||||
|
||||
if (queue.length > 0) {
|
||||
const subscribers = queue.splice(0, workerBatchSize);
|
||||
workAssignment.set(workerId, {campaignId, subscribers});
|
||||
function selectNextTask() {
|
||||
const allocationMap = new Map();
|
||||
const allocation = [];
|
||||
|
||||
if (queue.length === 0 && messageQueueCont.has(campaignId)) {
|
||||
setImmediate(messageQueueCont.get(campaignId));
|
||||
messageQueueCont.delete(campaignId);
|
||||
function initAllocation(attrName, queues, assignWorkerHandler) {
|
||||
for (const id of queues.keys()) {
|
||||
const key = attrName + ':' + id;
|
||||
|
||||
const queue = queues.get(id);
|
||||
|
||||
const task = {
|
||||
[attrName]: id,
|
||||
existingWorkers: 0,
|
||||
isEmpty: queue.length === 0,
|
||||
queue,
|
||||
assignWorkerHandler
|
||||
};
|
||||
|
||||
allocationMap.set(key, task);
|
||||
allocation.push(task);
|
||||
}
|
||||
|
||||
for (const wa of workAssignment.values()) {
|
||||
if (wa[attrName]) {
|
||||
const key = attrName + ':' + wa[attrName];
|
||||
const task = allocationMap.get(key);
|
||||
task.existingWorkers += 1;
|
||||
}
|
||||
|
||||
sendToWorker(workerId, 'process-messages', {
|
||||
campaignId,
|
||||
subscribers
|
||||
});
|
||||
workerId = await getAvailableWorker();
|
||||
|
||||
keepLooping = true;
|
||||
}
|
||||
}
|
||||
|
||||
initAllocation('sendConfigurationId', sendConfigurationMessageQueue, assignSendConfigurationTaskToWorker);
|
||||
initAllocation('campaignId', campaignMessageQueue, assignCampaignTaskToWorker);
|
||||
|
||||
let minTask = null;
|
||||
let minExistingWorkers;
|
||||
|
||||
for (const task of allocation) {
|
||||
if (!task.isEmpty && (minTask === null || minExistingWorkers > task.existingWorkers)) {
|
||||
minTask = task;
|
||||
minExistingWorkers = task.existingWorkers;
|
||||
}
|
||||
}
|
||||
|
||||
return minTask;
|
||||
}
|
||||
|
||||
idleWorkers.push(workerId);
|
||||
|
||||
workerSchedulerRunning = false;
|
||||
while (true) {
|
||||
const task = selectNextTask();
|
||||
|
||||
if (task) {
|
||||
const workerId = await getAvailableWorker();
|
||||
task.assignWorkerHandler(workerId, task);
|
||||
|
||||
} else {
|
||||
await notifier.waitFor('workAvailable');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function processCampaign(campaignId) {
|
||||
async function finish() {
|
||||
let workerRunning = false;
|
||||
for (const wa of workAssignment.values()) {
|
||||
if (wa.campaignId === campaignId) {
|
||||
workerRunning = true;
|
||||
const msgQueue = campaignMessageQueue.get(campaignId);
|
||||
|
||||
async function finish(newStatus) {
|
||||
const isCompleted = () => {
|
||||
if (msgQueue.length > 0) return false;
|
||||
|
||||
let workerRunning = false;
|
||||
|
||||
for (const wa of workAssignment.values()) {
|
||||
if (wa.campaignId === campaignId) {
|
||||
workerRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
return !workerRunning;
|
||||
};
|
||||
|
||||
while (!isCompleted()) {
|
||||
await notifier.waitFor('workerFinished');
|
||||
}
|
||||
|
||||
if (workerRunning) {
|
||||
const workerFinished = new Promise(resolve => {
|
||||
campaignFinishCont.set(campaignId, resolve);
|
||||
});
|
||||
campaignMessageQueue.delete(campaignId);
|
||||
|
||||
await workerFinished;
|
||||
setImmediate(finish);
|
||||
}
|
||||
|
||||
await knex('campaigns').where('id', campaignId).update({status: CampaignStatus.FINISHED});
|
||||
await activityLog.logEntityActivity('campaign', CampaignActivityType.STATUS_CHANGE, campaignId, {status: CampaignStatus.FINISHED});
|
||||
|
||||
messageQueue.delete(campaignId);
|
||||
await knex('campaigns').where('id', campaignId).update({status: newStatus});
|
||||
await activityLog.logEntityActivity('campaign', CampaignActivityType.STATUS_CHANGE, campaignId, {status: newStatus});
|
||||
}
|
||||
|
||||
const msgQueue = [];
|
||||
messageQueue.set(campaignId, msgQueue);
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const cpg = await knex('campaigns').where('id', campaignId).first();
|
||||
|
||||
if (cpg.status === CampaignStatus.PAUSED) {
|
||||
messageQueue.delete(campaignId);
|
||||
if (cpg.status === CampaignStatus.PAUSING) {
|
||||
msgQueue.splice(0);
|
||||
await finish(CampaignStatus.PAUSED);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -154,21 +225,21 @@ async function processCampaign(campaignId) {
|
|||
});
|
||||
|
||||
if (qryGen) {
|
||||
let subscribersInProcessing = [...msgQueue];
|
||||
let messagesInProcessing = [...msgQueue];
|
||||
for (const wa of workAssignment.values()) {
|
||||
if (wa.campaignId === campaignId) {
|
||||
subscribersInProcessing = subscribersInProcessing.concat(wa.subscribers);
|
||||
messagesInProcessing = messagesInProcessing.concat(wa.messages);
|
||||
}
|
||||
}
|
||||
|
||||
const qry = qryGen(knex)
|
||||
.whereNotIn('pending_subscriptions.email', subscribersInProcessing.map(x => x.email))
|
||||
.whereNotIn('pending_subscriptions.email', messagesInProcessing.map(x => x.email))
|
||||
.select(['pending_subscriptions.email', 'campaign_lists.list'])
|
||||
.limit(retrieveBatchSize);
|
||||
const subs = await qry;
|
||||
|
||||
if (subs.length === 0) {
|
||||
await finish();
|
||||
await finish(CampaignStatus.FINISHED);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -179,21 +250,19 @@ async function processCampaign(campaignId) {
|
|||
});
|
||||
}
|
||||
|
||||
const nextBatchNeeded = new Promise(resolve => {
|
||||
messageQueueCont.set(campaignId, resolve);
|
||||
});
|
||||
notifier.notify('workAvailable');
|
||||
|
||||
setImmediate(scheduleWorkers);
|
||||
|
||||
await nextBatchNeeded;
|
||||
while (msgQueue.length > 0) {
|
||||
await notifier.waitFor(`campaignMessageQueueEmpty:${campaignId}`);
|
||||
}
|
||||
|
||||
} else {
|
||||
await finish();
|
||||
await finish(CampaignStatus.FINISHED);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Senders', `Sending campaign ${campaignId} failed with error: ${err.message}`)
|
||||
log.error('Senders', `Sending campaign ${campaignId} failed with error: ${err.message}`);
|
||||
log.verbose(err.stack);
|
||||
}
|
||||
}
|
||||
|
@ -226,6 +295,8 @@ async function scheduleCampaigns() {
|
|||
});
|
||||
|
||||
if (campaignId) {
|
||||
campaignMessageQueue.set(campaignId, []);
|
||||
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
processCampaign(campaignId);
|
||||
|
||||
|
@ -234,16 +305,81 @@ async function scheduleCampaigns() {
|
|||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Senders', `Scheduling campaigns failed with error: ${err.message}`)
|
||||
log.error('Senders', `Scheduling campaigns failed with error: ${err.message}`);
|
||||
log.verbose(err.stack);
|
||||
}
|
||||
|
||||
|
||||
campaignSchedulerRunning = false;
|
||||
}
|
||||
|
||||
|
||||
async function processQueued() {
|
||||
async function processQueuedBySendConfiguration(sendConfigurationId) {
|
||||
const msgQueue = sendConfigurationMessageQueue.get(sendConfigurationId);
|
||||
|
||||
const isCompleted = () => {
|
||||
if (msgQueue.length > 0) return false;
|
||||
|
||||
let workerRunning = false;
|
||||
|
||||
for (const wa of workAssignment.values()) {
|
||||
if (wa.sendConfigurationId === sendConfigurationId) {
|
||||
workerRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
return !workerRunning;
|
||||
};
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
let messagesInProcessing = [...msgQueue];
|
||||
for (const wa of workAssignment.values()) {
|
||||
if (wa.sendConfigurationId === sendConfigurationId) {
|
||||
messagesInProcessing = messagesInProcessing.concat(wa.messages);
|
||||
}
|
||||
}
|
||||
|
||||
const rows = await knex('queued')
|
||||
.orderByRaw(`FIELD(type, ${MessageType.TRIGGERED}, ${MessageType.TEST}) DESC, id ASC`) // This orders MessageType.TEST messages before MessageType.TRIGGERED ones
|
||||
.where('send_configuration', sendConfigurationId)
|
||||
.whereNotIn('id', messagesInProcessing.map(x => x.queuedMessage.id))
|
||||
.limit(retrieveBatchSize);
|
||||
|
||||
if (rows.length === 0) {
|
||||
if (isCompleted()) {
|
||||
sendConfigurationMessageQueue.delete(sendConfigurationId);
|
||||
return;
|
||||
|
||||
} else {
|
||||
while (!isCompleted()) {
|
||||
await notifier.waitFor('workerFinished');
|
||||
}
|
||||
|
||||
// At this point, there might be new messages in the queued that could belong to us. Thus we have to try again instead for returning.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
row.data = JSON.parse(row.data);
|
||||
msgQueue.push({
|
||||
queuedMessage: row
|
||||
});
|
||||
}
|
||||
|
||||
notifier.notify('workAvailable');
|
||||
|
||||
while (msgQueue.length > 0) {
|
||||
await notifier.waitFor(`sendConfigurationMessageQueueEmpty:${sendConfigurationId}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Senders', `Sending queued messages for send configuration ${sendConfigurationId} failed with error: ${err.message}`);
|
||||
log.verbose(err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
async function scheduleQueued() {
|
||||
if (queuedSchedulerRunning) {
|
||||
return;
|
||||
}
|
||||
|
@ -252,34 +388,23 @@ async function processQueued() {
|
|||
|
||||
try {
|
||||
while (true) {
|
||||
const rows = await knex('queued')
|
||||
.orderBy('id', 'asc')
|
||||
.where('id', '>', queuedLastId)
|
||||
.limit(retrieveBatchSize);
|
||||
const sendConfigurationsInProcessing = [...sendConfigurationMessageQueue.keys()];
|
||||
|
||||
if (rows.length === 0) {
|
||||
break;
|
||||
}
|
||||
const rows = await knex('queued')
|
||||
.whereNotIn('send_configuration', sendConfigurationsInProcessing)
|
||||
.groupBy('send_configuration')
|
||||
.select(['send_configuration']);
|
||||
|
||||
for (const row of rows) {
|
||||
let msgQueue = messageQueue.get(row.campaign);
|
||||
if (!msgQueue) {
|
||||
msgQueue = [];
|
||||
messageQueue.set(row.campaign, msgQueue);
|
||||
}
|
||||
const sendConfigurationId = row.send_configuration;
|
||||
sendConfigurationMessageQueue.set(sendConfigurationId, []);
|
||||
|
||||
msgQueue.push({
|
||||
listId: row.list,
|
||||
subscriptionId: row.subscription
|
||||
});
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
processQueuedBySendConfiguration(sendConfigurationId);
|
||||
}
|
||||
|
||||
queuedLastId = rows[rows.length - 1].id;
|
||||
|
||||
setImmediate(scheduleWorkers);
|
||||
}
|
||||
} catch (err) {
|
||||
log.error('Senders', `Processing queued messages failed with error: ${err.message}`)
|
||||
log.error('Senders', `Scheduling queued messages failed with error: ${err.message}`);
|
||||
log.verbose(err.stack);
|
||||
}
|
||||
|
||||
|
@ -337,7 +462,7 @@ function periodicCampaignsCheck() {
|
|||
scheduleCampaigns();
|
||||
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
processQueued();
|
||||
scheduleQueued();
|
||||
|
||||
setTimeout(periodicCampaignsCheck, campaignsCheckPeriod);
|
||||
}
|
||||
|
@ -345,7 +470,7 @@ function periodicCampaignsCheck() {
|
|||
async function init() {
|
||||
const spawnWorkerFutures = [];
|
||||
let workerId;
|
||||
for (workerId = 0; workerId < config.queue.processes; workerId++) {
|
||||
for (workerId = 0; workerId < workersCount; workerId++) {
|
||||
spawnWorkerFutures.push(spawnWorker(workerId));
|
||||
}
|
||||
|
||||
|
@ -358,6 +483,7 @@ async function init() {
|
|||
if (type === 'schedule-check') {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
scheduleCampaigns();
|
||||
scheduleQueued();
|
||||
|
||||
} else if (type === 'reload-config') {
|
||||
for (const workerId of workerProcesses.keys()) {
|
||||
|
@ -376,6 +502,8 @@ async function init() {
|
|||
});
|
||||
|
||||
periodicCampaignsCheck();
|
||||
|
||||
setImmediate(workersLoop);
|
||||
}
|
||||
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
|
|
|
@ -3,14 +3,13 @@
|
|||
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 {CampaignSender} = require('../lib/campaign-sender');
|
||||
require('../lib/fork');
|
||||
|
||||
const workerId = Number.parseInt(process.argv[2]);
|
||||
let running = false;
|
||||
|
||||
async function processMessages(campaignId, subscribers) {
|
||||
async function processCampaignMessages(campaignId, messages) {
|
||||
if (running) {
|
||||
log.error('Senders', `Worker ${workerId} assigned work while working`);
|
||||
return;
|
||||
|
@ -19,23 +18,40 @@ async function processMessages(campaignId, subscribers) {
|
|||
running = true;
|
||||
|
||||
const cs = new CampaignSender();
|
||||
await cs.init({campaignId})
|
||||
await cs.initByCampaignId(campaignId);
|
||||
|
||||
for (const subData of subscribers) {
|
||||
for (const msgData of messages) {
|
||||
try {
|
||||
if (subData.email) {
|
||||
await cs.sendMessageByEmail(subData.listId, subData.email);
|
||||
await cs.sendRegularMessage(msgData.listId, msgData.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);
|
||||
log.verbose('Senders', 'Message sent and status updated for %s:%s', msgData.listId, msgData.email);
|
||||
} catch (err) {
|
||||
log.error('Senders', `Sending message to ${subData.listId}:${subData.email} failed with error: ${err.message}`)
|
||||
log.error('Senders', `Sending message to ${msgData.listId}:${msgData.email} failed with error: ${err.message}`);
|
||||
log.verbose(err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
running = false;
|
||||
|
||||
sendToMaster('messages-processed');
|
||||
}
|
||||
|
||||
async function processQueuedMessages(sendConfigurationId, messages) {
|
||||
if (running) {
|
||||
log.error('Senders', `Worker ${workerId} assigned work while working`);
|
||||
return;
|
||||
}
|
||||
|
||||
running = true;
|
||||
|
||||
for (const msgData of messages) {
|
||||
const queuedMessage = msgData.queuedMessage;
|
||||
try {
|
||||
await CampaignSender.sendQueuedMessage(queuedMessage);
|
||||
|
||||
log.verbose('Senders', 'Message sent and status updated for %s:%s', queuedMessage.list, queuedMessage.subscription);
|
||||
} catch (err) {
|
||||
log.error('Senders', `Sending message to ${queuedMessage.list}:${queuedMessage.subscription} failed with error: ${err.message}`);
|
||||
log.verbose(err.stack);
|
||||
}
|
||||
}
|
||||
|
@ -58,11 +74,14 @@ process.on('message', msg => {
|
|||
if (type === 'reload-config') {
|
||||
mailers.invalidateMailer(msg.data.sendConfigurationId);
|
||||
|
||||
} else if (type === 'process-messages') {
|
||||
} else if (type === 'process-campaign-messages') {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
processMessages(msg.data.campaignId, msg.data.subscribers)
|
||||
}
|
||||
processCampaignMessages(msg.data.campaignId, msg.data.messages)
|
||||
|
||||
} else if (type === 'process-queued-messages') {
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
processQueuedMessages(msg.data.sendConfigurationId, msg.data.messages)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ const { Entity, Event } = require('../../shared/triggers');
|
|||
const { SubscriptionStatus } = require('../../shared/lists');
|
||||
const links = require('../models/links');
|
||||
const contextHelpers = require('../lib/context-helpers');
|
||||
const {MessageType, CampaignSender} = require('../lib/campaign-sender');
|
||||
|
||||
const triggerCheckPeriod = 30 * 1000;
|
||||
const triggerFirePeriod = 120 * 1000;
|
||||
|
@ -151,12 +152,13 @@ async function run() {
|
|||
subscription: subscriber.id
|
||||
});
|
||||
|
||||
await tx('queued').insert({
|
||||
campaign: campaign.id,
|
||||
list: cpgList.list,
|
||||
subscription: subscriber.id,
|
||||
trigger: trigger.id
|
||||
});
|
||||
await CampaignSender.queueMessageTx(tx,
|
||||
campaign.send_configuration, cpgList.list, subscriber.id, MessageType.TRIGGERED,
|
||||
{
|
||||
campaignId: campaign.id,
|
||||
triggerId: trigger.id
|
||||
}
|
||||
);
|
||||
|
||||
await tx('triggers').increment('count').where('id', trigger.id);
|
||||
|
||||
|
|
|
@ -753,11 +753,11 @@ async function migrateSettings(knex) {
|
|||
|
||||
if (settings.dkimApiKey) {
|
||||
mailer_type = MailerType.ZONE_MTA;
|
||||
mailer_settings.dkimApiKey = settings.dkimApiKey;
|
||||
mailer_settings.dkimApiKey = settings.dkimApiKey || '';
|
||||
mailer_settings.zoneMtaType = ZoneMTAType.WITH_HTTP_CONF;
|
||||
mailer_settings.dkimDomain = settings.dkimDomain;
|
||||
mailer_settings.dkimSelector = settings.dkimSelector;
|
||||
mailer_settings.dkimPrivateKey = settings.dkimPrivateKey;
|
||||
mailer_settings.dkimDomain = settings.dkimDomain || '';
|
||||
mailer_settings.dkimSelector = settings.dkimSelector || '';
|
||||
mailer_settings.dkimPrivateKey = settings.dkimPrivateKey || '';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -777,7 +777,7 @@ async function migrateSettings(knex) {
|
|||
verp_hostname: settings.verpUse ? settings.verpHostname : null,
|
||||
mailer_type,
|
||||
mailer_settings: JSON.stringify(mailer_settings),
|
||||
x_mailer: settings.x_mailer,
|
||||
x_mailer: settings.x_mailer || '',
|
||||
namespace: getGlobalNamespaceId()
|
||||
});
|
||||
|
||||
|
@ -810,7 +810,7 @@ async function addFiles(knex) {
|
|||
table.string('mimetype');
|
||||
table.integer('size');
|
||||
table.timestamp('created').defaultTo(knex.fn.now());
|
||||
table.index(['entity', 'originalname'])
|
||||
table.index(['entity', 'originalname']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
const entityTypesWithFiles = {
|
||||
campaign: {
|
||||
file: 'files_campaign_file',
|
||||
attachment: 'files_campaign_attachment',
|
||||
},
|
||||
template: {
|
||||
file: 'files_template_file'
|
||||
},
|
||||
mosaico_template: {
|
||||
file: 'files_mosaico_template_file',
|
||||
block: 'files_mosaico_template_block'
|
||||
}
|
||||
};
|
||||
|
||||
exports.up = (knex, Promise) => (async() => {
|
||||
await knex.schema.table('queued', table => {
|
||||
table.integer('send_configuration').unsigned().notNullable();
|
||||
table.integer('type').unsigned().notNullable(); // The values come from campaign-sender.js:MessageType
|
||||
table.text('data', 'longtext');
|
||||
});
|
||||
|
||||
const queued = await knex('queued')
|
||||
.leftJoin('campaigns', 'queued.campaign', 'campaigns.id')
|
||||
.select(['queued.id', 'queued.trigger', 'queued.campaign', 'campaigns.send_configuration']);
|
||||
|
||||
for (const queuedEntry of queued) {
|
||||
const data = {};
|
||||
|
||||
if (queued.trigger) {
|
||||
data.triggerId = queuedEntry.trigger;
|
||||
data.campaignId = queuedEntry.campaign;
|
||||
}
|
||||
|
||||
knex('queued')
|
||||
.where('id', queuedEntry.id)
|
||||
.update({
|
||||
send_configuration: queuedEntry.send_configuration,
|
||||
data: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
await knex.schema.table('queued', table => {
|
||||
table.dropColumn('trigger');
|
||||
table.dropColumn('campaign');
|
||||
});
|
||||
|
||||
|
||||
for (const type in entityTypesWithFiles) {
|
||||
const typeEntry = entityTypesWithFiles[type];
|
||||
|
||||
for (const subType in typeEntry) {
|
||||
const subTypeEntry = typeEntry[subType];
|
||||
|
||||
await knex.schema.table(subTypeEntry, table => {
|
||||
table.boolean('delete_pending').notNullable().defaultTo(false);
|
||||
table.integer('lock_count').notNullable().defaultTo(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
})();
|
|
@ -0,0 +1,15 @@
|
|||
exports.up = (knex, Promise) => (async() => {
|
||||
await knex.schema.table('send_configurations', table => {
|
||||
table.dropColumn('subject');
|
||||
table.dropColumn('subject_overridable');
|
||||
});
|
||||
|
||||
await knex.schema.table('campaigns', table => {
|
||||
table.renameColumn('subject_override', 'subject');
|
||||
});
|
||||
|
||||
await knex('campaigns').whereNull('subject').update('subject', '');
|
||||
})();
|
||||
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
})();
|
Loading…
Add table
Add a link
Reference in a new issue