Merge branch 'development' of https://github.com/Mailtrain-org/mailtrain into transactional-mail-v2

This commit is contained in:
Alexey Zinkevych 2019-03-31 11:52:42 +03:00
commit e3e1e7a086
153 changed files with 10570 additions and 9267 deletions

View file

@ -1,76 +1,74 @@
'use strict';
"use strict";
const config = require('config');
const log = require('./lib/log');
const config = require("config");
const log = require("./lib/log");
const express = require('express');
const expressLocale = require('express-locale');
const bodyParser = require('body-parser');
const path = require('path');
const favicon = require('serve-favicon');
const logger = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const flash = require('connect-flash');
const hbs = require('hbs');
const compression = require('compression');
const passport = require('./lib/passport');
const contextHelpers = require('./lib/context-helpers');
const express = require("express");
const expressLocale = require("express-locale");
const bodyParser = require("body-parser");
const path = require("path");
const favicon = require("serve-favicon");
const logger = require("morgan");
const cookieParser = require("cookie-parser");
const session = require("express-session");
const flash = require("connect-flash");
const hbs = require("hbs");
const compression = require("compression");
const passport = require("./lib/passport");
const contextHelpers = require("./lib/context-helpers");
const api = require('./routes/api');
const api = require("./routes/api");
// These are routes for the new React-based client
const reports = require('./routes/reports');
const subscriptions = require('./routes/subscriptions');
const subscription = require('./routes/subscription');
const sandboxedMosaico = require('./routes/sandboxed-mosaico');
const sandboxedCKEditor = require('./routes/sandboxed-ckeditor');
const sandboxedGrapesJS = require('./routes/sandboxed-grapesjs');
const sandboxedCodeEditor = require('./routes/sandboxed-codeeditor');
const files = require('./routes/files');
const links = require('./routes/links');
const archive = require('./routes/archive');
const webhooks = require('./routes/webhooks');
const reports = require("./routes/reports");
const subscriptions = require("./routes/subscriptions");
const subscription = require("./routes/subscription");
const sandboxedMosaico = require("./routes/sandboxed-mosaico");
const sandboxedCKEditor = require("./routes/sandboxed-ckeditor");
const sandboxedGrapesJS = require("./routes/sandboxed-grapesjs");
const sandboxedCodeEditor = require("./routes/sandboxed-codeeditor");
const files = require("./routes/files");
const links = require("./routes/links");
const archive = require("./routes/archive");
const webhooks = require("./routes/webhooks");
const namespacesRest = require('./routes/rest/namespaces');
const sendConfigurationsRest = require('./routes/rest/send-configurations');
const usersRest = require('./routes/rest/users');
const accountRest = require('./routes/rest/account');
const reportTemplatesRest = require('./routes/rest/report-templates');
const reportsRest = require('./routes/rest/reports');
const campaignsRest = require('./routes/rest/campaigns');
const triggersRest = require('./routes/rest/triggers');
const listsRest = require('./routes/rest/lists');
const formsRest = require('./routes/rest/forms');
const fieldsRest = require('./routes/rest/fields');
const importsRest = require('./routes/rest/imports');
const importRunsRest = require('./routes/rest/import-runs');
const sharesRest = require('./routes/rest/shares');
const segmentsRest = require('./routes/rest/segments');
const subscriptionsRest = require('./routes/rest/subscriptions');
const templatesRest = require('./routes/rest/templates');
const mosaicoTemplatesRest = require('./routes/rest/mosaico-templates');
const blacklistRest = require('./routes/rest/blacklist');
const editorsRest = require('./routes/rest/editors');
const filesRest = require('./routes/rest/files');
const settingsRest = require('./routes/rest/settings');
const namespacesRest = require("./routes/rest/namespaces");
const sendConfigurationsRest = require("./routes/rest/send-configurations");
const usersRest = require("./routes/rest/users");
const accountRest = require("./routes/rest/account");
const reportTemplatesRest = require("./routes/rest/report-templates");
const reportsRest = require("./routes/rest/reports");
const campaignsRest = require("./routes/rest/campaigns");
const triggersRest = require("./routes/rest/triggers");
const listsRest = require("./routes/rest/lists");
const formsRest = require("./routes/rest/forms");
const fieldsRest = require("./routes/rest/fields");
const importsRest = require("./routes/rest/imports");
const importRunsRest = require("./routes/rest/import-runs");
const sharesRest = require("./routes/rest/shares");
const segmentsRest = require("./routes/rest/segments");
const subscriptionsRest = require("./routes/rest/subscriptions");
const templatesRest = require("./routes/rest/templates");
const mosaicoTemplatesRest = require("./routes/rest/mosaico-templates");
const blacklistRest = require("./routes/rest/blacklist");
const editorsRest = require("./routes/rest/editors");
const filesRest = require("./routes/rest/files");
const settingsRest = require("./routes/rest/settings");
const index = require('./routes/index');
const index = require("./routes/index");
const interoperableErrors = require('../shared/interoperable-errors');
const { getTrustedUrl } = require('./lib/urls');
const { AppType } = require('../shared/app');
const interoperableErrors = require("../shared/interoperable-errors");
const { getTrustedUrl } = require("./lib/urls");
const { AppType } = require("../shared/app");
let isReady = false;
function setReady() {
isReady = true;
isReady = true;
}
hbs.registerPartials(__dirname + '/views/partials');
hbs.registerPartials(__dirname + '/views/subscription/partials/');
hbs.registerPartials(__dirname + "/views/partials");
hbs.registerPartials(__dirname + "/views/subscription/partials/");
/**
* We need this helper to make sure that we consume flash messages only
@ -78,337 +76,388 @@ hbs.registerPartials(__dirname + '/views/subscription/partials/');
* in a situation where we consume a flash messages but then comes a redirect
* and the message is never displayed
*/
hbs.registerHelper('flash_messages', function () { // eslint-disable-line prefer-arrow-callback
if (typeof this.flash !== 'function') { // eslint-disable-line no-invalid-this
return '';
}
hbs.registerHelper("flash_messages", function() {
// eslint-disable-line prefer-arrow-callback
if (typeof this.flash !== "function") {
// eslint-disable-line no-invalid-this
return "";
}
const messages = this.flash(); // eslint-disable-line no-invalid-this
const response = [];
const messages = this.flash(); // eslint-disable-line no-invalid-this
const response = [];
// group messages by type
for (const key in messages) {
let el = '<div class="alert alert-' + key + ' alert-dismissible" role="alert"><button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>';
// group messages by type
for (const key in messages) {
let el =
'<div class="alert alert-' +
key +
' alert-dismissible" role="alert"><button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>';
if (key === 'danger') {
el += '<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> ';
}
if (key === "danger") {
el +=
'<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> ';
}
let rows = [];
let rows = [];
for (const message of messages[key]) {
rows.push(hbs.handlebars.escapeExpression(message).replace(/(\r\n|\n|\r)/gm, '<br>'));
}
for (const message of messages[key]) {
rows.push(
hbs.handlebars
.escapeExpression(message)
.replace(/(\r\n|\n|\r)/gm, "<br>")
);
}
if (rows.length > 1) {
el += '<p>' + rows.join('</p>\n<p>') + '</p>';
} else {
el += rows.join('');
}
if (rows.length > 1) {
el += "<p>" + rows.join("</p>\n<p>") + "</p>";
} else {
el += rows.join("");
}
el += '</div>';
el += "</div>";
response.push(el);
}
response.push(el);
}
return new hbs.handlebars.SafeString(
response.join('\n')
);
return new hbs.handlebars.SafeString(response.join("\n"));
});
function createApp(appType) {
const app = express();
const app = express();
function install404Fallback(url) {
app.use(url, (req, res, next) => {
next(new interoperableErrors.NotFoundError());
});
function install404Fallback(url) {
app.use(url, (req, res, next) => {
next(new interoperableErrors.NotFoundError());
});
app.use(url + '/*', (req, res, next) => {
next(new interoperableErrors.NotFoundError());
});
}
app.use(url + "/*", (req, res, next) => {
next(new interoperableErrors.NotFoundError());
});
}
function useWith404Fallback(url, route) {
app.use(url, route);
install404Fallback(url);
}
function useWith404Fallback(url, route) {
app.use(url, route);
install404Fallback(url);
}
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'hbs');
// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "hbs");
// Handle proxies. Needed to resolve client IP
if (config.www.proxy) {
app.set('trust proxy', config.www.proxy);
}
// Handle proxies. Needed to resolve client IP
if (config.www.proxy) {
app.set("trust proxy", config.www.proxy);
}
// Do not expose software used
app.disable('x-powered-by');
// Do not expose software used
app.disable("x-powered-by");
app.use(compression());
app.use(favicon(path.join(__dirname, '..', 'client', 'static', 'favicon.ico')));
app.use(compression());
app.use(
favicon(path.join(__dirname, "..", "client", "static", "favicon.ico"))
);
app.use(logger(config.www.log, {
stream: {
write: message => {
message = (message || '').toString();
if (message) {
log.info('HTTP', message.replace('\n', '').trim());
}
}
}
}));
app.use(
logger(config.www.log, {
stream: {
write: message => {
message = (message || "").toString();
if (message) {
log.info("HTTP", message.replace("\n", "").trim());
}
}
}
})
);
app.use(cookieParser());
app.use(cookieParser());
if (config.redis.enabled) {
const RedisStore = require('connect-redis')(session);
if (config.redis.enabled) {
const RedisStore = require("connect-redis")(session);
app.use(session({
store: new RedisStore(config.redis),
secret: config.www.secret,
saveUninitialized: false,
resave: false
}));
} else {
app.use(session({
store: false,
secret: config.www.secret,
saveUninitialized: false,
resave: false
}));
}
app.use(
session({
store: new RedisStore(config.redis),
secret: config.www.secret,
saveUninitialized: false,
resave: false
})
);
} else {
app.use(
session({
store: false,
secret: config.www.secret,
saveUninitialized: false,
resave: false
})
);
}
app.use(expressLocale({
priority: ['query', 'cookie', 'accept-language', 'default'],
query: {
name: 'locale'
},
cookie: {
name: 'i18nextLng'
},
default: config.defaultLanguage
}));
app.use(
expressLocale({
priority: ["query", "cookie", "accept-language", "default"],
query: {
name: "locale"
},
cookie: {
name: "i18nextLng"
},
default: config.defaultLanguage
})
);
app.use(flash());
app.use(flash());
app.use(bodyParser.urlencoded({
extended: true,
limit: config.www.postSize
}));
app.use(
bodyParser.urlencoded({
extended: true,
limit: config.www.postSize
})
);
app.use(bodyParser.text({
limit: config.www.postSize
}));
app.use(
bodyParser.text({
limit: config.www.postSize
})
);
app.use(bodyParser.json({
limit: config.www.postSize
}));
app.use(
bodyParser.json({
limit: config.www.postSize
})
);
app.use((req, res, next) => {
if (isReady) {
next();
} else {
res.status(500);
res.render("error", {
message: "Mailtrain is starting. Try again after a few seconds.",
error: {}
});
}
});
app.use((req, res, next) => {
if (isReady) {
next();
} else {
res.status(500);
res.render('error', {
message: 'Mailtrain is starting. Try again after a few seconds.',
error: {}
});
}
});
if (appType === AppType.TRUSTED) {
passport.setupRegularAuth(app);
} else if (appType === AppType.SANDBOXED) {
app.use(passport.tryAuthByRestrictedAccessToken);
}
if (appType === AppType.TRUSTED) {
passport.setupRegularAuth(app);
} else if (appType === AppType.SANDBOXED) {
app.use(passport.tryAuthByRestrictedAccessToken);
}
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
// Endpoint under /api are authenticated by access token
app.all("/api/*", passport.authByAccessToken);
}
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
// Endpoint under /api are authenticated by access token
app.all('/api/*', passport.authByAccessToken);
}
useWith404Fallback(
"/static",
express.static(path.join(__dirname, "..", "client", "static"))
);
useWith404Fallback(
"/client",
express.static(path.join(__dirname, "..", "client", "dist"))
);
useWith404Fallback('/static', express.static(path.join(__dirname, '..', 'client', 'static')));
useWith404Fallback('/client', express.static(path.join(__dirname, '..', 'client', 'dist')));
useWith404Fallback(
"/static-npm/fontawesome",
express.static(
path.join(
__dirname,
"..",
"client",
"node_modules",
"@fortawesome",
"fontawesome-free",
"webfonts"
)
)
);
useWith404Fallback(
"/static-npm/jquery.min.js",
express.static(
path.join(
__dirname,
"..",
"client",
"node_modules",
"jquery",
"dist",
"jquery.min.js"
)
)
);
useWith404Fallback(
"/static-npm/popper.min.js",
express.static(
path.join(
__dirname,
"..",
"client",
"node_modules",
"popper.js",
"dist",
"umd",
"popper.min.js"
)
)
);
useWith404Fallback(
"/static-npm/bootstrap.min.js",
express.static(
path.join(
__dirname,
"..",
"client",
"node_modules",
"bootstrap",
"dist",
"js",
"bootstrap.min.js"
)
)
);
useWith404Fallback(
"/static-npm/coreui.min.js",
express.static(
path.join(
__dirname,
"..",
"client",
"node_modules",
"@coreui",
"coreui",
"dist",
"js",
"coreui.min.js"
)
)
);
useWith404Fallback('/static-npm/fontawesome', express.static(path.join(__dirname, '..', 'client', 'node_modules', '@fortawesome', 'fontawesome-free', 'webfonts')));
useWith404Fallback('/static-npm/jquery.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'jquery', 'dist', 'jquery.min.js')));
useWith404Fallback('/static-npm/popper.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'popper.js', 'dist', 'umd', 'popper.min.js')));
useWith404Fallback('/static-npm/bootstrap.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'bootstrap', 'dist', 'js', 'bootstrap.min.js')));
useWith404Fallback('/static-npm/coreui.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', '@coreui', 'coreui', 'dist', 'js', 'coreui.min.js')));
// Make sure flash messages are available
// Currently, flash messages are used only from routes/subscription.js
app.use((req, res, next) => {
res.locals.flash = req.flash.bind(req);
next();
});
// Marks the following endpoint to return JSON object when error occurs
app.all("/api/*", (req, res, next) => {
req.needsAPIJSONResponse = true;
next();
});
// Make sure flash messages are available
// Currently, flash messages are used only from routes/subscription.js
app.use((req, res, next) => {
res.locals.flash = req.flash.bind(req);
next();
});
app.all("/rest/*", (req, res, next) => {
req.needsRESTJSONResponse = true;
next();
});
// Marks the following endpoint to return JSON object when error occurs
app.all('/api/*', (req, res, next) => {
req.needsAPIJSONResponse = true;
next();
});
// Initializes the request context to be used for authorization
app.use((req, res, next) => {
req.context = contextHelpers.getRequestContext(req);
next();
});
app.all('/rest/*', (req, res, next) => {
req.needsRESTJSONResponse = true;
next();
});
if (appType === AppType.PUBLIC) {
useWith404Fallback("/subscription", subscription);
useWith404Fallback("/links", links);
useWith404Fallback("/archive", archive);
useWith404Fallback("/files", files);
}
// Initializes the request context to be used for authorization
app.use((req, res, next) => {
req.context = contextHelpers.getRequestContext(req);
next();
});
useWith404Fallback("/mosaico", sandboxedMosaico.getRouter(appType));
useWith404Fallback("/ckeditor", sandboxedCKEditor.getRouter(appType));
useWith404Fallback("/grapesjs", sandboxedGrapesJS.getRouter(appType));
useWith404Fallback("/codeeditor", sandboxedCodeEditor.getRouter(appType));
if (appType === AppType.PUBLIC) {
useWith404Fallback('/subscription', subscription);
useWith404Fallback('/links', links);
useWith404Fallback('/archive', archive);
useWith404Fallback('/files', files);
}
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
useWith404Fallback("/subscriptions", subscriptions);
useWith404Fallback("/webhooks", webhooks);
useWith404Fallback('/mosaico', sandboxedMosaico.getRouter(appType));
useWith404Fallback('/ckeditor', sandboxedCKEditor.getRouter(appType));
useWith404Fallback('/grapesjs', sandboxedGrapesJS.getRouter(appType));
useWith404Fallback('/codeeditor', sandboxedCodeEditor.getRouter(appType));
if (config.reports && config.reports.enabled === true) {
useWith404Fallback("/rpts", reports); // This needs to be different from "reports", which is already used by the UI
}
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
useWith404Fallback('/subscriptions', subscriptions);
useWith404Fallback('/webhooks', webhooks);
// API endpoints
useWith404Fallback("/api", api);
if (config.reports && config.reports.enabled === true) {
useWith404Fallback('/rpts', reports); // This needs to be different from "reports", which is already used by the UI
}
// REST endpoints
app.use("/rest", namespacesRest);
app.use("/rest", sendConfigurationsRest);
app.use("/rest", usersRest);
app.use("/rest", accountRest);
app.use("/rest", campaignsRest);
app.use("/rest", triggersRest);
app.use("/rest", listsRest);
app.use("/rest", formsRest);
app.use("/rest", fieldsRest);
app.use("/rest", importsRest);
app.use("/rest", importRunsRest);
app.use("/rest", sharesRest);
app.use("/rest", segmentsRest);
app.use("/rest", subscriptionsRest);
app.use("/rest", templatesRest);
app.use("/rest", mosaicoTemplatesRest);
app.use("/rest", blacklistRest);
app.use("/rest", editorsRest);
app.use("/rest", filesRest);
app.use("/rest", settingsRest);
// API endpoints
useWith404Fallback('/api', api);
if (config.reports && config.reports.enabled === true) {
app.use("/rest", reportTemplatesRest);
app.use("/rest", reportsRest);
}
install404Fallback("/rest");
}
// REST endpoints
app.use('/rest', namespacesRest);
app.use('/rest', sendConfigurationsRest);
app.use('/rest', usersRest);
app.use('/rest', accountRest);
app.use('/rest', campaignsRest);
app.use('/rest', triggersRest);
app.use('/rest', listsRest);
app.use('/rest', formsRest);
app.use('/rest', fieldsRest);
app.use('/rest', importsRest);
app.use('/rest', importRunsRest);
app.use('/rest', sharesRest);
app.use('/rest', segmentsRest);
app.use('/rest', subscriptionsRest);
app.use('/rest', templatesRest);
app.use('/rest', mosaicoTemplatesRest);
app.use('/rest', blacklistRest);
app.use('/rest', editorsRest);
app.use('/rest', filesRest);
app.use('/rest', settingsRest);
app.use("/", index.getRouter(appType));
if (config.reports && config.reports.enabled === true) {
app.use('/rest', reportTemplatesRest);
app.use('/rest', reportsRest);
}
install404Fallback('/rest');
}
app.use((err, req, res, next) => {
if (!err) {
return next();
}
app.use('/', index.getRouter(appType));
if (req.needsRESTJSONResponse) {
const resp = {
message: err.message,
error: config.sendStacktracesToClient ? err : {}
};
// Error handlers
if (app.get('env') === 'development' || app.get('env') === 'test') {
// development error handler
// will print stacktrace
app.use((err, req, res, next) => {
if (!err) {
return next();
}
if (err instanceof interoperableErrors.InteroperableError) {
resp.type = err.type;
resp.data = err.data;
}
if (req.needsRESTJSONResponse) {
const resp = {
message: err.message,
error: err
};
log.verbose("HTTP", err);
res.status(err.status || 500).json(resp);
} else if (req.needsAPIJSONResponse) {
const resp = {
error: err.message || err,
data: []
};
if (err instanceof interoperableErrors.InteroperableError) {
resp.type = err.type;
resp.data = err.data;
}
log.verbose("HTTP", err);
return res.status(err.status || 500).json(resp);
} else {
// TODO: Render interoperable errors using a special client that does internationalization of the error message
res.status(err.status || 500).json(resp);
if (err instanceof interoperableErrors.NotLoggedInError) {
return res.redirect(
getTrustedUrl("/login?next=" + encodeURIComponent(req.originalUrl))
);
} else {
log.verbose("HTTP", err);
res.status(err.status || 500);
res.render("error", {
message: err.message,
error: config.sendStacktracesToClient ? err : {}
});
}
}
});
} else if (req.needsAPIJSONResponse) {
const resp = {
error: err.message || err,
data: []
};
return res.status(err.status || 500).json(resp);
} else {
if (err instanceof interoperableErrors.NotLoggedInError) {
return res.redirect(getTrustedUrl('/login?next=' + encodeURIComponent(req.originalUrl)));
} else {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
}
}
});
} else {
// production error handler
// no stacktraces leaked to user
app.use((err, req, res, next) => {
if (!err) {
return next();
}
if (req.needsRESTJSONResponse) {
const resp = {
message: err.message,
error: {}
};
if (err instanceof interoperableErrors.InteroperableError) {
resp.type = err.type;
resp.data = err.data;
}
res.status(err.status || 500).json(resp);
} else if (req.needsAPIJSONResponse) {
const resp = {
error: err.message || err,
data: []
};
return res.status(err.status || 500).json(resp);
} else {
// TODO: Render interoperable errors using a special client that does internationalization of the error message
if (err instanceof interoperableErrors.NotLoggedInError) {
return res.redirect(getTrustedUrl('/login?next=' + encodeURIComponent(req.originalUrl)));
} else {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
}
}
});
}
return app;
return app;
}
module.exports.createApp = createApp;

