v1.11.0
This commit is contained in:
parent
3fa0e109af
commit
9bd6db2624
13 changed files with 201 additions and 51 deletions
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## 1.11.0 2016-05-31
|
||||
|
||||
* Retry transactional mail if failed with soft error (4xx)
|
||||
* New feature to preview campaigns using selected test users
|
||||
|
||||
## 1.10.1 2016-05-26
|
||||
|
||||
* Fix a bug with SMTP transport instance where campaign sending stalled until server was restarted
|
||||
|
|
|
@ -72,7 +72,24 @@ module.exports.sendMail = (mail, template, callback) => {
|
|||
});
|
||||
}
|
||||
|
||||
module.exports.transport.sendMail(mail, callback);
|
||||
let tryCount = 0;
|
||||
let trySend = () => {
|
||||
tryCount++;
|
||||
|
||||
module.exports.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
|
||||
log.verbose('Mail', 'Retrying after %s sec. ...', tryCount);
|
||||
return setTimeout(trySend, tryCount * 1000);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, info);
|
||||
});
|
||||
};
|
||||
setImmediate(trySend);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,6 +40,44 @@ module.exports.list = (listId, start, limit, callback) => {
|
|||
});
|
||||
};
|
||||
|
||||
module.exports.listTestUsers = (listId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
|
||||
if (listId < 1) {
|
||||
return callback(new Error('Missing List ID'));
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
connection.query('SELECT id, cid, email, first_name, last_name FROM `subscription__' + listId + '` WHERE is_test=1 LIMIT 100', (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!rows || !rows.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
let subscribers = rows.map(subscriber => {
|
||||
subscriber = tools.convertKeys(subscriber);
|
||||
let fullName = [].concat(subscriber.firstName || []).concat(subscriber.lastName || []).join(' ');
|
||||
if (fullName) {
|
||||
subscriber.displayName = fullName + ' <' + subscriber.email + '>';
|
||||
} else {
|
||||
subscriber.displayName = subscriber.email;
|
||||
}
|
||||
return subscriber;
|
||||
});
|
||||
return callback(null, subscribers);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
module.exports.filter = (listId, request, columns, segmentId, callback) => {
|
||||
listId = Number(listId) || 0;
|
||||
segmentId = Number(segmentId) || 0;
|
||||
|
@ -291,13 +329,16 @@ module.exports.insert = (listId, meta, subscription, callback) => {
|
|||
let keys = [];
|
||||
let values = [];
|
||||
|
||||
let allowedKeys = ['first_name', 'last_name', 'tz'];
|
||||
let allowedKeys = ['first_name', 'last_name', 'tz', 'is_test'];
|
||||
Object.keys(subscription).forEach(key => {
|
||||
let value = subscription[key];
|
||||
key = tools.toDbKey(key);
|
||||
if (key === 'tz') {
|
||||
value = (value || '').toString().toLowerCase().trim();
|
||||
}
|
||||
if (key === 'is_test') {
|
||||
value = value ? '1' : '0';
|
||||
}
|
||||
if (allowedKeys.indexOf(key) >= 0) {
|
||||
keys.push(key);
|
||||
values.push(value);
|
||||
|
@ -551,7 +592,7 @@ module.exports.update = (listId, cid, updates, allowEmail, callback) => {
|
|||
return callback(err);
|
||||
}
|
||||
|
||||
let allowedKeys = ['first_name', 'last_name', 'tz'];
|
||||
let allowedKeys = ['first_name', 'last_name', 'tz', 'is_test'];
|
||||
|
||||
if (allowEmail) {
|
||||
allowedKeys.unshift('email');
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"schemaVersion": 13
|
||||
"schemaVersion": 14
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "mailtrain",
|
||||
"private": true,
|
||||
"version": "1.10.1",
|
||||
"version": "1.11.0",
|
||||
"description": "Self hosted email newsletter app",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -36,8 +36,8 @@
|
|||
"config": "^1.20.4",
|
||||
"connect-flash": "^0.1.1",
|
||||
"connect-redis": "^3.0.2",
|
||||
"cookie-parser": "^1.4.2",
|
||||
"csurf": "^1.8.3",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"csurf": "^1.9.0",
|
||||
"csv-parse": "^1.1.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"express": "^4.13.4",
|
||||
|
@ -51,7 +51,7 @@
|
|||
"humanize": "0.0.9",
|
||||
"is-url": "^1.2.1",
|
||||
"isemail": "^2.1.2",
|
||||
"jsdom": "^9.2.0",
|
||||
"jsdom": "^9.2.1",
|
||||
"juice": "^2.0.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"morgan": "^1.7.0",
|
||||
|
|
|
@ -59,7 +59,7 @@ router.get('/:campaign/:list/:subscription', (req, res, next) => {
|
|||
return res.redirect('/');
|
||||
}
|
||||
|
||||
if (!mail) {
|
||||
if (!mail && !req.user) {
|
||||
err = new Error('Not Found');
|
||||
err.status = 404;
|
||||
return next(err);
|
||||
|
|
|
@ -6,6 +6,7 @@ let lists = require('../lib/models/lists');
|
|||
let fields = require('../lib/models/fields');
|
||||
let templates = require('../lib/models/templates');
|
||||
let campaigns = require('../lib/models/campaigns');
|
||||
let subscriptions = require('../lib/models/subscriptions');
|
||||
let settings = require('../lib/models/settings');
|
||||
let tools = require('../lib/tools');
|
||||
let striptags = require('striptags');
|
||||
|
@ -306,51 +307,70 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => {
|
|||
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.isInactive = campaign.status === 5;
|
||||
campaign.isActive = campaign.status === 6;
|
||||
|
||||
campaign.isNormal = campaign.type === 1 || campaign.type === 3;
|
||||
campaign.isRss = campaign.type === 2;
|
||||
|
||||
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
|
||||
subscriptions.listTestUsers(list.id, (err, testUsers) => {
|
||||
if (err || !testUsers) {
|
||||
testUsers = [];
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
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.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));
|
||||
});
|
||||
|
||||
router.get('/opened/:id', passport.csrfProtection, (req, res) => {
|
||||
campaigns.get(req.params.id, true, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
|
|
|
@ -410,6 +410,7 @@ router.post('/subscription/delete', passport.parseForm, passport.csrfProtection,
|
|||
});
|
||||
|
||||
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) {
|
||||
|
|
|
@ -185,7 +185,7 @@ CREATE TABLE `settings` (
|
|||
`value` text NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `key` (`key`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=31 DEFAULT CHARSET=utf8mb4;
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','localhost');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (2,'smtp_port','465');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','TLS');
|
||||
|
@ -202,7 +202,7 @@ INSERT INTO `settings` (`id`, `key`, `value`) VALUES (13,'default_from','My Awes
|
|||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (14,'default_address','admin@example.com');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (15,'default_subject','Test message');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (16,'default_homepage','http://localhost:3000/');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','13');
|
||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','14');
|
||||
CREATE TABLE `subscription` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||
|
@ -212,6 +212,7 @@ CREATE TABLE `subscription` (
|
|||
`tz` varchar(100) CHARACTER SET ascii DEFAULT NULL,
|
||||
`imported` int(11) unsigned DEFAULT NULL,
|
||||
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
|
||||
`is_test` tinyint(4) unsigned NOT NULL DEFAULT '0',
|
||||
`status_change` timestamp NULL DEFAULT NULL,
|
||||
`latest_open` timestamp NULL DEFAULT NULL,
|
||||
`latest_click` timestamp NULL DEFAULT NULL,
|
||||
|
@ -224,7 +225,8 @@ CREATE TABLE `subscription` (
|
|||
KEY `status` (`status`),
|
||||
KEY `first_name` (`first_name`(191)),
|
||||
KEY `last_name` (`last_name`(191)),
|
||||
KEY `subscriber_tz` (`tz`)
|
||||
KEY `subscriber_tz` (`tz`),
|
||||
KEY `is_test` (`is_test`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE `templates` (
|
||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||
|
|
17
setup/sql/upgrade-00014.sql
Normal file
17
setup/sql/upgrade-00014.sql
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Header section
|
||||
# Define incrementing schema version number
|
||||
SET @schema_version = '14';
|
||||
|
||||
-- {{#each tables.subscription}}
|
||||
|
||||
# Adds new column 'tz' to subscriptions table
|
||||
# Indicates subscriber time zone, use UTC as default
|
||||
ALTER TABLE `{{this}}` ADD COLUMN `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0' AFTER `status`;
|
||||
CREATE INDEX is_test ON `{{this}}` (`is_test`);
|
||||
|
||||
-- {{/each}}
|
||||
|
||||
# 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;
|
|
@ -87,6 +87,31 @@
|
|||
|
||||
{{#if isNormal}}
|
||||
|
||||
<dt>Preview campaign as</dt>
|
||||
<dd>
|
||||
<form method="post" action="/campaigns/preview/{{id}}" class="form-inline">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="campaign" value="{{cid}}">
|
||||
<input type="hidden" name="list" value="{{list.cid}}">
|
||||
<input type="hidden" name="listId" value="{{list.id}}">
|
||||
<div class="form-group">
|
||||
<select name="subscriber" class="form-control" required>
|
||||
{{#each testUsers}}
|
||||
<option value="{{cid}}">{{displayName}}</option>
|
||||
{{/each}}
|
||||
{{#if testUsers}}
|
||||
<optgroup label="Actions">
|
||||
<option value="_create">Add new test user ...</option>
|
||||
</optgroup>
|
||||
{{else}}
|
||||
<option value="_create">No test users yet, create one here ...</option>
|
||||
{{/if}}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-default">Go</button>
|
||||
</form>
|
||||
</dd>
|
||||
|
||||
{{#unless isIdling}}
|
||||
<dt>Delivered <a href="/campaigns/status/{{id}}/delivered" title="List subscribers who received this message"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
|
||||
<dd>{{delivered}}</dd>
|
||||
|
|
|
@ -131,6 +131,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="is-test" {{#if isTest}} checked {{/if}}> Test user?
|
||||
<span class="help-block">If checked then this subscription can be used for previewing campaign messages</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
|
|
|
@ -144,6 +144,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="is-test" {{#if isTest}} checked {{/if}}> Test user?
|
||||
<span class="help-block">If checked then this subscription can be used for previewing campaign messages</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
|
|
Loading…
Reference in a new issue