Various fixes.
This commit is contained in:
parent
dd9b8b464a
commit
83ce716d94
21 changed files with 99 additions and 114 deletions
|
@ -109,12 +109,10 @@ export default class List extends Component {
|
|||
link: `/campaigns/${data[0]}/status`
|
||||
});
|
||||
|
||||
if (status === CampaignStatus.SENDING || status === CampaignStatus.PAUSED || status === CampaignStatus.FINISHED) {
|
||||
actions.push({
|
||||
label: <Icon icon="signal" title={t('statistics')}/>,
|
||||
link: `/campaigns/${data[0]}/statistics`
|
||||
});
|
||||
}
|
||||
actions.push({
|
||||
label: <Icon icon="signal" title={t('statistics')}/>,
|
||||
link: `/campaigns/${data[0]}/statistics`
|
||||
});
|
||||
}
|
||||
|
||||
if (perms.includes('edit')) {
|
||||
|
|
|
@ -35,7 +35,6 @@ export default class Statistics extends Component {
|
|||
|
||||
this.state = {
|
||||
entity: props.entity,
|
||||
statisticsOverview: props.statisticsOverview
|
||||
};
|
||||
|
||||
this.refreshTimeoutHandler = ::this.periodicRefreshTask;
|
||||
|
@ -43,8 +42,7 @@ export default class Statistics extends Component {
|
|||
}
|
||||
|
||||
static propTypes = {
|
||||
entity: PropTypes.object,
|
||||
statisticsOverview: PropTypes.object
|
||||
entity: PropTypes.object
|
||||
}
|
||||
|
||||
@withAsyncErrorHandler
|
||||
|
@ -54,12 +52,8 @@ export default class Statistics extends Component {
|
|||
resp = await axios.get(getUrl(`rest/campaigns-stats/${this.props.entity.id}`));
|
||||
const entity = resp.data;
|
||||
|
||||
resp = await axios.get(getUrl(`rest/campaign-statistics/${this.props.entity.id}/overview`));
|
||||
const statisticsOverview = resp.data;
|
||||
|
||||
this.setState({
|
||||
entity,
|
||||
statisticsOverview
|
||||
entity
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -85,11 +79,10 @@ export default class Statistics extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
const entity = this.state.entity;
|
||||
|
||||
const stats = this.state.statisticsOverview;
|
||||
const total = entity.subscriptionsToSend === undefined ? undefined : entity.subscriptionsToSend + entity.delivered;
|
||||
|
||||
const renderMetrics = (key, label, showZoomIn = true) => {
|
||||
const val = stats[key]
|
||||
const val = entity[key]
|
||||
|
||||
return (
|
||||
<AlignedRow label={label}><span className={styles.statsMetrics}>{val}</span>{showZoomIn && <span className={styles.zoomIn}><Link to={`/campaigns/${entity.id}/statistics/${key}`}><Icon icon="zoom-in"/></Link></span>}</AlignedRow>
|
||||
|
@ -97,13 +90,13 @@ export default class Statistics extends Component {
|
|||
}
|
||||
|
||||
const renderMetricsWithProgress = (key, label, progressBarClass, showZoomIn = true) => {
|
||||
const val = stats[key]
|
||||
const val = entity[key]
|
||||
|
||||
if (!stats.total) {
|
||||
if (!total) {
|
||||
return renderMetrics(key, label);
|
||||
}
|
||||
|
||||
const rate = Math.round(val / stats.total * 100);
|
||||
const rate = Math.round(val / total * 100);
|
||||
|
||||
return (
|
||||
<AlignedRow label={label}>
|
||||
|
@ -112,9 +105,6 @@ export default class Statistics extends Component {
|
|||
<div
|
||||
className={`progress-bar progress-bar-${progressBarClass}`}
|
||||
role="progressbar"
|
||||
aria-valuenow={stats.bounced}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
style={{minWidth: '6em', width: rate + '%'}}>
|
||||
{val} ({rate}%)
|
||||
</div>
|
||||
|
|
|
@ -60,7 +60,7 @@ export default class StatisticsOpened extends Component {
|
|||
async refreshEntity() {
|
||||
let resp;
|
||||
|
||||
resp = await axios.get(getUrl(`rest/campaigns-stats/${this.props.entity.id}`));
|
||||
resp = await axios.get(getUrl(`rest/campaigns-settings/${this.props.entity.id}`));
|
||||
const entity = resp.data;
|
||||
|
||||
resp = await axios.get(getUrl(`rest/campaign-statistics/${this.props.entity.id}/opened`));
|
||||
|
@ -132,7 +132,7 @@ export default class StatisticsOpened extends Component {
|
|||
<h4 className={styles.chartTitle}>{t('Distribution by device type')}</h4>
|
||||
<Chart
|
||||
width="100%"
|
||||
height="300px"
|
||||
height="380px"
|
||||
chartType="PieChart"
|
||||
loader={<div>{t('Loading chart')}</div>}
|
||||
data={[
|
||||
|
@ -144,7 +144,7 @@ export default class StatisticsOpened extends Component {
|
|||
left: "25%",
|
||||
top: 15,
|
||||
width: "100%",
|
||||
height: 270
|
||||
height: 350
|
||||
},
|
||||
tooltip: {
|
||||
showColorCode: true
|
||||
|
@ -169,7 +169,7 @@ export default class StatisticsOpened extends Component {
|
|||
<div className={`col-md-6 ${styles.chart}`}>
|
||||
<Chart
|
||||
width="100%"
|
||||
height="300px"
|
||||
height="380px"
|
||||
chartType="PieChart"
|
||||
loader={<div>{t('Loading chart')}</div>}
|
||||
data={[
|
||||
|
@ -181,7 +181,7 @@ export default class StatisticsOpened extends Component {
|
|||
left: "25%",
|
||||
top: 15,
|
||||
width: "100%",
|
||||
height: 270
|
||||
height: 350
|
||||
},
|
||||
tooltip: {
|
||||
showColorCode: true
|
||||
|
@ -199,7 +199,7 @@ export default class StatisticsOpened extends Component {
|
|||
<div className={`col-md-6 ${styles.chart}`}>
|
||||
<Chart
|
||||
width="100%"
|
||||
height="300px"
|
||||
height="380px"
|
||||
chartType="GeoChart"
|
||||
data={[
|
||||
['Country', 'Count'],
|
||||
|
|
|
@ -273,7 +273,7 @@ class SendControls extends Component {
|
|||
|
||||
if (entity.status === CampaignStatus.IDLE || entity.status === CampaignStatus.PAUSED || (entity.status === CampaignStatus.SCHEDULED && entity.scheduled)) {
|
||||
|
||||
const subscrInfo = entity.subscriptionsTotal === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers-1')})`;
|
||||
const subscrInfo = entity.subscriptionsToSend === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers-1')})`;
|
||||
|
||||
return (
|
||||
<div>{yesNoDialog}
|
||||
|
@ -316,7 +316,7 @@ class SendControls extends Component {
|
|||
);
|
||||
|
||||
} else if (entity.status === CampaignStatus.FINISHED) {
|
||||
const subscrInfo = entity.subscriptionsTotal === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers-1')})`;
|
||||
const subscrInfo = entity.subscriptionsToSend === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers-1')})`;
|
||||
|
||||
return (
|
||||
<div>{yesNoDialog}
|
||||
|
@ -479,7 +479,7 @@ export default class Status extends Component {
|
|||
<Title>{t('campaignStatus')}</Title>
|
||||
|
||||
<AlignedRow label={t('name')}>{entity.name}</AlignedRow>
|
||||
<AlignedRow label={t('subscribers')}>{entity.subscriptionsTotal === undefined ? t('computing') : entity.subscriptionsTotal}</AlignedRow>
|
||||
<AlignedRow label={t('Delivered')}>{entity.delivered}</AlignedRow>
|
||||
<AlignedRow label={t('status')}>{this.campaignStatusLabels[entity.status]}</AlignedRow>
|
||||
|
||||
{sendSettings}
|
||||
|
|
|
@ -62,12 +62,9 @@ function getMenus(t) {
|
|||
},
|
||||
statistics: {
|
||||
title: t('statistics'),
|
||||
resolve: {
|
||||
statisticsOverview: params => `rest/campaign-statistics/${params.campaignId}/overview`
|
||||
},
|
||||
link: params => `/campaigns/${params.campaignId}/statistics`,
|
||||
visible: resolved => resolved.campaign.permissions.includes('viewStats') && (resolved.campaign.status === CampaignStatus.SENDING || resolved.campaign.status === CampaignStatus.PAUSED || resolved.campaign.status === CampaignStatus.FINISHED),
|
||||
panelRender: props => <Statistics entity={props.resolved.campaign} statisticsOverview={props.resolved.statisticsOverview} />,
|
||||
visible: resolved => resolved.campaign.permissions.includes('viewStats'),
|
||||
panelRender: props => <Statistics entity={props.resolved.campaign} />,
|
||||
children: {
|
||||
delivered: {
|
||||
title: t('Delivered'),
|
||||
|
|
|
@ -201,7 +201,7 @@ export class UntrustedContentRoot extends Component {
|
|||
this.clientHeight = newHeight;
|
||||
this.sendMessage('clientHeight', newHeight);
|
||||
}
|
||||
//this.periodicTimeoutId = setTimeout(this.periodicTimeoutHandler, 250);
|
||||
this.periodicTimeoutId = setTimeout(this.periodicTimeoutHandler, 250);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -216,7 +216,7 @@ export default class CUD extends Component {
|
|||
|
||||
<TextArea id="description" label={t('description')}/>
|
||||
|
||||
<InputField id="contact_email" label={t('contactEmail')} help={t('contactEmailUsedInSubscriptionFormsAnd')}/>
|
||||
<InputField id="contact_email" label={t('contactEmail')} help={t('Contact email shown in the list subscription and management forms. If no contact email is given, the admin email from Global settings is used.')}/>
|
||||
<InputField id="homepage" label={t('homepage')} help={t('homepageUrlUsedInSubscriptionFormsAnd')}/>
|
||||
<InputField id="to_name" label={t('recipientsNameTemplate')} help={t('specifyUsingMergeTagsOfThisListHowTo')}/>
|
||||
<TableSelect id="send_configuration" label={t('sendConfiguration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} help={t('sendConfigurationThatWillBeUsedFor')}/>
|
||||
|
|
|
@ -90,7 +90,7 @@ export default class CUD extends Component {
|
|||
' Email\n' +
|
||||
' </th>\n' +
|
||||
' <th>\n' +
|
||||
' Tracker Count\n' +
|
||||
' Open Count\n' +
|
||||
' </th>\n' +
|
||||
' </thead>\n' +
|
||||
' {{#if results}}\n' +
|
||||
|
|
|
@ -271,7 +271,7 @@ export function getMailerTypes(t) {
|
|||
beforeSaveGenericSMTP(data, zoneMtaType === ZoneMTAType.BUILTIN);
|
||||
|
||||
data.mailer_settings.zoneMtaType = zoneMtaType;
|
||||
if (zoneMtaType === ZoneMTAType.WITH_HTTP_CONF || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF) {
|
||||
if (zoneMtaType === ZoneMTAType.BUILTIN || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF) {
|
||||
data.mailer_settings.dkimDomain = data.dkimDomain;
|
||||
data.mailer_settings.dkimSelector = data.dkimSelector;
|
||||
data.mailer_settings.dkimPrivateKey = data.dkimPrivateKey;
|
||||
|
|
|
@ -10,9 +10,9 @@ const crypto = require('crypto');
|
|||
let zoneMtaProcess;
|
||||
|
||||
const zoneMtaDir = path.join(__dirname, '..', '..', 'zone-mta');
|
||||
const zoneMtaBuiltingConfig = path.join(zoneMtaDir, 'config', 'builtin-zonemta.json')
|
||||
const zoneMtaBuiltingConfig = path.join(zoneMtaDir, 'config', 'builtin-zonemta.json');
|
||||
|
||||
const password = crypto.randomBytes(20).toString('hex').toLowerCase();
|
||||
const password = process.env.BUILTIN_ZONE_MTA_PASSWORD || crypto.randomBytes(20).toString('hex').toLowerCase();
|
||||
|
||||
function getUsername() {
|
||||
return 'mailtrain';
|
||||
|
|
|
@ -95,11 +95,18 @@ async function _sendMail(transport, mail, template) {
|
|||
}
|
||||
|
||||
async function _sendTransactionalMail(transport, mail, template) {
|
||||
const sendConfiguration = transport.mailer.sendConfiguration;
|
||||
|
||||
if (!mail.headers) {
|
||||
mail.headers = {};
|
||||
}
|
||||
mail.headers['X-Sending-Zone'] = 'transactional';
|
||||
|
||||
mail.from = {
|
||||
name: sendConfiguration.from_name,
|
||||
address: sendConfiguration.from_email
|
||||
};
|
||||
|
||||
const htmlRenderer = await tools.getTemplate(template.html, template.locale);
|
||||
|
||||
if (htmlRenderer) {
|
||||
|
|
|
@ -5,6 +5,7 @@ const log = require('./log');
|
|||
const path = require('path');
|
||||
const knex = require('./knex');
|
||||
const {CampaignStatus} = require('../../shared/campaigns');
|
||||
const builtinZoneMta = require('./builtin-zone-mta');
|
||||
|
||||
let messageTid = 0;
|
||||
let senderProcess;
|
||||
|
@ -16,7 +17,10 @@ function spawn(callback) {
|
|||
.then(() => {
|
||||
senderProcess = fork(path.join(__dirname, '..', 'services', 'sender-master.js'), [], {
|
||||
cwd: path.join(__dirname, '..'),
|
||||
env: {NODE_ENV: process.env.NODE_ENV}
|
||||
env: {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
BUILTIN_ZONE_MTA_PASSWORD: builtinZoneMta.getPassword()
|
||||
}
|
||||
});
|
||||
|
||||
senderProcess.on('message', msg => {
|
||||
|
|
|
@ -110,7 +110,7 @@ async function _sendMail(list, email, template, locale, subjectKey, relativeUrls
|
|||
const data = {
|
||||
title: list.name,
|
||||
homepage: configItems.defaultHomepage || getTrustedUrl(),
|
||||
contactAddress: list.from_email || configItems.adminEmail,
|
||||
contactAddress: list.contact_email || configItems.adminEmail,
|
||||
};
|
||||
|
||||
for (let relativeUrlKey in relativeUrls) {
|
||||
|
@ -140,10 +140,6 @@ async function _sendMail(list, email, template, locale, subjectKey, relativeUrls
|
|||
if (list.send_configuration) {
|
||||
const mailer = await mailers.getOrCreateMailer(list.send_configuration);
|
||||
await mailer.sendTransactionalMail({
|
||||
from: {
|
||||
name: configItems.defaultFrom,
|
||||
address: configItems.defaultAddress
|
||||
},
|
||||
to: {
|
||||
name: getDisplayName(flds, subscription),
|
||||
address: email
|
||||
|
|
|
@ -324,6 +324,7 @@ async function rawGetByTx(tx, key, id) {
|
|||
'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.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`)
|
||||
])
|
||||
.first();
|
||||
|
@ -361,18 +362,12 @@ async function getByIdTx(tx, context, id, withPermissions = true, content = Cont
|
|||
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'viewStats');
|
||||
|
||||
const unsentQryGen = await getSubscribersQueryGeneratorTx(tx, id, true);
|
||||
const unsentQryGen = await getSubscribersQueryGeneratorTx(tx, id);
|
||||
if (unsentQryGen) {
|
||||
const res = await unsentQryGen(tx).count('* AS subscriptionsToSend').first();
|
||||
entity.subscriptionsToSend = res.subscriptionsToSend;
|
||||
}
|
||||
|
||||
const totalQryGen = await getSubscribersQueryGeneratorTx(tx, id, false);
|
||||
if (totalQryGen) {
|
||||
const res = await totalQryGen(tx).count('* AS subscriptionsTotal').first();
|
||||
entity.subscriptionsTotal = res.subscriptionsTotal;
|
||||
}
|
||||
|
||||
} else if (content === Content.WITHOUT_SOURCE_CUSTOM) {
|
||||
delete entity.data.sourceCustom;
|
||||
|
||||
|
@ -765,7 +760,7 @@ async function updateMessageResponse(context, message, response, responseId) {
|
|||
});
|
||||
}
|
||||
|
||||
async function getSubscribersQueryGeneratorTx(tx, campaignId, onlyUnsent) {
|
||||
async function getSubscribersQueryGeneratorTx(tx, campaignId) {
|
||||
/*
|
||||
This is supposed to produce queries like this:
|
||||
|
||||
|
@ -812,7 +807,7 @@ async function getSubscribersQueryGeneratorTx(tx, campaignId, onlyUnsent) {
|
|||
|
||||
if (subsQrys.length > 0) {
|
||||
let subsQry;
|
||||
const unsentWhere = onlyUnsent ? ' where `sent` = false' : '';
|
||||
const unsentWhere = ' where `sent` = false';
|
||||
|
||||
if (subsQrys.length === 1) {
|
||||
const subsUnionSql = '(select `email`, `campaign_list_id`, `sent` from (' + subsQrys[0].sql + ') as `pending_subscriptions_all`' + unsentWhere + ') as `pending_subscriptions`'
|
||||
|
@ -905,30 +900,12 @@ async function disable(context, campaignId) {
|
|||
}
|
||||
|
||||
|
||||
async function getStatisticsOverview(context, id) {
|
||||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'viewStats');
|
||||
|
||||
const stats = await tx('campaigns').where('id', id).select(['delivered', 'unsubscribed', 'bounced', 'complained', 'blacklisted', 'opened', 'clicks']).first();
|
||||
|
||||
const totalQryGen = await getSubscribersQueryGeneratorTx(tx, id, false);
|
||||
if (totalQryGen) {
|
||||
const res = await totalQryGen(tx).count('* AS subscriptionsTotal').first();
|
||||
stats.total = res.subscriptionsTotal;
|
||||
} else {
|
||||
stats.total = 0;
|
||||
}
|
||||
|
||||
return stats;
|
||||
});
|
||||
}
|
||||
|
||||
async function getStatisticsOpened(context, id) {
|
||||
return await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'viewStats');
|
||||
|
||||
const devices = await tx('campaign_links').where('campaign', id).groupBy('device_type').select('device_type AS key').count('* as count');
|
||||
const countries = await tx('campaign_links').where('campaign', id).groupBy('country').select('country AS key').count('* as count');
|
||||
const devices = await tx('campaign_links').where('campaign', id).where('link', LinkId.OPEN).groupBy('device_type').select('device_type AS key').count('* as count');
|
||||
const countries = await tx('campaign_links').where('campaign', id).where('link', LinkId.OPEN).groupBy('country').select('country AS key').count('* as count');
|
||||
|
||||
return {
|
||||
devices,
|
||||
|
@ -976,5 +953,4 @@ module.exports.disable = disable;
|
|||
|
||||
module.exports.rawGetByTx = rawGetByTx;
|
||||
module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx;
|
||||
module.exports.getStatisticsOverview = getStatisticsOverview;
|
||||
module.exports.getStatisticsOpened = getStatisticsOpened;
|
||||
|
|
|
@ -32,7 +32,10 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
|
|||
const subscription = await subscriptions.getByCidTx(tx, contextHelpers.getAdminContext(), list.id, subscriptionCid);
|
||||
|
||||
const country = geoip.lookupCountry(remoteIp) || null;
|
||||
const device = uaParser(userAgent, { unknownUserAgentDeviceType: 'desktop', emptyUserAgentDeviceType: 'desktop' });
|
||||
const device = uaParser(userAgent, {
|
||||
unknownUserAgentDeviceType: 'desktop',
|
||||
emptyUserAgentDeviceType: 'desktop'
|
||||
});
|
||||
const now = new Date();
|
||||
|
||||
const _countLink = async (clickLinkId, incrementOnDup) => {
|
||||
|
@ -66,7 +69,6 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
|
|||
};
|
||||
|
||||
|
||||
|
||||
// Update opened and click timestamps
|
||||
const latestUpdates = {};
|
||||
|
||||
|
@ -82,7 +84,6 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
|
|||
await tx(subscriptions.getSubscriptionTableName(list.id)).update(latestUpdates).where('id', subscription.id);
|
||||
}
|
||||
|
||||
|
||||
// Update clicks
|
||||
if (linkId > LinkId.GENERAL_CLICK && !campaign.click_tracking_disabled) {
|
||||
await tx('links').increment('hits').where('id', linkId);
|
||||
|
@ -125,6 +126,8 @@ async function addOrGet(campaignId, url) {
|
|||
id: ids[0],
|
||||
cid
|
||||
};
|
||||
} else {
|
||||
return link;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -255,31 +255,24 @@ async function _getCampaignStatistics(campaign, select, unionQryFn, listQryFn, a
|
|||
} else {
|
||||
return knex.raw(subsSql, subsBindings);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (subsQrys.length === 1) {
|
||||
subsSql = subsQrys[0].sql;
|
||||
subsBindings = subsQrys[0].bindings;
|
||||
|
||||
if (asStream) {
|
||||
return await applyUnionQryFn(subsSql, subsBindings).stream();
|
||||
} else {
|
||||
return await applyUnionQryFn(subsSql, subsBindings);
|
||||
}
|
||||
|
||||
} else {
|
||||
subsSql = subsQrys.map(qry => '(' + qry.sql + ')').join(' UNION ALL ');
|
||||
subsBindings = Array.prototype.concat(...subsQrys.map(qry => qry.bindings));
|
||||
}
|
||||
|
||||
if (asStream) {
|
||||
return applyUnionQryFn(subsSql, subsBindings).stream();
|
||||
if (asStream) {
|
||||
return applyUnionQryFn(subsSql, subsBindings).stream();
|
||||
} else {
|
||||
const res = await applyUnionQryFn(subsSql, subsBindings);
|
||||
if (res[0] && Array.isArray(res[0])) {
|
||||
return res[0]; // UNION ALL generates an array with result and schema
|
||||
} else {
|
||||
const res = await applyUnionQryFn(subsSql, subsBindings);
|
||||
if (res[0] && Array.isArray(res[0])) {
|
||||
return res[0]; // UNION ALL generates an array with result and schema
|
||||
} else {
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -304,9 +304,6 @@ async function sendPasswordReset(locale, usernameOrEmail) {
|
|||
|
||||
const mailer = await mailers.getOrCreateMailer();
|
||||
await mailer.sendTransactionalMail({
|
||||
from: {
|
||||
address: adminEmail
|
||||
},
|
||||
to: {
|
||||
address: user.email
|
||||
},
|
||||
|
|
|
@ -13,9 +13,10 @@ router.getAsync('/:campaign/:list/:subscription/:link', async (req, res) => {
|
|||
|
||||
if (link) {
|
||||
// In Mailtrain v1 we would do the URL expansion here based on merge tags. We don't do it here anymore. Instead, the URLs are expanded when message is sent out (in links.updateLinks)
|
||||
res.redirect(link.url);
|
||||
res.redirect(302, link.url);
|
||||
|
||||
await links.countLink(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, link.id);
|
||||
|
||||
} else {
|
||||
log.error('Redirect', 'Unresolved URL: <%s>', req.url);
|
||||
throw new interoperableErrors.NotFoundError('Oops, we couldn\'t find a link for the URL you clicked');
|
||||
|
|
|
@ -94,10 +94,6 @@ router.postAsync('/campaign-disable/:campaignId', passport.loggedIn, passport.cs
|
|||
return res.json(await campaigns.disable(req.context, castToInteger(req.params.campaignId), null));
|
||||
});
|
||||
|
||||
router.getAsync('/campaign-statistics/:campaignId/overview', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await campaigns.getStatisticsOverview(req.context, castToInteger(req.params.campaignId)));
|
||||
});
|
||||
|
||||
router.getAsync('/campaign-statistics/:campaignId/opened', passport.loggedIn, async (req, res) => {
|
||||
return res.json(await campaigns.getStatisticsOpened(req.context, castToInteger(req.params.campaignId)));
|
||||
});
|
||||
|
|
|
@ -670,8 +670,8 @@ async function webNotice(type, req, res) {
|
|||
|
||||
const data = {
|
||||
title: list.name,
|
||||
homepage: configItems.defaultHomepage || getTrustedUrl(),
|
||||
contactAddress: list.from_email || configItems.adminEmail,
|
||||
homepage: list.homepage || configItems.defaultHomepage || getTrustedUrl(),
|
||||
contactAddress: list.contact_email || configItems.adminEmail,
|
||||
template: {
|
||||
template: 'subscription/web-' + type + '-notice.mjml.hbs',
|
||||
layout: 'subscription/layout.mjml.hbs',
|
||||
|
|
|
@ -8,6 +8,7 @@ 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');
|
||||
|
||||
let messageTid = 0;
|
||||
const workerProcesses = new Map();
|
||||
|
@ -24,6 +25,7 @@ const workerBatchSize = 100;
|
|||
|
||||
const messageQueue = new Map(); // campaignId -> [{listId, email}]
|
||||
const messageQueueCont = new Map(); // campaignId -> next batch callback
|
||||
const campaignFinishCont = new Map(); // campaignId -> worker finished callback
|
||||
|
||||
const workAssignment = new Map(); // workerId -> { campaignId, subscribers: [{listId, email}] }
|
||||
|
||||
|
@ -32,6 +34,8 @@ let queuedLastId = 0;
|
|||
|
||||
|
||||
function messagesProcessed(workerId) {
|
||||
const wa = workAssignment.get(workerId);
|
||||
|
||||
workAssignment.delete(workerId);
|
||||
idleWorkers.push(workerId);
|
||||
|
||||
|
@ -40,6 +44,11 @@ function messagesProcessed(workerId) {
|
|||
setImmediate(workerSchedulerCont);
|
||||
workerSchedulerCont = null;
|
||||
}
|
||||
|
||||
if (campaignFinishCont.has(wa.campaignId)) {
|
||||
setImmediate(campaignFinishCont.get(wa.campaignId));
|
||||
campaignFinishCont.delete(wa.campaignId);
|
||||
}
|
||||
}
|
||||
|
||||
async function scheduleWorkers() {
|
||||
|
@ -78,8 +87,8 @@ async function scheduleWorkers() {
|
|||
workAssignment.set(workerId, {campaignId, subscribers});
|
||||
|
||||
if (queue.length === 0 && messageQueueCont.has(campaignId)) {
|
||||
const scheduleMessages = messageQueueCont.get(campaignId);
|
||||
setImmediate(scheduleMessages);
|
||||
setImmediate(messageQueueCont.get(campaignId));
|
||||
messageQueueCont.delete(campaignId);
|
||||
}
|
||||
|
||||
sendToWorker(workerId, 'process-messages', {
|
||||
|
@ -99,9 +108,24 @@ async function scheduleWorkers() {
|
|||
}
|
||||
|
||||
|
||||
|
||||
async function processCampaign(campaignId) {
|
||||
async function finish() {
|
||||
let workerRunning = false;
|
||||
for (const wa of workAssignment.values()) {
|
||||
if (wa.campaignId === campaignId) {
|
||||
workerRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (workerRunning) {
|
||||
const workerFinished = new Promise(resolve => {
|
||||
campaignFinishCont.set(campaignId, resolve);
|
||||
});
|
||||
|
||||
await workerFinished;
|
||||
setImmediate(finish);
|
||||
}
|
||||
|
||||
await knex('campaigns').where('id', campaignId).update({status: CampaignStatus.FINISHED});
|
||||
messageQueue.delete(campaignId);
|
||||
}
|
||||
|
@ -120,7 +144,7 @@ async function processCampaign(campaignId) {
|
|||
|
||||
let qryGen;
|
||||
await knex.transaction(async tx => {
|
||||
qryGen = await campaigns.getSubscribersQueryGeneratorTx(tx, campaignId, true);
|
||||
qryGen = await campaigns.getSubscribersQueryGeneratorTx(tx, campaignId);
|
||||
});
|
||||
|
||||
if (qryGen) {
|
||||
|
@ -262,7 +286,10 @@ async function spawnWorker(workerId) {
|
|||
|
||||
const senderProcess = fork(path.join(__dirname, 'sender-worker.js'), [workerId], {
|
||||
cwd: path.join(__dirname, '..'),
|
||||
env: {NODE_ENV: process.env.NODE_ENV}
|
||||
env: {
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
BUILTIN_ZONE_MTA_PASSWORD: builtinZoneMta.getPassword()
|
||||
}
|
||||
});
|
||||
|
||||
senderProcess.on('message', msg => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue