- 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
|
@ -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
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue