Obsoleting some old files

Transition to SPA-style client
Basis for Mosaico template editor
This commit is contained in:
Tomas Bures 2018-02-25 20:54:15 +01:00
parent 7750232716
commit c85f2d4440
942 changed files with 86311 additions and 967 deletions

View file

@ -1,15 +0,0 @@
'use strict';
const _ = require('../lib/translate')._;
const clientHelpers = require('../lib/client-helpers');
const router = require('../lib/router-async').create();
router.get('/logout', (req, res) => {
req.logout();
res.redirect('/');
});
clientHelpers.registerRootRoute(router, 'account', _('Account'));
module.exports = router;

View file

@ -1,166 +0,0 @@
'use strict';
const { nodeifyFunction } = require('../lib/nodeify');
const getSettings = nodeifyFunction(require('../models/settings').get);
let campaigns = require('../lib/models/campaigns');
let links = require('../lib/models/links');
let lists = require('../lib/models/lists');
let subscriptions = require('../lib/models/subscriptions');
let tools = require('../lib/tools');
let express = require('express');
let request = require('request');
let router = new express.Router();
let passport = require('../lib/passport');
let marked = require('marked');
let _ = require('../lib/translate')._;
let util = require('util');
router.get('/:campaign/:list/:subscription', passport.csrfProtection, (req, res, next) => {
getSettings('serviceUrl', (err, serviceUrl) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
campaigns.getByCid(req.params.campaign, (err, campaign) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!campaign) {
err = new Error(_('Not Found'));
err.status = 404;
return next(err);
}
lists.getByCid(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
err = new Error(_('Not Found'));
err.status = 404;
return next(err);
}
subscriptions.getWithMergeTags(list.id, req.params.subscription, (err, subscription) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!subscription) {
err = new Error(_('Not Found'));
err.status = 404;
return next(err);
}
campaigns.getAttachments(campaign.id, (err, attachments) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
let renderHtml = (html, renderTags) => {
let render = (view, layout) => {
res.render(view, {
layout,
message: renderTags ? tools.formatMessage(serviceUrl, campaign, list, subscription, html, false, true) : html,
campaign,
list,
subscription,
attachments,
csrfToken: req.csrfToken()
});
};
if (campaign.editorName && campaign.editorName !== 'summernote' && campaign.editorName !== 'codeeditor') {
res.render('partials/tracking-scripts', {
layout: 'archive/layout-raw'
}, (err, scripts) => {
if (err) {
return next(err);
}
html = scripts ? html.replace(/<\/body\b/i, match => scripts + match) : html;
render('archive/view-raw', 'archive/layout-raw');
});
} else {
render('archive/view', 'archive/layout');
}
};
let renderAndShow = (html, renderTags) => {
if (req.query.track === 'no') {
return renderHtml(html, renderTags);
}
// rewrite links to count clicks
links.updateLinks(campaign, list, subscription, serviceUrl, html, (err, html) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
renderHtml(html, renderTags);
});
};
if (campaign.sourceUrl) {
let form = tools.getMessageLinks(serviceUrl, campaign, list, subscription);
Object.keys(subscription.mergeTags).forEach(key => {
form[key] = subscription.mergeTags[key];
});
request.post({
url: campaign.sourceUrl,
form
}, (err, httpResponse, body) => {
if (err) {
return next(err);
}
if (httpResponse.statusCode !== 200) {
return next(new Error(util.format(_('Received status code %s from %s'), httpResponse.statusCode, campaign.sourceUrl)));
}
renderAndShow(body && body.toString(), false);
});
} else {
renderAndShow(campaign.html || marked(campaign.text, {
breaks: true,
sanitize: true,
gfm: true,
tables: true,
smartypants: true
}), true);
}
});
});
});
});
});
});
router.post('/attachment/download', passport.parseForm, passport.csrfProtection, (req, res) => {
let url = '/archive/' + encodeURIComponent(req.body.campaign || '') + '/' + encodeURIComponent(req.body.list || '') + '/' + encodeURIComponent(req.body.subscription || '');
campaigns.getByCid(req.body.campaign, (err, campaign) => {
if (err || !campaign) {
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
return res.redirect(url);
}
campaigns.getAttachment(campaign.id, Number(req.body.attachment), (err, attachment) => {
if (err) {
req.flash('danger', err && err.message || err);
return res.redirect(url);
} else if (!attachment) {
req.flash('warning', _('Attachment not found'));
return res.redirect(url);
}
res.set('Content-Disposition', 'attachment; filename="' + encodeURIComponent(attachment.filename).replace(/['()]/g, escape) + '"');
res.set('Content-Type', attachment.contentType);
res.send(attachment.content);
});
});
});
module.exports = router;

View file

@ -1,10 +0,0 @@
'use strict';
const _ = require('../lib/translate')._;
const clientHelpers = require('../lib/client-helpers');
const router = require('../lib/router-async').create();
clientHelpers.registerRootRoute(router, 'blacklist', _('Blacklist'));
module.exports = router;

View file

@ -1,919 +0,0 @@
'use strict';
const { nodeifyFunction } = require('../lib/nodeify');
const getSettings = nodeifyFunction(require('../models/settings').get);
let config = require('config');
let express = require('express');
let router = new express.Router();
let lists = require('../lib/models/lists');
let templates = require('../lib/models/templates');
let campaigns = require('../lib/models/campaigns');
let subscriptions = require('../lib/models/subscriptions');
let tools = require('../lib/tools');
let editorHelpers = require('../lib/editor-helpers.js');
let striptags = require('striptags');
let passport = require('../lib/passport');
let htmlescape = require('escape-html');
let multer = require('multer');
let _ = require('../lib/translate')._;
let util = require('util');
let uploadStorage = multer.memoryStorage();
let uploads = multer({
storage: uploadStorage
});
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', _('Need to be logged in to access restricted content'));
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
}
res.setSelectedMenu('campaigns');
next();
});
router.get('/', (req, res) => {
res.render('campaigns/campaigns', {
title: _('Campaigns')
});
});
router.get('/create', passport.csrfProtection, (req, res) => {
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
if (/^\d+:\d+$/.test(data.list)) {
data.segment = Number(data.list.split(':').pop());
data.list = Number(data.list.split(':').shift());
}
getSettings(['defaultFrom', 'defaultAddress', 'defaultSubject'], (err, configItems) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
lists.quicklist((err, listItems) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (Number(data.list)) {
listItems.forEach(list => {
list.segments.forEach(segment => {
if (segment.id === data.segment) {
segment.selected = true;
}
});
if (list.id === data.list && !data.segment) {
list.selected = true;
}
});
}
templates.quicklist((err, templateItems) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (Number(data.template)) {
templateItems.forEach(item => {
if (item.id === Number(data.template)) {
item.selected = true;
}
});
}
data.csrfToken = req.csrfToken();
data.listItems = listItems;
data.templateItems = templateItems;
data.from = data.from || configItems.defaultFrom;
data.address = data.address || configItems.defaultAddress;
data.replyTo = data.replyTo || '';
data.subject = data.subject || configItems.defaultSubject;
let view;
switch (req.query.type) {
case 'rss':
view = 'campaigns/create-rss';
break;
case 'triggered':
view = 'campaigns/create-triggered';
break;
default:
view = 'campaigns/create';
}
res.render(view, data);
});
});
});
});
router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.create(req.body, false, (err, id) => {
if (err || !id) {
req.flash('danger', err && err.message || err || _('Could not create campaign'));
return res.redirect('/campaigns/create?' + tools.queryParams(req.body));
}
req.flash('success', util.format(_('Campaign “%s” created'), req.body.name));
res.redirect((req.body.type === 'rss') ?
'/campaigns/edit/' + id :
'/campaigns/edit/' + id + '?tab=template'
);
});
});
router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
campaigns.get(req.params.id, false, (err, campaign) => {
if (err || !campaign) {
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
return res.redirect('/campaigns');
}
campaigns.getAttachments(campaign.id, (err, attachments) => {
if (err) {
return next(err);
}
campaign.attachments = attachments;
getSettings(['disableWysiwyg'], (err, configItems) => {
if (err) {
return next(err);
}
lists.quicklist((err, listItems) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (Number(campaign.list)) {
listItems.forEach(list => {
list.segments.forEach(segment => {
if (segment.id === campaign.segment) {
segment.selected = true;
}
});
if (list.id === campaign.list && !campaign.segment) {
list.selected = true;
}
});
}
campaign.csrfToken = req.csrfToken();
campaign.listItems = listItems;
campaign.useEditor = true;
campaign.editorName = campaign.editorName || 'summernote';
campaign.editorConfig = config[campaign.editorName];
campaign.disableWysiwyg = configItems.disableWysiwyg;
campaign.showGeneral = req.query.tab === 'general' || !req.query.tab;
campaign.showTemplate = req.query.tab === 'template';
campaign.showAttachments = req.query.tab === 'attachments';
let view;
switch (campaign.type) {
case 4: //triggered
view = 'campaigns/edit-triggered';
break;
case 2: //rss
view = 'campaigns/edit-rss';
break;
case 1:
default:
view = 'campaigns/edit';
}
editorHelpers.getMergeTagsForResource(campaign, (err, mergeTags) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
campaign.mergeTags = mergeTags;
res.render(view, campaign);
});
});
});
});
});
});
router.post('/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.update(req.body.id, req.body, (err, updated) => {
if (err) {
req.flash('danger', err.message || err);
} else if (updated) {
req.flash('success', _('Campaign settings updated'));
} else {
req.flash('info', _('Campaign settings not updated'));
}
if (req.body.id) {
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
} else {
return res.redirect('/campaigns');
}
});
});
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.delete(req.body.id, (err, deleted) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (deleted) {
req.flash('success', _('Campaign deleted'));
} else {
req.flash('info', _('Could not delete specified campaign'));
}
return res.redirect('/campaigns');
});
});
router.post('/ajax', (req, res) => {
campaigns.filter(req.body, Number(req.query.parent) || false, (err, data, total, filteredTotal) => {
if (err) {
return res.json({
error: err.message || err,
data: []
});
}
let getStatusText = data => {
switch (data.status) {
case 1:
return _('Idling');
case 2:
if (data.scheduled && data.scheduled > new Date()) {
return _('Scheduled');
}
return '<span class="glyphicon glyphicon-refresh spinning"></span> ' + _('Sending') + '…';
case 3:
return _('Finished');
case 4:
return _('Paused');
case 5:
return _('Inactive');
case 6:
return _('Active');
}
return _('Other');
};
res.json({
draw: req.body.draw,
recordsTotal: total,
recordsFiltered: filteredTotal,
data: data.map((row, i) => [
(Number(req.body.start) || 0) + 1 + i,
'<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> <a href="/campaigns/view/' + row.id + '">' + htmlescape(row.name || '') + '</a>',
htmlescape(striptags(row.description) || ''),
getStatusText(row),
'<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>'
].concat('<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/campaigns/edit/' + row.id + '">' + _('Edit') + '</a>'))
});
});
});
router.get('/view/:id', passport.csrfProtection, (req, res) => {
campaigns.get(req.params.id, true, (err, campaign) => {
if (err || !campaign) {
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
return res.redirect('/campaigns');
}
let getList = (listId, callback) => {
lists.get(listId, (err, list) => {
if (err) {
return callback(err);
}
if (!list) {
list = {
id: listId
};
}
subscriptions.listTestUsers(listId, (err, testUsers) => {
if (err || !testUsers) {
testUsers = [];
}
return callback(null, list, testUsers);
});
});
};
getList(campaign.list, (err, list, testUsers) => {
if (err) {
req.flash('danger', err && err.message || err);
return res.redirect('/campaigns');
}
campaign.csrfToken = req.csrfToken();
campaign.list = list;
campaign.testUsers = testUsers;
campaign.isIdling = campaign.status === 1;
campaign.isSending = campaign.status === 2;
campaign.isFinished = campaign.status === 3;
campaign.isPaused = campaign.status === 4;
campaign.isInactive = campaign.status === 5;
campaign.isActive = campaign.status === 6;
campaign.isNormal = campaign.type === 1 || campaign.type === 3;
campaign.isRss = campaign.type === 2;
campaign.isTriggered = campaign.type === 4;
campaign.isScheduled = campaign.scheduled && campaign.scheduled > new Date();
// show only messages that weren't bounced as delivered
campaign.delivered = campaign.delivered - campaign.bounced;
campaign.openRate = campaign.delivered ? Math.round((campaign.opened / campaign.delivered) * 10000) / 100 : 0;
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 10000) / 100 : 0;
campaign.bounceRate = campaign.delivered ? Math.round((campaign.bounced / campaign.delivered) * 10000) / 100 : 0;
campaign.complaintRate = campaign.delivered ? Math.round((campaign.complained / campaign.delivered) * 10000) / 100 : 0;
campaign.unsubscribeRate = campaign.delivered ? Math.round((campaign.unsubscribed / campaign.delivered) * 10000) / 100 : 0;
campaigns.getLinks(campaign.id, (err, links) => {
if (err) {
// ignore
}
let index = 0;
campaign.links = (links || []).map(link => {
link.index = ++index;
link.totalPercentage = campaign.delivered ? Math.round(((link.clicks / campaign.delivered) * 100) * 1000) / 1000 : 0;
link.relPercentage = campaign.clicks ? Math.round(((link.clicks / campaign.clicks) * 100) * 1000) / 1000 : 0;
link.short = link.url.replace(/^https?:\/\/(www.)?/i, '');
if (link.short > 63) {
link.short = link.short.substr(0, 60) + '…';
}
return link;
});
campaign.showOverview = !req.query.tab || req.query.tab === 'overview';
campaign.showLinks = req.query.tab === 'links';
res.render('campaigns/view', campaign);
});
});
});
});
router.post('/preview/:id', passport.parseForm, passport.csrfProtection, (req, res) => {
let campaign = req.body.campaign;
let list = req.body.list;
let listId = req.body.listId;
let subscription = req.body.subscriber;
if (subscription === '_create') {
return res.redirect('/lists/subscription/' + encodeURIComponent(listId) + '/add/?is-test=true');
}
res.redirect('/archive/' + encodeURIComponent(campaign) + '/' + encodeURIComponent(list) + '/' + encodeURIComponent(subscription) + '?track=no');
});
router.get('/opened/:id', passport.csrfProtection, (req, res) => {
campaigns.get(req.params.id, true, (err, campaign) => {
if (err || !campaign) {
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
return res.redirect('/campaigns');
}
lists.get(campaign.list, (err, list) => {
if (err || !campaign) {
req.flash('danger', err && err.message || err);
return res.redirect('/campaigns');
}
campaign.csrfToken = req.csrfToken();
campaign.list = list;
// show only messages that weren't bounced as delivered
campaign.delivered = campaign.delivered - campaign.bounced;
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0;
res.render('campaigns/opened', campaign);
});
});
});
router.get('/status/:id/:status', passport.csrfProtection, (req, res) => {
let id = Number(req.params.id) || 0;
let status;
switch (req.params.status) {
case 'delivered':
status = 1;
break;
case 'unsubscribed':
status = 2;
break;
case 'bounced':
status = 3;
break;
case 'complained':
status = 4;
break;
case 'blacklisted':
status = 5;
break;
default:
req.flash('danger', _('Unknown status selector'));
return res.redirect('/campaigns');
}
campaigns.get(id, true, (err, campaign) => {
if (err || !campaign) {
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
return res.redirect('/campaigns');
}
let getList = (listId, callback) => {
lists.get(listId, (err, list) => {
if (err) {
return callback(err);
}
if (!list) {
list = {
id: listId
};
}
return callback(null, list);
});
};
getList(campaign.list, (err, list) => {
if (err) {
req.flash('danger', err && err.message || err);
return res.redirect('/campaigns');
}
campaign.csrfToken = req.csrfToken();
campaign.list = list;
// show only messages that weren't bounced as delivered
campaign.delivered = campaign.delivered - campaign.bounced;
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0;
campaign.status = status;
res.render('campaigns/' + req.params.status, campaign);
});
});
});
router.get('/clicked/:id/:linkId', passport.csrfProtection, (req, res) => {
campaigns.get(req.params.id, true, (err, campaign) => {
if (err || !campaign) {
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
return res.redirect('/campaigns');
}
let getList = (listId, callback) => {
lists.get(listId, (err, list) => {
if (err) {
return callback(err);
}
if (!list) {
list = {
id: listId
};
}
return callback(null, list);
});
};
getList(campaign.list, (err, list) => {
if (err) {
req.flash('danger', err && err.message || err);
return res.redirect('/campaigns');
}
campaign.csrfToken = req.csrfToken();
campaign.list = list;
// show only messages that weren't bounced as delivered
campaign.delivered = campaign.delivered - campaign.bounced;
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0;
if (req.params.linkId === 'all') {
campaign.aggregated = true;
campaign.link = {
id: 0
};
res.render('campaigns/clicked', campaign);
} else {
campaigns.getLinks(campaign.id, req.params.linkId, (err, links) => {
if (err) {
// ignore
}
let index = 0;
campaign.link = (links || []).map(link => {
link.index = ++index;
link.totalPercentage = campaign.delivered ? Math.round(((link.clicks / campaign.delivered) * 100) * 1000) / 1000 : 0;
link.relPercentage = campaign.clicks ? Math.round(((link.clicks / campaign.clicks) * 100) * 1000) / 1000 : 0;
link.short = link.url.replace(/^https?:\/\/(www.)?/i, '');
if (link.short > 63) {
link.short = link.short.substr(0, 60) + '…';
}
return link;
}).shift();
res.render('campaigns/clicked', campaign);
});
}
});
});
});
router.post('/clicked/ajax/:id/:linkId', (req, res) => {
let linkId = Number(req.params.linkId) || 0;
campaigns.get(req.params.id, true, (err, campaign) => {
if (err || !campaign) {
return res.json({
error: err && err.message || err || _('Campaign not found'),
data: []
});
}
lists.get(campaign.list, (err, list) => {
if (err) {
return res.json({
error: err && err.message || err,
data: []
});
}
let campaignCid = campaign.cid;
let listCid = list.cid;
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '.created', 'count'];
campaigns.filterClickedSubscribers(campaign, linkId, req.body, columns, (err, data, total, filteredTotal) => {
if (err) {
return res.json({
error: err.message || err,
data: []
});
}
res.json({
draw: req.body.draw,
recordsTotal: total,
recordsFiltered: filteredTotal,
data: data.map((row, i) => [
'<a href="/archive/' + encodeURIComponent(campaignCid) + '/' + encodeURIComponent(listCid) + '/' + encodeURIComponent(row.cid) + '?track=no">' + ((Number(req.body.start) || 0) + 1 + i) + '</a>',
htmlescape(row.email || ''),
htmlescape(row.firstName || ''),
htmlescape(row.lastName || ''),
row.created && row.created.toISOString ? '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>' : 'N/A',
row.count,
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/subscription/' + campaign.list + '/edit/' + row.cid + '">' + _('Edit') + '</a>'
])
});
});
});
});
});
router.post('/clicked/ajax/:id/:linkId/stats', (req, res) => {
let linkId = Number(req.params.linkId) || 0;
campaigns.get(req.params.id, true, (err, campaign) => {
if (err || !campaign) {
return res.json({
error: err && err.message || err || _('Campaign not found'),
data: []
});
}
lists.get(campaign.list, (err, list) => {
if (err) {
return res.json({
error: err && err.message || err,
data: []
});
}
let column = req.body.column;
let limit = req.body.limit;
campaigns.statsClickedSubscribersByColumn(campaign, linkId, req.body, column, limit, (err, data, total) => {
if (err) {
return res.json({
error: err.message || err,
data: []
});
}
res.json({
draw: req.body.draw,
total: total,
data: data
});
});
});
});
});
router.post('/status/ajax/:id/:status', (req, res) => {
let status = Number(req.params.status) || 0;
campaigns.get(req.params.id, true, (err, campaign) => {
if (err || !campaign) {
return res.json({
error: err && err.message || err || _('Campaign not found'),
data: []
});
}
lists.get(campaign.list, (err, list) => {
if (err) {
return res.json({
error: err && err.message || err,
data: []
});
}
let campaignCid = campaign.cid;
let listCid = list.cid;
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign__' + campaign.id + '.updated'];
campaigns.filterStatusSubscribers(campaign, status, req.body, columns, (err, data, total, filteredTotal) => {
if (err) {
return res.json({
error: err.message || err,
data: []
});
}
res.json({
draw: req.body.draw,
recordsTotal: total,
recordsFiltered: filteredTotal,
data: data.map((row, i) => [
'<a href="/archive/' + encodeURIComponent(campaignCid) + '/' + encodeURIComponent(listCid) + '/' + encodeURIComponent(row.cid) + '?track=no">' + ((Number(req.body.start) || 0) + 1 + i) + '</a>',
htmlescape(row.email || ''),
htmlescape(row.firstName || ''),
htmlescape(row.lastName || ''),
htmlescape(row.response || ''),
row.updated && row.created.toISOString ? '<span class="datestring" data-date="' + row.updated.toISOString() + '" title="' + row.updated.toISOString() + '">' + row.updated.toISOString() + '</span>' : 'N/A',
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/subscription/' + campaign.list + '/edit/' + row.cid + '">' + _('Edit') + '</a>'
])
});
});
});
});
});
router.post('/clicked/ajax/:id/:linkId', (req, res) => {
let linkId = Number(req.params.linkId) || 0;
campaigns.get(req.params.id, true, (err, campaign) => {
if (err || !campaign) {
return res.json({
error: err && err.message || err || _('Campaign not found'),
data: []
});
}
lists.get(campaign.list, (err, list) => {
if (err) {
return res.json({
error: err && err.message || err,
data: []
});
}
let campaignCid = campaign.cid;
let listCid = list.cid;
let columns = ['#', 'email', 'first_name', 'last_name', 'campaign_tracker__' + campaign.id + '.created', 'count'];
campaigns.filterClickedSubscribers(campaign, linkId, req.body, columns, (err, data, total, filteredTotal) => {
if (err) {
return res.json({
error: err.message || err,
data: []
});
}
res.json({
draw: req.body.draw,
recordsTotal: total,
recordsFiltered: filteredTotal,
data: data.map((row, i) => [
'<a href="/archive/' + encodeURIComponent(campaignCid) + '/' + encodeURIComponent(listCid) + '/' + encodeURIComponent(row.cid) + '?track=no">' + ((Number(req.body.start) || 0) + 1 + i) + '</a>',
htmlescape(row.email || ''),
htmlescape(row.firstName || ''),
htmlescape(row.lastName || ''),
row.created && row.created.toISOString ? '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>' : 'N/A',
row.count,
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/subscription/' + campaign.list + '/edit/' + row.cid + '">' + _('Edit') + '</a>'
])
});
});
});
});
});
router.post('/quicklist/ajax', (req, res) => {
campaigns.filterQuicklist(req.body, (err, data, total, filteredTotal) => {
if (err) {
return res.json({
error: err.message || err,
data: []
});
}
res.json({
draw: req.body.draw,
recordsTotal: total,
recordsFiltered: filteredTotal,
data: data.map((row, i) => ({
"0": (Number(req.body.start) || 0) + 1 + i,
"1": '<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> <a href="/campaigns/view/' + row.id + '">' + htmlescape(row.name || '') + '</a>',
"2": htmlescape(striptags(row.description) || ''),
"3": '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>',
"DT_RowId": row.id
}))
});
});
});
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.delete(req.body.id, (err, deleted) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (deleted) {
req.flash('success', _('Campaign deleted'));
} else {
req.flash('info', _('Could not delete specified campaign'));
}
return res.redirect('/campaigns');
});
});
router.post('/send', passport.parseForm, passport.csrfProtection, (req, res) => {
let delayHours = Math.max(Number(req.body['delay-hours']) || 0, 0);
let delayMinutes = Math.max(Number(req.body['delay-minutes']) || 0, 0);
let scheduled = new Date(Date.now() + delayHours * 3600 * 1000 + delayMinutes * 60 * 1000);
campaigns.send(req.body.id, scheduled, (err, scheduled) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (scheduled) {
req.flash('success', _('Scheduled sending'));
} else {
req.flash('info', _('Could not schedule sending'));
}
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
});
});
router.post('/resume', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.send(req.body.id, false, (err, scheduled) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (scheduled) {
req.flash('success', _('Sending resumed'));
} else {
req.flash('info', _('Could not resume sending'));
}
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
});
});
router.post('/reset', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.reset(req.body.id, (err, reset) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (reset) {
req.flash('success', _('Sending reset'));
} else {
req.flash('info', _('Could not reset sending'));
}
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
});
});
router.post('/pause', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.pause(req.body.id, (err, reset) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (reset) {
req.flash('success', _('Sending paused'));
} else {
req.flash('info', _('Could not pause sending'));
}
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
});
});
router.post('/activate', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.activate(req.body.id, (err, reset) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (reset) {
req.flash('success', _('Sending activated'));
} else {
req.flash('info', _('Could not activate sending'));
}
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
});
});
router.post('/inactivate', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.inactivate(req.body.id, (err, reset) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (reset) {
req.flash('success', _('Sending paused'));
} else {
req.flash('info', _('Could not pause sending'));
}
return res.redirect('/campaigns/view/' + encodeURIComponent(req.body.id));
});
});
router.post('/attachment', uploads.single('attachment'), passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.get(req.body.id, false, (err, campaign) => {
if (err || !campaign) {
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
return res.redirect('/campaigns');
}
campaigns.addAttachment(campaign.id, {
filename: req.file.originalname,
contentType: req.file.mimetype,
content: req.file.buffer
}, (err, attachmentId) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (attachmentId) {
req.flash('success', _('Attachment uploaded'));
} else {
req.flash('info', _('Could not store attachment'));
}
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
});
});
});
router.post('/attachment/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.get(req.body.id, false, (err, campaign) => {
if (err || !campaign) {
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
return res.redirect('/campaigns');
}
campaigns.deleteAttachment(campaign.id, Number(req.body.attachment), (err, deleted) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (deleted) {
req.flash('success', _('Attachment deleted'));
} else {
req.flash('info', _('Could not delete attachment'));
}
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
});
});
});
router.post('/attachment/download', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.get(req.body.id, false, (err, campaign) => {
if (err || !campaign) {
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
return res.redirect('/campaigns');
}
campaigns.getAttachment(campaign.id, Number(req.body.attachment), (err, attachment) => {
if (err) {
req.flash('danger', err && err.message || err);
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
} else if (!attachment) {
req.flash('warning', _('Attachment not found'));
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
}
res.set('Content-Disposition', 'attachment; filename="' + encodeURIComponent(attachment.filename).replace(/['()]/g, escape) + '"');
res.set('Content-Type', attachment.contentType);
res.send(attachment.content);
});
});
});
router.get('/attachment/:campaign', passport.csrfProtection, (req, res) => {
campaigns.get(req.params.campaign, false, (err, campaign) => {
if (err || !campaign) {
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
return res.redirect('/campaigns');
}
campaign.csrfToken = req.csrfToken();
res.render('campaigns/upload-attachment', campaign);
});
});
module.exports = router;

View file

@ -1,532 +0,0 @@
'use strict';
const { nodeifyFunction } = require('../lib/nodeify');
const getSettings = nodeifyFunction(require('../models/settings').get);
const log = require('npmlog');
const config = require('config');
const express = require('express');
const router = new express.Router();
const passport = require('../lib/passport');
const os = require('os');
const fs = require('fs');
const path = require('path');
const mkdirp = require('mkdirp');
const crypto = require('crypto');
const events = require('events');
const httpMocks = require('node-mocks-http');
const multiparty = require('multiparty');
const escapeStringRegexp = require('escape-string-regexp');
const jqueryFileUpload = require('jquery-file-upload-middleware');
const gm = require('gm').subClass({
imageMagick: true
});
const url = require('url');
const htmlToText = require('html-to-text');
const premailerApi = require('premailer-api');
const _ = require('../lib/translate')._;
const mailer = require('../lib/mailer');
const templates = require('../lib/models/templates');
const campaigns = require('../lib/models/campaigns');
router.all('/*', (req, res, next) => {
if (!req.user) {
return res.status(403).send(_('Need to be logged in to access restricted content'));
}
if (req.originalUrl.startsWith('/editorapi/img?')) {
return next();
}
if (!config.editors.map(e => e[0]).includes(req.query.editor)) {
return res.status(500).send(_('Invalid editor name'));
}
next();
});
jqueryFileUpload.on('begin', fileInfo => {
fileInfo.name = fileInfo.name
.toLowerCase()
.replace(/ /g, '-')
.replace(/[^a-z0-9+-.]+/g, '');
});
const listImages = (dir, dirURL, callback) => {
fs.readdir(dir, (err, files = []) => {
if (err && err.code !== 'ENOENT') {
return callback(err.message || err);
}
files = files.filter(name => /\.(jpe?g|png|gif)$/i.test(name));
files = files.map(name => ({
// mosaico
name,
url: dirURL + '/' + name,
thumbnailUrl: dirURL + '/thumbnail/' + name,
// grapejs
src: dirURL + '/' + name
}));
callback(null, files);
});
};
const placeholderImage = (width, height, callback) => {
const magick = gm(width, height, '#707070');
const size = 40;
let x = 0;
let y = 0;
// stripes
while (y < height) {
magick
.fill('#808080')
.drawPolygon([x, y], [x + size, y], [x + size * 2, y + size], [x + size * 2, y + size * 2])
.drawPolygon([x, y + size], [x + size, y + size * 2], [x, y + size * 2]);
x = x + size * 2;
if (x > width) {
x = 0;
y = y + size * 2;
}
}
// text
magick
.fill('#B0B0B0')
.fontSize(20)
.drawText(0, 0, width + ' x ' + height, 'center');
magick.stream('png', (err, stream) => {
if (err) {
return callback(err);
}
const image = {
format: 'PNG',
stream
};
callback(null, image);
});
};
const resizedImage = (src, method, width, height, callback) => {
const pathname = path.join('/', url.parse(src).pathname);
const filePath = path.join(__dirname, '..', 'public', pathname);
const magick = gm(filePath);
magick.format((err, format) => {
if (err) {
return callback(err);
}
const streamHandler = (err, stream) => {
if (err) {
return callback(err);
}
const image = {
format,
stream
};
callback(null, image);
};
switch (method) {
case 'resize':
return magick
.autoOrient()
.resize(width, height)
.stream(streamHandler);
case 'cover':
return magick
.autoOrient()
.resize(width, height + '^')
.gravity('Center')
.extent(width, height + '>')
.stream(streamHandler);
default:
return callback(new Error(_('Method not supported')));
}
});
};
const getProcessedImage = (dynamicUrl, callback) => {
if (!dynamicUrl.includes('/editorapi/img?')) {
return callback(new Error('Invalid dynamicUrl'));
}
const {
src,
method,
params = '600,null'
} = url.parse(dynamicUrl, true).query;
let width = params.split(',')[0];
let height = params.split(',')[1];
const sanitizeSize = (val, min, max, defaultVal, allowNull) => {
if (val === 'null' && allowNull) {
return null;
}
val = Number(val) || defaultVal;
val = Math.max(min, val);
val = Math.min(max, val);
return val;
};
if (method === 'placeholder') {
width = sanitizeSize(width, 1, 2048, 600, false);
height = sanitizeSize(height, 1, 2048, 300, false);
placeholderImage(width, height, callback);
} else {
width = sanitizeSize(width, 1, 2048, 600, false);
height = sanitizeSize(height, 1, 2048, 300, true);
resizedImage(src, method, width, height, callback);
}
};
const getStaticImageUrl = (dynamicUrl, staticDir, staticDirUrl, callback) => {
if (!dynamicUrl.includes('/editorapi/img?')) {
return callback(null, dynamicUrl);
}
mkdirp(staticDir, err => {
if (err) {
return callback(err);
}
fs.readdir(staticDir, (err, files) => {
if (err) {
return callback(err);
}
const hash = crypto.createHash('md5').update(dynamicUrl).digest('hex');
const match = files.find(el => el.startsWith(hash));
if (match) {
return callback(null, staticDirUrl + '/' + match);
}
getProcessedImage(dynamicUrl, (err, image) => {
if (err) {
return callback(err);
}
const fileName = hash + '.' + image.format.toLowerCase();
const filePath = path.join(staticDir, fileName);
const fileUrl = staticDirUrl + '/' + fileName;
const writeStream = fs.createWriteStream(filePath);
writeStream.on('error', err => callback(err));
writeStream.on('finish', () => callback(null, fileUrl));
image.stream.pipe(writeStream);
});
});
});
};
const prepareHtml = (html, editorName, callback) => {
getSettings('serviceUrl', (err, serviceUrl) => {
if (err) {
return callback(err.message || err);
}
const srcs = new Map();
const re = /<img[^>]+src="([^"]*\/editorapi\/img\?[^"]+)"/ig;
let jobs = 0;
let result;
while ((result = re.exec(html)) !== null) {
srcs.set(result[1], result[1]);
}
const done = () => {
if (jobs === 0) {
for (const [key, value] of srcs) {
// console.log(`replace dynamicUrl: ${key} - with staticUrl: ${value}`);
html = html.replace(new RegExp(escapeStringRegexp(key), 'g'), value);
}
return callback(null, html);
}
};
const staticDir = path.join(__dirname, '..', 'public', editorName, 'uploads', 'static');
const staticDirUrl = url.resolve(serviceUrl, editorName + '/uploads/static');
for (const key of srcs.keys()) {
jobs++;
const dynamicUrl = key.replace(/&amp;/g, '&');
getStaticImageUrl(dynamicUrl, staticDir, staticDirUrl, (err, staticUrl) => {
if (err) {
// TODO: Send a warning back to the editor. For now we just skip image resizing.
log.error('editorapi', err);
if (dynamicUrl.includes('/editorapi/img?')) {
staticUrl = url.parse(dynamicUrl, true).query.src || dynamicUrl;
} else {
staticUrl = dynamicUrl;
}
if (!/^https?:\/\/|^\/\//i.test(staticUrl)) {
staticUrl = url.resolve(serviceUrl, staticUrl);
}
}
srcs.set(key, staticUrl);
jobs--;
done();
});
}
done();
});
};
// URL structure defined by Mosaico
// /editorapi/img?src=" + encodeURIComponent(src) + "&method=" + encodeURIComponent(method) + "&params=" + encodeURIComponent(width + "," + height);
router.get('/img', (req, res) => {
getProcessedImage(req.originalUrl, (err, image) => {
if (err) {
res.status(err.status || 500);
res.send(err.message || err);
return;
}
res.set('Content-Type', 'image/' + image.format.toLowerCase());
image.stream.pipe(res);
});
});
router.post('/update', passport.parseForm, passport.csrfProtection, (req, res) => {
const sendResponse = err => {
if (err) {
return res.status(500).send(err.message || err);
}
res.send('ok');
};
prepareHtml(req.body.html, req.query.editor, (err, html) => {
if (err) {
return sendResponse(err);
}
req.body.html = html;
switch (req.query.type) {
case 'template':
return templates.update(req.body.id, req.body, sendResponse);
case 'campaign':
return campaigns.update(req.body.id, req.body, sendResponse);
default:
return sendResponse(new Error(_('Invalid resource type')));
}
});
});
// https://github.com/artf/grapesjs/wiki/API-Asset-Manager
// https://github.com/aguidrevitch/jquery-file-upload-middleware
router.get('/upload', passport.csrfProtection, (req, res) => {
getSettings('serviceUrl', (err, serviceUrl) => {
if (err) {
return res.status(500).send(err.message || err);
}
const baseDir = path.join(__dirname, '..', 'public', req.query.editor, 'uploads');
const baseDirUrl = serviceUrl + req.query.editor + '/uploads';
listImages(path.join(baseDir, '0'), baseDirUrl + '/0', (err, sharedImages) => {
if (err) {
return res.status(500).send(err.message || err);
}
if (req.query.type === 'campaign' && Number(req.query.id) > 0) {
listImages(path.join(baseDir, req.query.id), baseDirUrl + '/' + req.query.id, (err, campaignImages) => {
if (err) {
return res.status(500).send(err.message || err);
}
res.json({
files: sharedImages.concat(campaignImages)
});
});
} else {
res.json({
files: sharedImages
});
}
});
});
});
router.post('/upload', passport.csrfProtection, (req, res) => {
getSettings('serviceUrl', (err, serviceUrl) => {
if (err) {
return res.status(500).send(err.message || err);
}
const getDirName = () => {
switch (req.query.type) {
case 'template':
return '0';
case 'campaign':
return Number(req.query.id) > 0 ? req.query.id : false;
default:
return false;
}
};
const dirName = getDirName();
const serviceUrlParts = url.parse(serviceUrl);
if (dirName === false) {
return res.status(500).send(_('Invalid resource type or ID'));
}
const opts = {
tmpDir: config.www.tmpdir || os.tmpdir(),
imageVersions: req.query.editor === 'mosaico' ? {
thumbnail: {
width: 90,
height: 90
}
} : {},
uploadDir: path.join(__dirname, '..', 'public', req.query.editor, 'uploads', dirName),
uploadUrl: '/' + req.query.editor + '/uploads/' + dirName, // must be root relative
acceptFileTypes: /\.(gif|jpe?g|png)$/i,
hostname: serviceUrlParts.host, // include port
ssl: serviceUrlParts.protocol === 'https:'
};
const mockres = httpMocks.createResponse({
eventEmitter: events.EventEmitter
});
mockres.on('error', err => {
res.status(500).json({
error: err.message || err,
data: []
});
});
mockres.on('end', () => {
const data = [];
try {
JSON.parse(mockres._getData()).files.forEach(file => {
data.push({
src: file.url
});
});
res.json({
data
});
} catch(err) {
res.status(500).json({
error: err.message || err,
data
});
}
});
jqueryFileUpload.fileHandler(opts)(req, req.query.editor === 'grapejs' ? mockres : res);
});
});
router.post('/download', passport.csrfProtection, (req, res) => {
prepareHtml(req.body.html, req.query.editor, (err, html) => {
if (err) {
return res.status(500).send(err.message || err);
}
res.setHeader('Content-disposition', 'attachment; filename=' + req.body.filename);
res.setHeader('Content-type', 'text/html');
res.send(html);
});
});
const parseGrapejsMultipartTestForm = (req, res, next) => {
if (req.query.editor === 'grapejs') {
new multiparty.Form().parse(req, (err, fields) => {
if (err) {
return next(err);
}
req.body.email = fields.email[0];
req.body.subject = fields.subject[0];
req.body.html = fields.html[0];
req.body._csrf = fields._csrf[0];
next();
});
} else {
next();
}
};
router.post('/test', parseGrapejsMultipartTestForm, passport.csrfProtection, (req, res) => {
const sendError = err => {
if (req.query.editor === 'grapejs') {
res.status(500).json({
errors: err.message || err
});
} else {
res.status(500).send(err.message || err);
}
};
prepareHtml(req.body.html, req.query.editor, (err, html) => {
if (err) {
return sendError(err);
}
getSettings(['defaultAddress', 'defaultFrom'], (err, configItems) => {
if (err) {
return sendError(err);
}
mailer.getMailer((err, transport) => {
if (err) {
return sendError(err);
}
const opts = {
from: {
name: configItems.defaultFrom,
address: configItems.defaultAddress
},
to: req.body.email,
subject: req.body.subject,
text: htmlToText.fromString(html, {
wordwrap: 100
}),
html
};
transport.sendMail(opts, err => {
if (err) {
return sendError(err);
}
if (req.query.editor === 'grapejs') {
res.json({
data: 'ok'
});
} else {
res.send('ok');
}
});
});
});
});
});
router.post('/html-to-text', passport.parseForm, passport.csrfProtection, (req, res) => {
premailerApi.prepare({
html: req.body.html,
fetchHTML: false
}, (err, email) => {
if (err) {
return res.status(500).send(err.message || err);
}
res.send(email.text.replace(/%5B/g, '[').replace(/%5D/g, ']'));
});
});
module.exports = router;

View file

@ -1,192 +0,0 @@
'use strict';
let express = require('express');
let router = new express.Router();
let lists = require('../lib/models/lists');
let fields = require('../lib/models/fields');
let tools = require('../lib/tools');
let passport = require('../lib/passport');
let _ = require('../lib/translate')._;
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', _('Need to be logged in to access restricted content'));
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
}
res.setSelectedMenu('lists');
next();
});
router.get('/:list', (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
fields.list(list.id, (err, rows) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
}
let index = 0;
res.render('lists/fields/fields', {
rows: rows.map(row => {
row.index = ++index;
row.type = fields.types[row.type];
if (Array.isArray(row.options)) {
row.options.forEach(option => {
option.index = ++index;
});
}
return row;
}),
list
});
});
});
});
router.get('/:list/create', passport.csrfProtection, (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
fields.list(list.id, (err, rows) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
}
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
data.csrfToken = req.csrfToken();
data.list = list;
if (data.type) {
data['selected' + (data.type || '').toString().trim().replace(/(?:^|\-)([a-z])/g, (m, c) => c.toUpperCase())] = true;
}
if (!('visible' in data) && !data.name) {
data.visible = true;
}
data.groups = rows.filter(row => fields.grouped.indexOf(row.type) >= 0).map(row => {
row.selected = Number(req.query.group) === row.id;
return row;
});
res.render('lists/fields/create', data);
});
});
});
router.post('/:list/create', passport.parseForm, passport.csrfProtection, (req, res) => {
fields.create(req.params.list, req.body, (err, id) => {
if (err || !id) {
req.flash('danger', err && err.message || err || _('Could not create custom field'));
return res.redirect('/fields/' + encodeURIComponent(req.params.list) + '/create?' + tools.queryParams(req.body));
}
req.flash('success', 'Custom field created');
res.redirect('/fields/' + encodeURIComponent(req.params.list));
});
});
router.get('/:list/edit/:field', passport.csrfProtection, (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
fields.get(req.params.field, (err, field) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
}
if (!field) {
req.flash('danger', _('Selected field not found'));
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
}
let data = {
csrfToken: req.csrfToken(),
field,
list
};
if (field.type) {
data['selected' + (field.type || '').toString().trim().replace(/(?:^|\-)([a-z])/g, (m, c) => c.toUpperCase())] = true;
}
fields.list(list.id, (err, rows) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
}
data.groups = field.type === 'option' ? rows.filter(row => fields.grouped.indexOf(row.type) >= 0).map(row => {
row.selected = Number(field.group) === row.id;
return row;
}) : false;
res.render('lists/fields/edit', data);
});
});
});
});
router.post('/:list/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
fields.update(req.body.id, req.body, (err, updated) => {
if (err) {
req.flash('danger', err.message || err);
} else if (updated) {
req.flash('success', _('Field settings updated'));
} else {
req.flash('info', _('Field settings not updated'));
}
if (req.body.id) {
return res.redirect('/fields/' + encodeURIComponent(req.params.list) + '/edit/' + encodeURIComponent(req.body.id));
} else {
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
}
});
});
router.post('/:list/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
fields.delete(req.body.id, (err, deleted) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (deleted) {
req.flash('success', _('Custom field deleted'));
} else {
req.flash('info', _('Could not delete specified field'));
}
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
});
});
module.exports = router;

