Fixes in reports (generating a CSV).

Added caching of generated images in mosaico handler.
Various other fixes.
This commit is contained in:
Tomas Bures 2019-04-22 02:41:40 +02:00
parent 055c4c6b51
commit 66702b5edc
39 changed files with 545 additions and 278 deletions

View file

@ -7018,9 +7018,9 @@
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"moment-timezone": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.23.tgz",
"integrity": "sha512-WHFH85DkCfiNMDX5D3X7hpNH3/PUhjTGcD0U1SgfBGZxJ3qUmJh5FdvaFjcClxOvB3rzdfj4oRffbI38jEnC1w==",
"version": "0.5.25",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.25.tgz",
"integrity": "sha512-DgEaTyN/z0HFaVcVbSyVCUU6HeFdnNC3vE4c9cgu2dgMTvjBUBdBzWfasTBmAW45u5OIMeCJtU8yNjM22DHucw==",
"requires": {
"moment": ">= 2.9.0"
}

View file

@ -38,7 +38,7 @@
"lodash": "^4.17.11",
"mjml4-in-browser": "^1.1.1",
"moment": "^2.23.0",
"moment-timezone": "^0.5.23",
"moment-timezone": "^0.5.25",
"popper.js": "^1.14.6",
"prop-types": "^15.6.2",
"querystringify": "^2.1.0",
@ -56,9 +56,9 @@
"react-i18next": "^9.0.1",
"react-router-dom": "^4.3.1",
"react-sortable-tree": "^2.6.0",
"slugify": "^1.3.4",
"shallowequal": "^1.1.0",
"shortid": "^2.2.14",
"slugify": "^1.3.4",
"url-parse": "^1.4.4"
},
"devDependencies": {

View file

@ -45,12 +45,14 @@ export default class ViewAndOutput extends Component {
url: 'rest/report-content',
getTitle: name => t('reportName', { name }),
loading: t('loadingReport'),
finishedStates: new Set([ReportState.FINISHED]),
getContent: content => <div dangerouslySetInnerHTML={{ __html: content }}/>
},
output: {
url: 'rest/report-output',
getTitle: name => t('outputForReportName', { name }),
loading: t('loadingReportOutput'),
finishedStates: new Set([ReportState.FINISHED, ReportState.FAILED]),
getContent: content => <pre>{content}</pre>
}
}
@ -108,7 +110,7 @@ export default class ViewAndOutput extends Component {
if (this.state.report) {
let reportContent = null;
if (this.state.report.state === ReportState.FINISHED) {
if (viewType.finishedStates.has(this.state.report.state)) {
reportContent = viewType.getContent(this.state.content);
} else if (this.state.report.state === ReportState.SCHEDULED || this.state.report.state === ReportState.PROCESSING) {
reportContent = <div className="alert alert-info" role="alert">{t('reportIsBeingGenerated')}</div>;

View file

@ -130,23 +130,17 @@ export default class CUD extends Component {
' "maxOccurences": 1\n' +
' }\n' +
']',
js:
'const sampleRowTransform = new stream.Transform({\n' +
' objectMode: true,\n' +
' transform(row, encoding, callback) {\n' +
' callback(null, row)\n' +
' }\n' +
'})\n' +
js: 'const results = await campaigns.getCampaignOpenStatisticsStream(inputs.campaign, [\'subscription:email\', \'tracker:count\'], null, (query, col) => query.where(col(\'subscription:status\'), SubscriptionStatus.SUBSCRIBED));\n' +
'\n' +
'const results = await campaigns.getCampaignOpenStatisticsStream(inputs.campaign, [\'subscription:email\', \'tracker:count\'])\n' +
'\n' +
'results.pipe(sampleRowTransform)\n' +
'\n' +
'await renderCsvFromStream(sampleRowTransform, {\n' +
' header: true,\n' +
' columns: [ { key: \'subscription:email\', header: \'Email\' }, { key: \'tracker:count\', header: \'Open count\' } ],\n' +
' delimiter: \',\'\n' +
'})',
'await renderCsvFromStream(\n' +
' results, \n' +
' {\n' +
' header: true,\n' +
' columns: [ { key: \'subscription:email\', header: \'Email\' }, { key: \'tracker:count\', header: \'Open count\' } ],\n' +
' delimiter: \',\'\n' +
' },\n' +
' async (row, encoding) => row\n' +
');',
hbs: ''
});
@ -167,9 +161,9 @@ export default class CUD extends Component {
' }\n' +
']',
js:
'const results = await campaigns.getCampaignOpenStatistics(inputs.campaign, ["field:country", "count_opened", "count_all"], query =>\n' +
'const results = await campaigns.getCampaignOpenStatistics(inputs.campaign, ["field:country", "count_opened", "count_all"], (query, col) =>\n' +
' query.count("* AS count_all")\n' +
' .select(knex.raw("SUM(IF(`tracker:count` IS NULL, 0, 1)) AS count_opened"))\n' +
' .select(knex.raw("SUM(IF(`" + col(tracker:count) +"` IS NULL, 0, 1)) AS count_opened"))\n' +
' .groupBy("field:country")\n' +
')\n' +
'\n' +

View file

@ -30,12 +30,10 @@ services:
- "3004:3004"
volumes:
- mailtrain-files:/app/server/files
- mailtrain-reports:/app/protected/reports
volumes:
mysql-data:
redis-data:
mongo-data:
mailtrain-files:
mailtrain-reports:

View file

@ -30,12 +30,10 @@ services:
- "3004:3004"
volumes:
- mailtrain-files:/app/server/files
- mailtrain-reports:/app/protected/reports
volumes:
mysql-data:
redis-data:
mongo-data:
mailtrain-files:
mailtrain-reports:

View file

@ -118,7 +118,7 @@ hbs.registerHelper('flash_messages', function () { // eslint-disable-line prefer
function createApp(appType) {
async function createApp(appType) {
const app = express();
function install404Fallback(url) {
@ -273,10 +273,10 @@ function createApp(appType) {
useWith404Fallback('/files', files);
}
useWith404Fallback('/mosaico', sandboxedMosaico.getRouter(appType));
useWith404Fallback('/ckeditor', sandboxedCKEditor.getRouter(appType));
useWith404Fallback('/grapesjs', sandboxedGrapesJS.getRouter(appType));
useWith404Fallback('/codeeditor', sandboxedCodeEditor.getRouter(appType));
useWith404Fallback('/mosaico', await sandboxedMosaico.getRouter(appType));
useWith404Fallback('/ckeditor', await sandboxedCKEditor.getRouter(appType));
useWith404Fallback('/grapesjs', await sandboxedGrapesJS.getRouter(appType));
useWith404Fallback('/codeeditor', await sandboxedCodeEditor.getRouter(appType));
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
useWith404Fallback('/subscriptions', subscriptions);
@ -318,7 +318,7 @@ function createApp(appType) {
install404Fallback('/rest');
}
app.use('/', index.getRouter(appType));
app.use('/', await index.getRouter(appType));
app.use((err, req, res, next) => {
if (!err) {

View file

@ -176,6 +176,12 @@ mosaico:
# Inject custom scripts
# customscripts:
# - /mosaico/custom/my-mosaico-plugin.js
fileCache:
blockThumbnails:
maxSize: 100 # megabytes
images:
maxSize: 1024 # megabytes
grapesjs:
# Installed templates

View file

@ -19,11 +19,13 @@ const reportProcessor = require('./lib/report-processor');
const executor = require('./lib/executor');
const privilegeHelpers = require('./lib/privilege-helpers');
const knex = require('./lib/knex');
const bluebird = require('bluebird');
const shares = require('./models/shares');
const { AppType } = require('../shared/app');
const builtinZoneMta = require('./lib/builtin-zone-mta');
const { uploadedFilesDir } = require('./lib/file-helpers');
const { reportFilesDir } = require('./lib/report-helpers');
const { filesDir } = require('./models/files');
const trustedPort = config.www.trustedPort;
@ -36,8 +38,8 @@ if (config.title) {
}
function startHTTPServer(appType, appName, port, callback) {
const app = appBuilder.createApp(appType);
async function startHTTPServer(appType, appName, port) {
const app = await appBuilder.createApp(appType);
app.set('port', port);
const server = http.createServer(app);
@ -68,81 +70,58 @@ function startHTTPServer(appType, appName, port, callback) {
log.info('Express', 'WWW server [%s] listening on %s', appName, bind);
});
server.listen({port, host}, callback);
const serverListenAsync = bluebird.promisify(server.listen.bind(server));
await serverListenAsync({port, host});
}
// ---------------------------------------------------------------------------------------
// Start the whole circus here
// Start the whole circus
// ---------------------------------------------------------------------------------------
dbcheck(err => { // Check if database needs upgrading before starting the server - legacy migration first
if (err) {
log.error('DB', err.message || err);
return process.exit(1);
}
async function init() {
await dbcheck();
knex.migrate.latest() // And now the current migration with Knex
await knex.migrate.latest(); // And now the current migration with Knex
.then(() => shares.regenerateRoleNamesTable())
.then(() => shares.rebuildPermissions())
await shares.regenerateRoleNamesTable();
await shares.rebuildPermissions();
/* Simplified startup without services - only for debugging the UI and models
.then(() =>
startHTTPServer(AppType.TRUSTED, 'trusted', trustedPort, () =>
startHTTPServer(AppType.SANDBOXED, 'sandbox', sandboxPort, () =>
startHTTPServer(AppType.PUBLIC, 'public', publicPort, async () => {
/*
await executor.spawn();
await testServer.spawn();
await verpServer.spawn();
await builtinZoneMta.spawn();
*/
await privilegeHelpers.ensureMailtrainDir(uploadedFilesDir);
await startHTTPServer(AppType.TRUSTED, 'trusted', trustedPort);
await startHTTPServer(AppType.SANDBOXED, 'sandbox', sandboxPort);
await startHTTPServer(AppType.PUBLIC, 'public', publicPort);
privilegeHelpers.dropRootPrivileges();
await privilegeHelpers.ensureMailtrainDir(filesDir);
await privilegeHelpers.ensureMailtrainDir(uploadedFilesDir);
await privilegeHelpers.ensureMailtrainDir(reportFilesDir);
tzupdate.start();
privilegeHelpers.dropRootPrivileges();
log.info('Service', 'All services started');
appBuilder.setReady();
})
)
)
);
*/
/*
tzupdate.start();
.then(() =>
executor.spawn(() =>
testServer(() =>
verpServer(() =>
builtinZoneMta.spawn(() =>
startHTTPServer(AppType.TRUSTED, 'trusted', trustedPort, () =>
startHTTPServer(AppType.SANDBOXED, 'sandbox', sandboxPort, () =>
startHTTPServer(AppType.PUBLIC, 'public', publicPort, async () => {
await importer.spawn();
await feedcheck.spawn();
await senders.spawn();
await privilegeHelpers.ensureMailtrainDir(filesDir);
await privilegeHelpers.ensureMailtrainDir(uploadedFilesDir);
triggers.start();
gdprCleanup.start();
privilegeHelpers.dropRootPrivileges();
await postfixBounceServer.spawn();
tzupdate.start();
await reportProcessor.init();
*/
importer.spawn(() =>
feedcheck.spawn(() =>
senders.spawn(() => {
triggers.start();
gdprCleanup.start();
log.info('Service', 'All services started');
appBuilder.setReady();
}
postfixBounceServer(async () => {
await reportProcessor.init();
log.info('Service', 'All services started');
appBuilder.setReady();
});
})
)
);
})
)
)
)
)
)
)
);
});
init().catch(err => {log.error('', err); process.exit(1); });