View file

@ -42,6 +42,7 @@ defaultLanguage: en-US
# Enabled languages
enabledLanguages:
- en-US
- es-ES
- fk-FK
# Inject custom scripts in subscription/layout.mjml.hbs
@ -72,6 +73,7 @@ redis:
log:
# silly|verbose|info|http|warn|error|silent
level: info
sendStacktracesToClient: false
www:
# HTTP port to listen on for trusted requests (logged-in users)
@ -217,6 +219,8 @@ builtinZoneMTA:
redis: redis://localhost:6379/2
log:
level: warn
processes: 2
connections: 5
seleniumWebDriver:
browser: phantomjs
@ -228,7 +232,7 @@ roles:
name: Global Master
admin: true
description: All permissions
permissions: [rebuildPermissions, createJavascriptWithROAccess, manageBlacklist, manageSettings, setupAutomation]
permissions: [rebuildPermissions, createJavascriptWithROAccess, displayManageUsers, manageBlacklist, manageSettings, setupAutomation]
rootNamespaceRole: master
campaignsAdmin:
name: Campaigns Admin

View file

@ -24,6 +24,7 @@ const { AppType } = require('../shared/app');
const builtinZoneMta = require('./lib/builtin-zone-mta');
const { uploadedFilesDir } = require('./lib/file-helpers');
const { filesDir } = require('./models/files');
const trustedPort = config.www.trustedPort;
const sandboxPort = config.www.sandboxPort;
@ -113,6 +114,7 @@ dbcheck(err => { // Check if database needs upgrading before starting the server
startHTTPServer(AppType.SANDBOXED, 'sandbox', sandboxPort, () =>
startHTTPServer(AppType.PUBLIC, 'public', publicPort, async () => {
await privilegeHelpers.ensureMailtrainDir(filesDir);
await privilegeHelpers.ensureMailtrainDir(uploadedFilesDir);
privilegeHelpers.dropRootPrivileges();

View file

@ -0,0 +1,54 @@
'use strict';
async function _logActivity(typeId, data) {
// TODO
}
/*
Extra data:
campaign:
- status : CampaignStatus
list:
- subscriptionId
- subscriptionStatus : SubscriptionStatus
- fieldId
- segmentId
- importId
- importStatus : ImportStatus
*/
async function logEntityActivity(entityTypeId, activityType, entityId, extraData = {}) {
const data = {
...extraData,
type: activityType,
entity: entityId
};
await _logActivity(entityTypeId, data);
}
async function logCampaignTrackerActivity(activityType, campaignId, listId, subscriptionId, extraData = {}) {
const data = {
...extraData,
type: activityType,
campaign: campaignId,
list: listId,
subscription: subscriptionId
};
await _logActivity('campaign_tracker', data);
}
async function logBlacklistActivity(activityType, email) {
const data = {
type: activityType,
email
};
await _logActivity('blacklist', data);
}
module.exports.logEntityActivity = logEntityActivity;
module.exports.logBlacklistActivity = logBlacklistActivity;
module.exports.logCampaignTrackerActivity = logCampaignTrackerActivity;

View file

@ -108,8 +108,8 @@ async function createConfig() {
default: {
preferIPv6: false,
ignoreIPv6: true,
processes: 1,
connections: 5,
processes: config.builtinZoneMTA.processes,
connections: config.builtinZoneMTA.connections,
pool: 'default'
}
}

View file

@ -0,0 +1,32 @@
'use strict';
function convertFileURLs(sourceCustom, fromEntityType, fromEntityId, toEntityType, toEntityId) {
function convertText(text) {
if (text) {
const fromUrl = `/files/${fromEntityType}/file/${fromEntityId}`;
const toUrl = `/files/${toEntityType}/file/${toEntityId}`;
const encodedFromUrl = encodeURIComponent(fromUrl);
const encodedToUrl = encodeURIComponent(toUrl);
text = text.split('[URL_BASE]' + fromUrl).join('[URL_BASE]' + toUrl);
text = text.split('[SANDBOX_URL_BASE]' + fromUrl).join('[SANDBOX_URL_BASE]' + toUrl);
text = text.split('[ENCODED_URL_BASE]' + encodedFromUrl).join('[ENCODED_URL_BASE]' + encodedToUrl);
text = text.split('[ENCODED_SANDBOX_URL_BASE]' + encodedFromUrl).join('[ENCODED_SANDBOX_URL_BASE]' + encodedToUrl);
}
return text;
}
sourceCustom.html = convertText(sourceCustom.html);
sourceCustom.text = convertText(sourceCustom.text);
if (sourceCustom.type === 'mosaico' || sourceCustom.type === 'mosaicoWithFsTemplate') {
sourceCustom.data.model = convertText(sourceCustom.data.model);
sourceCustom.data.model = convertText(sourceCustom.data.model);
sourceCustom.data.metadata = convertText(sourceCustom.data.metadata);
}
}
module.exports.convertFileURLs = convertFileURLs;

View file

@ -394,6 +394,18 @@ class CampaignSender {
try {
const info = await mailer.sendMassMail(mail);
status = SubscriptionStatus.SUBSCRIBED;
/*
ZoneMTA
info.response: 250 Message queued as 1691ad7f7ae00080fd
info.messageId: <e65c9386-e899-7d01-b21e-ec03c3a9d9b4@sathyasai.org>
Postal Mail Server
info.response: 250 OK
info.messageId: <xxxxxxxxx@xxx.xx> (postal messageId)
*/
console.log(`response: ${info.response} messageId: ${info.messageId}`);
response = info.response || info.messageId;
await knex('campaigns').where('id', campaign.id).increment('delivered');

View file

@ -1,151 +1,151 @@
'use strict';
const ReplacementBehavior = {
NONE: 1,
REPLACE: 2,
RENAME: 3
};
const entityTypes = {
namespace: {
entitiesTable: 'namespaces',
sharesTable: 'shares_namespace',
permissionsTable: 'permissions_namespace',
clientLink: id => `/namespaces/${id}`
},
list: {
entitiesTable: 'lists',
sharesTable: 'shares_list',
permissionsTable: 'permissions_list',
clientLink: id => `/lists/${id}`
},
customForm: {
entitiesTable: 'custom_forms',
sharesTable: 'shares_custom_form',
permissionsTable: 'permissions_custom_form',
clientLink: id => `/lists/forms/${id}`
},
campaign: {
entitiesTable: 'campaigns',
sharesTable: 'shares_campaign',
permissionsTable: 'permissions_campaign',
dependentPermissions: {
extraColumns: ['parent'],
getParent: entity => entity.parent
},
files: {
file: {
table: 'files_campaign_file',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
},
attachment: {
table: 'files_campaign_attachment',
permissions: {
view: 'viewAttachments',
manage: 'manageAttachments'
},
defaultReplacementBehavior: ReplacementBehavior.NONE
}
},
clientLink: id => `/campaigns/${id}`
},
template: {
entitiesTable: 'templates',
sharesTable: 'shares_template',
permissionsTable: 'permissions_template',
files: {
file: {
table: 'files_template_file',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
}
},
clientLink: id => `/templates/${id}`
},
sendConfiguration: {
entitiesTable: 'send_configurations',
sharesTable: 'shares_send_configuration',
permissionsTable: 'permissions_send_configuration',
clientLink: id => `/send-configurations/${id}`
},
report: {
entitiesTable: 'reports',
sharesTable: 'shares_report',
permissionsTable: 'permissions_report',
clientLink: id => `/reports/${id}`
},
reportTemplate: {
entitiesTable: 'report_templates',
sharesTable: 'shares_report_template',
permissionsTable: 'permissions_report_template',
clientLink: id => `/reports/templates/${id}`
},
mosaicoTemplate: {
entitiesTable: 'mosaico_templates',
sharesTable: 'shares_mosaico_template',
permissionsTable: 'permissions_mosaico_template',
files: {
file: {
table: 'files_mosaico_template_file',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
},
block: {
table: 'files_mosaico_template_block',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
}
},
clientLink: id => `/templates/mosaico/${id}`
},
user: {
entitiesTable: 'users',
clientLink: id => `/users/${id}`
}
};
const entityTypesWithPermissions = {};
for (const key in entityTypes) {
if (entityTypes[key].permissionsTable) {
entityTypesWithPermissions[key] = entityTypes[key];
}
}
function getEntityTypes() {
return entityTypes;
}
function getEntityTypesWithPermissions() {
return entityTypesWithPermissions;
}
function getEntityType(entityTypeId) {
const entityType = entityTypes[entityTypeId];
if (!entityType) {
throw new Error(`Unknown entity type ${entityTypeId}`);
}
return entityType
}
module.exports = {
getEntityTypes,
getEntityTypesWithPermissions,
getEntityType,
ReplacementBehavior
'use strict';
const ReplacementBehavior = {
NONE: 1,
REPLACE: 2,
RENAME: 3
};
const entityTypes = {
namespace: {
entitiesTable: 'namespaces',
sharesTable: 'shares_namespace',
permissionsTable: 'permissions_namespace',
clientLink: id => `/namespaces/${id}`
},
list: {
entitiesTable: 'lists',
sharesTable: 'shares_list',
permissionsTable: 'permissions_list',
clientLink: id => `/lists/${id}`
},
customForm: {
entitiesTable: 'custom_forms',
sharesTable: 'shares_custom_form',
permissionsTable: 'permissions_custom_form',
clientLink: id => `/lists/forms/${id}`
},
campaign: {
entitiesTable: 'campaigns',
sharesTable: 'shares_campaign',
permissionsTable: 'permissions_campaign',
dependentPermissions: {
extraColumns: ['parent'],
getParent: entity => entity.parent
},
files: {
file: {
table: 'files_campaign_file',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
},
attachment: {
table: 'files_campaign_attachment',
permissions: {
view: 'viewAttachments',
manage: 'manageAttachments'
},
defaultReplacementBehavior: ReplacementBehavior.NONE
}
},
clientLink: id => `/campaigns/${id}`
},
template: {
entitiesTable: 'templates',
sharesTable: 'shares_template',
permissionsTable: 'permissions_template',
files: {
file: {
table: 'files_template_file',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
}
},
clientLink: id => `/templates/${id}`
},
sendConfiguration: {
entitiesTable: 'send_configurations',
sharesTable: 'shares_send_configuration',
permissionsTable: 'permissions_send_configuration',
clientLink: id => `/send-configurations/${id}`
},
report: {
entitiesTable: 'reports',
sharesTable: 'shares_report',
permissionsTable: 'permissions_report',
clientLink: id => `/reports/${id}`
},
reportTemplate: {
entitiesTable: 'report_templates',
sharesTable: 'shares_report_template',
permissionsTable: 'permissions_report_template',
clientLink: id => `/reports/templates/${id}`
},
mosaicoTemplate: {
entitiesTable: 'mosaico_templates',
sharesTable: 'shares_mosaico_template',
permissionsTable: 'permissions_mosaico_template',
files: {
file: {
table: 'files_mosaico_template_file',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
},
block: {
table: 'files_mosaico_template_block',
permissions: {
view: 'viewFiles',
manage: 'manageFiles'
},
defaultReplacementBehavior: ReplacementBehavior.REPLACE
}
},
clientLink: id => `/templates/mosaico/${id}`
},
user: {
entitiesTable: 'users',
clientLink: id => `/users/${id}`
}
};
const entityTypesWithPermissions = {};
for (const key in entityTypes) {
if (entityTypes[key].permissionsTable) {
entityTypesWithPermissions[key] = entityTypes[key];
}
}
function getEntityTypes() {
return entityTypes;
}
function getEntityTypesWithPermissions() {
return entityTypesWithPermissions;
}
function getEntityType(entityTypeId) {
const entityType = entityTypes[entityTypeId];
if (!entityType) {
throw new Error(`Unknown entity type ${entityTypeId}`);
}
return entityType
}
module.exports = {
getEntityTypes,
getEntityTypesWithPermissions,
getEntityType,
ReplacementBehavior
}

View file

@ -5,6 +5,8 @@ const fork = require('child_process').fork;
const log = require('./log');
const path = require('path');
const {ImportStatus, RunStatus} = require('../../shared/imports');
const {ListActivityType} = require('../../shared/activity-log');
const activityLog = require('./activity-log');
let messageTid = 0;
let importerProcess;
@ -18,11 +20,17 @@ function spawn(callback) {
log.verbose('Importer', 'Spawning importer process');
knex.transaction(async tx => {
await tx('imports').where('status', ImportStatus.PREP_RUNNING).update({status: ImportStatus.PREP_SCHEDULED});
await tx('imports').where('status', ImportStatus.PREP_STOPPING).update({status: ImportStatus.PREP_FAILED});
const updateStatus = async (fromStatus, toStatus) => {
for (const impt of await tx('imports').where('status', fromStatus).select(['id', 'list'])) {
await tx('imports').where('id', impt.id).update({status: toStatus});
await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: toStatus});
}
}
await tx('imports').where('status', ImportStatus.RUN_RUNNING).update({status: ImportStatus.RUN_SCHEDULED});
await tx('imports').where('status', ImportStatus.RUN_STOPPING).update({status: ImportStatus.RUN_FAILED});
await updateStatus(ImportStatus.PREP_RUNNING, ImportStatus.PREP_SCHEDULED);
await updateStatus(ImportStatus.PREP_STOPPING, ImportStatus.PREP_FAILED);
await updateStatus(ImportStatus.RUN_RUNNING, ImportStatus.RUN_SCHEDULED);
await updateStatus(ImportStatus.RUN_STOPPING, ImportStatus.RUN_FAILED);
await tx('import_runs').where('status', RunStatus.RUNNING).update({status: RunStatus.SCHEDULED});
await tx('import_runs').where('status', RunStatus.STOPPING).update({status: RunStatus.FAILED});

View file

@ -1,8 +1,8 @@
'use strict';
const config = require('config');
const log = require('npmlog');
log.level = config.log.level;
'use strict';
const config = require('config');
const log = require('npmlog');
log.level = config.log.level;
module.exports = log;

View file

@ -173,7 +173,7 @@ async function _createTransport(sendConfiguration) {
}
};
if (mailerType === MailerType.ZONE_MTA || mailerSettings.zoneMTAType === ZoneMTAType.BUILTIN) {
if (mailerType === MailerType.ZONE_MTA && mailerSettings.zoneMtaType === ZoneMTAType.BUILTIN) {
transportOptions.host = config.builtinZoneMTA.host;
transportOptions.port = config.builtinZoneMTA.port;
transportOptions.secure = false;

View file

@ -1,24 +1,24 @@
'use strict';
const { enforce } = require('./helpers');
const interoperableErrors = require('../../shared/interoperable-errors');
const shares = require('../models/shares');
async function validateEntity(tx, entity) {
enforce(entity.namespace, 'Entity namespace not set');
if (!await tx('namespaces').where('id', entity.namespace).first()) {
throw new interoperableErrors.NamespaceNotFoundError();
}
}
async function validateMove(context, entity, existing, entityTypeId, createOperation, deleteOperation) {
if (existing.namespace !== entity.namespace) {
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, createOperation);
await shares.enforceEntityPermission(context, entityTypeId, entity.id, deleteOperation);
}
}
module.exports = {
validateEntity,
validateMove
'use strict';
const { enforce } = require('./helpers');
const interoperableErrors = require('../../shared/interoperable-errors');
const shares = require('../models/shares');
async function validateEntity(tx, entity) {
enforce(entity.namespace, 'Entity namespace not set');
if (!await tx('namespaces').where('id', entity.namespace).first()) {
throw new interoperableErrors.NamespaceNotFoundError();
}
}
async function validateMove(context, entity, existing, entityTypeId, createOperation, deleteOperation) {
if (existing.namespace !== entity.namespace) {
await shares.enforceEntityPermission(context, 'namespace', entity.namespace, createOperation);
await shares.enforceEntityPermission(context, entityTypeId, entity.id, deleteOperation);
}
}
module.exports = {
validateEntity,
validateMove
};

View file

@ -1,15 +1,15 @@
'use strict';
const nodeify = require('nodeify');
module.exports.nodeifyPromise = nodeify;
module.exports.nodeifyFunction = (asyncFun) => {
return (...args) => {
const callback = args.pop();
const promise = asyncFun(...args);
return module.exports.nodeifyPromise(promise, callback);
};
};
'use strict';
const nodeify = require('nodeify');
module.exports.nodeifyPromise = nodeify;
module.exports.nodeifyFunction = (asyncFun) => {
return (...args) => {
const callback = args.pop();
const promise = asyncFun(...args);
return module.exports.nodeifyPromise(promise, callback);
};
};

View file

@ -129,7 +129,7 @@ async function _sendMail(list, email, template, locale, subjectKey, relativeUrls
};
if (list.default_form) {
const form = await forms.getById(contextHelpers.getAdminContext(), list.default_form);
const form = await forms.getById(contextHelpers.getAdminContext(), list.default_form, false);
text.template = form['mail_' + template + '_text'] || text.template;
html.template = form['mail_' + template + '_html'] || html.template;

View file

@ -13,6 +13,7 @@ function loadLanguage(longCode) {
}
loadLanguage('en-US');
loadLanguage('es-ES');
resourcesCommon['fk-FK'] = convertToFake(resourcesCommon['en-US']);
const resources = {};

View file

@ -1,72 +1,72 @@
'use strict';
const config = require('config');
const urllib = require('url');
const {anonymousRestrictedAccessToken} = require('../../shared/urls');
const {getLangCodeFromExpressLocale} = require('./translate');
function getTrustedUrlBase() {
return urllib.resolve(config.www.trustedUrlBase, '');
}
function getSandboxUrlBase() {
return urllib.resolve(config.www.sandboxUrlBase, '');
}
function getPublicUrlBase() {
return urllib.resolve(config.www.publicUrlBase, '');
}
function _getUrl(urlBase, path, opts) {
const url = new URL(path || '', urlBase);
if (opts && opts.locale) {
url.searchParams.append('locale', getLangCodeFromExpressLocale(opts.locale));
}
return url.toString();
}
function getTrustedUrl(path, opts) {
return _getUrl(config.www.trustedUrlBase, path || '', opts);
}
function getSandboxUrl(path, context, opts) {
if (context && context.user && context.user.restrictedAccessToken) {
return _getUrl(config.www.sandboxUrlBase, context.user.restrictedAccessToken + '/' + (path || ''), opts);
} else {
return _getUrl(config.www.sandboxUrlBase, anonymousRestrictedAccessToken + '/' + (path || ''), opts);
}
}
function getPublicUrl(path, opts) {
return _getUrl(config.www.publicUrlBase, path || '', opts);
}
function getTrustedUrlBaseDir() {
const mailtrainUrl = urllib.parse(config.www.trustedUrlBase);
return mailtrainUrl.pathname;
}
function getSandboxUrlBaseDir() {
const mailtrainUrl = urllib.parse(config.www.sandboxUrlBase);
return mailtrainUrl.pathname;
}
function getPublicUrlBaseDir() {
const mailtrainUrl = urllib.parse(config.www.publicUrlBase);
return mailtrainUrl.pathname;
}
module.exports = {
getTrustedUrl,
getSandboxUrl,
getPublicUrl,
getTrustedUrlBase,
getSandboxUrlBase,
getPublicUrlBase,
getTrustedUrlBaseDir,
getSandboxUrlBaseDir,
getPublicUrlBaseDir
'use strict';
const config = require('config');
const urllib = require('url');
const {anonymousRestrictedAccessToken} = require('../../shared/urls');
const {getLangCodeFromExpressLocale} = require('./translate');
function getTrustedUrlBase() {
return urllib.resolve(config.www.trustedUrlBase, '');
}
function getSandboxUrlBase() {
return urllib.resolve(config.www.sandboxUrlBase, '');
}
function getPublicUrlBase() {
return urllib.resolve(config.www.publicUrlBase, '');
}
function _getUrl(urlBase, path, opts) {
const url = new URL(path || '', urlBase);
if (opts && opts.locale) {
url.searchParams.append('locale', getLangCodeFromExpressLocale(opts.locale));
}
return url.toString();
}
function getTrustedUrl(path, opts) {
return _getUrl(config.www.trustedUrlBase, path || '', opts);
}
function getSandboxUrl(path, context, opts) {
if (context && context.user && context.user.restrictedAccessToken) {
return _getUrl(config.www.sandboxUrlBase, context.user.restrictedAccessToken + '/' + (path || ''), opts);
} else {
return _getUrl(config.www.sandboxUrlBase, anonymousRestrictedAccessToken + '/' + (path || ''), opts);
}
}
function getPublicUrl(path, opts) {
return _getUrl(config.www.publicUrlBase, path || '', opts);
}
function getTrustedUrlBaseDir() {
const mailtrainUrl = urllib.parse(config.www.trustedUrlBase);
return mailtrainUrl.pathname;
}
function getSandboxUrlBaseDir() {
const mailtrainUrl = urllib.parse(config.www.sandboxUrlBase);
return mailtrainUrl.pathname;
}
function getPublicUrlBaseDir() {
const mailtrainUrl = urllib.parse(config.www.publicUrlBase);
return mailtrainUrl.pathname;
}
module.exports = {
getTrustedUrl,
getSandboxUrl,
getPublicUrl,
getTrustedUrlBase,
getSandboxUrlBase,
getPublicUrlBase,
getTrustedUrlBaseDir,
getSandboxUrlBaseDir,
getPublicUrlBaseDir
};

View file

@ -6,6 +6,10 @@ const shares = require('./shares');
const tools = require('../lib/tools');
const { enforce } = require('../lib/helpers');
const {BlacklistActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
async function listDTAjax(context, params) {
shares.enforceGlobalPermission(context, 'manageBlacklist');
@ -44,14 +48,21 @@ async function add(context, email) {
if (!existing) {
await tx('blacklist').insert({email});
}
await activityLog.logBlacklistActivity(BlacklistActivityType.ADD, email);
});
}
async function remove(context, email) {
enforce(email, 'Email has to be set');
shares.enforceGlobalPermission(context, 'manageBlacklist');
await knex('blacklist').where('email', email).del();
return await knex.transaction(async tx => {
shares.enforceGlobalPermission(context, 'manageBlacklist');
await tx('blacklist').where('email', email).del();
await activityLog.logBlacklistActivity(BlacklistActivityType.REMOVE, email);
});
}
async function isBlacklisted(email) {

View file

@ -20,6 +20,10 @@ const senders = require('../lib/senders');
const {LinkId} = require('./links');
const feedcheck = require('../lib/feedcheck');
const contextHelpers = require('../lib/context-helpers');
const {convertFileURLs} = require('../lib/campaign-content');
const {EntityActivityType, CampaignActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
const allowedKeysCommon = ['name', 'description', 'segment', 'namespace',
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
@ -60,18 +64,32 @@ function hash(entity, content) {
return hasher.hash(filteredEntity);
}
async function listDTAjax(context, params) {
async function _listDTAjax(context, namespaceId, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
params,
builder => builder.from('campaigns')
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
.whereNull('campaigns.parent'),
builder => {
builder = builder.from('campaigns')
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
.whereNull('campaigns.parent');
if (namespaceId) {
builder = builder.where('namespaces.id', namespaceId);
}
return builder;
},
['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
);
}
async function listDTAjax(context, params) {
return await _listDTAjax(context, undefined, params);
}
async function listByNamespaceDTAjax(context, namespaceId, params) {
return await _listDTAjax(context, namespaceId, params);
}
async function listChildrenDTAjax(context, campaignId, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
@ -427,35 +445,6 @@ async function _validateAndPreprocess(tx, context, entity, isCreate, content) {
}
}
function convertFileURLs(sourceCustom, fromEntityType, fromEntityId, toEntityType, toEntityId) {
function convertText(text) {
if (text) {
const fromUrl = `/files/${fromEntityType}/file/${fromEntityId}`;
const toUrl = `/files/${toEntityType}/file/${toEntityId}`;
const encodedFromUrl = encodeURIComponent(fromUrl);
const encodedToUrl = encodeURIComponent(toUrl);
text = text.split('[URL_BASE]' + fromUrl).join('[URL_BASE]' + toUrl);
text = text.split('[SANDBOX_URL_BASE]' + fromUrl).join('[SANDBOX_URL_BASE]' + toUrl);
text = text.split('[ENCODED_URL_BASE]' + encodedFromUrl).join('[ENCODED_URL_BASE]' + encodedToUrl);
text = text.split('[ENCODED_SANDBOX_URL_BASE]' + encodedFromUrl).join('[ENCODED_SANDBOX_URL_BASE]' + encodedToUrl);
}
return text;
}
sourceCustom.html = convertText(sourceCustom.html);
sourceCustom.text = convertText(sourceCustom.text);
if (sourceCustom.type === 'mosaico' || sourceCustom.type === 'mosaicoWithFsTemplate') {
sourceCustom.data.model = convertText(sourceCustom.data.model);
sourceCustom.data.model = convertText(sourceCustom.data.model);
sourceCustom.data.metadata = convertText(sourceCustom.data.metadata);
}
}
async function _createTx(tx, context, entity, content) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCampaign');
@ -533,6 +522,8 @@ async function _createTx(tx, context, entity, content) {
}).where('id', id);
}
await activityLog.logEntityActivity('campaign', EntityActivityType.CREATE, id, {status: filteredEntity.status});
return id;
});
}
@ -566,7 +557,7 @@ async function updateWithConsistencyCheck(context, entity, content) {
} else if (content === Content.WITHOUT_SOURCE_CUSTOM) {
filteredEntity.data.sourceCustom = existing.data.sourceCustom;
await namespaceHelpers.validateMove(context, filteredEntity, existing, 'campaign', 'createCampaign', 'delete');
await namespaceHelpers.validateMove(context, filteredEntity, existing, 'campaign', 'createCampaign', 'delete'); // XXX TB - try with entity
} else if (content === Content.ONLY_SOURCE_CUSTOM) {
const data = existing.data;
@ -591,6 +582,8 @@ async function updateWithConsistencyCheck(context, entity, content) {
await tx('campaigns').where('id', entity.id).update(filteredEntity);
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: entity.id });
await activityLog.logEntityActivity('campaign', EntityActivityType.UPDATE, entity.id, {status: filteredEntity.status});
});
}
@ -628,6 +621,8 @@ async function _removeTx(tx, context, id, existing = null) {
.del();
await tx('campaigns').where('id', id).del();
await activityLog.logEntityActivity('campaign', EntityActivityType.REMOVE, id);
}
@ -727,9 +722,7 @@ async function _changeStatusByMessageTx(tx, context, message, subscriptionStatus
const statusField = statusFieldMapping[subscriptionStatus];
if (message.status === SubscriptionStatus.SUBSCRIBED) {
await tx('campaigns').increment(statusField, 1).where('id', message.campaign);
}
await tx('campaigns').increment(statusField, 1).where('id', message.campaign);
await tx('campaign_messages')
.where('id', message.id)
@ -745,10 +738,14 @@ async function changeStatusByCampaignCidAndSubscriptionIdTx(tx, context, campaig
const message = await tx('campaign_messages')
.innerJoin('campaigns', 'campaign_messages.campaign', 'campaigns.id')
.where('campaigns.cid', campaignCid)
.where({subscription: subscriptionId, list: listId});
.where({subscription: subscriptionId, list: listId})
.select([
'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'campaign_messages.subscription', 'campaign_messages.status'
])
.first();
if (!message) {
throw new Error('Invalid campaign.')
throw new Error('Invalid campaign.');
}
await _changeStatusByMessageTx(tx, context, message, subscriptionStatus);
@ -863,6 +860,8 @@ async function _changeStatus(context, campaignId, permittedCurrentStates, newSta
status: newState,
scheduled
});
await activityLog.logEntityActivity('campaign', CampaignActivityType.STATUS_CHANGE, campaignId, {status: newState});
});
senders.scheduleCheck();
@ -949,6 +948,7 @@ module.exports.Content = Content;
module.exports.hash = hash;
module.exports.listDTAjax = listDTAjax;
module.exports.listByNamespaceDTAjax = listByNamespaceDTAjax;
module.exports.listChildrenDTAjax = listChildrenDTAjax;
module.exports.listWithContentDTAjax = listWithContentDTAjax;
module.exports.listOthersWhoseListsAreIncludedDTAjax = listOthersWhoseListsAreIncludedDTAjax;

View file

@ -16,6 +16,8 @@ const { cleanupFromPost } = require('../lib/helpers');
const Handlebars = require('handlebars');
const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
const { getMergeTagsForBases } = require('../../shared/templates');
const {ListActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
const allowedKeysCreate = new Set(['name', 'key', 'default_value', 'type', 'group', 'settings']);
@ -565,6 +567,8 @@ async function createTx(tx, context, listId, entity) {
await knex.schema.raw('ALTER TABLE `subscription__' + listId + '` ADD `source_' + columnName +'` int(11) DEFAULT NULL');
}
await activityLog.logEntityActivity('list', ListActivityType.CREATE_FIELD, listId, {fieldId: id});
return id;
}
@ -594,6 +598,8 @@ async function updateWithConsistencyCheck(context, listId, entity) {
await tx('custom_fields').where({list: listId, id: entity.id}).update(filterObject(entity, allowedKeysUpdate));
await _sortIn(tx, listId, entity.id, entity.orderListBefore, entity.orderSubscribeBefore, entity.orderManageBefore);
await activityLog.logEntityActivity('list', ListActivityType.UPDATE_FIELD, listId, {fieldId: entity.id});
});
}
@ -620,6 +626,8 @@ async function removeTx(tx, context, listId, id) {
await segments.removeRulesByColumnTx(tx, context, listId, existing.column);
}
await activityLog.logEntityActivity('list', ListActivityType.REMOVE_FIELD, listId, {fieldId: id});
}
async function remove(context, listId, id) {

View file

@ -10,6 +10,8 @@ const {ImportSource, MappingType, ImportStatus, RunStatus, prepFinished, prepFin
const fs = require('fs-extra-promise');
const path = require('path');
const importer = require('../lib/importer');
const {ListActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
const files = require('./files');
const filesDir = path.join(files.filesDir, 'imports');
@ -117,6 +119,8 @@ async function create(context, listId, entity, files) {
const ids = await tx('imports').insert(filteredEntity);
const id = ids[0];
await activityLog.logEntityActivity('list', ListActivityType.CREATE_IMPORT, listId, {importId: id, importStatus: entity.status});
return id;
});
@ -148,6 +152,8 @@ async function updateWithConsistencyCheck(context, listId, entity) {
filteredEntity.mapping = JSON.stringify(filteredEntity.mapping);
await tx('imports').where({list: listId, id: entity.id}).update(filteredEntity);
await activityLog.logEntityActivity('list', ListActivityType.UPDATE_IMPORT, listId, {importId: entity.id, importStatus: entity.status});
});
}
@ -170,6 +176,8 @@ async function removeTx(tx, context, listId, id) {
await tx('import_failed').whereIn('run', function() {this.from('import_runs').select('id').where('import', id)}).del();
await tx('import_runs').where('import', id).del();
await tx('imports').where({list: listId, id}).del();
await activityLog.logEntityActivity('list', ListActivityType.REMOVE_IMPORT, listId, {importId: id});
}
async function remove(context, listId, id) {
@ -208,6 +216,8 @@ async function start(context, listId, id) {
status: RunStatus.SCHEDULED,
mapping: entity.mapping
});
await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, listId, {importId: id, importStatus: ImportStatus.RUN_SCHEDULED});
});
importer.scheduleCheck();
@ -234,6 +244,8 @@ async function stop(context, listId, id) {
await tx('import_runs').where('import', id).whereIn('status', [RunStatus.SCHEDULED, RunStatus.RUNNING]).update({
status: RunStatus.STOPPING
});
await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, listId, {importId: id, importStatus: ImportStatus.RUN_STOPPING});
});
importer.scheduleCheck();