View file

@ -1,336 +0,0 @@
'use strict';
let express = require('express');
let router = new express.Router();
let passport = require('../lib/passport');
let tools = require('../lib/tools');
let helpers = require('../lib/helpers');
let _ = require('../lib/translate')._;
let lists = require('../lib/models/lists');
let fields = require('../lib/models/fields');
let forms = require('../lib/models/forms');
let subscriptions = require('../lib/models/subscriptions');
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', _('Need to be logged in to access restricted content'));
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
}
res.setSelectedMenu('lists');
next();
});
router.get('/:list', passport.csrfProtection, (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
forms.list(list.id, (err, rows) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
}
let index = 0;
res.render('lists/forms/forms', {
customForms: rows.map(row => {
row.index = ++index;
row.isDefaultForm = list.defaultForm === row.id;
return row;
}),
list,
csrfToken: req.csrfToken()
});
});
});
});
router.get('/:list/create', passport.csrfProtection, (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
let data = {};
data.csrfToken = req.csrfToken();
data.list = list;
res.render('lists/forms/create', data);
});
});
router.post('/:list/create', passport.parseForm, passport.csrfProtection, (req, res) => {
forms.create(req.params.list, req.body, (err, id) => {
if (err || !id) {
req.flash('danger', err && err.message || err || _('Could not create custom form'));
return res.redirect('/forms/' + encodeURIComponent(req.params.list) + '/create?' + tools.queryParams(req.body));
}
req.flash('success', 'Custom form created');
res.redirect('/forms/' + encodeURIComponent(req.params.list) + '/edit/' + id);
});
});
router.get('/:list/edit/:form', passport.csrfProtection, (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
forms.get(req.params.form, (err, form) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
}
if (!form) {
req.flash('danger', _('Selected form not found'));
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
}
fields.list(list.id, (err, rows) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
}
let customFields = rows.map(row => {
row.type = fields.types[row.type];
return row;
});
let allFields = helpers.filterCustomFields(customFields, [], 'exclude');
let fieldsShownOnSubscribe = allFields;
let fieldsHiddenOnSubscribe = [];
let fieldsShownOnManage = allFields;
let fieldsHiddenOnManage = [];
if (form.fieldsShownOnSubscribe) {
fieldsShownOnSubscribe = helpers.filterCustomFields(customFields, form.fieldsShownOnSubscribe, 'include');
fieldsHiddenOnSubscribe = helpers.filterCustomFields(customFields, form.fieldsShownOnSubscribe, 'exclude');
}
if (form.fieldsShownOnManage) {
fieldsShownOnManage = helpers.filterCustomFields(customFields, form.fieldsShownOnManage, 'include');
fieldsHiddenOnManage = helpers.filterCustomFields(customFields, form.fieldsShownOnManage, 'exclude');
}
let helpEmailText = _('The plaintext version for this email');
let helpMjmlBase = _('Custom forms use MJML for formatting');
let helpMjmlDocLink = _('See the MJML documentation <a class="mjml-documentation">here</a>');
let helpMjmlGeneral = helpMjmlBase + ' ' + helpMjmlDocLink;
let templateOptgroups = [
{
label: _('General'),
opts: [{
name: 'layout',
label: _('Layout'),
type: 'mjml',
help: helpMjmlGeneral,
isLayout: true
}, {
name: 'form_input_style',
label: _('Form Input Style'),
type: 'css',
help: _('This CSS stylesheet defines the appearance of form input elements and alerts')
}]
}, {
label: _('Subscribe'),
opts: [{
name: 'web_subscribe',
label: _('Web - Subscribe'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'web_confirm_subscription_notice',
label: _('Web - Confirm Subscription Notice'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'mail_confirm_subscription_html',
label: _('Mail - Confirm Subscription (MJML)'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'mail_confirm_subscription_text',
label: _('Mail - Confirm Subscription (Text)'),
type: 'text',
help: helpEmailText
}, {
name: 'mail_already_subscribed_html',
label: _('Mail - Already Subscribed (MJML)'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'mail_already_subscribed_text',
label: _('Mail - Already Subscribed (Text)'),
type: 'text',
help: helpEmailText
}, {
name: 'web_subscribed_notice',
label: _('Web - Subscribed Notice'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'mail_subscription_confirmed_html',
label: _('Mail - Subscription Confirmed (MJML)'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'mail_subscription_confirmed_text',
label: _('Mail - Subscription Confirmed (Text)'),
type: 'text',
help: helpEmailText
}]
}, {
label: _('Manage'),
opts: [{
name: 'web_manage',
label: _('Web - Manage Preferences'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'web_manage_address',
label: _('Web - Manage Address'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'web_updated_notice',
label: _('Web - Updated Notice'),
type: 'mjml',
help: helpMjmlGeneral
}]
}, {
label: _('Unsubscribe'),
opts: [{
name: 'web_unsubscribe',
label: _('Web - Unsubscribe'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'web_confirm_unsubscription_notice',
label: _('Web - Confirm Unsubscription Notice'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'mail_confirm_unsubscription_html',
label: _('Mail - Confirm Unsubscription (MJML)'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'mail_confirm_unsubscription_text',
label: _('Mail - Confirm Unsubscription (Text)'),
type: 'text',
help: helpEmailText
}, {
name: 'mail_confirm_address_change_html',
label: _('Mail - Confirm Address Change (MJML)'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'mail_confirm_address_change_text',
label: _('Mail - Confirm Address Change (Text)'),
type: 'text',
help: helpEmailText
}, {
name: 'web_unsubscribed_notice',
label: _('Web - Unsubscribed Notice'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'mail_unsubscription_confirmed_html',
label: _('Mail - Unsubscription Confirmed (MJML)'),
type: 'mjml',
help: helpMjmlGeneral
}, {
name: 'mail_unsubscription_confirmed_text',
label: _('Mail - Unsubscription Confirmed (Text)'),
type: 'text',
help: helpEmailText
}, {
name: 'web_manual_unsubscribe_notice',
label: _('Web - Manual Unsubscribe Notice'),
type: 'mjml',
help: helpMjmlGeneral
}]
}
];
templateOptgroups.forEach(group => {
group.opts.forEach(opt => {
let key = tools.fromDbKey(opt.name);
opt.value = form[key];
});
});
subscriptions.listTestUsers(list.id, (err, testUsers) => {
res.render('lists/forms/edit', {
csrfToken: req.csrfToken(),
list,
form,
templateOptgroups,
fieldsShownOnSubscribe,
fieldsHiddenOnSubscribe,
fieldsShownOnManage,
fieldsHiddenOnManage,
testUsers,
useEditor: true
});
});
});
});
});
});
router.post('/:list/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
forms.update(req.body.id, req.body, (err, updated) => {
if (err) {
req.flash('danger', err.message || err);
} else if (updated) {
req.flash('success', _('Form settings updated'));
} else {
req.flash('info', _('Form settings not updated'));
}
if (req.body.id) {
return res.redirect('/forms/' + encodeURIComponent(req.params.list) + '/edit/' + encodeURIComponent(req.body.id));
} else {
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
}
});
});
router.post('/:list/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
forms.delete(req.body.id, (err, deleted) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (deleted) {
req.flash('success', _('Custom form deleted'));
} else {
req.flash('info', _('Could not delete specified form'));
}
return res.redirect('/forms/' + encodeURIComponent(req.params.list));
});
});
module.exports = router;

View file

@ -1,75 +0,0 @@
'use strict';
const { nodeifyFunction } = require('../lib/nodeify');
const getSettings = nodeifyFunction(require('../models/settings').get);
const config = require('config');
const express = require('express');
const router = new express.Router();
const passport = require('../lib/passport');
const _ = require('../lib/translate')._;
const fs = require('fs');
const path = require('path');
const editorHelpers = require('../lib/editor-helpers')
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', _('Need to be logged in to access restricted content'));
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
}
next();
});
router.get('/editor', passport.csrfProtection, (req, res) => {
getSettings('serviceUrl', (err, serviceUrl) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
editorHelpers.getResource(req.query.type, req.query.id, (err, resource) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
try {
resource.editorData = JSON.parse(resource.editorData);
} catch (err) {
resource.editorData = {
template: req.query.template || 'demo'
}
}
if (!resource.html && !resource.editorData.html && !resource.editorData.mjml) {
const base = path.join(__dirname, '..', 'public', 'grapejs', 'templates', path.join('/', resource.editorData.template));
try {
resource.editorData.mjml = fs.readFileSync(path.join(base, 'index.mjml'), 'utf8');
} catch (err) {
try {
resource.html = fs.readFileSync(path.join(base, 'index.html'), 'utf8');
} catch (err) {
resource.html = err.message || err;
}
}
}
res.render('grapejs/editor', {
layout: 'grapejs/layout-editor',
type: req.query.type,
stringifiedResource: JSON.stringify(resource),
resource,
editor: {
name: resource.editorName || 'grapejs',
mode: resource.editorData.mjml ? 'mjml' : 'html',
config: config.grapejs
},
csrfToken: req.csrfToken(),
serviceUrl
});
});
});
});
module.exports = router;

View file

@ -1,15 +0,0 @@
'use strict';
let express = require('express');
let router = new express.Router();
let _ = require('../lib/translate')._;
/* GET home page. */
router.get('/', (req, res) => {
res.render('index', {
indexPage: true,
title: _('Self Hosted Newsletter App')
});
});
module.exports = router;

View file

@ -1,111 +0,0 @@
'use strict';
const { nodeifyFunction } = require('../lib/nodeify');
const getSettings = nodeifyFunction(require('../models/settings').get);
let links = require('../lib/models/links');
let lists = require('../lib/models/lists');
let subscriptions = require('../lib/models/subscriptions');
let tools = require('../lib/tools');
let _ = require('../lib/translate')._;
let log = require('npmlog');
let express = require('express');
let router = new express.Router();
let trackImg = new Buffer('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
router.get('/:campaign/:list/:subscription', (req, res) => {
res.writeHead(200, {
'Content-Type': 'image/gif',
'Content-Length': trackImg.length
});
links.countOpen(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, (err, opened) => {
if (err) {
log.error('Redirect', err);
}
if (opened) {
log.verbose('Redirect', 'First open for %s:%s:%s', req.params.campaign, req.params.list, req.params.subscription);
}
});
res.end(trackImg);
});
router.get('/:campaign/:list/:subscription/:link', (req, res) => {
let notFound = () => {
res.status(404);
return res.render('archive/view', {
layout: 'archive/layout',
message: _('Oops, we couldn\'t find a link for the URL you clicked'),
campaign: {
subject: 'Error 404'
}
});
};
links.resolve(req.params.link, (err, linkId, url) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!linkId || !url) {
log.error('Redirect', 'Unresolved URL: <%s>', req.url);
return notFound();
}
links.countClick(req.ip, req.headers['user-agent'], req.params.campaign, req.params.list, req.params.subscription, linkId, (err, status) => {
if (err) {
log.error('Redirect', err);
}
if (status) {
log.verbose('Redirect', 'First click for %s:%s:%s (%s)', req.params.campaign, req.params.list, req.params.subscription, url);
}
});
if (!/\[[^\]]+\]/.test(url)) {
// no special tags, just pass on the link
return res.redirect(url);
}
// url might include variables, need to rewrite those just as we do with message content
lists.getByCid(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
log.error('Redirect', 'Could not resolve list for merge tags: <%s>', req.url);
return notFound();
}
getSettings('serviceUrl', (err, serviceUrl) => {
if (err) {
// ignore
}
serviceUrl = (serviceUrl || '').toString().trim();
subscriptions.getWithMergeTags(list.id, req.params.subscription, (err, subscription) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!subscription) {
log.error('Redirect', 'Could not resolve subscription for merge tags: <%s>', req.url);
return notFound();
}
url = tools.formatMessage(serviceUrl, {
cid: req.params.campaign
}, list, subscription, url, str => encodeURIComponent(str));
res.redirect(url);
});
});
});
});
});
module.exports = router;

