This commit is contained in:
Andris Reinman 2016-05-25 23:58:17 +03:00
parent f29a8a1b67
commit 773977dd96
10 changed files with 194 additions and 132 deletions

View file

@ -1,5 +1,11 @@
# Changelog
## 1.10.0 2016-05-25
* Fetch multiple unsent messages at once to speed up delivery
* Fixed a bug of counting unsubscribers correctly
* Use LONGTEXT for template text fields (messages might include inlined images which are larger than 64kB)
## 1.9.0 2016-05-16
* New look

28
lib/caches.js Normal file
View file

@ -0,0 +1,28 @@
'use strict';
let cache = module.exports.cache = new Map();
module.exports.push = (name, value) => {
if (!cache.has(name)) {
cache.set(name, []);
} else if (!Array.isArray(cache.get(name))) {
cache.set(name, [].concat(cache.get(name) || []));
}
cache.get(name).push(value);
};
module.exports.shift = name => {
if (!cache.has(name)) {
return false;
}
if (!Array.isArray(cache.get(name))) {
let value = cache.get(name);
cache.delete(name);
return value;
}
let value = cache.get(name).shift();
if (!cache.get(name).length) {
cache.delete(name);
}
return value;
};

View file

@ -11,6 +11,7 @@ let isUrl = require('is-url');
let feed = require('../feed');
let log = require('npmlog');
let mailer = require('../mailer');
let caches = require('../caches');
let allowedKeys = ['description', 'from', 'address', 'subject', 'template', 'source_url', 'list', 'segment', 'html', 'text'];
@ -809,6 +810,7 @@ module.exports.pause = (id, callback) => {
connection.release();
return callback(err);
}
caches.cache.delete('sender queue');
return callback(null, true);
});
});
@ -821,7 +823,7 @@ module.exports.reset = (id, callback) => {
return callback(err);
}
if (campaign.status !== 3 && !(campaign.status === 2 && campaign.scheduled > new Date())) {
if (campaign.status !== 3 && campaign.status !== 4 && !(campaign.status === 2 && campaign.scheduled > new Date())) {
return callback(null, false);
}
@ -835,6 +837,8 @@ module.exports.reset = (id, callback) => {
connection.release();
return callback(err);
}
caches.cache.delete('sender queue');
connection.query('DELETE FROM links WHERE campaign=?', [id], err => {
if (err) {
connection.release();

View file

@ -1,3 +1,3 @@
{
"schemaVersion": 12
"schemaVersion": 13
}

View file

@ -1,7 +1,7 @@
{
"name": "mailtrain",
"private": true,
"version": "1.9.0",
"version": "1.10.0",
"description": "Self hosted email newsletter app",
"main": "index.js",
"scripts": {

View file

@ -15,9 +15,7 @@ let shortid = require('shortid');
let url = require('url');
let htmlToText = require('html-to-text');
let request = require('request');
// to speed things up fetch several unsent messages and store these into a cache
let fetchCache = [];
let caches = require('../lib/caches');
function findUnsent(callback) {
@ -49,8 +47,8 @@ function findUnsent(callback) {
});
};
if (fetchCache.length) {
let cached = fetchCache.shift();
if (caches.cache.has('sender queue')) {
let cached = caches.shift('sender queue');
return returnUnsent(cached.row, cached.campaign);
}
@ -125,7 +123,7 @@ function findUnsent(callback) {
connection.release();
rows.forEach(row => {
fetchCache.push({
caches.push('sender queue', {
row,
campaign
});

View file

@ -14,7 +14,9 @@ CREATE TABLE `campaign` (
PRIMARY KEY (`id`),
UNIQUE KEY `list` (`list`,`segment`,`subscription`),
KEY `created` (`created`),
KEY `response_id` (`response_id`)
KEY `response_id` (`response_id`),
KEY `status_index` (`status`),
KEY `subscription_index` (`subscription`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `campaign_tracker` (
`list` int(11) unsigned NOT NULL,
@ -43,9 +45,9 @@ CREATE TABLE `campaigns` (
`from` varchar(255) DEFAULT '',
`address` varchar(255) DEFAULT '',
`subject` varchar(255) DEFAULT '',
`html` text,
`html_prepared` text,
`text` text,
`html` longtext,
`html_prepared` longtext,
`text` longtext,
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
`scheduled` timestamp NULL DEFAULT NULL,
`status_change` timestamp NULL DEFAULT NULL,
@ -174,7 +176,7 @@ CREATE TABLE `segments` (
`created` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `list` (`list`),
KEY `name` (`name`(191)),
KEY `name` (`name`),
CONSTRAINT `segments_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `settings` (
@ -183,7 +185,7 @@ CREATE TABLE `settings` (
`value` text NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `key` (`key`)
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4;
) ENGINE=InnoDB AUTO_INCREMENT=30 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');
@ -200,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','10');
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','13');
CREATE TABLE `subscription` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
@ -228,8 +230,8 @@ CREATE TABLE `templates` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL DEFAULT '',
`description` text,
`html` text,
`text` text,
`html` longtext,
`text` longtext,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `name` (`name`(191))

View file

@ -0,0 +1,15 @@
# Header section
# Define incrementing schema version number
SET @schema_version = '13';
-- {{#each tables.campaign}}
# Adds separate index for 'subscription' on campaign messages table
CREATE INDEX subscription_index ON `{{this}}` (`subscription`);
-- {{/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;

View file

@ -210,13 +210,21 @@
{{#if isPaused}}
<div class="pull-right">
<form class="form-horizontal confirm-submit" data-confirm-message="Are you sure? This action would resume sending messages to the selected list" method="post" action="/campaigns/resume">
<form id="resume-sending" class="form-horizontal confirm-submit" data-confirm-message="Are you sure? This action would resume sending messages to the selected list" method="post" action="/campaigns/resume">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" name="id" value="{{id}}" />
<button type="submit" class="btn btn-info"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Resume
</button>
</form>
<form id="reset-sending" class="confirm-submit" data-confirm-message="Are you sure? This action would reset all stats about current progress" method="post" action="/campaigns/reset">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" name="id" value="{{id}}" />
</form>
<button type="submit" form="resume-sending" class="btn btn-info"><span class="glyphicon glyphicon-play" aria-hidden="true"></span> Resume
</button>
<button type="submit" form="reset-sending" class="btn btn-danger"><span class="glyphicon glyphicon-refresh" aria-hidden="true"></span> Reset
</button>
</div>
<h4>Sending paused</h4>
{{/if}}

View file

@ -61,140 +61,141 @@
<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="{{#if showSubscriptions}}active{{/if}}"><a href="#subscriptions" aria-controls="subscriptions" role="tab" data-toggle="tab">Subscriptions</a></li>
<li role="presentation" class="{{#if showImports}}active{{/if}}"><a href="#imports" aria-controls="imports" role="tab" data-toggle="tab">Imports</a></li>
<li role="presentation" class="{{#if showSubscriptions}}active{{/if}}"><a href="/lists/view/{{id}}" aria-controls="subscriptions" role="tab">Subscriptions</a></li>
<li role="presentation" class="{{#if showImports}}active{{/if}}"><a href="/lists/view/{{id}}?tab=imports" aria-controls="imports" role="tab">Imports</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane {{#if showSubscriptions}}active{{/if}}" id="subscriptions">
{{#if showSubscriptions}}
<p></p>
<p></p>
<div class="table-responsive">
<table data-topic-url="/lists" data-topic-id="{{id}}" data-sort-column="1" data-sort-order="asc" {{#if useSegment}} data-topic-args="segment={{useSegment}}" {{/if}} class="table table-bordered table-hover data-table-ajax display nowrap" width="100%" data-row-sort="0,1,1,1{{customSort}},1,0">
<thead>
<tr>
<th class="col-md-1">
#
</th>
<th>
Address
</th>
<th>
First Name
</th>
<th>
Last Name
</th>
{{#each customFields}}
<th>
{{name}}
</th>
{{/each}}
<th>
Status
</th>
<th></th>
</tr>
</thead>
</table>
</div>
<div class="table-responsive">
<table data-topic-url="/lists" data-topic-id="{{id}}" data-sort-column="1" data-sort-order="asc" {{#if useSegment}} data-topic-args="segment={{useSegment}}" {{/if}} class="table table-bordered table-hover data-table-ajax display nowrap" width="100%" data-row-sort="0,1,1,1{{customSort}},1,0">
<thead>
<tr>
{{/if}}
</div>
<div role="tabpanel" class="tab-pane {{#if showImports}}active{{/if}}" id="imports">
{{#if showImports}}
<p></p>
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<th class="col-md-1">
#
</th>
<th>
Address
Created
</th>
<th>
First Name
Finished
</th>
<th>
Last Name
Type
</th>
<th>
Added
</th>
<th>
Updated
</th>
<th>
Failed
</th>
{{#each customFields}}
<th>
{{name}}
</th>
{{/each}}
<th>
Status
</th>
<th></th>
</tr>
</thead>
</table>
</div>
<th>
</div>
<div role="tabpanel" class="tab-pane {{#if showImports}}active{{/if}}" id="imports">
</th>
</thead>
<tbody>
{{#if imports}}
{{#each imports}}
<tr>
<th scope="row">
{{index}}
</th>
<p></p>
<td>
<span class="datestring" data-date="{{created}}" title="{{created}}">{{created}}</span>
</td>
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<th class="col-md-1">
#
</th>
<th>
Created
</th>
<th>
Finished
</th>
<th>
Type
</th>
<th>
Added
</th>
<th>
Updated
</th>
<th>
Failed
</th>
<th>
Status
</th>
<th>
<td>
{{#if finished}}
<span class="datestring" data-date="{{finished}}" title="{{finished}}">{{finished}}</span>
{{else}}
No
{{/if}}
</td>
</th>
</thead>
<tbody>
{{#if imports}}
{{#each imports}}
<td>
{{importType}}
</td>
<td>
{{new}}
</td>
<td>
{{updated}}
</td>
<td>
{{#if failed}}<a href="/lists/subscription/{{../id}}/import/{{id}}/failed">{{failed}}</a>{{else}}0 {{/if}}
</td>
<td class="{{#if error}}text-danger{{/if}}">
{{#if error}}
<strong><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> {{importStatus}}</strong>
{{else}}
<strong>{{importStatus}}</strong>
{{/if}}
</td>
<td class="text-center">
<form method="post" class="confirm-submit" data-confirm-message="Are you sure? This action should only be called to resolve stalled imports" action="/lists/subscription/import-restart">
<input type="hidden" name="_csrf" value="{{../csrfToken}}">
<input type="hidden" name="list" value="{{list}}">
<input type="hidden" name="import" value="{{id}}">
<button type="submit" class="btn btn-info btn-xs"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> Restart</button>
</form>
</td>
</tr>
{{/each}}
{{else}}
<tr>
<th scope="row">
{{index}}
</th>
<td>
<span class="datestring" data-date="{{created}}" title="{{created}}">{{created}}</span>
</td>
<td>
{{#if finished}}
<span class="datestring" data-date="{{finished}}" title="{{finished}}">{{finished}}</span>
{{else}}
No
{{/if}}
</td>
<td>
{{importType}}
</td>
<td>
{{new}}
</td>
<td>
{{updated}}
</td>
<td>
{{#if failed}}<a href="/lists/subscription/{{../id}}/import/{{id}}/failed">{{failed}}</a>{{else}}0 {{/if}}
</td>
<td class="{{#if error}}text-danger{{/if}}">
{{#if error}}
<strong><span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> {{importStatus}}</strong>
{{else}}
<strong>{{importStatus}}</strong>
{{/if}}
</td>
<td class="text-center">
<form method="post" class="confirm-submit" data-confirm-message="Are you sure? This action should only be called to resolve stalled imports" action="/lists/subscription/import-restart">
<input type="hidden" name="_csrf" value="{{../csrfToken}}">
<input type="hidden" name="list" value="{{list}}">
<input type="hidden" name="import" value="{{id}}">
<button type="submit" class="btn btn-info btn-xs"><span class="glyphicon glyphicon-repeat" aria-hidden="true"></span> Restart</button>
</form>
<td colspan="9">
No data available in table
</td>
</tr>
{{/each}}
{{else}}
<tr>
<td colspan="9">
No data available in table
</td>
</tr>
{{/if}}
</tbody>
</table>
</div>
{{/if}}
</tbody>
</table>
</div>
{{/if}}
</div>
</div>