View file

@ -14,6 +14,9 @@ const imports = require('./imports');
const entitySettings = require('../lib/entity-settings');
const dependencyHelpers = require('../lib/dependency-helpers');
const {EntityActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
const {UnsubscriptionMode, FieldWizard} = require('../../shared/lists');
const allowedKeys = new Set(['name', 'description', 'default_form', 'public_subscribe', 'unsubscription_mode', 'contact_email', 'homepage', 'namespace', 'to_name', 'listunsubscribe_disabled', 'send_configuration']);
@ -23,16 +26,22 @@ function hash(entity) {
}
async function listDTAjax(context, params) {
async function _listDTAjax(context, namespaceId, params) {
const campaignEntityType = entitySettings.getEntityType('campaign');
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'list', requiredOperations: ['view'] }],
params,
builder => builder
.from('lists')
.innerJoin('namespaces', 'namespaces.id', 'lists.namespace'),
builder => {
builder = builder
.from('lists')
.innerJoin('namespaces', 'namespaces.id', 'lists.namespace');
if (namespaceId) {
builder = builder.where('lists.namespace', namespaceId);
}
return builder;
},
['lists.id', 'lists.name', 'lists.cid', 'lists.subscribers', 'lists.description', 'namespaces.name',
{
name: 'triggerCount',
@ -50,6 +59,14 @@ async function listDTAjax(context, params) {
);
}
async function listDTAjax(context, params) {
return await _listDTAjax(context, undefined, params);
}
async function listByNamespaceDTAjax(context, namespaceId, params) {
return await _listDTAjax(context, namespaceId, params);
}
async function listWithSegmentByCampaignDTAjax(context, campaignId, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
@ -196,6 +213,8 @@ async function create(context, entity) {
await fields.createTx(tx, context, id, fld);
}
await activityLog.logEntityActivity('list', EntityActivityType.CREATE, id);
return id;
});
}
@ -221,6 +240,8 @@ async function updateWithConsistencyCheck(context, entity) {
await tx('lists').where('id', entity.id).update(filterObject(entity, allowedKeys));
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'list', entityId: entity.id });
await activityLog.logEntityActivity('list', EntityActivityType.UPDATE, entity.id);
});
}
@ -244,6 +265,8 @@ async function remove(context, id) {
await tx('lists').where('id', id).del();
await knex.schema.dropTableIfExists('subscription__' + id);
await activityLog.logEntityActivity('list', EntityActivityType.REMOVE, id);
});
}
@ -251,6 +274,7 @@ async function remove(context, id) {
module.exports.UnsubscriptionMode = UnsubscriptionMode;
module.exports.hash = hash;
module.exports.listDTAjax = listDTAjax;
module.exports.listByNamespaceDTAjax = listByNamespaceDTAjax;
module.exports.listWithSegmentByCampaignDTAjax = listWithSegmentByCampaignDTAjax;
module.exports.getByIdTx = getByIdTx;
module.exports.getById = getById;

View file

@ -10,6 +10,8 @@ const moment = require('moment');
const fields = require('./fields');
const subscriptions = require('./subscriptions');
const dependencyHelpers = require('../lib/dependency-helpers');
const {ListActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
const allowedKeys = new Set(['name', 'settings']);
@ -304,6 +306,8 @@ async function create(context, listId, entity) {
const ids = await tx('segments').insert(filteredEntity);
const id = ids[0];
await activityLog.logEntityActivity('list', ListActivityType.CREATE_SEGMENT, listId, {segmentId: id});
return id;
});
}
@ -327,6 +331,8 @@ async function updateWithConsistencyCheck(context, listId, entity) {
await _validateAndPreprocess(tx, listId, entity, false);
await tx('segments').where({list: listId, id: entity.id}).update(filterObject(entity, allowedKeys));
await activityLog.logEntityActivity('list', ListActivityType.UPDATE_SEGMENT, listId, {segmentId: entity.id});
});
}
@ -346,6 +352,8 @@ async function removeTx(tx, context, listId, id) {
// The listId "where" is here to prevent deleting segment of a list for which a user does not have permission
await tx('segments').where({list: listId, id}).del();
await activityLog.logEntityActivity('list', ListActivityType.REMOVE_SEGMENT, listId, {segmentId: id});
}
async function remove(context, listId, id) {

View file

@ -22,18 +22,32 @@ function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys));
}
async function listDTAjax(context, params) {
async function _listDTAjax(context, namespaceId, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'sendConfiguration', requiredOperations: ['viewPublic'] }],
params,
builder => builder
.from('send_configurations')
.innerJoin('namespaces', 'namespaces.id', 'send_configurations.namespace'),
builder => {
builder = builder
.from('send_configurations')
.innerJoin('namespaces', 'namespaces.id', 'send_configurations.namespace');
if (namespaceId) {
builder = builder.where('send_configurations.namespace', namespaceId);
}
return builder;
},
['send_configurations.id', 'send_configurations.name', 'send_configurations.cid', 'send_configurations.description', 'send_configurations.mailer_type', 'send_configurations.created', 'namespaces.name']
);
}
async function listDTAjax(context, params) {
return await _listDTAjax(context, undefined, params);
}
async function listByNamespaceDTAjax(context, namespaceId, params) {
return await _listDTAjax(context, namespaceId, params);
}
async function listWithSendPermissionDTAjax(context, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
@ -175,6 +189,7 @@ async function getSystemSendConfiguration() {
module.exports.hash = hash;
module.exports.listDTAjax = listDTAjax;
module.exports.listByNamespaceDTAjax = listByNamespaceDTAjax;
module.exports.listWithSendPermissionDTAjax = listWithSendPermissionDTAjax;
module.exports.getByIdTx = getByIdTx;
module.exports.getById = getById;

View file

@ -10,6 +10,7 @@ const shares = require('./shares');
const reports = require('./reports');
const files = require('./files');
const dependencyHelpers = require('../lib/dependency-helpers');
const {convertFileURLs} = require('../lib/campaign-content');
const allowedKeys = new Set(['name', 'description', 'type', 'data', 'html', 'text', 'namespace']);
@ -35,16 +36,30 @@ async function getById(context, id, withPermissions = true) {
});
}
async function listDTAjax(context, params) {
async function _listDTAjax(context, namespaceId, params) {
return await dtHelpers.ajaxListWithPermissions(
context,
[{ entityTypeId: 'template', requiredOperations: ['view'] }],
params,
builder => builder.from('templates').innerJoin('namespaces', 'namespaces.id', 'templates.namespace'),
builder => {
builder = builder.from('templates').innerJoin('namespaces', 'namespaces.id', 'templates.namespace');
if (namespaceId) {
builder = builder.where('namespaces.id', namespaceId);
}
return builder;
},
[ 'templates.id', 'templates.name', 'templates.description', 'templates.type', 'templates.created', 'namespaces.name' ]
);
}
async function listDTAjax(context, params) {
return await _listDTAjax(context, undefined, params);
}
async function listByNamespaceDTAjax(context, namespaceId, params) {
return await _listDTAjax(context, namespaceId, params);
}
async function _validateAndPreprocess(tx, entity) {
await namespaceHelpers.validateEntity(tx, entity);
@ -57,6 +72,15 @@ async function create(context, entity) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createTemplate');
if (entity.fromSourceTemplate) {
const template = await getByIdTx(tx, context, entity.sourceTemplate, false);
entity.type = template.type;
entity.data = template.data;
entity.html = template.html;
entity.text = template.text;
}
await _validateAndPreprocess(tx, entity);
const ids = await tx('templates').insert(filterObject(entity, allowedKeys));
@ -64,6 +88,13 @@ async function create(context, entity) {
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'template', entityId: id });
if (entity.fromSourceTemplate) {
await files.copyAllTx(tx, context, 'template', 'file', entity.sourceTemplate, 'template', 'file', id);
convertFileURLs(entity, 'template', entity.sourceTemplate, 'template', id);
await tx('templates').update(filterObject(entity, allowedKeys)).where('id', id);
}
return id;
});
}
@ -118,6 +149,7 @@ module.exports.hash = hash;
module.exports.getByIdTx = getByIdTx;
module.exports.getById = getById;
module.exports.listDTAjax = listDTAjax;
module.exports.listByNamespaceDTAjax = listByNamespaceDTAjax;
module.exports.create = create;
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
module.exports.remove = remove;