View file

@ -1,10 +0,0 @@
'use strict';
const _ = require('../lib/translate')._;
const clientHelpers = require('../lib/client-helpers');
const router = require('../lib/router-async').create();
clientHelpers.registerRootRoute(router, 'lists', _('Lists'));
module.exports = router;

View file

@ -1,839 +0,0 @@
'use strict';
let config = require('config');
let openpgp = require('openpgp');
let passport = require('../lib/passport');
let express = require('express');
let router = new express.Router();
let lists = require('../lib/models/lists');
let subscriptions = require('../lib/models/subscriptions');
let fields = require('../lib/models/fields');
let forms = require('../lib/models/forms');
let tools = require('../lib/tools');
let striptags = require('striptags');
let htmlescape = require('escape-html');
let multer = require('multer');
let os = require('os');
let humanize = require('humanize');
let mkdirp = require('mkdirp');
let pathlib = require('path');
let log = require('npmlog');
let _ = require('../lib/translate')._;
let util = require('util');
let uploadStorage = multer.diskStorage({
destination: (req, file, callback) => {
log.verbose('tmpdir', os.tmpdir());
let tmp = config.www.tmpdir || os.tmpdir();
let dir = pathlib.join(tmp, 'mailtrain');
mkdirp(dir, err => {
if (err) {
log.error('Upload', err);
log.verbose('Upload', 'Storing upload to <%s>', tmp);
return callback(null, tmp);
}
log.verbose('Upload', 'Storing upload to <%s>', dir);
callback(null, dir);
});
}
});
let uploads = multer({
storage: uploadStorage
});
let csvparse = require('csv-parse');
let fs = require('fs');
let moment = require('moment-timezone');
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', _('Need to be logged in to access restricted content'));
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
}
res.setSelectedMenu('lists');
next();
});
/* REPLACED
router.get('/', (req, res) => {
res.render('lists/lists', {
title: _('Lists')
});
});
*/
/* REPLACED
router.get('/create', passport.csrfProtection, (req, res) => {
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
data.csrfToken = req.csrfToken();
if (!('publicSubscribe' in data)) {
data.publicSubscribe = true;
}
data.unsubscriptionModeOptions = getUnsubscriptionModeOptions(data.unsubscriptionMode || lists.UnsubscriptionMode.ONE_STEP);
res.render('lists/create', data);
});
*/
/* REPLACED
router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) => {
lists.create(req.body, (err, id) => {
if (err || !id) {
req.flash('danger', err && err.message || err || _('Could not create list'));
return res.redirect('/lists/create?' + tools.queryParams(req.body));
}
req.flash('success', _('List created'));
res.redirect('/lists/view/' + id);
});
});
*/
/* REPLACED
router.get('/edit/:id', passport.csrfProtection, (req, res) => {
lists.get(req.params.id, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
return res.redirect('/lists');
}
forms.list(list.id, (err, customForms) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/lists');
}
list.customForms = customForms.map(row => {
row.selected = list.defaultForm === row.id;
return row;
});
list.unsubscriptionModeOptions = getUnsubscriptionModeOptions(list.unsubscriptionMode);
list.csrfToken = req.csrfToken();
res.render('lists/edit', list);
});
});
});
*/
/* REPLACED
router.post('/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
lists.update(req.body.id, req.body, (err, updated) => {
if (err) {
req.flash('danger', err.message || err);
} else if (updated) {
req.flash('success', _('List settings updated'));
} else {
req.flash('info', _('List settings not updated'));
}
if (req.query.next) {
return res.redirect(req.query.next);
} else if (req.body.id) {
return res.redirect('/lists/edit/' + encodeURIComponent(req.body.id));
} else {
return res.redirect('/lists');
}
});
});
*/
/* REPLACED
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
lists.delete(req.body.id, (err, deleted) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (deleted) {
req.flash('success', _('List deleted'));
} else {
req.flash('info', _('Could not delete specified list'));
}
return res.redirect('/lists');
});
});
*/
router.post('/ajax', (req, res) => {
lists.filter(req.body, Number(req.query.parent) || false, (err, data, total, filteredTotal) => {
if (err) {
return res.json({
error: err.message || err,
data: []
});
}
res.json({
draw: req.body.draw,
recordsTotal: total,
recordsFiltered: filteredTotal,
data: data.map((row, i) => [
(Number(req.body.start) || 0) + 1 + i,
'<span class="glyphicon glyphicon-list-alt" aria-hidden="true"></span> <a href="/lists/view/' + row.id + '">' + htmlescape(row.name || '') + '</a>',
'<code>' + row.cid + '</code>',
row.subscribers,
htmlescape(striptags(row.description) || ''),
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/edit/' + row.id + '">' + _('Edit') + '</a>' ]
)
});
});
});
router.post('/ajax/:id', (req, res) => {
lists.get(req.params.id, (err, list) => {
if (err || !list) {
return res.json({
error: err && err.message || err || _('List not found'),
data: []
});
}
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
}
let columns = ['#', 'email', 'first_name', 'last_name'].concat(fieldList.filter(field => field.visible).map(field => field.column)).concat(['status', 'created']);
subscriptions.filter(list.id, req.body, columns, req.query.segment, (err, data, total, filteredTotal) => {
if (err) {
return res.json({
error: err.message || err,
data: []
});
}
data.forEach(row => {
row.subscriptionStatus = row.status === 1 ? true : false;
row.customFields = fields.getRow(fieldList, row);
});
let statuses = [_('Unknown'), _('Subscribed'), _('Unsubscribed'), _('Bounced'), _('Complained')];
res.json({
draw: req.body.draw,
recordsTotal: total,
recordsFiltered: filteredTotal,
data: data.map((row, i) => [
(Number(req.body.start) || 0) + 1 + i,
htmlescape(row.email || ''),
htmlescape(row.firstName || ''),
htmlescape(row.lastName || '')
].concat(fields.getRow(fieldList, row).map(cRow => {
if (cRow.type === 'number') {
return htmlescape(cRow.value && humanize.numberFormat(cRow.value, 0) || '');
} else if (cRow.type === 'longtext') {
let value = (cRow.value || '');
if (value.length > 50) {
value = value.substr(0, 47).trim() + '…';
}
return htmlescape(value);
} else if (cRow.type === 'gpg') {
let value = (cRow.value || '').trim();
try {
value = openpgp.key.readArmored(value);
if (value) {
let keys = value.keys;
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
switch (key.verifyPrimaryKey()) {
case 0:
return _('Invalid key');
case 1:
return _('Expired key');
case 2:
return _('Revoked key');
}
}
value = value.keys && value.keys[0] && value.keys[0].primaryKey.fingerprint;
if (value) {
value = '0x' + value.substr(-16).toUpperCase();
}
}
} catch (E) {
value = 'parse error';
}
return htmlescape(value || '');
} else {
return htmlescape(cRow.value || '');
}
})).concat(statuses[row.status]).concat(row.created && row.created.toISOString ? '<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>' : 'N/A').concat('<a href="/lists/subscription/' + list.id + '/edit/' + row.cid + '">' + _('Edit') + '</a>'))
});
});
});
});
});
router.get('/view/:id', passport.csrfProtection, (req, res) => {
if (Number(req.query.segment) === -1) {
return res.redirect('/segments/' + encodeURIComponent(req.params.id) + '/create');
}
lists.get(req.params.id, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
return res.redirect('/lists');
}
subscriptions.listImports(list.id, (err, imports) => {
if (err) {
// not important, ignore
imports = [];
}
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
}
list.imports = imports.map((entry, i) => {
entry.index = i + 1;
entry.importType = entry.type === 0 ? _('Subscribe') : (entry.type === 1 ? _('Force Subscribe') : _('Unsubscribe'));
switch (entry.status) {
case 0:
entry.importStatus = _('Initializing');
break;
case 1:
entry.importStatus = _('Initialized');
break;
case 2:
entry.importStatus = _('Importing') + '…';
break;
case 3:
entry.importStatus = _('Finished');
break;
default:
entry.importStatus = _('Errored') + (entry.error ? ' (' + entry.error + ')' : '');
entry.error = true;
}
entry.created = entry.created && entry.created.toISOString();
entry.finished = entry.finished && entry.finished.toISOString();
entry.updated = entry.processed - entry.new;
entry.processed = humanize.numberFormat(entry.processed, 0);
return entry;
});
list.csrfToken = req.csrfToken();
list.customFields = fieldList.filter(field => field.visible);
list.customSort = list.customFields.length ? ',' + list.customFields.map(() => '0').join(',') : '';
list.showSubscriptions = req.query.tab === 'subscriptions' || !req.query.tab;
list.showImports = req.query.tab === 'imports';
list.segments.forEach(segment => {
if (segment.id === (Number(req.query.segment) || 0)) {
segment.selected = true;
list.useSegment = req.query.segment;
list.segment = segment.id;
}
});
res.render('lists/view', list);
});
});
});
});
router.get('/subscription/:id/add', passport.csrfProtection, (req, res) => {
lists.get(req.params.id, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
return res.redirect('/lists');
}
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
}
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
data.list = list;
data.csrfToken = req.csrfToken();
data.customFields = fields.getRow(fieldList, data, false, true);
data.useEditor = true;
data.timezones = moment.tz.names().map(tz => {
let selected = false;
if (tz.toLowerCase().trim() === (data.tz || 'UTC').toLowerCase().trim()) {
selected = true;
}
return {
key: tz,
value: tz,
selected
};
});
res.render('lists/subscription/add', data);
});
});
});
router.get('/subscription/:id/edit/:cid', passport.csrfProtection, (req, res) => {
lists.get(req.params.id, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
return res.redirect('/lists');
}
subscriptions.get(list.id, req.params.cid, (err, subscription) => {
if (err || !subscription) {
req.flash('danger', err && err.message || err || _('Could not find subscriber with specified ID'));
return res.redirect('/lists/view/' + req.params.id);
}
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
}
subscription.list = list;
subscription.csrfToken = req.csrfToken();
subscription.customFields = fields.getRow(fieldList, subscription, false, true);
subscription.useEditor = true;
subscription.isSubscribed = subscription.status === 1;
let tzfound = false;
subscription.timezones = moment.tz.names().map(tz => {
let selected = false;
if (tz.toLowerCase().trim() === (subscription.tz || '').toLowerCase().trim()) {
selected = true;
tzfound = true;
}
return {
key: tz,
value: tz,
selected
};
});
if (!tzfound && subscription.tz) {
subscription.timezones.push({
key: subscription.tz,
value: subscription.tz,
selected: true
});
}
res.render('lists/subscription/edit', subscription);
});
});
});
});
router.post('/subscription/add', passport.parseForm, passport.csrfProtection, (req, res) => {
subscriptions.insert(req.body.list, false, req.body, (err, response) => {
if (err) {
req.flash('danger', err && err.message || err || _('Could not add subscription'));
return res.redirect('/lists/subscription/' + encodeURIComponent(req.body.list) + '/add?' + tools.queryParams(req.body));
}
if (response.entryId) {
req.flash('success', util.format(_('%s was successfully added to your list'), req.body.email));
} else {
req.flash('warning', util.format(_('%s was not added to your list'), req.body.email));
}
res.redirect('/lists/subscription/' + encodeURIComponent(req.body.list) + '/add');
});
});
router.post('/subscription/unsubscribe', passport.parseForm, passport.csrfProtection, (req, res) => {
lists.get(req.body.list, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
return res.redirect('/lists');
}
subscriptions.get(list.id, req.body.cid, (err, subscription) => {
if (err || !subscription) {
req.flash('danger', err && err.message || err || _('Could not find subscriber with specified ID'));
return res.redirect('/lists/view/' + list.id);
}
subscriptions.changeStatus(list.id, subscription.id, false, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
if (err) {
req.flash('danger', err && err.message || err || _('Could not unsubscribe user'));
return res.redirect('/lists/subscription/' + list.id + '/edit/' + subscription.cid);
}
req.flash('success', util.format(_('%s was successfully unsubscribed from your list'), subscription.email));
res.redirect('/lists/view/' + list.id);
});
});
});
});
router.post('/subscription/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
lists.get(req.body.list, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
return res.redirect('/lists');
}
subscriptions.delete(list.id, req.body.cid, (err, email) => {
if (err || !email) {
req.flash('danger', err && err.message || err || _('Could not find subscriber with specified ID'));
return res.redirect('/lists/view/' + list.id);
}
req.flash('success', util.format(_('%s was successfully removed from your list'), email));
res.redirect('/lists/view/' + list.id);
});
});
});
router.post('/subscription/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
req.body['is-test'] = req.body['is-test'] ? '1' : '0';
subscriptions.update(req.body.list, req.body.cid, req.body, true, (err, updated) => {
if (err) {
if (err.code === 'ER_DUP_ENTRY') {
req.flash('danger', util.format(_('Another subscriber with email address %s already exists'), req.body.email));
return res.redirect('/lists/subscription/' + encodeURIComponent(req.body.list) + '/edit/' + req.body.cid);
} else {
req.flash('danger', err.message || err);
}
} else if (updated) {
req.flash('success', _('Subscription settings updated'));
} else {
req.flash('info', _('Subscription settings not updated'));
}
if (req.body.list) {
return res.redirect('/lists/view/' + encodeURIComponent(req.body.list));
} else {
return res.redirect('/lists');
}
});
});
router.get('/subscription/:id/import', passport.csrfProtection, (req, res) => {
lists.get(req.params.id, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
return res.redirect('/lists');
}
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
if (!('delimiter' in data)) {
data.delimiter = ',';
}
data.list = list;
data.csrfToken = req.csrfToken();
res.render('lists/subscription/import', data);
});
});
router.get('/subscription/:id/import/:importId', passport.csrfProtection, (req, res) => {
lists.get(req.params.id, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
return res.redirect('/lists');
}
subscriptions.getImport(req.params.id, req.params.importId, (err, data) => {
if (err || !data) {
req.flash('danger', err && err.message || err || _('Could not find import data with specified ID'));
return res.redirect('/lists');
}
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
}
data.list = list;
data.csrfToken = req.csrfToken();
data.customFields = fields.getRow(fieldList, data, false, true);
res.render('lists/subscription/import-preview', data);
});
});
});
});
router.post('/subscription/import', uploads.single('listimport'), passport.parseForm, passport.csrfProtection, (req, res) => {
lists.get(req.body.list, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
return res.redirect('/lists');
}
let delimiter = (req.body.delimiter || '').trim().charAt(0) || ',';
getPreview(req.file.path, req.file.size, delimiter, (err, rows) => {
if (err) {
req.flash('danger', err && err.message || err || _('Could not process CSV'));
return res.redirect('/lists');
} else {
let type = 0; // Use the existing subscription status or SUBSCRIBED
if (req.body.type === 'force_subscribed') {
type = subscriptions.Status.SUBSCRIBED;
} else if (req.body.type === 'unsubscribed') {
type = subscriptions.Status.UNSUBSCRIBED;
}
subscriptions.createImport(list.id, type, req.file.path, req.file.size, delimiter, req.body.emailcheck === 'enabled' ? 1 : 0, {
columns: rows[0],
example: rows[1] || []
}, (err, importId) => {
if (err) {
req.flash('danger', err && err.message || err || _('Could not create importer'));
return res.redirect('/lists');
}
return res.redirect('/lists/subscription/' + list.id + '/import/' + importId);
});
}
});
});
});
function getPreview(path, size, delimiter, callback) {
delimiter = (delimiter || '').trim().charAt(0) || ',';
size = Number(size);
fs.open(path, 'r', (err, fd) => {
if (err) {
return callback(err);
}
let bufLen = size;
let maxReadSize = 10 * 1024;
if (size > maxReadSize) {
bufLen = maxReadSize;
}
let buffer = new Buffer(bufLen);
fs.read(fd, buffer, 0, buffer.length, 0, (err, bytesRead, buffer) => {
if (err) {
return callback(err);
}
let input = buffer.toString().trim();
if (size !== bufLen) {
// remove last incomplete line
input = input.split(/\r?\n/);
input.pop();
input = input.join('\n');
}
csvparse(input, {
comment: '#',
delimiter
}, (err, data) => {
fs.close(fd, () => {
// just ignore
});
if (err) {
return callback(err);
}
if (!data || !data.length) {
return callback(new Error(_('Empty file')));
}
if (data.length < 2) {
return callback(new Error(_('Too few rows')));
}
callback(err, data);
});
});
});
}
router.post('/subscription/import-confirm', passport.parseForm, passport.csrfProtection, (req, res) => {
lists.get(req.body.list, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
return res.redirect('/lists');
}
subscriptions.getImport(list.id, req.body.import, (err, data) => {
if (err || !data) {
req.flash('danger', err && err.message || err || _('Could not find import data with specified ID'));
return res.redirect('/lists');
}
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
}
let allowedColumns = ['email', 'first_name', 'last_name', 'tz'];
fieldList.forEach(field => {
if (field.column) {
allowedColumns.push(field.column);
}
if (field.options) {
field.options.forEach(subField => {
if (subField.column) {
allowedColumns.push(subField.column);
}
});
}
});
data.mapping.mapping = {};
data.mapping.columns.forEach((column, i) => {
let colIndex = allowedColumns.indexOf(req.body['column-' + i]);
if (colIndex >= 0) {
data.mapping.mapping[allowedColumns[colIndex]] = i;
}
});
subscriptions.updateImport(list.id, req.body.import, {
status: 1,
mapping: JSON.stringify(data.mapping)
}, (err, importer) => {
if (err || !importer) {
req.flash('danger', err && err.message || err || _('Could not find import data with specified ID'));
return res.redirect('/lists');
}
req.flash('success', _('Import started'));
res.redirect('/lists/view/' + list.id + '?tab=imports');
});
});
});
});
});
router.post('/subscription/import-restart', passport.parseForm, passport.csrfProtection, (req, res) => {
lists.get(req.body.list, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
return res.redirect('/lists');
}
subscriptions.updateImport(list.id, req.body.import, {
status: 1,
error: null,
finished: null,
processed: 0,
new: 0,
failed: 0
}, (err, importer) => {
if (err || !importer) {
req.flash('danger', err && err.message || err || _('Could not find import data with specified ID'));
return res.redirect('/lists');
}
req.flash('success', _('Import restarted'));
res.redirect('/lists/view/' + list.id + '?tab=imports');
});
});
});
router.get('/subscription/:id/import/:importId/failed', (req, res) => {
let start = 0;
lists.get(req.params.id, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find list with specified ID'));
return res.redirect('/lists');
}
subscriptions.getImport(req.params.id, req.params.importId, (err, data) => {
if (err || !data) {
req.flash('danger', err && err.message || err || _('Could not find import data with specified ID'));
return res.redirect('/lists');
}
subscriptions.getFailedImports(req.params.importId, (err, rows) => {
if (err) {
req.flash('danger', err && err.message || err);
return res.redirect('/lists');
}
data.rows = rows.map((row, i) => {
row.index = start + i + 1;
return row;
});
data.list = list;
res.render('lists/subscription/import-failed', data);
});
});
});
});
router.post('/quicklist/ajax', (req, res) => {
lists.filterQuicklist(req.body, (err, data, total, filteredTotal) => {
if (err) {
return res.json({
error: err.message || err,
data: []
});
}
res.json({
draw: req.body.draw,
recordsTotal: total,
recordsFiltered: filteredTotal,
data: data.map((row, i) => ({
"0": (Number(req.body.start) || 0) + 1 + i,
"1": '<span class="glyphicon glyphicon-inbox" aria-hidden="true"></span> <a href="/lists/view/' + row.id + '">' + htmlescape(row.name || '') + '</a>',
"2": row.subscribers,
"DT_RowId": row.id
}))
});
});
});
function getUnsubscriptionModeOptions(unsubscriptionMode) {
const options = [];
options[lists.UnsubscriptionMode.ONE_STEP] = {
value: lists.UnsubscriptionMode.ONE_STEP,
selected: unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP,
label: _('One-step (i.e. no email with confirmation link)')
};
options[lists.UnsubscriptionMode.ONE_STEP_WITH_FORM] = {
value: lists.UnsubscriptionMode.ONE_STEP_WITH_FORM,
selected: unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM,
label: _('One-step with unsubscription form (i.e. no email with confirmation link)')
};
options[lists.UnsubscriptionMode.TWO_STEP] = {
value: lists.UnsubscriptionMode.TWO_STEP,
selected: unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP,
label: _('Two-step (i.e. an email with confirmation link will be sent)')
};
options[lists.UnsubscriptionMode.TWO_STEP_WITH_FORM] = {
value: lists.UnsubscriptionMode.TWO_STEP_WITH_FORM,
selected: unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM,
label: _('Two-step with unsubscription form (i.e. an email with confirmation link will be sent)')
};
options[lists.UnsubscriptionMode.MANUAL] = {
value: lists.UnsubscriptionMode.MANUAL,
selected: unsubscriptionMode === lists.UnsubscriptionMode.MANUAL,
label: _('Manual (i.e. unsubscription has to be performed by the list administrator)')
};
return options;
}
module.exports = router;