View file

@ -6,6 +6,7 @@ const log = require('./log');
const path = require('path');
const fs = require('fs-extra')
const crypto = require('crypto');
const bluebird = require('bluebird');
let zoneMtaProcess;
@ -155,6 +156,6 @@ function spawn(callback) {
}
}
module.exports.spawn = spawn;
module.exports.spawn = bluebird.promisify(spawn);
module.exports.getUsername = getUsername;
module.exports.getPassword = getPassword;

View file

@ -10,6 +10,7 @@ const log = require('./log');
const fs = require('fs');
const pathlib = require('path');
const Handlebars = require('handlebars');
const bluebird = require('bluebird');
const highestLegacySchemaVersion = 33;
@ -136,8 +137,30 @@ function runInitial(callback) {
}, callback);
}
function runUpdates(callback, runCount) {
runCount = Number(runCount) || 0;
function applyUpdate(update, callback) {
getSql(update.path, update.data, (err, sql) => {
if (err) {
return callback(err);
}
db.getConnection((err, connection) => {
if (err) {
return callback(err);
}
connection.query(sql, err => {
connection.release();
if (err) {
return callback(err);
}
return callback(null, true);
});
});
});
}
function runUpdates(runCount, callback) {
listTables((err, tables) => {
if (err) {
return callback(err);
@ -148,7 +171,7 @@ function runUpdates(callback, runCount) {
return callback(new Error('Settings table not found from database'));
}
log.info('sql', 'SQL not set up, initializing');
return runInitial(runUpdates.bind(null, callback, ++runCount));
return runInitial(runUpdates.bind(null, ++runCount, callback));
}
getSchemaVersion((err, schemaVersion) => {
@ -196,37 +219,13 @@ function runUpdates(callback, runCount) {
});
}
function applyUpdate(update, callback) {
getSql(update.path, update.data, (err, sql) => {
if (err) {
return callback(err);
}
const runUpdatesAsync = bluebird.promisify(runUpdates);
const dbEndAsync = bluebird.promisify(db.end.bind(db));
db.getConnection((err, connection) => {
if (err) {
return callback(err);
}
connection.query(sql, err => {
connection.release();
if (err) {
return callback(err);
}
return callback(null, true);
});
});
});
async function dbcheck() {
await runUpdatesAsync(0);
await dbEndAsync();
log.info('sql', 'Database check completed');
}
module.exports = callback => {
runUpdates(err => {
if (err) {
return callback(err);
}
db.end(() => {
log.info('sql', 'Database check completed');
return callback(null, true);
});
});
};
module.exports = dbcheck;

View file

@ -3,17 +3,12 @@
const fork = require('child_process').fork;
const log = require('./log');
const path = require('path');
const bluebird = require('bluebird');
const requestCallbacks = {};
let messageTid = 0;
let executorProcess;
module.exports = {
spawn,
start,
stop
};
function spawn(callback) {
log.verbose('Executor', 'Spawning executor process');
@ -81,3 +76,6 @@ function stop(tid) {
});
}
module.exports.spawn = bluebird.promisify(spawn);
module.exports.start = start;
module.exports.stop = stop;

View file

@ -4,15 +4,11 @@ const fork = require('child_process').fork;
const log = require('./log');
const path = require('path');
const senders = require('./senders');
const bluebird = require('bluebird');
let messageTid = 0;
let feedcheckProcess;
module.exports = {
spawn,
scheduleCheck
};
function spawn(callback) {
log.verbose('Feed', 'Spawning feedcheck process');
@ -46,3 +42,5 @@ function scheduleCheck() {
messageTid++;
}
module.exports.spawn = bluebird.promisify(spawn);
module.exports.scheduleCheck = scheduleCheck;

166
server/lib/file-cache.js Normal file
View file

@ -0,0 +1,166 @@
'use strict';
const { filesDir } = require('../models/files');
const path = require('path');
const fs = require('fs-extra-promise');
const stream = require('stream');
const privilegeHelpers = require('./privilege-helpers');
const synchronized = require('./synchronized');
const { tmpName } = require('tmp-promise');
const fileCacheFilesDir = path.join(filesDir, 'cache');
const fileCaches = new Map();
async function _fileCache(typeId, cacheConfig, fileNameGen) {
if (fileCaches.has(typeId)) {
return fileCaches.get(typeId);
}
const localFilesDir = path.join(fileCacheFilesDir, typeId);
await fs.emptyDirAsync(localFilesDir);
await privilegeHelpers.ensureMailtrainDir(localFilesDir);
const cachedFiles = new Map();
let nextFilesOrder = 1;
const pruneCache = async() => {
const entries = [];
for (const entry of cachedFiles.values()) {
if (entry.isReady) {
entries.push(entry);
}
}
entries.sort((x, y) => y.order - x.order);
let cumulativeSize = 0;
const maxSize = cacheConfig.maxSize * 1048576;
for (const entry of entries) {
cumulativeSize += entry.size;
if (cumulativeSize > maxSize) {
entry.isReady = false;
await fs.unlinkAsync(path.join(localFilesDir, entry.fileName));
cachedFiles.delete(entry.fileName);
}
}
};
const thisFileCache = (req, res, next) => {
const fileName = fileNameGen ? fileNameGen(req) : req.url.substring(1);
const localFilePath = path.join(localFilesDir, fileName);
const fileInfo = cachedFiles.get(fileName);
if (fileInfo && fileInfo.isReady) {
res.sendFile(
localFilePath,
{
headers: fileInfo.headers
},
err => {
if (err) next(err);
}
);
} else {
// This means that the file is not present. We thus generate it and cache it.
let fileStream = null;
let tmpFilePath = null;
// If the file does not exist yet, we store. If we receive a simulataneous request, while the file is being generate and stored,
// we only generate it (but not store it) in the second parallel request.
const isStoring = !fileInfo;
if (isStoring) {
cachedFiles.set(fileName, {
fileName,
isReady: false
});
}
const ensureFileStream = callback => {
if (!fileStream) {
tmpName().then(tmp => {
tmpFilePath = tmp;
fileStream = fs.createWriteStream(tmpFilePath);
callback();
})
} else {
callback();
}
};
let fileSize = 0;
res.fileCacheResponse = new stream.Writable({
write(chunk, encoding, callback) {
res.write(chunk, encoding);
if (isStoring) {
fileSize += chunk.length;
ensureFileStream(() => {
fileStream.write(chunk, encoding);
callback();
});
} else {
callback();
}
},
final(callback) {
res.end();
if (isStoring) {
ensureFileStream(() => {
fileStream.end(null, null, () => {
fs.moveAsync(tmpFilePath, localFilePath, {})
.then(() => {
cachedFiles.set(fileName, {
fileName,
size: fileSize,
order: nextFilesOrder,
headers: res.getHeaders(),
isReady: true
});
nextFilesOrder += 1;
callback();
// noinspection JSIgnoredPromiseFromCall
pruneCache();
})
.catch(err => next(err));
});
});
} else {
callback();
}
},
destroy(err, callback) {
res.destroy(err);
if (fileStream) {
fileStream.destroy(err);
fs.unlink(tmpFilePath, () => {
cachedFiles.delete(fileName);
callback();
});
} else {
callback();
}
}
});
next();
}
};
fileCaches.set(typeId, thisFileCache);
return thisFileCache;
}
const fileCache = synchronized(_fileCache);
module.exports.fileCache = fileCache;
module.exports.fileCacheFilesDir = fileCacheFilesDir;

View file

@ -7,15 +7,11 @@ const path = require('path');
const {ImportStatus, RunStatus} = require('../../shared/imports');
const {ListActivityType} = require('../../shared/activity-log');
const activityLog = require('./activity-log');
const bluebird = require('bluebird');
let messageTid = 0;
let importerProcess;
module.exports = {
spawn,
scheduleCheck
};
function spawn(callback) {
log.verbose('Importer', 'Spawning importer process');
@ -65,4 +61,5 @@ function scheduleCheck() {
messageTid++;
}
module.exports.spawn = bluebird.promisify(spawn);
module.exports.scheduleCheck = scheduleCheck;

View file

@ -53,7 +53,7 @@ function ensureMailtrainOwner(file, callback) {
fs.chown(file, ids.uid, ids.gid, callback);
}
async function ensureMailtrainDir(dir) {
async function ensureMailtrainDir(dir, recursive) {
const ids = getConfigUidGid();
await fs.ensureDir(dir);
await fs.chownAsync(dir, ids.uid, ids.gid);

View file

@ -11,9 +11,10 @@ function nameToFileName(name) {
replace(/--*/g, '-');
}
const reportFilesDir = path.join(__dirname, '..', 'files', 'reports');
function getReportFileBase(report) {
return path.join(__dirname, '..', 'protected', 'reports', report.id + '-' + nameToFileName(report.name));
return path.join(reportFilesDir, report.id + '-' + nameToFileName(report.name));
}
function getReportContentFile(report) {
@ -28,5 +29,6 @@ function getReportOutputFile(report) {
module.exports = {
getReportContentFile,
getReportOutputFile,
nameToFileName
nameToFileName,
reportFilesDir
};

View file

@ -6,6 +6,7 @@ const path = require('path');
const knex = require('./knex');
const {CampaignStatus} = require('../../shared/campaigns');
const builtinZoneMta = require('./builtin-zone-mta');
const bluebird = require('bluebird');
let messageTid = 0;
let senderProcess;
@ -59,9 +60,6 @@ function reloadConfig(sendConfigurationId) {
messageTid++;
}
module.exports = {
spawn,
scheduleCheck,
reloadConfig
};
module.exports.spawn = bluebird.promisify(spawn);
module.exports.scheduleCheck = scheduleCheck;
module.exports.reloadConfig = reloadConfig;

View file

@ -0,0 +1,26 @@
'use strict';
// This implements a simple wrapper around an async function that prevents concurrent execution of the function from two asynchronous chains
// It enforces that the running execution has to complete first before another one is started.
function synchronized(asyncFn) {
let ensurePromise = null;
return async (...args) => {
while (ensurePromise) {
try {
await ensurePromise;
} catch (err) {
}
}
ensurePromise = asyncFn(...args);
try {
return await ensurePromise;
} finally {
ensurePromise = null;
}
}
}
module.exports = synchronized;

View file

@ -12,7 +12,7 @@ const {getPublicUrl} = require('../lib/urls');
const crypto = require('crypto');
const bluebird = require('bluebird');
const cryptoPseudoRandomBytes = bluebird.promisify(crypto.pseudoRandomBytes);
const cryptoPseudoRandomBytes = bluebird.promisify(crypto.pseudoRandomBytes.bind(crypto));
const entityTypes = entitySettings.getEntityTypes();

View file

@ -155,7 +155,6 @@ async function _getCampaignStatistics(campaign, select, unionQryFn, listQryFn, a
let firstIteration = true;
for (const cpgList of campaign.lists) {
const cpgListId = cpgList.list;
const subsTable = subscriptions.getSubscriptionTableName(cpgListId);
const flds = await fields.list(contextHelpers.getAdminContext(), cpgListId);
@ -184,7 +183,6 @@ async function _getCampaignStatistics(campaign, select, unionQryFn, listQryFn, a
for (const cpgList of campaign.lists) {
const cpgListId = cpgList.list;
const subsTable = subscriptions.getSubscriptionTableName(cpgListId);
const campaignFieldsMapping = {
'list:id': {raw: knex.raw('?', [cpgListId])},
@ -203,6 +201,14 @@ async function _getCampaignStatistics(campaign, select, unionQryFn, listQryFn, a
...campaignFieldsMapping
};
const getColIdIfExists = (colId, getter) => {
if (colId in fieldsMapping) {
return getter(colId);
} else {
throw new Error(`Unknown column id ${colId}`);
}
}
const getSelField = item => {
const itemMapping = fieldsMapping[item];
if (typeof itemMapping === 'string') {
@ -226,17 +232,22 @@ async function _getCampaignStatistics(campaign, select, unionQryFn, listQryFn, a
let query = knex(`subscription__${cpgListId} AS subscriptions`)
.leftJoin('campaign_messages', {
'campaign_messages.subscription': 'subscriptions.id',
'campaign_messages.list': knex.raw('?', [cpgListId])
'campaign_messages.campaign': knex.raw('?', [campaign.id]),
'campaign_messages.list': knex.raw('?', [cpgListId]),
'campaign_messages.subscription': 'subscriptions.id'
})
.leftJoin('campaign_links', {
'campaign_links.subscription': 'subscriptions.id',
'campaign_links.list': knex.raw('?', [cpgListId])
'campaign_links.campaign': knex.raw('?', [campaign.id]),
'campaign_links.list': knex.raw('?', [cpgListId]),
'campaign_links.subscription': 'subscriptions.id'
})
.select(selFields);
if (listQryFn) {
query = listQryFn(query);
query = listQryFn(
query,
colId => getColIdIfExists(colId, x => fieldsMapping[x])
);
}
subsQrys.push(query.toSQL().toNative());
@ -250,7 +261,8 @@ async function _getCampaignStatistics(campaign, select, unionQryFn, listQryFn, a
return unionQryFn(
knex.from(function() {
return knex.raw('(' + subsSql + ')', subsBindings);
})
}),
colId => getColIdIfExists(colId, x => x)
);
} else {
return knex.raw(subsSql, subsBindings);
@ -267,6 +279,7 @@ async function _getCampaignStatistics(campaign, select, unionQryFn, listQryFn, a
if (asStream) {
return applyUnionQryFn(subsSql, subsBindings).stream();
} else {
const res = await applyUnionQryFn(subsSql, subsBindings);
if (res[0] && Array.isArray(res[0])) {
@ -299,10 +312,11 @@ async function _getCampaignOpenStatistics(campaign, select, unionQryFn, listQryF
campaign,
select,
unionQryFn,
qry => listQryFn(
(qry, col) => listQryFn(
qry.where(function() {
this.whereNull('campaign_links.link').orWhere('campaign_links.link', LinkId.OPEN)
})
}),
col
),
asStream
);
@ -317,10 +331,11 @@ async function _getCampaignClickStatistics(campaign, select, unionQryFn, listQry
campaign,
select,
unionQryFn,
qry => listQryFn(
(qry, col) => listQryFn(
qry.where(function() {
this.whereNull('campaign_links.link').orWhere('campaign_links.link', LinkId.GENERAL_CLICK)
})
}),
col
),
asStream
);
@ -335,15 +350,24 @@ async function _getCampaignLinkClickStatistics(campaign, select, unionQryFn, lis
campaign,
select,
unionQryFn,
qry => listQryFn(
(qry, col) => listQryFn(
qry.where(function() {
this.whereNull('campaign_links.link').orWhere('campaign_links.link', '>', LinkId.GENERAL_CLICK)
})
}),
col
),
asStream
);
}
async function getCampaignStatistics(campaign, select, unionQryFn, listQryFn) {
return await _getCampaignStatistics(campaign, select, unionQryFn, listQryFn, false);
}
async function getCampaignStatisticsStream(campaign, select, unionQryFn, listQryFn) {
return await _getCampaignStatistics(campaign, select, unionQryFn, listQryFn, true);
}
async function getCampaignOpenStatistics(campaign, select, unionQryFn, listQryFn) {
return await _getCampaignOpenStatistics(campaign, select, unionQryFn, listQryFn, false);
}
@ -381,6 +405,8 @@ module.exports.remove = remove;
module.exports.updateFields = updateFields;
module.exports.listByState = listByState;
module.exports.bulkChangeState = bulkChangeState;
module.exports.getCampaignStatistics = getCampaignStatistics;
module.exports.getCampaignStatisticsStream = getCampaignStatisticsStream;
module.exports.getCampaignOpenStatistics = getCampaignOpenStatistics;
module.exports.getCampaignClickStatistics = getCampaignClickStatistics;
module.exports.getCampaignLinkClickStatistics = getCampaignLinkClickStatistics;

View file

@ -16,8 +16,8 @@ const { tUI } = require('../lib/translate');
const bluebird = require('bluebird');
const bcrypt = require('bcrypt-nodejs');
const bcryptHash = bluebird.promisify(bcrypt.hash);
const bcryptCompare = bluebird.promisify(bcrypt.compare);
const bcryptHash = bluebird.promisify(bcrypt.hash.bind(bcrypt));
const bcryptCompare = bluebird.promisify(bcrypt.compare.bind(bcrypt));
const mailers = require('../lib/mailers');

124
server/package-lock.json generated
View file

@ -575,9 +575,9 @@
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
"aws-sdk": {
"version": "2.437.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.437.0.tgz",
"integrity": "sha512-sDZb5QBOO6FOMvuKDEdO16YQRk0WUhnQd38EaSt0yUCi4Gev8uypODyYONgODZcXe8Cr1GMwC8scUKr00S/I5w==",
"version": "2.440.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.440.0.tgz",
"integrity": "sha512-/sXI7pZggvFMr2J9qCpGgE2XO/4uBErRVSGoHTR3PkGWvf352w+HebnuGdRKK9D3lnGuGMnEY8w9IS44LUKsxw==",
"requires": {
"buffer": "4.9.1",
"events": "1.1.1",
@ -722,9 +722,9 @@
}
},
"bignumber.js": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-4.1.0.tgz",
"integrity": "sha512-eJzYkFYy9L4JzXsbymsFn3p54D+llV27oTQ+ziJG7WFRheJcNZilgVXMG0LoZtlQSKBsJdWtLFqOD0u+U0jZKA=="
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz",
"integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ=="
},
"binary": {
"version": "0.3.0",
@ -1223,24 +1223,6 @@
"vary": "~1.1.2"
}
},
"compressjs": {
"version": "github:openpgpjs/compressjs#bfbb371a34d1750afa34bfa49156461acdab79a9",
"from": "github:openpgpjs/compressjs",
"requires": {
"amdefine": "~1.0.0",
"commander": "~2.8.1"
},
"dependencies": {
"commander": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz",
"integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=",
"requires": {
"graceful-readlink": ">= 1.0.0"
}
}
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1477,9 +1459,9 @@
}
},
"csv-parse": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.4.0.tgz",
"integrity": "sha512-rvoRlZxu6Ap8jOkhoQQeI+5y/eTPqEIVk20bxZmo81k2ArUiNLv8LAERTEKarOQuC7BKXGyzSqAKWox115bg7A=="
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.4.1.tgz",
"integrity": "sha512-uFe5phPfmwBXSPWz5GYHeaEc2Oezn2kY5iLIvG1sJjc32Y4GU7T/b/uX5ffZh4CBDWwJQjwAuxrDEdl3Z5Qv+g=="
},
"csv-stringify": {
"version": "5.3.0",
@ -1768,19 +1750,6 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"elliptic": {
"version": "github:openpgpjs/elliptic#ad81845f693effa5b4b6d07db2e82112de222f48",
"from": "github:openpgpjs/elliptic",
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
}
},
"email-addresses": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-3.0.3.tgz",
@ -5602,9 +5571,9 @@
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"moment-timezone": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.23.tgz",
"integrity": "sha512-WHFH85DkCfiNMDX5D3X7hpNH3/PUhjTGcD0U1SgfBGZxJ3qUmJh5FdvaFjcClxOvB3rzdfj4oRffbI38jEnC1w==",
"version": "0.5.25",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.25.tgz",
"integrity": "sha512-DgEaTyN/z0HFaVcVbSyVCUU6HeFdnNC3vE4c9cgu2dgMTvjBUBdBzWfasTBmAW45u5OIMeCJtU8yNjM22DHucw==",
"requires": {
"moment": ">= 2.9.0"
}
@ -5652,11 +5621,11 @@
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
},
"mysql": {
"version": "2.16.0",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.16.0.tgz",
"integrity": "sha512-dPbN2LHonQp7D5ja5DJXNbCLe/HRdu+f3v61aguzNRQIrmZLOeRoymBYyeThrR6ug+FqzDL95Gc9maqZUJS+Gw==",
"version": "2.17.1",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.17.1.tgz",
"integrity": "sha512-7vMqHQ673SAk5C8fOzTG2LpPcf3bNt0oL3sFpxPEEFp1mdlDcrLK0On7z8ZYKaaHrHwNcQ/MTUz7/oobZ2OyyA==",
"requires": {
"bignumber.js": "4.1.0",
"bignumber.js": "7.2.1",
"readable-stream": "2.3.6",
"safe-buffer": "5.1.2",
"sqlstring": "2.3.1"
@ -5816,6 +5785,35 @@
"ieee754": "^1.1.4"
}
},
"commander": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz",
"integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=",
"requires": {
"graceful-readlink": ">= 1.0.0"
}
},
"compressjs": {
"version": "github:openpgpjs/compressjs#bfbb371a34d1750afa34bfa49156461acdab79a9",
"from": "github:openpgpjs/compressjs#bfbb371a34d1750afa34bfa49156461acdab79a9",
"requires": {
"amdefine": "~1.0.0",
"commander": "~2.8.1"
}
},
"elliptic": {
"version": "github:openpgpjs/elliptic#ad81845f693effa5b4b6d07db2e82112de222f48",
"from": "github:openpgpjs/elliptic#ad81845f693effa5b4b6d07db2e82112de222f48",
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
}
},
"openpgp": {
"version": "3.0.11",
"resolved": "https://registry.npmjs.org/openpgp/-/openpgp-3.0.11.tgz",
@ -5833,6 +5831,29 @@
"node-localstorage": "~1.3.0",
"pako": "^1.0.6",
"rusha": "^0.8.12"
},
"dependencies": {
"compressjs": {
"version": "github:openpgpjs/compressjs#bfbb371a34d1750afa34bfa49156461acdab79a9",
"from": "github:openpgpjs/compressjs",
"requires": {
"amdefine": "~1.0.0",
"commander": "~2.8.1"
}
},
"elliptic": {
"version": "github:openpgpjs/elliptic#ad81845f693effa5b4b6d07db2e82112de222f48",
"from": "github:openpgpjs/elliptic",
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
}
}
}
}
}
@ -7511,6 +7532,15 @@
"os-tmpdir": "~1.0.2"
}
},
"tmp-promise": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-1.0.5.tgz",
"integrity": "sha512-hOabTz9Tp49wCozFwuJe5ISrOqkECm6kzw66XTP23DuzNU7QS/KiZq5LC9Y7QSy8f1rPSLy4bKaViP0OwGI1cA==",
"requires": {
"bluebird": "^3.5.0",
"tmp": "0.0.33"
}
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",

View file

@ -42,7 +42,7 @@
"posix": "^4.1.2"
},
"dependencies": {
"aws-sdk": "^2.383.0",
"aws-sdk": "^2.440.0",
"bcrypt-nodejs": "0.0.3",
"bluebird": "^3.5.3",
"body-parser": "^1.18.3",
@ -56,7 +56,7 @@
"cors": "^2.8.5",
"crypto": "^1.0.1",
"csurf": "^1.9.0",
"csv-parse": "^4.3.0",
"csv-parse": "^4.4.1",
"csv-stringify": "^5.1.2",
"device": "^0.3.9",
"dompurify": "^1.0.8",
@ -80,16 +80,16 @@
"isemail": "^3.2.0",
"jsdom": "^13.1.0",
"juice": "^5.1.0",
"knex": "^0.16.3",
"knex": "^0.16.5",
"libmime": "^4.0.1",
"mailparser": "^2.4.3",
"memory-cache": "^0.2.0",
"mjml": "^4.3.0",
"moment": "^2.23.0",
"moment-timezone": "^0.5.23",
"moment-timezone": "^0.5.25",
"morgan": "^1.9.1",
"multer": "^1.4.1",
"mysql": "^2.16.0",
"mysql": "^2.17.1",
"node-ipc": "^9.1.1",
"node-mocks-http": "^1.7.3",
"node-object-hash": "^1.4.1",
@ -107,6 +107,7 @@
"shortid": "^2.2.14",
"slugify": "^1.3.4",
"smtp-server": "^3.4.7",
"tmp-promise": "^1.0.5",
"toml": "^2.3.3",
"try-require": "^1.2.1",
"xmldom": "^0.1.27"

View file

@ -7,7 +7,7 @@ const { AppType } = require('../../shared/app');
const routerFactory = require('../lib/router-async');
function getRouter(appType) {
async function getRouter(appType) {
const router = routerFactory.create();
if (appType === AppType.TRUSTED) {

View file

@ -4,7 +4,7 @@ const passport = require('../../lib/passport');
const bluebird = require('bluebird');
const premailerApi = require('premailer-api');
const premailerPrepareAsync = bluebird.promisify(premailerApi.prepare);
const premailerPrepareAsync = bluebird.promisify(premailerApi.prepare.bind(premailerApi));
const router = require('../../lib/router-async').create();

View file

@ -33,7 +33,7 @@ users.registerRestrictedAccessTokenMethod('ckeditor', async ({entityTypeId, enti
});
function getRouter(appType) {
async function getRouter(appType) {
const router = routerFactory.create();
if (appType === AppType.SANDBOXED) {

View file

@ -33,7 +33,7 @@ users.registerRestrictedAccessTokenMethod('codeeditor', async ({entityTypeId, en
});
function getRouter(appType) {
async function getRouter(appType) {
const router = routerFactory.create();
if (appType === AppType.SANDBOXED) {

View file

@ -33,7 +33,7 @@ users.registerRestrictedAccessTokenMethod('grapesjs', async ({entityTypeId, enti
});
function getRouter(appType) {
async function getRouter(appType) {
const router = routerFactory.create();
if (appType === AppType.SANDBOXED) {

View file

@ -31,6 +31,8 @@ const { AppType } = require('../../shared/app');
const {castToInteger} = require('../lib/helpers');
const { fileCache } = require('../lib/file-cache');
users.registerRestrictedAccessTokenMethod('mosaico', async ({entityTypeId, entityId}) => {
if (entityTypeId === 'template') {
@ -131,7 +133,7 @@ function sanitizeSize(val, min, max, defaultVal, allowNull) {
function getRouter(appType) {
async function getRouter(appType) {
const router = routerFactory.create();
if (appType === AppType.SANDBOXED) {
@ -161,14 +163,14 @@ function getRouter(appType) {
router.use('/templates/:mosaicoTemplateId/edres', express.static(path.join(__dirname, '..', '..', 'client', 'static', 'mosaico', 'templates', 'versafix-1', 'edres')));
// This is the final fallback for a block thumbnail, so that at least something gets returned
router.getAsync('/templates/:mosaicoTemplateId/edres/:fileName', async (req, res, next) => {
router.getAsync('/templates/:mosaicoTemplateId/edres/:fileName', await fileCache('mosaico-block-thumbnails', config.mosaico.fileCache.blockThumbnails, req => req.params.fileName), async (req, res) => {
let labelText = req.params.fileName.replace(/\.png$/, '');
labelText = labelText.replace(/[_]/g, ' ');
labelText = capitalize.words(labelText);
const image = await placeholderImage(340, 100, labelText, '#ffffff');
res.set('Content-Type', 'image/' + image.format);
image.stream.pipe(res);
image.stream.pipe(res.fileCacheResponse);
});
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', files.ReplacementBehavior.RENAME, null, 'file', resp => {
@ -225,7 +227,7 @@ function getRouter(appType) {
} else if (appType === AppType.TRUSTED || appType === AppType.PUBLIC) { // Mosaico editor loads the images from TRUSTED endpoint. This is hard to change because the index.html has to come from TRUSTED.
// So we serve /mosaico/img under both endpoints. There is no harm in it.
router.getAsync('/img', async (req, res) => {
router.getAsync('/img', await fileCache('mosaico-images', config.mosaico.fileCache.images), async (req, res) => {
const method = req.query.method;
const params = req.query.params;
let [width, height] = params.split(',');
@ -248,7 +250,7 @@ function getRouter(appType) {
height = sanitizeSize(height, 1, 2048, 300, true);
let filePath;
const url = req.query.src;
const url = req.query.src || '';
const mosaicoLegacyUrlPrefix = getTrustedUrl(`mosaico/uploads/`);
if (url.startsWith(mosaicoLegacyUrlPrefix)) {
@ -262,7 +264,7 @@ function getRouter(appType) {
}
res.set('Content-Type', 'image/' + image.format);
image.stream.pipe(res);
image.stream.pipe(res.fileCacheResponse);
});
}

View file

@ -334,7 +334,7 @@ router.getAsync('/:cid/widget', cors(corsOptions), async (req, res) => {
await injectCustomFormData(req.query.fid || list.default_form, 'web_subscribe', data);
const renderAsync = bluebird.promisify(res.render);
const renderAsync = bluebird.promisify(res.render.bind(res));
const html = await renderAsync('subscription/widget-subscribe', data);
const response = {

View file

@ -6,6 +6,7 @@ const net = require('net');
const campaigns = require('../models/campaigns');
const contextHelpers = require('../lib/context-helpers');
const { SubscriptionStatus } = require('../../shared/lists');
const bluebird = require('bluebird');
const seenIds = new Set();
@ -33,7 +34,7 @@ async function readNextChunks() {
try {
const match = /\bstatus=(bounced|sent)\b/.test(line) && line.match(/\bpostfix\/\w+\[\d+\]:\s*([^:]+).*?status=(\w+)/);
if (match) {
let queueId = match[1];
const queueId = match[1];
let queued = '';
let queuedAs = '';
@ -41,7 +42,7 @@ async function readNextChunks() {
seenIds.add(queueId);
// Losacno: Check for local requeue
let status = match[2];
const status = match[2];
log.verbose('POSTFIXBOUNCE', 'Checking message %s for local requeue (status: %s)', queueId, status);
if (status === 'sent') {
// Save new queueId to update message's previous queueId (thanks @mfechner )
@ -82,7 +83,7 @@ async function readNextChunks() {
}
}
module.exports = callback => {
function spawn(callback) {
if (!config.postfixbounce.enabled) {
return setImmediate(callback);
}
@ -122,4 +123,7 @@ module.exports = callback => {
log.info('POSTFIXBOUNCE', 'Server listening on port %s', config.postfixbounce.port);
setImmediate(callback);
});
};
}
module.exports.spawn = bluebird.promisify(spawn);

View file

@ -5,6 +5,7 @@ const config = require('config');
const crypto = require('crypto');
const humanize = require('humanize');
const http = require('http');
const bluebird = require('bluebird');
const SMTPServer = require('smtp-server').SMTPServer;
const simpleParser = require('mailparser').simpleParser;
@ -22,7 +23,7 @@ const mailstore = {
},
getMail(address, callback) {
if (!this.accounts[address] || this.accounts[address].length === 0) {
let err = new Error('No mail for ' + address);
const err = new Error('No mail for ' + address);
err.status = 404;
return callback(err);
}
@ -55,8 +56,8 @@ const server = new SMTPServer({
// Setup authentication
onAuth: (auth, session, callback) => {
let username = config.testServer.username;
let password = config.testServer.password;
const username = config.testServer.username;
const password = config.testServer.password;
// check username and password
if (auth.username === username && auth.password === password) {
@ -80,15 +81,13 @@ const server = new SMTPServer({
// Validate RCPT TO envelope address. Example allows all addresses that do not start with 'deny'
// If this method is not set, all addresses are allowed
onRcptTo: (address, session, callback) => {
let err;
if (/^deny/i.test(address.address)) {
return callback(new Error('Not accepted'));
}
// Reject messages larger than 100 bytes to an over-quota user
if (/^full/i.test(address.address) && Number(session.envelope.mailFrom.args.SIZE) > 100) {
err = new Error('Insufficient channel storage: ' + address.address);
const err = new Error('Insufficient channel storage: ' + address.address);
err.responseCode = 452;
return callback(err);
}
@ -98,7 +97,7 @@ const server = new SMTPServer({
// Handle message stream
onData: (stream, session, callback) => {
let hash = crypto.createHash('md5');
const hash = crypto.createHash('md5');
let message = '';
stream.on('data', chunk => {
hash.update(chunk);
@ -107,9 +106,8 @@ const server = new SMTPServer({
}
});
stream.on('end', () => {
let err;
if (stream.sizeExceeded) {
err = new Error('Error: message exceeds fixed maximum message size 10 MB');
const err = new Error('Error: message exceeds fixed maximum message size 10 MB');
err.responseCode = 552;
return callback(err);
}
@ -129,15 +127,15 @@ server.on('error', err => {
log.error('Test SMTP', err.stack);
});
let mailBoxServer = http.createServer((req, res) => {
let renderer = data => (
const mailBoxServer = http.createServer((req, res) => {
const renderer = data => (
'<!doctype html><html><head><title>' + data.title + '</title></head><body>' + data.body + '</body></html>'
);
let address = req.url.substring(1);
const address = req.url.substring(1);
mailstore.getMail(address, (err, mail) => {
if (err) {
let html = renderer({
const html = renderer({
title: 'error',
body: err.message || err
});
@ -155,7 +153,7 @@ let mailBoxServer = http.createServer((req, res) => {
delete mail.textAsHtml;
delete mail.attachments;
let script = '<script> var mailObject = ' + JSON.stringify(mail) + '; console.log(mailObject); </script>';
const script = '<script> var mailObject = ' + JSON.stringify(mail) + '; console.log(mailObject); </script>';
html = html.replace(/<\/body\b/i, match => script + match);
html = html.replace(/target="_blank"/g, 'target="_self"');
@ -168,7 +166,7 @@ mailBoxServer.on('error', err => {
log.error('Test SMTP Mailbox Server', err);
});
module.exports = callback => {
function spawn(callback) {
if (config.testServer.enabled) {
server.listen(config.testServer.port, config.testServer.host, () => {
log.info('Test SMTP', 'Server listening on port %s', config.testServer.port);
@ -194,4 +192,6 @@ module.exports = callback => {
} else {
setImmediate(callback);
}
};
}
module.exports.spawn = bluebird.promisify(spawn);

View file

@ -7,6 +7,7 @@ const {MailerError} = require('../lib/mailers');
const campaigns = require('../models/campaigns');
const contextHelpers = require('../lib/context-helpers');
const {SubscriptionStatus} = require('../../shared/lists');
const bluebird = require('bluebird');
const BounceHandler = require('bounce-handler').BounceHandler;
const SMTPServer = require('smtp-server').SMTPServer;
@ -85,7 +86,7 @@ const server = new SMTPServer({
onData: onData
});
module.exports = callback => {
function spawn(callback) {
if (!config.verp.enabled) {
return setImmediate(callback);
}
@ -131,7 +132,7 @@ module.exports = callback => {
started = true;
return setImmediate(callback);
}
let host = hosts[pos++];
const host = hosts[pos++];
server.listen(config.verp.port, host, () => {
if (started) {
return server.close();
@ -142,4 +143,6 @@ module.exports = callback => {
};
startNextHost();
};
}
module.exports.spawn = bluebird.promisify(spawn);

View file

@ -1,9 +1,9 @@
'use strict';
const reports = require('../../../models/reports');
const reportTemplates = require('../../../models/report-templates');
const lists = require('../../../models/lists');
const subscriptions = require('../../../models/subscriptions');
const { SubscriptionSource, SubscriptionStatus } = require('../../../../shared/lists');
const campaigns = require('../../../models/campaigns');
const handlebars = require('handlebars');
const hbs = require('hbs');
@ -50,9 +50,11 @@ async function main() {
}
const campaignsProxy = {
getCampaignStatistics: reports.getCampaignStatistics,
getCampaignOpenStatistics: reports.getCampaignOpenStatistics,
getCampaignClickStatistics: reports.getCampaignClickStatistics,
getCampaignLinkClickStatistics: reports.getCampaignLinkClickStatistics,
getCampaignStatisticsStream: reports.getCampaignStatisticsStream,
getCampaignOpenStatisticsStream: reports.getCampaignOpenStatisticsStream,
getCampaignClickStatisticsStream: reports.getCampaignClickStatisticsStream,
getCampaignLinkClickStatisticsStream: reports.getCampaignLinkClickStatisticsStream,
@ -71,17 +73,45 @@ async function main() {
knex,
process,
inputs,
SubscriptionSource,
SubscriptionStatus,
renderCsvFromStream: async (readable, opts) => {
const stringifier = csvStringify(opts);
renderCsvFromStream: async (readable, opts, transform) => {
const finished = new Promise((success, fail) => {
stringifier.on('finish', () => success())
stringifier.on('error', (err) => fail(err))
});
let lastReadable = readable;
stringifier.pipe(process.stdout);
readable.pipe(stringifier);
const stringifier = csvStringify(opts);
stringifier.on('finish', () => success());
stringifier.on('error', err => fail(err));
if (transform) {
const rowTransform = new stream.Transform({
objectMode: true,
transform(row, encoding, callback) {
async function performTransform() {
try {
const newRow = await transform(row, encoding);
callback(null, newRow);
} catch (err) {
callback(err);
}
}
// noinspection JSIgnoredPromiseFromCall
performTransform();
}
});
lastReadable.on('error', err => fail(err));
lastReadable.pipe(rowTransform);
lastReadable = rowTransform;
}
stringifier.pipe(process.stdout);
lastReadable.pipe(stringifier);
});
await finished;
},

View file

@ -206,7 +206,7 @@ mysql:
password: "$mysqlRoPassword"
EOT
reinstallModules
reinstallAllModules
(cd client && npm run build)
@ -215,16 +215,25 @@ EOT
}
function reinstallModules {
function doForAllModules {
# Install required node packages
for idx in client shared server zone-mta mvis/client mvis/server mvis/test-embed mvis/ivis-core/client mvis/ivis-core/server mvis/ivis-core/shared mvis/ivis-core/embedding; do
if [ -d $idx ]; then
echo Reinstalling modules in $idx
(cd $idx && rm -rf node_modules && npm install)
($1 $idx)
fi
done
}
function reinstallModules {
local idx=$1
echo Reinstalling modules in $idx
cd $idx && rm -rf node_modules && npm install
}
function reinstallAllModules {
doForAllModules reinstallModules
}
function installHttpd {
local portTrusted="$1"

View file

@ -6,4 +6,4 @@ SCRIPT_PATH=$(dirname $(realpath -s $0))
. $SCRIPT_PATH/functions
cd $SCRIPT_PATH/..
reinstallModules
reinstallAllModules

View file

@ -4487,9 +4487,9 @@
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"moment-timezone": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.23.tgz",
"integrity": "sha512-WHFH85DkCfiNMDX5D3X7hpNH3/PUhjTGcD0U1SgfBGZxJ3qUmJh5FdvaFjcClxOvB3rzdfj4oRffbI38jEnC1w==",
"version": "0.5.25",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.25.tgz",
"integrity": "sha512-DgEaTyN/z0HFaVcVbSyVCUU6HeFdnNC3vE4c9cgu2dgMTvjBUBdBzWfasTBmAW45u5OIMeCJtU8yNjM22DHucw==",
"requires": {
"moment": ">= 2.9.0"
}

View file

@ -13,7 +13,7 @@
},
"dependencies": {
"moment": "^2.23.0",
"moment-timezone": "^0.5.23",
"moment-timezone": "^0.5.25",
"owasp-password-strength-test": "github:bures/owasp-password-strength-test"
},
"devDependencies": {