Initial import

This commit is contained in:
Andris Reinman 2016-04-04 15:36:30 +03:00
commit 54fa30701e
278 changed files with 37868 additions and 0 deletions

80
routes/archive.js Normal file
View file

@ -0,0 +1,80 @@
'use strict';
let settings = require('../lib/models/settings');
let campaigns = require('../lib/models/campaigns');
let lists = require('../lib/models/lists');
let subscriptions = require('../lib/models/subscriptions');
let tools = require('../lib/tools');
let express = require('express');
let router = new express.Router();
router.get('/:campaign/:list/:subscription', (req, res, next) => {
settings.get('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.get(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.getMail(campaign.id, list.id, subscription.id, (err, mail) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
if (!mail) {
err = new Error('Not Found');
err.status = 404;
return next(err);
}
res.render('archive/view', {
layout: 'archive/layout',
message: tools.formatMessage(serviceUrl, campaign, list, subscription, campaign.html),
campaign,
list,
subscription
});
});
});
});
});
});
});
module.exports = router;

288
routes/campaigns.js Normal file
View file

@ -0,0 +1,288 @@
'use strict';
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 settings = require('../lib/models/settings');
let tools = require('../lib/tools');
let striptags = require('striptags');
let passport = require('../lib/passport');
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', 'Need to be logged in to access restricted content');
return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl));
}
res.setSelectedMenu('campaigns');
next();
});
router.get('/', (req, res) => {
let limit = 999999999;
let start = 0;
campaigns.list(start, limit, (err, rows, total) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
res.render('campaigns/campaigns', {
rows: rows.map((row, i) => {
row.index = start + i + 1;
row.description = striptags(row.description);
switch (row.status) {
case 1:
row.statusText = 'Idling';
break;
case 2:
row.statusText = 'Sending';
break;
case 3:
row.statusText = 'Finished';
break;
case 4:
row.statusText = 'Paused';
break;
}
row.createdTimestamp = row.created.getTime();
row.created = row.created.toISOString();
return row;
}),
total
});
});
});
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());
}
settings.list(['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.subject = data.subject || configItems.defaultSubject;
res.render('campaigns/create', data);
});
});
});
});
router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.create(req.body, (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', 'Campaign “' + req.body.name + '” created');
res.redirect('/campaigns/edit/' + id + '?tab=template');
});
});
router.get('/edit/:id', passport.csrfProtection, (req, res) => {
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');
}
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.showGeneral = req.query.tab === 'general' || !req.query.tab;
campaign.showTemplate = req.query.tab === 'template';
res.render('campaigns/edit', 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.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');
}
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;
campaign.isIdling = campaign.status === 1;
campaign.isSending = campaign.status === 2;
campaign.isFinished = campaign.status === 3;
campaign.isPaused = campaign.status === 4;
campaign.openRate = campaign.delivered ? Math.round((campaign.opened / campaign.delivered) * 100) : 0;
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0;
res.render('campaigns/view', campaign);
});
});
});
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) => {
campaigns.send(req.body.id, (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('/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));
});
});
module.exports = router;

191
routes/fields.js Normal file
View file

@ -0,0 +1,191 @@
'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');
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', 'Need to be logged in to access restricted content');
return res.redirect('/users/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;

14
routes/index.js Normal file
View file

@ -0,0 +1,14 @@
'use strict';
let express = require('express');
let router = new express.Router();
/* GET home page. */
router.get('/', (req, res) => {
res.render('index', {
indexPage: true,
title: 'Self hosted email newsletter app'
});
});
module.exports = router;

47
routes/links.js Normal file
View file

@ -0,0 +1,47 @@
'use strict';
let links = require('../lib/models/links');
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.params.campaign, req.params.list, req.params.subscription, (err, opened) => {
if (err) {
log.error('Redirect', err.stack || 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) => {
links.resolve(req.params.campaign, req.params.link, (err, linkId, url) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
links.countClick(req.ip, req.params.campaign, req.params.list, req.params.subscription, linkId, (err, status) => {
if (err) {
log.error('Redirect', err.stack || err);
}
if (status) {
log.verbose('Redirect', 'First click for %s:%s:%s (%s)', req.params.campaign, req.params.list, req.params.subscription, url);
}
});
res.redirect(url);
});
});
module.exports = router;

592
routes/lists.js Normal file
View file

@ -0,0 +1,592 @@
'use strict';
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 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 uploads = multer({
dest: os.tmpdir()
});
let csvparse = require('csv-parse');
let fs = require('fs');
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', 'Need to be logged in to access restricted content');
return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl));
}
res.setSelectedMenu('lists');
next();
});
router.get('/', (req, res) => {
let limit = 999999999;
let start = 0;
lists.list(start, limit, (err, rows, total) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
res.render('lists/lists', {
rows: rows.map((row, i) => {
row.index = start + i + 1;
row.description = striptags(row.description);
return row;
}),
total
});
});
});
router.get('/create', passport.csrfProtection, (req, res) => {
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
data.csrfToken = req.csrfToken();
res.render('lists/create', data);
});
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);
});
});
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');
}
list.csrfToken = req.csrfToken();
res.render('lists/edit', list);
});
});
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.body.id) {
return res.redirect('/lists/edit/' + encodeURIComponent(req.body.id));
} else {
return res.redirect('/lists');
}
});
});
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/: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');
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 {
return htmlescape(cRow.value || '');
}
})).concat(statuses[row.status]).concat('<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span><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.processed = humanize.numberFormat(entry.processed, 0);
entry.importType = entry.type === 1 ? '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();
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;
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;
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, entryId) => {
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 (entryId) {
req.flash('success', req.body.email + ' was successfully added to your list');
} else {
req.flash('warning', req.body.email + ' was not added to your list');
}
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.unsubscribe(list.id, subscription.email, err => {
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', subscription.email + ' was successfully subscribed from your list');
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', email + ' was successfully removed from your list');
res.redirect('/lists/view/' + list.id);
});
});
});
router.post('/subscription/edit', passport.parseForm, passport.csrfProtection, (req, res) => {
subscriptions.update(req.body.list, req.body.cid, req.body, true, (err, updated) => {
if (err) {
if (err.code === 'ER_DUP_ENTRY') {
req.flash('danger', 'Another subscriber with email address ' + req.body.email + ' already exists');
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 || !list) {
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);
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 {
subscriptions.createImport(list.id, req.body.type === 'subscribed' ? 1 : 2, req.file.path, req.file.size, delimiter, {
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 (!data || !data.length) {
return callback(null, new Error('Empty file'));
}
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 || !list) {
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'];
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
}, (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');
});
});
});
module.exports = router;

435
routes/segments.js Normal file
View file

@ -0,0 +1,435 @@
'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');
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', 'Need to be logged in to access restricted content');
return res.redirect('/users/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, false, (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;

155
routes/settings.js Normal file
View file

@ -0,0 +1,155 @@
'use strict';
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 settings = require('../lib/models/settings');
let allowedKeys = ['service_url', 'smtp_hostname', 'smtp_port', 'smtp_encryption', 'smtp_user', 'smtp_pass', 'admin_email', 'smtp_log', 'smtp_max_connections', 'smtp_max_messages', 'default_from', 'default_address', 'default_subject', 'default_homepage', 'default_postaddress', 'default_sender'];
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', 'Need to be logged in to access restricted content');
return res.redirect('/users/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.csrfToken = req.csrfToken();
res.render('settings', configItems);
});
});
router.post('/update', passport.parseForm, passport.csrfProtection, (req, res) => {
let data = tools.convertKeys(req.body);
tools.validateEmail(data.adminEmail, false, err => {
if (err) {
req.flash('danger', err && err.message || err);
return res.redirect('/settings');
}
let keys = [];
let values = [];
Object.keys(data).forEach(key => {
let value = data[key].trim();
key = tools.toDbKey(key);
if (allowedKeys.indexOf(key) >= 0) {
keys.push(key);
values.push(value);
}
});
// checkbox is not included in value listing if left unchecked
if (keys.indexOf('smtp_log') < 0) {
keys.push('smtp_log');
values.push('');
}
let i = 0;
let storeSettings = () => {
if (i >= keys.length) {
mailer.update();
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', passport.parseForm, passport.csrfProtection, (req, res) => {
settings.list((err, configItems) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/settings');
}
let transport = nodemailer.createTransport({
host: configItems.smtpHostname,
port: Number(configItems.smtpPort) || false,
secure: configItems.smtpEncryption === 'TLS',
ignoreTLS: configItems.smtpEncryption === 'NONE',
auth: {
user: configItems.smtpUser,
pass: configItems.smtpPass
}
});
transport.verify(err => {
if (err) {
let message = '';
switch (err.code) {
case 'ECONNREFUSED':
message = 'Connection refused, check hostname and port.';
break;
case 'ETIMEDOUT':
if ((err.message || '').indexOf('Greeting never received') === 0) {
if (configItems.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) && configItems.smtpEncryption !== 'STARTTLS') {
message = 'Authentication not accepted, server expects STARTTLS to be used.';
} else {
message = 'Authentication failed, check username and password.';
}
break;
}
req.flash('warning', (message || 'Failed SMTP verification.') + (err.response ? ' Server responded with: "' + err.response + '"' : ''));
} else {
req.flash('info', 'SMTP settings verified, ready to send some mail!');
}
return res.redirect('/settings');
});
});
});
module.exports = router;

344
routes/subscription.js Normal file
View file

@ -0,0 +1,344 @@
'use strict';
let log = require('npmlog');
let tools = require('../lib/tools');
let mailer = require('../lib/mailer');
let passport = require('../lib/passport');
let express = require('express');
let urllib = require('url');
let router = new express.Router();
let lists = require('../lib/models/lists');
let fields = require('../lib/models/fields');
let subscriptions = require('../lib/models/subscriptions');
let settings = require('../lib/models/settings');
router.get('/subscribe/:cid', (req, res, next) => {
subscriptions.subscribe(req.params.cid, req.ip, (err, subscription) => {
if (!err && !subscription) {
err = new Error('Selected subscription not found');
err.status = 404;
}
if (err) {
return next(err);
}
lists.get(subscription.list, (err, list) => {
if (!err && !list) {
err = new Error('Selected list not found');
err.status = 404;
}
if (err) {
return next(err);
}
settings.list(['defaultHomepage', 'serviceUrl'], (err, configItems) => {
if (err) {
return next(err);
}
res.render('subscription/subscribed', {
title: list.name,
layout: 'subscription/layout',
homepage: configItems.defaultHomepage || configItems.serviceUrl
});
});
});
});
});
router.get('/:cid', passport.csrfProtection, (req, res, next) => {
lists.getByCid(req.params.cid, (err, list) => {
if (!err && !list) {
err = new Error('Selected list not found');
err.status = 404;
}
if (err) {
return next(err);
}
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
data.layout = 'subscription/layout';
data.title = list.name;
data.cid = list.cid;
data.csrfToken = req.csrfToken();
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
}
data.customFields = fields.getRow(fieldList, data);
data.useEditor = true;
res.render('subscription/subscribe', data);
});
});
});
router.get('/:cid/confirm-notice', (req, res, next) => {
lists.getByCid(req.params.cid, (err, list) => {
if (!err && !list) {
err = new Error('Selected list not found');
err.status = 404;
}
if (err) {
return next(err);
}
settings.list(['defaultHomepage', 'serviceUrl'], (err, configItems) => {
if (err) {
return next(err);
}
res.render('subscription/confirm-notice', {
title: list.name,
layout: 'subscription/layout',
homepage: configItems.defaultHomepage || configItems.serviceUrl
});
});
});
});
router.get('/:cid/updated-notice', (req, res, next) => {
lists.getByCid(req.params.cid, (err, list) => {
if (!err && !list) {
err = new Error('Selected list not found');
err.status = 404;
}
if (err) {
return next(err);
}
settings.list(['defaultHomepage', 'serviceUrl'], (err, configItems) => {
if (err) {
return next(err);
}
res.render('subscription/updated-notice', {
title: list.name,
layout: 'subscription/layout',
homepage: configItems.defaultHomepage || configItems.serviceUrl
});
});
});
});
router.get('/:cid/unsubscribe-notice', (req, res, next) => {
lists.getByCid(req.params.cid, (err, list) => {
if (!err && !list) {
err = new Error('Selected list not found');
err.status = 404;
}
if (err) {
return next(err);
}
settings.list(['defaultHomepage', 'serviceUrl'], (err, configItems) => {
if (err) {
return next(err);
}
res.render('subscription/unsubscribe-notice', {
title: list.name,
layout: 'subscription/layout',
homepage: configItems.defaultHomepage || configItems.serviceUrl
});
});
});
});
router.post('/:cid/subscribe', passport.parseForm, passport.csrfProtection, (req, res, next) => {
let email = (req.body.email || '').toString().trim();
if (!email) {
req.flash('danger', 'Email address not set');
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
}
lists.getByCid(req.params.cid, (err, list) => {
if (!err && !list) {
err = new Error('Selected list not found');
err.status = 404;
}
if (err) {
return next(err);
}
let data = {};
Object.keys(req.body).forEach(key => {
if (key !== 'email' && key.charAt(0) !== '_') {
data[key] = (req.body[key] || '').toString().trim();
}
});
data = tools.convertKeys(data);
subscriptions.addConfirmation(list.id, email, data, (err, confirmCid) => {
if (!err && !confirmCid) {
err = new Error('Could not store confirmation data');
}
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
}
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'serviceUrl'], (err, configItems) => {
if (err) {
return next(err);
}
res.redirect('/subscription/' + req.params.cid + '/confirm-notice');
mailer.sendMail({
from: {
name: configItems.defaultFrom,
address: configItems.defaultAddress
},
to: {
name: [].concat(data.firstName || []).concat(data.lastName || []).join(' '),
address: email
},
subject: list.name + ': Please Confirm Subscription'
}, {
template: 'emails/confirm-mail.hbs',
data: {
title: list.name,
contactAddress: configItems.defaultAddress,
confirmUrl: urllib.resolve(configItems.serviceUrl, '/subscription/subscribe/' + confirmCid)
}
}, err => {
if (err) {
log.error('Subscription', err.stack);
}
});
});
});
});
});
router.get('/:lcid/manage/:ucid', passport.csrfProtection, (req, res, next) => {
lists.getByCid(req.params.lcid, (err, list) => {
if (!err && !list) {
err = new Error('Selected list not found');
err.status = 404;
}
if (err) {
return next(err);
}
fields.list(list.id, (err, fieldList) => {
if (err) {
return next(err);
}
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
if (!err && !subscription) {
err = new Error('Subscription not found from this list');
err.status = 404;
}
if (err) {
return next(err);
}
subscription.lcid = req.params.lcid;
subscription.title = list.name;
subscription.csrfToken = req.csrfToken();
subscription.layout = 'subscription/layout';
subscription.customFields = fields.getRow(fieldList, subscription);
subscription.useEditor = true;
res.render('subscription/manage', subscription);
});
});
});
});
router.post('/:lcid/manage', passport.parseForm, passport.csrfProtection, (req, res, next) => {
lists.getByCid(req.params.lcid, (err, list) => {
if (!err && !list) {
err = new Error('Selected list not found');
err.status = 404;
}
if (err) {
return next(err);
}
subscriptions.update(list.id, req.body.cid, req.body, false, err => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/manage/' + encodeURIComponent(req.body.cid) + '?' + tools.queryParams(req.body));
}
res.redirect('/subscription/' + req.params.lcid + '/updated-notice');
});
});
});
router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next) => {
lists.getByCid(req.params.lcid, (err, list) => {
if (!err && !list) {
err = new Error('Selected list not found');
err.status = 404;
}
if (err) {
return next(err);
}
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
if (!err && !list) {
err = new Error('Subscription not found from this list');
err.status = 404;
}
if (err) {
return next(err);
}
subscription.lcid = req.params.lcid;
subscription.title = list.name;
subscription.csrfToken = req.csrfToken();
subscription.layout = 'subscription/layout';
subscription.autosubmit = !!req.query.auto;
subscription.campaign = req.query.c;
res.render('subscription/unsubscribe', subscription);
});
});
});
router.post('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, (req, res, next) => {
lists.getByCid(req.params.lcid, (err, list) => {
if (!err && !list) {
err = new Error('Selected list not found');
err.status = 404;
}
if (err) {
return next(err);
}
let email = req.body.email;
subscriptions.unsubscribe(list.id, email, req.body.campaign, err => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/unsubscribe/' + encodeURIComponent(req.body.cid) + '?' + tools.queryParams(req.body));
}
res.redirect('/subscription/' + req.params.lcid + '/unsubscribe-notice');
});
});
});
module.exports = router;