View file

@ -1,57 +1,49 @@
'use strict';
let config = require('config');
let express = require('express');
let router = new express.Router();
let passport = require('../lib/passport');
let fs = require('fs');
let path = require('path');
let _ = require('../lib/translate')._;
let editorHelpers = require('../lib/editor-helpers');
const config = require('config');
const router = require('../lib/router-async').create();
const passport = require('../lib/passport');
const clientHelpers = require('../lib/client-helpers');
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', _('Need to be logged in to access restricted content'));
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
const bluebird = require('bluebird');
const fsReadFile = bluebird.promisify(require('fs').readFile);
const path = require('path');
// FIXME - add authentication by sandboxToken
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
const resourceType = req.query.type;
const resourceId = req.query.id;
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context);
let languageStrings = null;
if (config.language && config.language !== 'en') {
const lang = config.language.split('_')[0];
try {
const file = path.join(__dirname, '..', 'client', 'public', 'mosaico', 'lang', 'mosaico-' + lang + '.json');
languageStrings = await fsReadFile(file, 'utf8');
} catch (err) {
}
}
next();
});
router.get('/editor', passport.csrfProtection, (req, res) => {
editorHelpers.getResource(req.query.type, req.query.id, (err, resource) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
let getLanguageStrings = language => {
if (!language ||  language === 'en') {
return null;
}
language = language.split('_')[0];
try {
let file = path.join(__dirname, '..', 'public', 'mosaico', 'dist', 'lang', 'mosaico-' + language + '.json');
return fs.readFileSync(file, 'utf8');
} catch (err) {
return null;
}
}
/* ????
resource.editorName = resource.editorName ||  'mosaico';
resource.editorData = !resource.editorData ?
{
template: req.query.template || 'versafix-1'
} :
JSON.parse(resource.editorData);
*/
res.render('mosaico/editor', {
layout: 'mosaico/layout-editor',
type: req.query.type,
resource,
editorConfig: config.mosaico,
languageStrings: getLanguageStrings(config.language),
csrfToken: req.csrfToken(),
});
res.render('mosaico/root', {
layout: 'mosaico/layout',
editorConfig: config.mosaico,
languageStrings: languageStrings,
reactCsrfToken: req.csrfToken(),
mailtrainConfig: JSON.stringify(mailtrainConfig)
});
});

View file

@ -1,10 +0,0 @@
'use strict';
const _ = require('../lib/translate')._;
const clientHelpers = require('../lib/client-helpers');
const router = require('../lib/router-async').create();
clientHelpers.registerRootRoute(router, 'namespaces', _('Namespaces'));
module.exports = router;

View file

@ -1,10 +0,0 @@
'use strict';
const _ = require('../lib/translate')._;
const clientHelpers = require('../lib/client-helpers');
const router = require('../lib/router-async').create();
clientHelpers.registerRootRoute(router, 'reports', _('Reports'));
module.exports = router;

View file

@ -42,7 +42,7 @@ router.postAsync('/access-token-reset', passport.loggedIn, passport.csrfProtecti
router.post('/login', passport.csrfProtection, passport.restLogin);
router.post('/logout', passport.csrfProtection, passport.restLogout); // TODO - this endpoint is currently not in use. It will become relevant once we switch to SPA
router.post('/logout', passport.csrfProtection, passport.restLogout);
router.postAsync('/password-reset-send', passport.csrfProtection, async (req, res) => {
await users.sendPasswordReset(req.body.usernameOrEmail);

22
routes/root.js Normal file
View file

@ -0,0 +1,22 @@
'use strict';
const passport = require('../lib/passport');
const _ = require('../lib/translate')._;
const clientHelpers = require('../lib/client-helpers');
const router = require('../lib/router-async').create();
router.getAsync('/*', passport.csrfProtection, async (req, res) => {
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context);
if (req.user) {
Object.assign(mailtrainConfig, await clientHelpers.getAuthenticatedConfig(req.context));
}
res.render('root', {
reactCsrfToken: req.csrfToken(),
mailtrainConfig: JSON.stringify(mailtrainConfig)
});
});
module.exports = router;

View file

@ -1,437 +0,0 @@
'use strict';
let express = require('express');
let router = new express.Router();
let passport = require('../lib/passport');
let lists = require('../lib/models/lists');
let segments = require('../lib/models/segments');
let tools = require('../lib/tools');
let _ = require('../lib/translate')._;
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', _('Need to be logged in to access restricted content'));
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
}
res.setSelectedMenu('lists');
next();
});
router.get('/:list', (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
segments.list(list.id, (err, rows) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/fields/' + encodeURIComponent(req.params.list));
}
let index = 0;
res.render('lists/segments/segments', {
rows: rows.map(row => {
row.index = ++index;
row.type = row.type === 1 ? 'ALL' : 'ANY';
return row;
}),
list
});
});
});
});
router.get('/:list/create', passport.csrfProtection, (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
let segment = tools.convertKeys(req.query, {
skip: ['layout']
});
segment.csrfToken = req.csrfToken();
segment.list = list;
switch (Number(segment.type) || 0) {
case 1:
segment.matchAll = true;
break;
case 2:
segment.matchAny = true;
break;
}
res.render('lists/segments/create', segment);
});
});
router.post('/:list/create', passport.parseForm, passport.csrfProtection, (req, res) => {
segments.create(req.params.list, req.body, (err, id) => {
if (err || !id) {
req.flash('danger', err && err.message || err || _('Could not create segment'));
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/create?' + tools.queryParams(req.body));
}
req.flash('success', _('Segment created'));
res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/view/' + id);
});
});
router.get('/:list/view/:id', (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
segments.get(req.params.id, (err, segment) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!segment) {
req.flash('danger', _('Selected segment ID not found'));
return res.redirect('/');
}
segment.list = list;
segment.rules.forEach((rule, i) => {
rule.index = i + 1;
});
switch (Number(segment.type) || 0) {
case 1:
segment.type = 'ALL';
break;
case 2:
segment.type = 'ANY';
break;
}
segments.subscribers(req.params.id, true, (err, subscribers) => {
if (err) {
// ignore
}
segment.subscribers = subscribers || 0;
res.render('lists/segments/view', segment);
});
});
});
});
router.get('/:list/edit/:segment', passport.csrfProtection, (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
segments.get(req.params.segment, (err, segment) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
if (!segment) {
req.flash('danger', 'Selected segment not found');
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
segment.csrfToken = req.csrfToken();
segment.list = list;
switch (Number(segment.type) || 0) {
case 1:
segment.matchAll = true;
break;
case 2:
segment.matchAny = true;
break;
}
res.render('lists/segments/edit', segment);
});
});
});
router.post('/:list/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
segments.update(req.body.id, req.body, (err, updated) => {
if (err) {
req.flash('danger', err.message || err);
} else if (updated) {
req.flash('success', _('Segment settings updated'));
} else {
req.flash('info', _('Segment settings not updated'));
}
if (req.body.id) {
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/view/' + encodeURIComponent(req.body.id));
} else {
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
});
});
router.post('/:list/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
segments.delete(req.body.id, (err, deleted) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (deleted) {
req.flash('success', _('Segment deleted'));
} else {
req.flash('info', _('Could not delete specified segment'));
}
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
});
});
router.get('/:list/rules/:segment/create', passport.csrfProtection, (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
segments.get(req.params.segment, (err, segment) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
if (!segment) {
req.flash('danger', 'Selected segment not found');
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
segment.csrfToken = req.csrfToken();
segment.list = list;
res.render('lists/segments/rule-create', segment);
});
});
});
router.post('/:list/rules/:segment/next', passport.parseForm, passport.csrfProtection, (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
segments.get(req.params.segment, (err, segment) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
if (!segment) {
req.flash('danger', _('Selected segment not found'));
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
let column = segment.columns.filter(column => column.column === req.body.column).pop();
if (!column) {
req.flash('danger', _('Invalid rule type'));
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/rules/' + segment.id + '/create?' + tools.queryParams(req.body));
}
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/rules/' + segment.id + '/configure?' + tools.queryParams(req.body));
});
});
});
router.get('/:list/rules/:segment/configure', passport.csrfProtection, (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
segments.get(req.params.segment, (err, segment) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
if (!segment) {
req.flash('danger', _('Selected segment not found'));
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
let column = segment.columns.filter(column => column.column === req.query.column).pop();
if (!column) {
req.flash('danger', _('Invalid rule type'));
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/rules/' + segment.id + '/create?' + tools.queryParams(req.body));
}
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
segment.csrfToken = req.csrfToken();
segment.value = data;
segment.list = list;
segment.column = column;
segment['columnType' + column.type.replace(/^[a-z]/, c => c.toUpperCase())] = true;
segment.useEditor = true;
res.render('lists/segments/rule-configure', segment);
});
});
});
router.post('/:list/rules/:segment/create', passport.parseForm, passport.csrfProtection, (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
segments.createRule(req.params.segment, req.body, (err, id) => {
if (err || !id) {
req.flash('danger', err && err.message || err || _('Could not create rule'));
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/rules/' + encodeURIComponent(req.params.segment) + '/configure?' + tools.queryParams(req.body));
}
req.flash('success', _('Rule created'));
res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/view/' + encodeURIComponent(req.params.segment));
});
});
});
router.get('/:list/rules/:segment/edit/:rule', passport.csrfProtection, (req, res) => {
lists.get(req.params.list, (err, list) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!list) {
req.flash('danger', _('Selected list ID not found'));
return res.redirect('/');
}
segments.get(req.params.segment, (err, segment) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
if (!segment) {
req.flash('danger', _('Selected segment not found'));
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
segments.getRule(req.params.rule, (err, rule) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
if (!segment) {
req.flash('danger', _('Selected segment not found'));
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
let column = segment.columns.filter(column => column.column === rule.column).pop();
if (!column) {
req.flash('danger', _('Invalid rule type'));
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/view/' + segment.id);
}
rule.csrfToken = req.csrfToken();
rule.list = list;
rule.segment = segment;
rule.column = column;
rule['columnType' + column.type.replace(/^[a-z]/, c => c.toUpperCase())] = true;
rule.useEditor = true;
res.render('lists/segments/rule-edit', rule);
});
});
});
});
router.post('/:list/rules/:segment/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
segments.updateRule(req.body.id, req.body, (err, updated) => {
if (err) {
req.flash('danger', err.message || err);
} else if (updated) {
req.flash('success', _('Rule settings updated'));
} else {
req.flash('info', _('Rule settings not updated'));
}
if (req.params.segment) {
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/view/' + encodeURIComponent(req.params.segment));
} else {
return res.redirect('/segments/' + encodeURIComponent(req.params.list));
}
});
});
router.post('/:list/rules/:segment/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
segments.deleteRule(req.body.id, (err, deleted) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (deleted) {
req.flash('success', _('Rule deleted'));
} else {
req.flash('info', _('Could not delete specified rule'));
}
return res.redirect('/segments/' + encodeURIComponent(req.params.list) + '/view/' + encodeURIComponent(req.params.segment));
});
});
module.exports = router;

