Various fixes.

This commit is contained in:
Tomas Bures 2018-12-23 19:27:29 +00:00
parent dd9b8b464a
commit 83ce716d94
21 changed files with 99 additions and 114 deletions

View file

@ -109,13 +109,11 @@ export default class List extends Component {
link: `/campaigns/${data[0]}/status` link: `/campaigns/${data[0]}/status`
}); });
if (status === CampaignStatus.SENDING || status === CampaignStatus.PAUSED || status === CampaignStatus.FINISHED) {
actions.push({ actions.push({
label: <Icon icon="signal" title={t('statistics')}/>, label: <Icon icon="signal" title={t('statistics')}/>,
link: `/campaigns/${data[0]}/statistics` link: `/campaigns/${data[0]}/statistics`
}); });
} }
}
if (perms.includes('edit')) { if (perms.includes('edit')) {
actions.push({ actions.push({

View file

@ -35,7 +35,6 @@ export default class Statistics extends Component {
this.state = { this.state = {
entity: props.entity, entity: props.entity,
statisticsOverview: props.statisticsOverview
}; };
this.refreshTimeoutHandler = ::this.periodicRefreshTask; this.refreshTimeoutHandler = ::this.periodicRefreshTask;
@ -43,8 +42,7 @@ export default class Statistics extends Component {
} }
static propTypes = { static propTypes = {
entity: PropTypes.object, entity: PropTypes.object
statisticsOverview: PropTypes.object
} }
@withAsyncErrorHandler @withAsyncErrorHandler
@ -54,12 +52,8 @@ export default class Statistics extends Component {
resp = await axios.get(getUrl(`rest/campaigns-stats/${this.props.entity.id}`)); resp = await axios.get(getUrl(`rest/campaigns-stats/${this.props.entity.id}`));
const entity = resp.data; const entity = resp.data;
resp = await axios.get(getUrl(`rest/campaign-statistics/${this.props.entity.id}/overview`));
const statisticsOverview = resp.data;
this.setState({ this.setState({
entity, entity
statisticsOverview
}); });
} }
@ -85,11 +79,10 @@ export default class Statistics extends Component {
render() { render() {
const t = this.props.t; const t = this.props.t;
const entity = this.state.entity; const entity = this.state.entity;
const total = entity.subscriptionsToSend === undefined ? undefined : entity.subscriptionsToSend + entity.delivered;
const stats = this.state.statisticsOverview;
const renderMetrics = (key, label, showZoomIn = true) => { const renderMetrics = (key, label, showZoomIn = true) => {
const val = stats[key] const val = entity[key]
return ( 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> <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 renderMetricsWithProgress = (key, label, progressBarClass, showZoomIn = true) => {
const val = stats[key] const val = entity[key]
if (!stats.total) { if (!total) {
return renderMetrics(key, label); return renderMetrics(key, label);
} }
const rate = Math.round(val / stats.total * 100); const rate = Math.round(val / total * 100);
return ( return (
<AlignedRow label={label}> <AlignedRow label={label}>
@ -112,9 +105,6 @@ export default class Statistics extends Component {
<div <div
className={`progress-bar progress-bar-${progressBarClass}`} className={`progress-bar progress-bar-${progressBarClass}`}
role="progressbar" role="progressbar"
aria-valuenow={stats.bounced}
aria-valuemin="0"
aria-valuemax="100"
style={{minWidth: '6em', width: rate + '%'}}> style={{minWidth: '6em', width: rate + '%'}}>
{val}&nbsp;({rate}%) {val}&nbsp;({rate}%)
</div> </div>

View file

@ -60,7 +60,7 @@ export default class StatisticsOpened extends Component {
async refreshEntity() { async refreshEntity() {
let resp; 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; const entity = resp.data;
resp = await axios.get(getUrl(`rest/campaign-statistics/${this.props.entity.id}/opened`)); 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> <h4 className={styles.chartTitle}>{t('Distribution by device type')}</h4>
<Chart <Chart
width="100%" width="100%"
height="300px" height="380px"
chartType="PieChart" chartType="PieChart"
loader={<div>{t('Loading chart')}</div>} loader={<div>{t('Loading chart')}</div>}
data={[ data={[
@ -144,7 +144,7 @@ export default class StatisticsOpened extends Component {
left: "25%", left: "25%",
top: 15, top: 15,
width: "100%", width: "100%",
height: 270 height: 350
}, },
tooltip: { tooltip: {
showColorCode: true showColorCode: true
@ -169,7 +169,7 @@ export default class StatisticsOpened extends Component {
<div className={`col-md-6 ${styles.chart}`}> <div className={`col-md-6 ${styles.chart}`}>
<Chart <Chart
width="100%" width="100%"
height="300px" height="380px"
chartType="PieChart" chartType="PieChart"
loader={<div>{t('Loading chart')}</div>} loader={<div>{t('Loading chart')}</div>}
data={[ data={[
@ -181,7 +181,7 @@ export default class StatisticsOpened extends Component {
left: "25%", left: "25%",
top: 15, top: 15,
width: "100%", width: "100%",
height: 270 height: 350
}, },
tooltip: { tooltip: {
showColorCode: true showColorCode: true
@ -199,7 +199,7 @@ export default class StatisticsOpened extends Component {
<div className={`col-md-6 ${styles.chart}`}> <div className={`col-md-6 ${styles.chart}`}>
<Chart <Chart
width="100%" width="100%"
height="300px" height="380px"
chartType="GeoChart" chartType="GeoChart"
data={[ data={[
['Country', 'Count'], ['Country', 'Count'],

View file

@ -273,7 +273,7 @@ class SendControls extends Component {
if (entity.status === CampaignStatus.IDLE || entity.status === CampaignStatus.PAUSED || (entity.status === CampaignStatus.SCHEDULED && entity.scheduled)) { 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 ( return (
<div>{yesNoDialog} <div>{yesNoDialog}
@ -316,7 +316,7 @@ class SendControls extends Component {
); );
} else if (entity.status === CampaignStatus.FINISHED) { } 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 ( return (
<div>{yesNoDialog} <div>{yesNoDialog}
@ -479,7 +479,7 @@ export default class Status extends Component {
<Title>{t('campaignStatus')}</Title> <Title>{t('campaignStatus')}</Title>
<AlignedRow label={t('name')}>{entity.name}</AlignedRow> <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> <AlignedRow label={t('status')}>{this.campaignStatusLabels[entity.status]}</AlignedRow>
{sendSettings} {sendSettings}

View file

@ -62,12 +62,9 @@ function getMenus(t) {
}, },
statistics: { statistics: {
title: t('statistics'), title: t('statistics'),
resolve: {
statisticsOverview: params => `rest/campaign-statistics/${params.campaignId}/overview`
},
link: params => `/campaigns/${params.campaignId}/statistics`, 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), visible: resolved => resolved.campaign.permissions.includes('viewStats'),
panelRender: props => <Statistics entity={props.resolved.campaign} statisticsOverview={props.resolved.statisticsOverview} />, panelRender: props => <Statistics entity={props.resolved.campaign} />,
children: { children: {
delivered: { delivered: {
title: t('Delivered'), title: t('Delivered'),

View file

@ -201,7 +201,7 @@ export class UntrustedContentRoot extends Component {
this.clientHeight = newHeight; this.clientHeight = newHeight;
this.sendMessage('clientHeight', newHeight); this.sendMessage('clientHeight', newHeight);
} }
//this.periodicTimeoutId = setTimeout(this.periodicTimeoutHandler, 250); this.periodicTimeoutId = setTimeout(this.periodicTimeoutHandler, 250);
} }

View file

@ -216,7 +216,7 @@ export default class CUD extends Component {
<TextArea id="description" label={t('description')}/> <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="homepage" label={t('homepage')} help={t('homepageUrlUsedInSubscriptionFormsAnd')}/>
<InputField id="to_name" label={t('recipientsNameTemplate')} help={t('specifyUsingMergeTagsOfThisListHowTo')}/> <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')}/> <TableSelect id="send_configuration" label={t('sendConfiguration')} withHeader dropdown dataUrl='rest/send-configurations-table' columns={sendConfigurationsColumns} selectionLabelIndex={1} help={t('sendConfigurationThatWillBeUsedFor')}/>

View file

@ -90,7 +90,7 @@ export default class CUD extends Component {
' Email\n' + ' Email\n' +
' </th>\n' + ' </th>\n' +
' <th>\n' + ' <th>\n' +
' Tracker Count\n' + ' Open Count\n' +
' </th>\n' + ' </th>\n' +
' </thead>\n' + ' </thead>\n' +
' {{#if results}}\n' + ' {{#if results}}\n' +

View file

@ -271,7 +271,7 @@ export function getMailerTypes(t) {
beforeSaveGenericSMTP(data, zoneMtaType === ZoneMTAType.BUILTIN); beforeSaveGenericSMTP(data, zoneMtaType === ZoneMTAType.BUILTIN);
data.mailer_settings.zoneMtaType = zoneMtaType; 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.dkimDomain = data.dkimDomain;
data.mailer_settings.dkimSelector = data.dkimSelector; data.mailer_settings.dkimSelector = data.dkimSelector;
data.mailer_settings.dkimPrivateKey = data.dkimPrivateKey; data.mailer_settings.dkimPrivateKey = data.dkimPrivateKey;

View file

@ -10,9 +10,9 @@ const crypto = require('crypto');
let zoneMtaProcess; let zoneMtaProcess;
const zoneMtaDir = path.join(__dirname, '..', '..', 'zone-mta'); 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() { function getUsername() {
return 'mailtrain'; return 'mailtrain';

View file

@ -95,11 +95,18 @@ async function _sendMail(transport, mail, template) {
} }
async function _sendTransactionalMail(transport, mail, template) { async function _sendTransactionalMail(transport, mail, template) {
const sendConfiguration = transport.mailer.sendConfiguration;
if (!mail.headers) { if (!mail.headers) {
mail.headers = {}; mail.headers = {};
} }
mail.headers['X-Sending-Zone'] = 'transactional'; 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); const htmlRenderer = await tools.getTemplate(template.html, template.locale);
if (htmlRenderer) { if (htmlRenderer) {

View file

@ -5,6 +5,7 @@ const log = require('./log');
const path = require('path'); const path = require('path');
const knex = require('./knex'); const knex = require('./knex');
const {CampaignStatus} = require('../../shared/campaigns'); const {CampaignStatus} = require('../../shared/campaigns');
const builtinZoneMta = require('./builtin-zone-mta');
let messageTid = 0; let messageTid = 0;
let senderProcess; let senderProcess;
@ -16,7 +17,10 @@ function spawn(callback) {
.then(() => { .then(() => {
senderProcess = fork(path.join(__dirname, '..', 'services', 'sender-master.js'), [], { senderProcess = fork(path.join(__dirname, '..', 'services', 'sender-master.js'), [], {
cwd: path.join(__dirname, '..'), 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 => { senderProcess.on('message', msg => {

View file

@ -110,7 +110,7 @@ async function _sendMail(list, email, template, locale, subjectKey, relativeUrls
const data = { const data = {
title: list.name, title: list.name,
homepage: configItems.defaultHomepage || getTrustedUrl(), homepage: configItems.defaultHomepage || getTrustedUrl(),
contactAddress: list.from_email || configItems.adminEmail, contactAddress: list.contact_email || configItems.adminEmail,
}; };
for (let relativeUrlKey in relativeUrls) { for (let relativeUrlKey in relativeUrls) {
@ -140,10 +140,6 @@ async function _sendMail(list, email, template, locale, subjectKey, relativeUrls
if (list.send_configuration) { if (list.send_configuration) {
const mailer = await mailers.getOrCreateMailer(list.send_configuration); const mailer = await mailers.getOrCreateMailer(list.send_configuration);
await mailer.sendTransactionalMail({ await mailer.sendTransactionalMail({
from: {
name: configItems.defaultFrom,
address: configItems.defaultAddress
},
to: { to: {
name: getDisplayName(flds, subscription), name: getDisplayName(flds, subscription),
address: email address: email

View file

@ -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.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_override',
'campaigns.data', 'campaigns.click_tracking_disabled', 'campaigns.open_tracking_disabled', 'campaigns.unsubscribe_url', 'campaigns.scheduled', '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`) knex.raw(`GROUP_CONCAT(CONCAT_WS(\':\', campaign_lists.list, campaign_lists.segment) ORDER BY campaign_lists.id SEPARATOR \';\') as lists`)
]) ])
.first(); .first();
@ -361,18 +362,12 @@ async function getByIdTx(tx, context, id, withPermissions = true, content = Cont
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'viewStats'); await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'viewStats');
const unsentQryGen = await getSubscribersQueryGeneratorTx(tx, id, true); const unsentQryGen = await getSubscribersQueryGeneratorTx(tx, id);
if (unsentQryGen) { if (unsentQryGen) {
const res = await unsentQryGen(tx).count('* AS subscriptionsToSend').first(); const res = await unsentQryGen(tx).count('* AS subscriptionsToSend').first();
entity.subscriptionsToSend = res.subscriptionsToSend; 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) { } else if (content === Content.WITHOUT_SOURCE_CUSTOM) {
delete entity.data.sourceCustom; 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: This is supposed to produce queries like this:
@ -812,7 +807,7 @@ async function getSubscribersQueryGeneratorTx(tx, campaignId, onlyUnsent) {
if (subsQrys.length > 0) { if (subsQrys.length > 0) {
let subsQry; let subsQry;
const unsentWhere = onlyUnsent ? ' where `sent` = false' : ''; const unsentWhere = ' where `sent` = false';
if (subsQrys.length === 1) { if (subsQrys.length === 1) {
const subsUnionSql = '(select `email`, `campaign_list_id`, `sent` from (' + subsQrys[0].sql + ') as `pending_subscriptions_all`' + unsentWhere + ') as `pending_subscriptions`' 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) { async function getStatisticsOpened(context, id) {
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'viewStats'); 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 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).groupBy('country').select('country 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 { return {
devices, devices,
@ -976,5 +953,4 @@ module.exports.disable = disable;
module.exports.rawGetByTx = rawGetByTx; module.exports.rawGetByTx = rawGetByTx;
module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx; module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx;
module.exports.getStatisticsOverview = getStatisticsOverview;
module.exports.getStatisticsOpened = getStatisticsOpened; module.exports.getStatisticsOpened = getStatisticsOpened;

View file

@ -32,7 +32,10 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
const subscription = await subscriptions.getByCidTx(tx, contextHelpers.getAdminContext(), list.id, subscriptionCid); const subscription = await subscriptions.getByCidTx(tx, contextHelpers.getAdminContext(), list.id, subscriptionCid);
const country = geoip.lookupCountry(remoteIp) || null; 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 now = new Date();
const _countLink = async (clickLinkId, incrementOnDup) => { const _countLink = async (clickLinkId, incrementOnDup) => {
@ -66,7 +69,6 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
}; };
// Update opened and click timestamps // Update opened and click timestamps
const latestUpdates = {}; 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); await tx(subscriptions.getSubscriptionTableName(list.id)).update(latestUpdates).where('id', subscription.id);
} }
// Update clicks // Update clicks
if (linkId > LinkId.GENERAL_CLICK && !campaign.click_tracking_disabled) { if (linkId > LinkId.GENERAL_CLICK && !campaign.click_tracking_disabled) {
await tx('links').increment('hits').where('id', linkId); await tx('links').increment('hits').where('id', linkId);
@ -125,6 +126,8 @@ async function addOrGet(campaignId, url) {
id: ids[0], id: ids[0],
cid cid
}; };
} else {
return link;
} }
}); });
} }

View file

@ -255,21 +255,15 @@ async function _getCampaignStatistics(campaign, select, unionQryFn, listQryFn, a
} else { } else {
return knex.raw(subsSql, subsBindings); return knex.raw(subsSql, subsBindings);
} }
} };
if (subsQrys.length === 1) { if (subsQrys.length === 1) {
subsSql = subsQrys[0].sql; subsSql = subsQrys[0].sql;
subsBindings = subsQrys[0].bindings; subsBindings = subsQrys[0].bindings;
if (asStream) {
return await applyUnionQryFn(subsSql, subsBindings).stream();
} else {
return await applyUnionQryFn(subsSql, subsBindings);
}
} else { } else {
subsSql = subsQrys.map(qry => '(' + qry.sql + ')').join(' UNION ALL '); subsSql = subsQrys.map(qry => '(' + qry.sql + ')').join(' UNION ALL ');
subsBindings = Array.prototype.concat(...subsQrys.map(qry => qry.bindings)); subsBindings = Array.prototype.concat(...subsQrys.map(qry => qry.bindings));
}
if (asStream) { if (asStream) {
return applyUnionQryFn(subsSql, subsBindings).stream(); return applyUnionQryFn(subsSql, subsBindings).stream();
@ -281,7 +275,6 @@ async function _getCampaignStatistics(campaign, select, unionQryFn, listQryFn, a
return res; return res;
} }
} }
}
} else { } else {
if (asStream) { if (asStream) {

View file

@ -304,9 +304,6 @@ async function sendPasswordReset(locale, usernameOrEmail) {
const mailer = await mailers.getOrCreateMailer(); const mailer = await mailers.getOrCreateMailer();
await mailer.sendTransactionalMail({ await mailer.sendTransactionalMail({
from: {
address: adminEmail
},
to: { to: {
address: user.email address: user.email
}, },

View file

@ -13,9 +13,10 @@ router.getAsync('/:campaign/:list/:subscription/:link', async (req, res) => {
if (link) { 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) // 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); await links.countLink(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, link.id);
} else { } else {
log.error('Redirect', 'Unresolved URL: <%s>', req.url); log.error('Redirect', 'Unresolved URL: <%s>', req.url);
throw new interoperableErrors.NotFoundError('Oops, we couldn\'t find a link for the URL you clicked'); throw new interoperableErrors.NotFoundError('Oops, we couldn\'t find a link for the URL you clicked');

View file

@ -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)); 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) => { router.getAsync('/campaign-statistics/:campaignId/opened', passport.loggedIn, async (req, res) => {
return res.json(await campaigns.getStatisticsOpened(req.context, castToInteger(req.params.campaignId))); return res.json(await campaigns.getStatisticsOpened(req.context, castToInteger(req.params.campaignId)));
}); });

View file

@ -670,8 +670,8 @@ async function webNotice(type, req, res) {
const data = { const data = {
title: list.name, title: list.name,
homepage: configItems.defaultHomepage || getTrustedUrl(), homepage: list.homepage || configItems.defaultHomepage || getTrustedUrl(),
contactAddress: list.from_email || configItems.adminEmail, contactAddress: list.contact_email || configItems.adminEmail,
template: { template: {
template: 'subscription/web-' + type + '-notice.mjml.hbs', template: 'subscription/web-' + type + '-notice.mjml.hbs',
layout: 'subscription/layout.mjml.hbs', layout: 'subscription/layout.mjml.hbs',

View file

@ -8,6 +8,7 @@ const knex = require('../lib/knex');
const {CampaignStatus, CampaignType} = require('../../shared/campaigns'); const {CampaignStatus, CampaignType} = require('../../shared/campaigns');
const { enforce } = require('../lib/helpers'); const { enforce } = require('../lib/helpers');
const campaigns = require('../models/campaigns'); const campaigns = require('../models/campaigns');
const builtinZoneMta = require('../lib/builtin-zone-mta');
let messageTid = 0; let messageTid = 0;
const workerProcesses = new Map(); const workerProcesses = new Map();
@ -24,6 +25,7 @@ const workerBatchSize = 100;
const messageQueue = new Map(); // campaignId -> [{listId, email}] const messageQueue = new Map(); // campaignId -> [{listId, email}]
const messageQueueCont = new Map(); // campaignId -> next batch callback 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}] } const workAssignment = new Map(); // workerId -> { campaignId, subscribers: [{listId, email}] }
@ -32,6 +34,8 @@ let queuedLastId = 0;
function messagesProcessed(workerId) { function messagesProcessed(workerId) {
const wa = workAssignment.get(workerId);
workAssignment.delete(workerId); workAssignment.delete(workerId);
idleWorkers.push(workerId); idleWorkers.push(workerId);
@ -40,6 +44,11 @@ function messagesProcessed(workerId) {
setImmediate(workerSchedulerCont); setImmediate(workerSchedulerCont);
workerSchedulerCont = null; workerSchedulerCont = null;
} }
if (campaignFinishCont.has(wa.campaignId)) {
setImmediate(campaignFinishCont.get(wa.campaignId));
campaignFinishCont.delete(wa.campaignId);
}
} }
async function scheduleWorkers() { async function scheduleWorkers() {
@ -78,8 +87,8 @@ async function scheduleWorkers() {
workAssignment.set(workerId, {campaignId, subscribers}); workAssignment.set(workerId, {campaignId, subscribers});
if (queue.length === 0 && messageQueueCont.has(campaignId)) { if (queue.length === 0 && messageQueueCont.has(campaignId)) {
const scheduleMessages = messageQueueCont.get(campaignId); setImmediate(messageQueueCont.get(campaignId));
setImmediate(scheduleMessages); messageQueueCont.delete(campaignId);
} }
sendToWorker(workerId, 'process-messages', { sendToWorker(workerId, 'process-messages', {
@ -99,9 +108,24 @@ async function scheduleWorkers() {
} }
async function processCampaign(campaignId) { async function processCampaign(campaignId) {
async function finish() { 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}); await knex('campaigns').where('id', campaignId).update({status: CampaignStatus.FINISHED});
messageQueue.delete(campaignId); messageQueue.delete(campaignId);
} }
@ -120,7 +144,7 @@ async function processCampaign(campaignId) {
let qryGen; let qryGen;
await knex.transaction(async tx => { await knex.transaction(async tx => {
qryGen = await campaigns.getSubscribersQueryGeneratorTx(tx, campaignId, true); qryGen = await campaigns.getSubscribersQueryGeneratorTx(tx, campaignId);
}); });
if (qryGen) { if (qryGen) {
@ -262,7 +286,10 @@ async function spawnWorker(workerId) {
const senderProcess = fork(path.join(__dirname, 'sender-worker.js'), [workerId], { const senderProcess = fork(path.join(__dirname, 'sender-worker.js'), [workerId], {
cwd: path.join(__dirname, '..'), 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 => { senderProcess.on('message', msg => {