128
routes/templates.js Normal file
View file

@ -0,0 +1,128 @@
'use strict';
let express = require('express');
let router = new express.Router();
let templates = require('../lib/models/templates');
let settings = require('../lib/models/settings');
let tools = require('../lib/tools');
let striptags = require('striptags');
let passport = require('../lib/passport');
let mailer = require('../lib/mailer');
router.all('/*', (req, res, next) => {
if (!req.user) {
req.flash('danger', 'Need to be logged in to access restricted content');
return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl));
}
res.setSelectedMenu('templates');
next();
});
router.get('/', (req, res) => {
let limit = 999999999;
let start = 0;
templates.list(start, limit, (err, rows, total) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
res.render('templates/templates', {
rows: rows.map((row, i) => {
row.index = start + i + 1;
row.description = striptags(row.description);
return row;
}),
total
});
});
});
router.get('/create', passport.csrfProtection, (req, res, next) => {
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
data.csrfToken = req.csrfToken();
data.useEditor = true;
settings.list(['defaultPostaddress', 'defaultSender'], (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);
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');
});
});
router.get('/edit/:id', passport.csrfProtection, (req, res) => {
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');
}
template.csrfToken = req.csrfToken();
template.useEditor = true;
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('/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');
});
});
module.exports = router;

99
routes/users.js Normal file
View file