View file

@ -1,231 +0,0 @@
'use strict';
// FIXME: This does not work now because of missing lib/models/settings
let config = require('config');
let passport = require('../lib/passport');
let express = require('express');
let router = new express.Router();
let tools = require('../lib/tools');
let nodemailer = require('nodemailer');
let mailer = require('../lib/mailer');
let url = require('url');
let multer = require('multer');
let upload = multer();
let aws = require('aws-sdk');
let util = require('util');
let _ = require('../lib/translate')._;
const senders = require('../lib/senders');
let settings = require('../lib/models/settings');
let allowedKeys = ['service_url', 'smtp_hostname', 'smtp_port', 'smtp_encryption', 'smtp_disable_auth', 'smtp_user', 'smtp_pass', 'admin_email', 'smtp_log', 'smtp_max_connections', 'smtp_max_messages', 'smtp_self_signed', 'default_from', 'default_address', 'default_subject', 'default_homepage', 'default_postaddress', 'default_sender', 'verp_hostname', 'verp_use', 'disable_wysiwyg', 'pgp_private_key', 'pgp_passphrase', 'ua_code', 'shoutout', 'disable_confirmations', 'smtp_throttling', 'dkim_api_key', 'dkim_private_key', 'dkim_selector', 'dkim_domain', 'mail_transport', 'ses_key', 'ses_secret', 'ses_region'];
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', _('Need to be logged in to access restricted content'));
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
}
res.setSelectedMenu('/settings');
next();
});
router.get('/', passport.csrfProtection, (req, res, next) => {
settings.list((err, configItems) => {
if (err) {
return next(err);
}
configItems.smtpEncryption = [{
checked: configItems.smtpEncryption === 'TLS' || !configItems.smtpEncryption,
key: 'TLS',
value: _('Use TLS'),
description: _('usually selected for port 465')
}, {
checked: configItems.smtpEncryption === 'STARTTLS',
key: 'STARTTLS',
value: _('Use STARTTLS'),
description: _('usually selected for port 587 and 25')
}, {
checked: configItems.smtpEncryption === 'NONE',
key: 'NONE',
value: _('Do not use encryption')
}];
configItems.sesRegion = [{
checked: configItems.sesRegion === 'us-east-1' || !configItems.sesRegion,
key: 'us-east-1',
value: 'US-EAST-1'
}, {
checked: configItems.sesRegion === 'us-west-2',
key: 'us-west-2',
value: 'US-WEST-2'
}, {
checked: configItems.sesRegion === 'eu-west-1',
key: 'eu-west-1',
value: 'EU-WEST-1'
}];
configItems.useSMTP = configItems.mailTransport === 'smtp' || !configItems.mailTransport;
configItems.useSES = configItems.mailTransport === 'ses';
let urlparts = url.parse(configItems.serviceUrl);
configItems.verpHostname = configItems.verpHostname || 'bounces.' + (urlparts.hostname || 'localhost');
configItems.verpEnabled = config.verp.enabled;
configItems.csrfToken = req.csrfToken();
res.render('settings', configItems);
});
});
router.post('/update', passport.parseForm, passport.csrfProtection, (req, res) => {
let data = tools.convertKeys(req.body);
let keys = [];
let values = [];
Object.keys(data).forEach(key => {
let value = data[key].trim();
key = tools.toDbKey(key);
// ensure trailing slash for service home page
if (key === 'service_url' && value && !/\/$/.test(value)) {
value = value + '/';
}
if (allowedKeys.indexOf(key) >= 0) {
keys.push(key);
values.push(value);
}
});
// checkboxs are not included in value listing if left unchecked
['smtp_log', 'smtp_self_signed', 'smtp_disable_auth', 'verp_use', 'disable_wysiwyg', 'disable_confirmations'].forEach(key => {
if (keys.indexOf(key) < 0) {
keys.push(key);
values.push('');
}
});
let i = 0;
let storeSettings = () => {
if (i >= keys.length) {
mailer.update();
senders.workers.forEach(worker => {
worker.send({
reload: true
});
});
req.flash('success', _('Settings updated'));
return res.redirect('/settings');
}
let key = keys[i];
let value = values[i];
i++;
settings.set(key, value, err => {
if (err) {
req.flash('danger', err && err.message || err);
return res.redirect('/settings');
}
storeSettings();
});
};
storeSettings();
});
router.post('/smtp-verify', upload.array(), passport.parseForm, passport.csrfProtection, (req, res) => {
let data = tools.convertKeys(req.body);
// checkboxs are not included in value listing if left unchecked
['smtpLog', 'smtpSelfSigned', 'smtpDisableAuth'].forEach(key => {
if (!data.hasOwnProperty(key)) {
data[key] = false;
} else {
data[key] = true;
}
});
let transportOptions;
if (data.mailTransport === 'smtp') {
transportOptions = {
host: data.smtpHostname,
port: Number(data.smtpPort) || false,
secure: data.smtpEncryption === 'TLS',
ignoreTLS: data.smtpEncryption === 'NONE',
auth: data.smtpDisableAuth ? false : {
user: data.smtpUser,
pass: data.smtpPass
},
tls: {
rejectUnauthorized: !data.smtpSelfSigned
}
};
} else if (data.mailTransport === 'ses') {
transportOptions = {
SES: new aws.SES({
apiVersion: '2010-12-01',
accessKeyId: data.sesKey,
secretAccessKey: data.sesSecret,
region: data.sesRegion
})
};
} else {
return res.json({
error: _('Invalid mail transport type')
});
}
let transport = nodemailer.createTransport(transportOptions);
transport.verify(err => {
if (err) {
let message = '';
switch (err.code) {
case 'InvalidClientTokenId':
message = _('Invalid Access Key');
break;
case 'SignatureDoesNotMatch':
message = _('Invalid AWS credentials');
break;
case 'ECONNREFUSED':
message = _('Connection refused, check hostname and port.');
break;
case 'ETIMEDOUT':
if ((err.message || '').indexOf('Greeting never received') === 0) {
if (data.smtpEncryption !== 'TLS') {
message = _('Did not receive greeting message from server. This might happen when connecting to a TLS port without using TLS.');
} else {
message = _('Did not receive greeting message from server.');
}
} else {
message = _('Connection timed out. Check your firewall settings, destination port is probably blocked.');
}
break;
case 'EAUTH':
if (/\b5\.7\.0\b/.test(err.message) && data.smtpEncryption !== 'STARTTLS') {
message = _('Authentication not accepted, server expects STARTTLS to be used.');
} else {
message = _('Authentication failed, check username and password.');
}
break;
}
if (!message && err.reason) {
message = err.reason;
}
res.json({
error: (message || _('Failed Mailer verification.')) + (err.response ? ' ' + util.format(_('Server responded with: "%s"'), err.response) : '')
});
} else {
res.json({
message: _('Mailer settings verified, ready to send some mail!')
});
}
});
});
module.exports = router;