View file

@ -1,3 +1,3 @@
*
!.gitignore
*
!.gitignore
!README.md

View file

@ -19,6 +19,10 @@ router.postAsync('/campaigns-others-by-list-table/:campaignId/:listIds', passpor
return res.json(await campaigns.listOthersWhoseListsAreIncludedDTAjax(req.context, castToInteger(req.params.campaignId), req.params.listIds.split(';').map(x => castToInteger(x)), req.body));
});
router.postAsync('/campaigns-by-namespace-table/:namespaceId', passport.loggedIn, async (req, res) => {
return res.json(await campaigns.listByNamespaceDTAjax(req.context, castToInteger(req.params.namespaceId), req.body));
});
router.postAsync('/campaigns-children/:campaignId', passport.loggedIn, async (req, res) => {
return res.json(await campaigns.listChildrenDTAjax(req.context, castToInteger(req.params.campaignId), req.body));
});

View file

@ -11,6 +11,10 @@ router.postAsync('/lists-table', passport.loggedIn, async (req, res) => {
return res.json(await lists.listDTAjax(req.context, req.body));
});
router.postAsync('/lists-by-namespace-table/:namespaceId', passport.loggedIn, async (req, res) => {
return res.json(await lists.listByNamespaceDTAjax(req.context, castToInteger(req.params.namespaceId), req.body));
});
router.postAsync('/lists-with-segment-by-campaign-table/:campaignId', passport.loggedIn, async (req, res) => {
return res.json(await lists.listWithSegmentByCampaignDTAjax(req.context, castToInteger(req.params.campaignId), req.body));
});