@ -0,0 +1,99 @@
'use strict';
let passport = require('../lib/passport');
let express = require('express');
let router = new express.Router();
let users = require('../lib/models/users');
router.get('/logout', (req, res) => passport.logout(req, res));
router.post('/login', passport.parseForm, (req, res, next) => passport.login(req, res, next));
router.get('/login', (req, res) => {
res.render('users/login', {
next: req.query.next
});
});
router.get('/forgot', passport.csrfProtection, (req, res) => {
res.render('users/forgot', {
csrfToken: req.csrfToken()
});
});
router.post('/forgot', passport.parseForm, passport.csrfProtection, (req, res) => {
users.sendReset(req.body.username, err => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/users/forgot');
} else {
req.flash('success', 'An email with password reset instructions has been sent to your email address, if it exists on our system.');
}
return res.redirect('/users/login');
});
});
router.get('/reset', passport.csrfProtection, (req, res) => {
users.checkResetToken(req.query.username, req.query.token, (err, status) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/users/login');
}
if (!status) {
req.flash('danger', 'Unknown or expired reset token');
return res.redirect('/users/login');
}
res.render('users/reset', {
csrfToken: req.csrfToken(),
username: req.query.username,
resetToken: req.query.token
});
});
});
router.post('/reset', passport.parseForm, passport.csrfProtection, (req, res) => {
users.resetPassword(req.body, (err, status) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/users/reset?username=' + encodeURIComponent(req.body.username) + '&token=' + encodeURIComponent(req.body['reset-token']));
} else if (!status) {
req.flash('danger', 'Unknown or expired reset token');
} else {
req.flash('success', 'Your password has been changed successfully');
}
return res.redirect('/users/login');
});
});
router.all('/account', (req, res, next) => {
if (!req.user) {
req.flash('danger', 'Need to be logged in to access restricted content');
return res.redirect('/users/login?next=' + encodeURIComponent(req.originalUrl));
}
next();
});
router.get('/account', passport.csrfProtection, (req, res) => {
let data = {
csrfToken: req.csrfToken(),
email: req.user.email
};
res.render('users/account', data);
});
router.post('/account', passport.parseForm, passport.csrfProtection, (req, res) => {
users.update(Number(req.user.id), req.body, (err, success) => {
if (err) {
req.flash('danger', err.message || err);
} else if (success) {
req.flash('success', 'Account information updated');
} else {
req.flash('info', 'Account information not updated');
}
return res.redirect('/users/account');
});
});
module.exports = router;