View file

@ -1,10 +0,0 @@
'use strict';
const _ = require('../lib/translate')._;
const clientHelpers = require('../lib/client-helpers');
const router = require('../lib/router-async').create();
clientHelpers.registerRootRoute(router, 'templates', _('Templates'));
module.exports = router;

View file

@ -1,191 +0,0 @@
'use strict';
const { nodeifyFunction } = require('../lib/nodeify');
const getSettings = nodeifyFunction(require('../models/settings').get);
let config = require('config');
let express = require('express');
let router = new express.Router();
let templates = require('../lib/models/templates');
let tools = require('../lib/tools');
let helpers = require('../lib/helpers');
let striptags = require('striptags');
let htmlescape = require('escape-html');
let passport = require('../lib/passport');
let mailer = require('../lib/mailer');
let _ = require('../lib/translate')._;
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', _('Need to be logged in to access restricted content'));
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
}
res.setSelectedMenu('templates');
next();
});
router.get('/', (req, res) => {
res.render('templates/templates', {
title: _('Templates')
});
});
router.get('/create', passport.csrfProtection, (req, res, next) => {
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
data.csrfToken = req.csrfToken();
data.useEditor = true;
getSettings(['defaultPostaddress', 'defaultSender', 'disableWysiwyg'], (err, configItems) => {
if (err) {
return next(err);
}
mailer.getTemplate('emails/stationery-html.hbs', (err, rendererHtml) => {
if (err) {
return next(err);
}
mailer.getTemplate('emails/stationery-text.hbs', (err, rendererText) => {
if (err) {
return next(err);
}
data.html = data.html || rendererHtml(configItems);
data.text = data.text || rendererText(configItems);
data.disableWysiwyg = configItems.disableWysiwyg;
data.editors = config.editors || [
['summernote', 'Summernote']
];
data.editors = data.editors.map(ed => {
let editor = {
name: ed[0],
label: ed[1]
};
if (config[editor.name] && config[editor.name].templates) {
editor.templates = config[editor.name].templates.map(tmpl => ({
name: tmpl[0],
label: tmpl[1]
}));
}
return editor;
});
res.render('templates/create', data);
});
});
});
});
router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) => {
templates.create(req.body, (err, id) => {
if (err || !id) {
req.flash('danger', err && err.message || err || _('Could not create template'));
return res.redirect('/templates/create?' + tools.queryParams(req.body));
}
req.flash('success', _('Template created'));
res.redirect('/templates/edit/' + id);
});
});
router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
templates.get(req.params.id, (err, template) => {
if (err || !template) {
req.flash('danger', err && err.message || err || _('Could not find template with specified ID'));
return res.redirect('/templates');
}
getSettings(['disableWysiwyg'], (err, configItems) => {
if (err) {
return next(err);
}
helpers.getDefaultMergeTags((err, defaultMergeTags) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/templates');
}
template.mergeTags = defaultMergeTags;
template.csrfToken = req.csrfToken();
template.useEditor = true;
template.editorName = template.editorName || 'summernote';
template.editorConfig = config[template.editorName];
template.disableWysiwyg = configItems.disableWysiwyg;
res.render('templates/edit', template);
});
});
});
});
router.post('/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
templates.update(req.body.id, req.body, (err, updated) => {
if (err) {
req.flash('danger', err.message || err);
} else if (updated) {
req.flash('success', _('Template settings updated'));
} else {
req.flash('info', _('Template settings not updated'));
}
if (req.body.id) {
return res.redirect('/templates/edit/' + encodeURIComponent(req.body.id));
} else {
return res.redirect('/templates');
}
});
});
router.post('/duplicate', passport.parseForm, passport.csrfProtection, (req, res) => {
templates.duplicate(req.body.id, (err, duplicated) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (duplicated) {
req.flash('success', _('Template duplicated'));
} else {
req.flash('info', _('Could not duplicate specified template'));
}
return res.redirect('/templates/edit/' + duplicated);
});
});
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
templates.delete(req.body.id, (err, deleted) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (deleted) {
req.flash('success', _('Template deleted'));
} else {
req.flash('info', _('Could not delete specified template'));
}
return res.redirect('/templates');
});
});
router.post('/ajax', (req, res) => {
templates.filter(req.body, Number(req.query.parent) || false, (err, data, total, filteredTotal) => {
if (err) {
return res.json({
error: err.message || err,
data: []
});
}
res.json({
draw: req.body.draw,
recordsTotal: total,
recordsFiltered: filteredTotal,
data: data.map((row, i) => [
(Number(req.body.start) || 0) + 1 + i,
'<span class="glyphicon glyphicon-file" aria-hidden="true"></span> ' + htmlescape(row.name || ''),
htmlescape(striptags(row.description) || ''),
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/templates/edit/' + row.id + '">' + _('Edit') + '</a>' ]
)
});
});
});
module.exports = router;

View file

@ -1,308 +0,0 @@
'use strict';
let express = require('express');
let router = new express.Router();
let triggers = require('../lib/models/triggers');
let campaigns = require('../lib/models/campaigns');
let lists = require('../lib/models/lists');
let fields = require('../lib/models/fields');
let striptags = require('striptags');
let passport = require('../lib/passport');
let tools = require('../lib/tools');
let htmlescape = require('escape-html');
let _ = require('../lib/translate')._;
let util = require('util');
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', _('Need to be logged in to access restricted content'));
return res.redirect('/account/login?next=' + encodeURIComponent(req.originalUrl));
}
res.setSelectedMenu('triggers');
next();
});
router.get('/', (req, res) => {
triggers.list((err, rows) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
res.render('triggers/triggers', {
rows: rows.map((row, i) => {
row.index = i + 1;
row.description = striptags(row.description);
return row;
})
});
});
});
router.get('/create-select', passport.csrfProtection, (req, res, next) => {
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
data.csrfToken = req.csrfToken();
lists.quicklist((err, listItems) => {
if (err) {
return next(err);
}
data.listItems = listItems;
res.render('triggers/create-select', data);
});
});
router.post('/create-select', passport.parseForm, passport.csrfProtection, (req, res) => {
if (!req.body.list) {
req.flash('danger', _('Could not find selected list'));
return res.redirect('/triggers/create-select');
}
res.redirect('/triggers/' + encodeURIComponent(req.body.list) + '/create');
});
router.get('/:listId/create', passport.csrfProtection, (req, res, next) => {
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
data.csrfToken = req.csrfToken();
data.days = Math.max(Number(data.days) || 1, 1);
lists.get(req.params.listId, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find selected list'));
return res.redirect('/triggers/create-select');
}
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
}
data.columns = triggers.defaultColumns.concat(fieldList.filter(field => fields.genericTypes[field.type] === 'date')).map(field => ({
column: field.column,
name: field.name,
selected: data.column === field.column
}));
campaigns.list(0, 300, (err, campaignList) => {
if (err) {
return next(err);
}
data.sourceCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id).map(campaign => ({
id: campaign.id,
name: campaign.name,
selected: Number(data.sourceCampaign) === campaign.id
}));
data.destCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id && campaign.type === 4).map(campaign => ({
id: campaign.id,
name: campaign.name,
selected: Number(data.destCampaign) === campaign.id
}));
data.list = list;
data.isSubscription = data.rule === 'subscription' || !data.rule;
data.isCampaign = data.rule === 'campaign';
data.campaignOptions = triggers.defaultCampaignEvents.map(evt => ({
option: evt.option,
name: evt.name,
selected: Number(data.sourceCampaign) === evt.option
}));
data.isSend = true;
res.render('triggers/create', data);
});
});
});
});
router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) => {
triggers.create(req.body, (err, id) => {
if (err || !id) {
req.flash('danger', err && err.message || err || _('Could not create trigger'));
if (req.body.list) {
return res.redirect('/triggers/' + encodeURIComponent(req.body.list) + '/create?' + tools.queryParams(req.body));
} else {
return res.redirect('/triggers');
}
}
req.flash('success', util.format(_('Trigger “%s” created'), req.body.name));
res.redirect('/triggers');
});
});
router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
triggers.get(req.params.id, (err, trigger) => {
if (err || !trigger) {
req.flash('danger', err && err.message || err || _('Could not find campaign with specified ID'));
return res.redirect('/campaigns');
}
trigger.csrfToken = req.csrfToken();
trigger.days = Math.round(trigger.seconds / (24 * 3600));
lists.get(trigger.list, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find selected list'));
return res.redirect('/triggers');
}
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
}
campaigns.list(0, 300, (err, campaignList) => {
if (err) {
return next(err);
}
trigger.sourceCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id).map(campaign => ({
id: campaign.id,
name: campaign.name,
selected: Number(trigger.sourceCampaign) === campaign.id
}));
trigger.destCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id && campaign.type === 4).map(campaign => ({
id: campaign.id,
name: campaign.name,
selected: Number(trigger.destCampaign) === campaign.id
}));
trigger.list = list;
trigger.isSubscription = trigger.rule === 'subscription' || !trigger.rule;
trigger.isCampaign = trigger.rule === 'campaign';
trigger.columns = triggers.defaultColumns.concat(fieldList.filter(field => fields.genericTypes[field.type] === 'date')).map(field => ({
column: field.column,
name: field.name,
selected: trigger.isSubscription && trigger.column === field.column
}));
trigger.campaignOptions = triggers.defaultCampaignEvents.map(evt => ({
option: evt.option,
name: evt.name,
selected: trigger.isCampaign && trigger.column === evt.option
}));
if (trigger.rule !== 'subscription') {
trigger.column = null;
}
trigger.isSend = true;
res.render('triggers/edit', trigger);
});
});
});
});
});
router.post('/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
triggers.update(req.body.id, req.body, (err, updated) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/triggers/edit/' + encodeURIComponent(req.body.id));
} else if (updated) {
req.flash('success', _('Trigger settings updated'));
} else {
req.flash('info', _('Trigger settings not updated'));
}
return res.redirect('/triggers');
});
});
router.post('/delete', passport.parseForm, passport.csrfProtection, (req, res) => {
triggers.delete(req.body.id, (err, deleted) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (deleted) {
req.flash('success', _('Trigger deleted'));
} else {
req.flash('info', _('Could not delete specified trigger'));
}
return res.redirect('/triggers');
});
});
router.get('/status/:id', passport.csrfProtection, (req, res) => {
let id = Number(req.params.id) || 0;
triggers.get(id, (err, trigger) => {
if (err || !trigger) {
req.flash('danger', err && err.message || err || _('Could not find trigger with specified ID'));
return res.redirect('/triggers');
}
trigger.csrfToken = req.csrfToken();
res.render('triggers/triggered', trigger);
});
});
router.post('/status/ajax/:id', (req, res) => {
triggers.get(req.params.id, (err, trigger) => {
if (err || !trigger) {
return res.json({
error: err && err.message || err || _('Trigger not found'),
data: []
});
}
let columns = ['#', 'email', 'first_name', 'last_name', 'trigger__' + trigger.id + '.created'];
triggers.filterSubscribers(trigger, req.body, columns, (err, data, total, filteredTotal) => {
if (err) {
return res.json({
error: err.message || err,
data: []
});
}
campaigns.get(trigger.destCampaign, false, (err, campaign) => {
if (err) {
return res.json({
error: err && err.message || err,
data: []
});
}
lists.get(trigger.list, (err, list) => {
if (err) {
return res.json({
error: err && err.message || err,
data: []
});
}
let campaignCid = campaign && campaign.cid;
let listCid = list && list.cid;
res.json({
draw: req.body.draw,
recordsTotal: total,
recordsFiltered: filteredTotal,
data: data.map((row, i) => [
'<a href="/archive/' + encodeURIComponent(campaignCid) + '/' + encodeURIComponent(listCid) + '/' + encodeURIComponent(row.cid) + '?track=no">' + ((Number(req.body.start) || 0) + 1 + i) + '</a>',
htmlescape(row.email || ''),
htmlescape(row.firstName || ''),
htmlescape(row.lastName || ''),
'<span class="datestring" data-date="' + row.created.toISOString() + '" title="' + row.created.toISOString() + '">' + row.created.toISOString() + '</span>',
'<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><a href="/lists/subscription/' + trigger.list + '/edit/' + row.cid + '">' + _('Edit') + '</a>'
])
});
});
});
});
});
});
module.exports = router;