View file

@ -40,6 +40,10 @@ router.postAsync('/send-configurations-table', passport.loggedIn, async (req, re
return res.json(await sendConfigurations.listDTAjax(req.context, req.body));
});
router.postAsync('/send-configurations-by-namespace-table/:namespaceId', passport.loggedIn, async (req, res) => {
return res.json(await sendConfigurations.listByNamespaceDTAjax(req.context, castToInteger(req.params.namespaceId), req.body));
});
router.postAsync('/send-configurations-with-send-permission-table', passport.loggedIn, async (req, res) => {
return res.json(await sendConfigurations.listWithSendPermissionDTAjax(req.context, req.body));
});

View file

@ -35,6 +35,10 @@ router.postAsync('/templates-table', passport.loggedIn, async (req, res) => {
return res.json(await templates.listDTAjax(req.context, req.body));
});
router.postAsync('/templates-by-namespace-table/:namespaceId', passport.loggedIn, async (req, res) => {
return res.json(await templates.listByNamespaceDTAjax(req.context, castToInteger(req.params.namespaceId), req.body));
});
router.postAsync('/template-test-send', passport.loggedIn, passport.csrfProtection, async (req, res) => {
const data = req.body;
const result = await CampaignSender.testSend(req.context, data.listCid, data.subscriptionCid, data.campaignId, data.sendConfigurationId, data.html, data.text);

View file

@ -156,7 +156,6 @@ function getRouter(appType) {
// This is a fallback to versafix-1 if the block thumbnail is not defined by the template
router.use('/templates/:mosaicoTemplateId/edres', express.static(path.join(__dirname, '..', '..', 'client', 'static', 'mosaico', 'templates', 'versafix-1', 'edres')));
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', files.ReplacementBehavior.RENAME, null, 'file', resp => {
return {
files: resp.files.map(f => ({name: f.name, url: f.url, size: f.size, thumbnailUrl: f.thumbnailUrl}))

View file

@ -183,7 +183,6 @@ async function _renderSubscribe(req, res, list, subscription) {
const htmlRenderer = await tools.getTemplate(data.template, req.locale);
data.isWeb = true;
data.needsJsWarning = true;
data.flashMessages = await captureFlashMessages(res);
@ -385,7 +384,6 @@ router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res)
const htmlRenderer = await tools.getTemplate(data.template, req.locale);
data.isWeb = true;
data.needsJsWarning = true;
data.isManagePreferences = true;
data.flashMessages = await captureFlashMessages(res);
@ -435,7 +433,6 @@ router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (r
const htmlRenderer = await tools.getTemplate(data.template, req.locale);
data.isWeb = true;
data.needsJsWarning = true;
data.isManagePreferences = true;
data.flashMessages = await captureFlashMessages(res);
@ -535,7 +532,6 @@ router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req,
const htmlRenderer = await tools.getTemplate(data.template, req.locale);
data.isWeb = true;
data.needsJsWarning = true;
data.flashMessages = await captureFlashMessages(res);
res.send(htmlRenderer(data));
@ -679,7 +675,7 @@ async function webNotice(type, req, res) {
}
};
await injectCustomFormData(req.query.fid || list.default_form, 'web_' + type + '_notice', data);
await injectCustomFormData(req.query.fid || list.default_form, 'web_' + type.replace('-', '_') + '_notice', data);
const htmlRenderer = await tools.getTemplate(data.template, req.locale);

View file

@ -10,8 +10,16 @@ const stringify = require('csv-stringify')
const fields = require('../models/fields');
const lists = require('../models/lists');
const moment = require('moment');
const {SubscriptionStatus} = require('../../shared/lists');
router.getAsync('/export/:listId/:segmentId', passport.loggedIn, async (req, res) => {
const statusStrings = {
[SubscriptionStatus.SUBSCRIBED]: 'subscribed',
[SubscriptionStatus.UNSUBSCRIBED]: 'unsubscribed',
[SubscriptionStatus.BOUNCED]: 'bounced',
[SubscriptionStatus.COMPLAINED]: 'complained'
};
const listId = castToInteger(req.params.listId);
const segmentId = castToInteger(req.params.segmentId);
@ -19,6 +27,7 @@ router.getAsync('/export/:listId/:segmentId', passport.loggedIn, async (req, res
const columns = [
{key: 'cid', header: 'cid'},
{key: 'status', header: 'status'},
{key: 'hash_email', header: 'HASH_EMAIL'},
{key: 'email', header: 'EMAIL'},
];
@ -50,6 +59,8 @@ router.getAsync('/export/:listId/:segmentId', passport.loggedIn, async (req, res
stringifier.pipe(res);
for await (const subscription of subscriptions.listIterator(req.context, listId, segmentId, false)) {
subscription.status = statusStrings[subscription.status];
stringifier.write(subscription);
}

View file

@ -15,6 +15,8 @@ const contextHelpers = require('../lib/context-helpers');
const tools = require('../lib/tools');
const shares = require('../models/shares');
const { tLog } = require('../lib/translate');
const {ListActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
const csvparse = require('csv-parse');
@ -41,6 +43,8 @@ function prepareCsv(impt) {
error: msg + '\n' + err.message
});
await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.PREP_FAILED});
await fsExtra.removeAsync(filePath);
};
@ -56,6 +60,8 @@ function prepareCsv(impt) {
error: null
});
await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.PREP_FINISHED});
await fsExtra.removeAsync(filePath);
};
@ -263,12 +269,16 @@ async function _execImportRun(impt, handlers) {
status: ImportStatus.RUN_FINISHED
});
await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.RUN_FINISHED});
} catch (err) {
await knex('imports').where('id', impt.id).update({
last_run: new Date(),
error: err.message,
status: ImportStatus.RUN_FAILED
});
await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.PREP_FAILED});
}
}
@ -361,14 +371,20 @@ async function getTask() {
if (impt.source === ImportSource.CSV_FILE && impt.status === ImportStatus.PREP_SCHEDULED) {
await tx('imports').where('id', impt.id).update('status', ImportStatus.PREP_RUNNING);
await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.PREP_RUNNING});
return () => prepareCsv(impt);
} else if (impt.status === ImportStatus.RUN_SCHEDULED && impt.mapping_type === MappingType.BASIC_SUBSCRIBE) {
await tx('imports').where('id', impt.id).update('status', ImportStatus.RUN_RUNNING);
await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.RUN_RUNNING});
return () => basicSubscribe(impt);
} else if (impt.status === ImportStatus.RUN_SCHEDULED && impt.mapping_type === MappingType.BASIC_UNSUBSCRIBE) {
await tx('imports').where('id', impt.id).update('status', ImportStatus.RUN_RUNNING);
await activityLog.logEntityActivity('list', ListActivityType.IMPORT_STATUS_CHANGE, impt.list, {importId: impt.id, importStatus: ImportStatus.RUN_RUNNING});
return () => basicUnsubscribe(impt);
}