363
routes/webhooks.js Normal file
View file

@ -0,0 +1,363 @@
'use strict';
let express = require('express');
let router = new express.Router();
let request = require('request');
let campaigns = require('../lib/models/campaigns');
let subscriptions = require('../lib/models/subscriptions');
let db = require('../lib/db');
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.findMail(req.body.Message.mail.messageId, (err, message) => {
if (err || !message) {
return;
}
switch (req.body.Message.notificationType) {
case 'Bounce':
updateMessage(message, 'bounced', ['Undetermined', 'Permanent'].indexOf(req.body.Message.bounce.bounceType) >= 0, (err, updated) => {
if (err) {
log.error('AWS', 'Failed updating message: %s', err.stack);
} else if (updated) {
log.verbose('AWS', 'Marked message %s as bounced', req.body.Message.mail.messageId);
}
});
break;
case 'Complaint':
if (req.body.Message.complaint) {
updateMessage(message, 'complained', true, (err, updated) => {
if (err) {
log.error('AWS', 'Failed updating message: %s', err.stack);
} 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();
}
getMessage(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 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.stack);
} else if (updated) {
log.verbose('Sparkpost', 'Marked message %s as bounced', evt.campaign_id);
}
return processEvents();
});
case 'spam_complaint':
return updateMessage(message, 'complained', true, (err, updated) => {
if (err) {
log.error('Sparkpost', 'Failed updating message: %s', err.stack);
} else if (updated) {
log.verbose('Sparkpost', 'Marked message %s as complaint', evt.campaign_id);
}
return processEvents();
});
case 'link_unsubscribe':
return updateMessage(message, 'unsubscribed', true, (err, updated) => {
if (err) {
log.error('Sparkpost', 'Failed updating message: %s', err.stack);
} 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) => {
console.log(require('util').inspect(req.body, false, 22)); // eslint-disable-line
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();
}
getMessage(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 updateMessage(message, 'bounced', true, (err, updated) => {
if (err) {
log.error('Sendgrid', 'Failed updating message: %s', err.stack);
} else if (updated) {
log.verbose('Sendgrid', 'Marked message %s as bounced', evt.campaign_id);
}
return processEvents();
});
case 'spamreport':
return updateMessage(message, 'complained', true, (err, updated) => {
if (err) {
log.error('Sendgrid', 'Failed updating message: %s', err.stack);
} else if (updated) {
log.verbose('Sendgrid', 'Marked message %s as complaint', evt.campaign_id);
}
return processEvents();
});
case 'group_unsubscribe':
case 'unsubscribe':
return updateMessage(message, 'unsubscribed', true, (err, updated) => {
if (err) {
log.error('Sendgrid', 'Failed updating message: %s', err.stack);
} 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;
getMessage([].concat(evt && evt.campaign_id || []).shift(), (err, message) => {
if (err || !message) {
return;
}
switch (evt.event) {
case 'bounced':
return updateMessage(message, 'bounced', true, (err, updated) => {
if (err) {
log.error('Mailgun', 'Failed updating message: %s', err.stack);
} else if (updated) {
log.verbose('Mailgun', 'Marked message %s as bounced', evt.campaign_id);
}
});
case 'complained':
return updateMessage(message, 'complained', true, (err, updated) => {
if (err) {
log.error('Mailgun', 'Failed updating message: %s', err.stack);
} else if (updated) {
log.verbose('Mailgun', 'Marked message %s as complaint', evt.campaign_id);
}
});
case 'unsubscribed':
return updateMessage(message, 'unsubscribed', true, (err, updated) => {
if (err) {
log.error('Mailgun', 'Failed updating message: %s', err.stack);
} else if (updated) {
log.verbose('Mailgun', 'Marked message %s as unsubscribed', evt.campaign_id);
}
});
}
});
return res.json({
success: true
});
});
module.exports = router;
function getMessage(messageHeader, callback) {
if (!messageHeader) {
return callback(null, false);
}
let parts = messageHeader.split('.');
let cCid = parts.shift();
let sCid = parts.pop();
db.getConnection((err, connection) => {
if (err) {
return callback(err);
}
let query = 'SELECT `id`, `list`, `segment` FROM `campaigns` WHERE `cid`=? LIMIT 1';
connection.query(query, [cCid], (err, rows) => {
if (err) {
connection.release();
return callback(err);
}
if (!rows || !rows.length) {
connection.release();
return callback(null, false);
}
let campaignId = rows[0].id;
let listId = rows[0].list;
let segmentId = rows[0].segment;
let query = 'SELECT id FROM `subscription__' + listId + '` WHERE cid=? LIMIT 1';
connection.query(query, [sCid], (err, rows) => {
if (err) {
connection.release();
return callback(err);
}
if (!rows || !rows.length) {
connection.release();
return callback(null, false);
}
let subscriptionId = rows[0].id;
let query = 'SELECT `id`, `list`, `segment`, `subscription` FROM `campaign__' + campaignId + '` WHERE `list`=? AND `segment`=? AND `subscription`=? LIMIT 1';
connection.query(query, [listId, segmentId, subscriptionId], (err, rows) => {
connection.release();
if (err) {
return callback(err);
}
if (!rows || !rows.length) {
return callback(null, false);
}
let message = rows[0];
message.campaign = campaignId;
return callback(null, message);
});
});
});
});
}
function updateMessage(message, status, updateSubscription, callback) {
db.getConnection((err, connection) => {
if (err) {
return callback(err);
}
let statusCode;
if (status === 'unsubscribed') {
statusCode = 2;
}
if (status === 'bounced') {
statusCode = 3;
}
if (status === 'complained') {
statusCode = 4;
}
let query = 'UPDATE `campaigns` SET `' + status + '`=`' + status + '`+1 WHERE id=? LIMIT 1';
connection.query(query, [message.campaign], () => {
let query = 'UPDATE `campaign__' + message.campaign + '` SET status=?, updated=NOW() WHERE id=? LIMIT 1';
connection.query(query, [statusCode, message.id], err => {
connection.release();
if (err) {
return callback(err);
}
if (updateSubscription) {
subscriptions.changeStatus(message.subscription, message.list, statusCode === 2 ? message.campaign : false, statusCode, callback);
} else {
return callback(null, true);
}
});
});
});
}