View file

@ -1,10 +0,0 @@
'use strict';
const _ = require('../lib/translate')._;
const clientHelpers = require('../lib/client-helpers');
const router = require('../lib/router-async').create();
clientHelpers.registerRootRoute(router, 'users', _('Users'));
module.exports = router;

View file

@ -1,334 +0,0 @@
'use strict';
const { nodeifyFunction } = require('../lib/nodeify');
const getSettings = nodeifyFunction(require('../models/settings').get);
let express = require('express');
let router = new express.Router();
let request = require('request');
let campaigns = require('../lib/models/campaigns');
let log = require('npmlog');
let multer = require('multer');
let uploads = multer();
router.post('/aws', (req, res, next) => {
if (typeof req.body === 'string') {
try {
req.body = JSON.parse(req.body);
} catch (E) {
return next(new Error('Could not parse input'));
}
}
switch (req.body.Type) {
case 'SubscriptionConfirmation':
if (req.body.SubscribeURL) {
request(req.body.SubscribeURL, () => false);
break;
} else {
let err = new Error('SubscribeURL not set');
err.status = 400;
return next(err);
}
case 'Notification':
if (req.body.Message) {
if (typeof req.body.Message === 'string') {
try {
req.body.Message = JSON.parse(req.body.Message);
} catch (E) {
return next('Could not parse Message object');
}
}
if (req.body.Message.mail && req.body.Message.mail.messageId) {
campaigns.findMailByResponse(req.body.Message.mail.messageId, (err, message) => {
if (err || !message) {
return;
}
switch (req.body.Message.notificationType) {
case 'Bounce':
campaigns.updateMessage(message, 'bounced', req.body.Message.bounce.bounceType === 'Permanent', (err, updated) => {
if (err) {
log.error('AWS', 'Failed updating message: %s', err);
} else if (updated) {
log.verbose('AWS', 'Marked message %s as bounced', req.body.Message.mail.messageId);
}
});
break;
case 'Complaint':
if (req.body.Message.complaint) {
campaigns.updateMessage(message, 'complained', true, (err, updated) => {
if (err) {
log.error('AWS', 'Failed updating message: %s', err);
} else if (updated) {
log.verbose('AWS', 'Marked message %s as complaint', req.body.Message.mail.messageId);
}
});
}
break;
}
});
}
}
break;
}
res.json({
success: true
});
});
router.post('/sparkpost', (req, res, next) => {
let events = [].concat(req.body || []);
let pos = 0;
let processEvents = () => {
if (pos >= events.length) {
return res.json({
success: true
});
}
let curEvent = events[pos++];
let msys = curEvent && curEvent.msys;
let evt;
if (msys && msys.message_event) {
evt = msys.message_event;
} else if (msys && msys.unsubscribe_event) {
evt = msys.unsubscribe_event;
}
if (!evt) {
return processEvents();
}
campaigns.findMailByCampaign(evt.campaign_id, (err, message) => {
if (err) {
return next(err);
}
if (!message) {
return processEvents();
}
switch (evt.type) {
case 'bounce':
// https://support.sparkpost.com/customer/portal/articles/1929896
return campaigns.updateMessage(message, 'bounced', [1, 10, 25, 30, 50].indexOf(Number(evt.bounce_class)) >= 0, (err, updated) => {
if (err) {
log.error('Sparkpost', 'Failed updating message: %s', err);
} else if (updated) {
log.verbose('Sparkpost', 'Marked message %s as bounced', evt.campaign_id);
}
return processEvents();
});
case 'spam_complaint':
return campaigns.updateMessage(message, 'complained', true, (err, updated) => {
if (err) {
log.error('Sparkpost', 'Failed updating message: %s', err);
} else if (updated) {
log.verbose('Sparkpost', 'Marked message %s as complaint', evt.campaign_id);
}
return processEvents();
});
case 'link_unsubscribe':
return campaigns.updateMessage(message, 'unsubscribed', true, (err, updated) => {
if (err) {
log.error('Sparkpost', 'Failed updating message: %s', err);
} else if (updated) {
log.verbose('Sparkpost', 'Marked message %s as unsubscribed', evt.campaign_id);
}
return processEvents();
});
default:
return processEvents();
}
});
};
processEvents();
});
router.post('/sendgrid', (req, res, next) => {
let events = [].concat(req.body || []);
let pos = 0;
let processEvents = () => {
if (pos >= events.length) {
return res.json({
success: true
});
}
let evt = events[pos++];
if (!evt) {
return processEvents();
}
campaigns.findMailByCampaign(evt.campaign_id, (err, message) => {
if (err) {
return next(err);
}
if (!message) {
return processEvents();
}
switch (evt.event) {
case 'bounce':
// https://support.sparkpost.com/customer/portal/articles/1929896
return campaigns.updateMessage(message, 'bounced', true, (err, updated) => {
if (err) {
log.error('Sendgrid', 'Failed updating message: %s', err);
} else if (updated) {
log.verbose('Sendgrid', 'Marked message %s as bounced', evt.campaign_id);
}
return processEvents();
});
case 'spamreport':
return campaigns.updateMessage(message, 'complained', true, (err, updated) => {
if (err) {
log.error('Sendgrid', 'Failed updating message: %s', err);
} else if (updated) {
log.verbose('Sendgrid', 'Marked message %s as complaint', evt.campaign_id);
}
return processEvents();
});
case 'group_unsubscribe':
case 'unsubscribe':
return campaigns.updateMessage(message, 'unsubscribed', true, (err, updated) => {
if (err) {
log.error('Sendgrid', 'Failed updating message: %s', err);
} else if (updated) {
log.verbose('Sendgrid', 'Marked message %s as unsubscribed', evt.campaign_id);
}
return processEvents();
});
default:
return processEvents();
}
});
};
processEvents();
});
router.post('/mailgun', uploads.any(), (req, res) => {
let evt = req.body;
campaigns.findMailByCampaign([].concat(evt && evt.campaign_id || []).shift(), (err, message) => {
if (err || !message) {
return;
}
switch (evt.event) {
case 'bounced':
return campaigns.updateMessage(message, 'bounced', true, (err, updated) => {
if (err) {
log.error('Mailgun', 'Failed updating message: %s', err);
} else if (updated) {
log.verbose('Mailgun', 'Marked message %s as bounced', evt.campaign_id);
}
});
case 'complained':
return campaigns.updateMessage(message, 'complained', true, (err, updated) => {
if (err) {
log.error('Mailgun', 'Failed updating message: %s', err);
} else if (updated) {
log.verbose('Mailgun', 'Marked message %s as complaint', evt.campaign_id);
}
});
case 'unsubscribed':
return campaigns.updateMessage(message, 'unsubscribed', true, (err, updated) => {
if (err) {
log.error('Mailgun', 'Failed updating message: %s', err);
} else if (updated) {
log.verbose('Mailgun', 'Marked message %s as unsubscribed', evt.campaign_id);
}
});
}
});
return res.json({
success: true
});
});
router.post('/zone-mta', (req, res, next) => {
if (typeof req.body === 'string') {
try {
req.body = JSON.parse(req.body);
} catch (E) {
return next(new Error('Could not parse input'));
}
}
if (req.body.id) {
campaigns.findMailByResponse(req.body.id, (err, message) => {
if (err || !message) {
return;
}
campaigns.updateMessage(message, 'bounced', true, (err, updated) => {
if (err) {
log.error('ZoneMTA', 'Failed updating message: %s', err);
} else if (updated) {
log.verbose('ZoneMTA', 'Marked message %s as bounced', req.body.id);
}
});
});
}
res.json({
success: true
});
});
router.post('/zone-mta/sender-config', (req, res) => {
if (!req.query.api_token) {
return res.json({
error: 'api_token value not set'
});
}
getSettings(['dkim_api_key', 'dkim_private_key', 'dkim_selector', 'dkim_domain'], (err, configItems) => {
if (err) {
return res.json({
error: err.message
});
}
if (configItems.dkimApiKey !== req.query.api_token) {
return res.json({
error: 'invalid api_token value'
});
}
configItems.dkimSelector = (configItems.dkimSelector || '').trim();
configItems.dkimPrivateKey = (configItems.dkimPrivateKey || '').trim();
if (!configItems.dkimSelector || !configItems.dkimPrivateKey) {
// empty response
return res.json({});
}
let from = (req.body.from || '').trim();
let domain = from.split('@').pop().toLowerCase().trim();
res.json({
dkim: {
keys: [{
domainName: configItems.dkimDomain || domain,
keySelector: configItems.dkimSelector,
privateKey: configItems.dkimPrivateKey
}]
}
});
});
});
module.exports = router;