View file

@ -9,6 +9,9 @@ const {CampaignStatus, CampaignType} = require('../../shared/campaigns');
const { enforce } = require('../lib/helpers');
const campaigns = require('../models/campaigns');
const builtinZoneMta = require('../lib/builtin-zone-mta');
const {CampaignActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
let messageTid = 0;
const workerProcesses = new Map();
@ -127,6 +130,8 @@ async function processCampaign(campaignId) {
}
await knex('campaigns').where('id', campaignId).update({status: CampaignStatus.FINISHED});
await activityLog.logEntityActivity('campaign', CampaignActivityType.STATUS_CHANGE, campaignId, {status: CampaignStatus.FINISHED});
messageQueue.delete(campaignId);
}
@ -214,6 +219,7 @@ async function scheduleCampaigns() {
if (scheduledCampaign) {
await tx('campaigns').where('id', scheduledCampaign.id).update({status: CampaignStatus.SENDING});
await activityLog.logEntityActivity('campaign', CampaignActivityType.STATUS_CHANGE, scheduledCampaign.id, {status: CampaignStatus.SENDING});
campaignId = scheduledCampaign.id;
}
});

View file

@ -11,6 +11,7 @@ defaultLanguage: en-US
# Enabled languages
enabledLanguages:
- en-US
- es-ES
- fk-FK
mysql:

