diff --git a/app.js b/app.js
index ba4e27bd..ab7880f2 100644
--- a/app.js
+++ b/app.js
@@ -36,6 +36,7 @@ let webhooks = require('./routes/webhooks');
let subscription = require('./routes/subscription');
let archive = require('./routes/archive');
let api = require('./routes/api');
+let blacklist = require('./routes/blacklist');
let editorapi = require('./routes/editorapi');
let grapejs = require('./routes/grapejs');
let mosaico = require('./routes/mosaico');
@@ -207,6 +208,7 @@ app.use('/lists', lists);
app.use('/templates', templates);
app.use('/campaigns', campaigns);
app.use('/settings', settings);
+app.use('/blacklist', blacklist);
app.use('/links', links);
app.use('/fields', fields);
app.use('/forms', forms);
diff --git a/lib/models/blacklist.js b/lib/models/blacklist.js
new file mode 100644
index 00000000..5aae8ccc
--- /dev/null
+++ b/lib/models/blacklist.js
@@ -0,0 +1,86 @@
+'use strict';
+
+let db = require('../db');
+
+module.exports.get = (start, limit, search, callback) => {
+ db.getConnection((err, connection) => {
+ if (err) {
+ return callback(err);
+ }
+ search = '%' + search + '%';
+ connection.query('SELECT SQL_CALC_FOUND_ROWS `email` FROM blacklist WHERE `email` LIKE ? ORDER BY `email` LIMIT ? OFFSET ?', [search, limit, start], (err, rows) => {
+ if (err) {
+ return callback(err);
+ }
+
+ connection.query('SELECT FOUND_ROWS() AS total', (err, total) => {
+ connection.release();
+ if (err) {
+ return callback(err);
+ }
+ let emails = [];
+ rows.forEach(email => {
+ emails.push(email.email);
+ });
+ return callback(null, emails, total && total[0] && total[0].total);
+ });
+ });
+ });
+};
+
+module.exports.add = (email, callback) => {
+ db.getConnection((err, connection) => {
+ if (err) {
+ return callback(err);
+ }
+
+ connection.query('INSERT IGNORE INTO `blacklist` (`email`) VALUES(?)', email, err => {
+ if (err) {
+ return callback(err);
+ }
+
+ connection.release();
+ return callback(null, null);
+
+ });
+ });
+};
+
+module.exports.delete = (email, callback) => {
+ db.getConnection((err, connection) => {
+ if (err) {
+ return callback(err);
+ }
+
+ connection.query('DELETE FROM `blacklist` WHERE `email`=?', email, err => {
+ if (err) {
+ return callback(err);
+ }
+
+ connection.release();
+ return callback(null, null);
+
+ });
+ });
+};
+
+module.exports.isblacklisted = (email, callback) => {
+ db.getConnection((err, connection) => {
+ if (err) {
+ return callback(err);
+ }
+
+ connection.query('SELECT `email` FROM blacklist WHERE `email`=?', email, (err, rows) => {
+ if (err) {
+ return callback(err);
+ }
+
+ connection.release();
+ if (rows.length > 0) {
+ return callback(null, true);
+ } else {
+ return callback(null, false);
+ }
+ });
+ });
+};
diff --git a/meta.json b/meta.json
index 58d89ef6..1302fec6 100644
--- a/meta.json
+++ b/meta.json
@@ -1,3 +1,3 @@
{
- "schemaVersion": 24
+ "schemaVersion": 25
}
diff --git a/routes/api.js b/routes/api.js
index e1d3943e..ce71a7c1 100644
--- a/routes/api.js
+++ b/routes/api.js
@@ -3,6 +3,7 @@
let users = require('../lib/models/users');
let lists = require('../lib/models/lists');
let fields = require('../lib/models/fields');
+let blacklist = require('../lib/models/blacklist');
let subscriptions = require('../lib/models/subscriptions');
let tools = require('../lib/tools');
let express = require('express');
@@ -326,4 +327,83 @@ router.post('/field/:listId', (req, res) => {
});
});
+router.post('/blacklist/add', (req, res) => {
+ let input = {};
+ Object.keys(req.body).forEach(key => {
+ input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
+ });
+ if (!(input.EMAIL) || (input.EMAIL === '')) {
+ res.status(500);
+ return res.json({
+ error: 'EMAIL argument are required',
+ data: []
+ });
+ }
+ blacklist.add(input.EMAIL, (err) =>{
+ if (err) {
+ res.status(500);
+ return res.json({
+ error: err.message || err,
+ data: []
+ });
+ }
+ res.status(200);
+ res.json({
+ data: []
+ });
+ });
+});
+
+router.post('/blacklist/delete', (req, res) => {
+ let input = {};
+ Object.keys(req.body).forEach(key => {
+ input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
+ });
+ if (!(input.EMAIL) || (input.EMAIL === '')) {
+ res.status(500);
+ return res.json({
+ error: 'EMAIL argument are required',
+ data: []
+ });
+ }
+ blacklist.delete(input.EMAIL, (err) =>{
+ if (err) {
+ res.status(500);
+ return res.json({
+ error: err.message || err,
+ data: []
+ });
+ }
+ res.status(200);
+ res.json({
+ data: []
+ });
+ });
+});
+
+router.get('/blacklist/get', (req, res) => {
+ let start = parseInt(req.query.start || 0, 10);
+ let limit = parseInt(req.query.limit || 10000, 10);
+ let search = req.query.search || '';
+
+ blacklist.get(start, limit, search, (err, data, total) => {
+ if (err) {
+ res.status(500);
+ return res.json({
+ error: err.message || err,
+ data: []
+ });
+ }
+ res.status(200);
+ res.json({
+ data: {
+ total: total,
+ start: start,
+ limit: limit,
+ emails: data
+ }
+ });
+ });
+});
+
module.exports = router;
diff --git a/routes/blacklist.js b/routes/blacklist.js
new file mode 100644
index 00000000..2f009267
--- /dev/null
+++ b/routes/blacklist.js
@@ -0,0 +1,68 @@
+'use strict';
+let express = require('express');
+let router = new express.Router();
+let passport = require('../lib/passport');
+let htmlescape = require('escape-html');
+let blacklist = require('../lib/models/blacklist');
+let tools = require('../lib/tools');
+let helpers = require('../lib/helpers');
+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('/users/login?next=' + encodeURIComponent(req.originalUrl));
+ }
+ res.setSelectedMenu('blacklist');
+ next();
+});
+
+router.get('/', passport.csrfProtection, (req, res) => {
+ res.render('blacklist', {csrfToken: req.csrfToken()});
+});
+
+router.post('/ajax/', (req, res) => {
+ let start = parseInt(req.body.start || 0, 10);
+ let limit = parseInt(req.body.length || 50, 10);
+ let search = req.body.search.value || '';
+ blacklist.get(start, limit, search, (err, data, total) => {
+ if (err) {
+ req.flash('danger', err.message || err);
+ return res.redirect('/');
+ }
+ res.json({
+ draw: req.body.draw,
+ recordsTotal: total,
+ recordsFiltered: total,
+ data: data.map((row, i) => [
+ (Number(req.body.start) || 0) + 1 + i,
+ htmlescape(row),
+ ''
+ ])
+ });
+ });
+});
+
+router.post('/ajax/add', passport.csrfProtection, (req, res) => {
+ let email = req.body.email;
+ blacklist.add(email, (err) => {
+ if (err) {
+ req.flash('danger', err.message || err);
+ return res.redirect(req.body.next);
+ }
+ return res.redirect(req.body.next)
+ });
+});
+
+router.post('/ajax/delete', passport.csrfProtection, (req, res) => {
+ let email = req.body.email;
+ blacklist.delete(email, (err) => {
+ if (err) {
+ req.flash('danger', err.message || err);
+ return res.redirect(req.body.next);
+ }
+ return res.redirect(req.body.next);
+ });
+});
+
+module.exports = router;
diff --git a/routes/campaigns.js b/routes/campaigns.js
index 81dcd2ba..08b3e230 100644
--- a/routes/campaigns.js
+++ b/routes/campaigns.js
@@ -425,6 +425,9 @@ router.get('/status/:id/:status', passport.csrfProtection, (req, res) => {
case 'complained':
status = 4;
break;
+ case 'blacklisted':
+ status = 5;
+ break;
default:
req.flash('danger', _('Unknown status selector'));
return res.redirect('/campaigns');
diff --git a/services/sender.js b/services/sender.js
index ab84485c..138850c9 100644
--- a/services/sender.js
+++ b/services/sender.js
@@ -8,6 +8,7 @@ let mailer = require('../lib/mailer');
let campaigns = require('../lib/models/campaigns');
let segments = require('../lib/models/segments');
let lists = require('../lib/models/lists');
+let blacklist = require('../lib/models/blacklist');
let fields = require('../lib/models/fields');
let settings = require('../lib/models/settings');
let links = require('../lib/models/links');
@@ -491,53 +492,86 @@ let sendLoop = () => {
return;
}
- let tryCount = 0;
- let trySend = () => {
- tryCount++;
+ blacklist.isblacklisted(mail.to.address, (err, blacklisted) => {
+ if (err) {
+ log.error('Mail', err);
+ setTimeout(getNext, mailing_timeout);
+ return;
+ }
+ if (!blacklisted) {
+ let tryCount = 0;
+ let trySend = () => {
+ tryCount++;
- // send the message
- mailer.transport.sendMail(mail, (err, info) => {
- if (err) {
- log.error('Mail', err.stack);
- if (err.responseCode && err.responseCode >= 400 && err.responseCode < 500 && tryCount <= 5) {
- // temporary error, try again
- return setTimeout(trySend, tryCount * 1000);
- }
- }
-
- let status = err ? 2 : 1;
- let response = err && (err.response || err.message) || info.response || info.messageId;
- let responseId = response.split(/\s+/).pop();
-
- db.getConnection((err, connection) => {
+ // send the message
+ mailer.transport.sendMail(mail, (err, info) => {
if (err) {
log.error('Mail', err.stack);
- return;
+ if (err.responseCode && err.responseCode >= 400 && err.responseCode < 500 && tryCount <= 5) {
+ // temporary error, try again
+ return setTimeout(trySend, tryCount * 1000);
+ }
}
- let query = 'UPDATE `campaigns` SET `delivered`=`delivered`+1 ' + (status === 2 ? ', `bounced`=`bounced`+1 ' : '') + ' WHERE id=? LIMIT 1';
+ let status = err ? 2 : 1;
+ let response = err && (err.response || err.message) || info.response || info.messageId;
+ let responseId = response.split(/\s+/).pop();
- connection.query(query, [message.campaignId], err => {
+ db.getConnection((err, connection) => {
if (err) {
log.error('Mail', err.stack);
+ return;
}
- let query = 'UPDATE `campaign__' + message.campaignId + '` SET status=?, response=?, response_id=?, updated=NOW() WHERE id=? LIMIT 1';
+ let query = 'UPDATE `campaigns` SET `delivered`=`delivered`+1 ' + (status === 2 ? ', `bounced`=`bounced`+1 ' : '') + ' WHERE id=? LIMIT 1';
- connection.query(query, [status, response, responseId, message.id], err => {
- connection.release();
+ connection.query(query, [message.campaignId], err => {
if (err) {
log.error('Mail', err.stack);
- } else {
- // log.verbose('Mail', 'Message sent and status updated for %s', message.subscription.cid);
}
+
+ let query = 'UPDATE `campaign__' + message.campaignId + '` SET status=?, response=?, response_id=?, updated=NOW() WHERE id=? LIMIT 1';
+
+ connection.query(query, [status, response, responseId, message.id], err => {
+ connection.release();
+ if (err) {
+ log.error('Mail', err.stack);
+ } else {
+ // log.verbose('Mail', 'Message sent and status updated for %s', message.subscription.cid);
+ }
+ });
});
});
});
- });
- };
- setImmediate(trySend);
- setImmediate(() => mailer.transport.checkThrottling(getNext));
+ };
+ setImmediate(trySend);
+ setImmediate(() => mailer.transport.checkThrottling(getNext));
+ } else {
+ db.getConnection((err, connection) => {
+ if (err) {
+ log.error('Mail', err);
+ return;
+ }
+
+ let query = 'UPDATE `campaigns` SET `blacklisted`=`blacklisted`+1 WHERE id=? LIMIT 1';
+
+ connection.query(query, [message.campaignId], err => {
+ if (err) {
+ log.error('Mail', err);
+ }
+
+ let query = 'UPDATE `campaign__' + message.campaignId + '` SET status=?, response=?, response_id=?, updated=NOW() WHERE id=? LIMIT 1';
+
+ connection.query(query, [5, 'blacklisted', 'blacklisted', message.id], err => {
+ connection.release();
+ if (err) {
+ log.error('Mail', err);
+ }
+ });
+ });
+ });
+ }
+ });
});
});
};
diff --git a/setup/sql/upgrade-00025.sql b/setup/sql/upgrade-00025.sql
new file mode 100644
index 00000000..9b3fba54
--- /dev/null
+++ b/setup/sql/upgrade-00025.sql
@@ -0,0 +1,17 @@
+# Header section
+# Define incrementing schema version number
+SET @schema_version = '25';
+
+# Create table to store global blacklist
+CREATE TABLE `blacklist` (
+ `email` varchar(255) NOT NULL,
+ PRIMARY KEY (`email`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+#Alter table campaigns
+ALTER TABLE `campaigns` ADD COLUMN `blacklisted` int(11) unsigned NOT NULL DEFAULT '0' AFTER `delivered`;
+
+# Footer section
+LOCK TABLES `settings` WRITE;
+INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
+UNLOCK TABLES;
diff --git a/views/blacklist.hbs b/views/blacklist.hbs
new file mode 100644
index 00000000..4a1fddca
--- /dev/null
+++ b/views/blacklist.hbs
@@ -0,0 +1,38 @@
+
+ - {{#translate}}Home{{/translate}}
+ - {{#translate}}Blacklist{{/translate}}
+
+
+{{#translate}}Blacklist{{/translate}}
+
+
+
+
+
+
+
+
+
+
+ #
+ |
+
+ {{#translate}}Email{{/translate}}
+ |
+
+
+ |
+
+
+
diff --git a/views/campaigns/blacklisted.hbs b/views/campaigns/blacklisted.hbs
new file mode 100644
index 00000000..f34d775a
--- /dev/null
+++ b/views/campaigns/blacklisted.hbs
@@ -0,0 +1,54 @@
+
+ - {{#translate}}Home{{/translate}}
+ - {{#translate}}Campaigns{{/translate}}
+ {{#if parent}}
+ - {{parent.name}}
+ {{/if}}
+ - {{name}}
+ - {{#translate}}Blacklisted info{{/translate}}
+
+
+
+
+
+
+{{#if description}}
+ {{{description}}}
+{{/if}}
+
+
+
+
+
+
{{#translate}}Subscribers who blacklisted by global blacklist:{{/translate}}
+
+
+
+
+
+
+ #
+ |
+
+ {{#translate}}Address{{/translate}}
+ |
+
+ {{#translate}}First Name{{/translate}}
+ |
+
+ {{#translate}}Last Name{{/translate}}
+ |
+
+ {{#translate}}Reason{{/translate}}
+ |
+
+ {{#translate}}Time{{/translate}}
+ |
+ |
+
+
+
+
+
+
+
diff --git a/views/campaigns/view.hbs b/views/campaigns/view.hbs
index 4528780b..a8e981b3 100644
--- a/views/campaigns/view.hbs
+++ b/views/campaigns/view.hbs
@@ -130,6 +130,13 @@
+ {{#translate}}Blacklisted{{/translate}}
+
+
+ {{blacklisted}}
+
+
+
{{#translate}}Bounced{{/translate}}