View file

@ -1,9 +1,9 @@
'use strict';
if (!process.env.NODE_CONFIG_DIR) {
process.env.NODE_CONFIG_DIR = __dirname + '/../../config';
}
const config = require('server/setup/knex/config');
module.exports = config;
'use strict';
if (!process.env.NODE_CONFIG_DIR) {
process.env.NODE_CONFIG_DIR = __dirname + '/../../config';
}
const config = require('server/setup/knex/config');
module.exports = config;

View file

@ -1,8 +1,8 @@
'use strict';
const config = require('./config');
module.exports = {
client: 'mysql',
connection: config.mysql
};
'use strict';
const config = require('./config');
module.exports = {
client: 'mysql',
connection: config.mysql
};

View file

@ -526,8 +526,9 @@ async function migrateSegments(knex) {
switch (fieldType) {
case 'text':
case 'string':
case 'website':
rules.push({ column: oldRule.column, value: oldSettings.value });
rules.push({ type: 'like', column: oldRule.column, value: oldSettings.value });
break;
case 'number':
if (oldSettings.range) {

View file

@ -1,18 +1,18 @@
{{#if uaCode}}
<script>
(function(i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function() {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
ga('create', '{{uaCode}}', 'auto');
ga('send', 'pageview');
</script>
{{/if}}
{{#if uaCode}}
<script>
(function(i, s, o, g, r, a, m) {
i['GoogleAnalyticsObject'] = r;
i[r] = i[r] || function() {
(i[r].q = i[r].q || []).push(arguments)
}, i[r].l = 1 * new Date();
a = s.createElement(o),
m = s.getElementsByTagName(o)[0];
a.async = 1;
a.src = g;
m.parentNode.insertBefore(a, m)
})(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga');
ga('create', '{{uaCode}}', 'auto');
ga('send', 'pageview');
</script>
{{/if}}

View file

@ -1,8 +1 @@
{{{flashMessages}}}
{{#if needsJsWarning}}
<div class="alert alert-danger js-warning" role="alert">
<strong>{{#translate}}warning!{{/translate}}</strong>
{{#translate}}javaScriptMustBeEnabledInOrderForThis{{/translate}}
</div>
{{/if}}