v1.18.0
This commit is contained in:
parent
6c34091634
commit
95379f731f
17 changed files with 187 additions and 99 deletions
|
@ -1,5 +1,11 @@
|
|||
# Changelog
|
||||
|
||||
## 1.18.0 2016-09-08
|
||||
|
||||
* Updated installation script to bundle ZoneMTA as the default sending engine
|
||||
* Added new option to disable clicked and opened tracking
|
||||
* Store remote IP for subscription confirmations
|
||||
|
||||
## 1.17.0 2016-08-29
|
||||
|
||||
* Added new custom field for JSON data that is rendered using Handlebars when included in an email
|
||||
|
|
|
@ -13,7 +13,7 @@ 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'];
|
||||
let allowedKeys = ['description', 'from', 'address', 'subject', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'tracking_disabled'];
|
||||
|
||||
module.exports.list = (start, limit, callback) => {
|
||||
db.getConnection((err, connection) => {
|
||||
|
@ -417,6 +417,8 @@ module.exports.create = (campaign, opts, callback) => {
|
|||
campaign = tools.convertKeys(campaign);
|
||||
let name = (campaign.name || '').toString().trim();
|
||||
|
||||
campaign.trackingDisabled = campaign.trackingDisabled ? 1 : 0;
|
||||
|
||||
opts = opts || {};
|
||||
|
||||
if (/^\d+:\d+$/.test(campaign.list)) {
|
||||
|
@ -631,6 +633,8 @@ module.exports.update = (id, updates, callback) => {
|
|||
let campaign = tools.convertKeys(updates);
|
||||
let name = (campaign.name || '').toString().trim();
|
||||
|
||||
campaign.trackingDisabled = campaign.trackingDisabled ? 1 : 0;
|
||||
|
||||
if (!name) {
|
||||
return callback(new Error('Campaign Name must be set'));
|
||||
}
|
||||
|
|
|
@ -39,9 +39,11 @@ module.exports.countClick = (remoteIp, campaignCid, listCid, subscriptionCid, li
|
|||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if(!data){
|
||||
|
||||
if (!data || data.campaign.trackingDisabled) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
@ -154,6 +156,10 @@ module.exports.countOpen = (remoteIp, campaignCid, listCid, subscriptionCid, cal
|
|||
return callback(err);
|
||||
}
|
||||
|
||||
if (!data || data.campaign.trackingDisabled) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
db.getConnection((err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
@ -260,6 +266,10 @@ module.exports.add = (url, campaignId, callback) => {
|
|||
};
|
||||
|
||||
module.exports.updateLinks = (campaign, list, subscription, serviceUrl, message, callback) => {
|
||||
if (campaign.trackingDisabled) {
|
||||
// tracking is disabled, do not modify the message
|
||||
return setImmediate(() => callback(null, message));
|
||||
}
|
||||
let re = /(<a[^>]* href\s*=[\s"']*)(http[^"'>\s]+)/gi;
|
||||
let urls = new Set();
|
||||
(message || '').replace(re, (match, prefix, url) => {
|
||||
|
@ -272,7 +282,7 @@ module.exports.updateLinks = (campaign, list, subscription, serviceUrl, message,
|
|||
// insert tracking image
|
||||
let inserted = false;
|
||||
let imgUrl = urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid)));
|
||||
let img = '<img src="' + imgUrl + '" width="1" height="1" alt="Tracking Image">';
|
||||
let img = '<img src="' + imgUrl + '" width="1" height="1" alt="mt">';
|
||||
message = message.replace(/<\/body\b/i, match => {
|
||||
inserted = true;
|
||||
return img + match;
|
||||
|
|
|
@ -172,7 +172,7 @@ module.exports.filter = (listId, request, columns, segmentId, callback) => {
|
|||
|
||||
};
|
||||
|
||||
module.exports.addConfirmation = (list, email, data, callback) => {
|
||||
module.exports.addConfirmation = (list, email, optInIp, data, callback) => {
|
||||
let cid = shortid.generate();
|
||||
|
||||
tools.validateEmail(email, false, err => {
|
||||
|
@ -185,8 +185,8 @@ module.exports.addConfirmation = (list, email, data, callback) => {
|
|||
return callback(err);
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO confirmations (cid, list, email, data) VALUES (?,?,?,?)';
|
||||
connection.query(query, [cid, list.id, email, JSON.stringify(data || {})], (err, result) => {
|
||||
let query = 'INSERT INTO confirmations (cid, list, email, opt_in_ip, data) VALUES (?,?,?,?,?)';
|
||||
connection.query(query, [cid, list.id, email, optInIp, JSON.stringify(data || {})], (err, result) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"schemaVersion": 17
|
||||
"schemaVersion": 18
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "mailtrain",
|
||||
"private": true,
|
||||
"version": "1.17.0",
|
||||
"version": "1.18.0",
|
||||
"description": "Self hosted email newsletter app",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -122,7 +122,7 @@ router.post('/subscribe/:listId', (req, res) => {
|
|||
}
|
||||
|
||||
if (/^(yes|true|1)$/i.test(input.REQUIRE_CONFIRMATION)) {
|
||||
subscriptions.addConfirmation(list, input.EMAIL, subscription, (err, cid) => {
|
||||
subscriptions.addConfirmation(list, input.EMAIL, req.ip, subscription, (err, cid) => {
|
||||
if (err) {
|
||||
log.error('API', err);
|
||||
res.status(500);
|
||||
|
|
|
@ -233,7 +233,6 @@ router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
|
|||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
campaign.mergeTags = mergeTags;
|
||||
res.render(view, campaign);
|
||||
});
|
||||
|
|
|
@ -234,7 +234,7 @@ router.post('/:cid/subscribe', passport.parseForm, passport.csrfProtection, (req
|
|||
});
|
||||
data = tools.convertKeys(data);
|
||||
|
||||
subscriptions.addConfirmation(list, email, data, (err, confirmCid) => {
|
||||
subscriptions.addConfirmation(list, email, req.ip, data, (err, confirmCid) => {
|
||||
if (!err && !confirmCid) {
|
||||
err = new Error('Could not store confirmation data');
|
||||
}
|
||||
|
|
12
setup/sql/upgrade-00018.sql
Normal file
12
setup/sql/upgrade-00018.sql
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Header section
|
||||
# Define incrementing schema version number
|
||||
SET @schema_version = '18';
|
||||
|
||||
# Add template field for group elements
|
||||
ALTER TABLE `campaigns` ADD COLUMN `tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0' AFTER `status`;
|
||||
ALTER TABLE `confirmations` ADD COLUMN `opt_in_ip` varchar(100) DEFAULT NULL AFTER `email`;
|
||||
|
||||
# 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;
|
|
@ -81,6 +81,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-offset-2">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> Disable clicked/opened tracking
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
|
|
|
@ -100,6 +100,15 @@
|
|||
<input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="Keep it relevant and non-spammy" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-offset-2">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> Disable clicked/opened tracking
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
|
|
|
@ -100,6 +100,15 @@
|
|||
<input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="Keep it relevant and non-spammy" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-offset-2">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> Disable clicked/opened tracking
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
|
|
|
@ -153,6 +153,15 @@
|
|||
<input type="email" class="form-control" name="address" id="address" value="{{address}}" placeholder="This is the address people will send replies to" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-offset-2">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> Disable clicked/opened tracking
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
|
||||
<hr />
|
||||
|
|
|
@ -99,6 +99,15 @@
|
|||
<input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="Keep it relevant and non-spammy" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-offset-2">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> Disable clicked/opened tracking
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</fieldset>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane {{#if showTemplate}}active{{/if}}" id="template">
|
||||
|
|
|
@ -98,6 +98,14 @@
|
|||
<input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="Keep it relevant and non-spammy" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-offset-2">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" name="tracking-disabled" value="1" {{#if trackingDisabled}} checked {{/if}}> Disable clicked/opened tracking
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane {{#if showTemplate}}active{{/if}}" id="template">
|
||||
|
|
|
@ -145,24 +145,27 @@
|
|||
</div>
|
||||
</dd>
|
||||
|
||||
<dt>Opened <a href="/campaigns/opened/{{id}}" title="List subscribers who opened this message"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
|
||||
<dd>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{openRate}}" aria-valuemin="0" aria-valuemax="100" style="min-width: 6em; width: {{openRate}}%;">
|
||||
{{opened}} ({{openRate}}%)
|
||||
</div>
|
||||
</div>
|
||||
</dd>
|
||||
{{#unless trackingDisabled}}
|
||||
|
||||
<dt>Clicked <a href="/campaigns/clicked/{{id}}/all" title="List subscribers who clicked on a link"> <span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
|
||||
<dd>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{clicksRate}}" aria-valuemin="0" aria-valuemax="100" style="min-width: 6em; width: {{clicksRate}}%;">
|
||||
{{clicks}} ({{clicksRate}}%)
|
||||
<dt>Opened <a href="/campaigns/opened/{{id}}" title="List subscribers who opened this message"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
|
||||
<dd>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{openRate}}" aria-valuemin="0" aria-valuemax="100" style="min-width: 6em; width: {{openRate}}%;">
|
||||
{{opened}} ({{openRate}}%)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dd>
|
||||
</dd>
|
||||
|
||||
<dt>Clicked <a href="/campaigns/clicked/{{id}}/all" title="List subscribers who clicked on a link"> <span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a></dt>
|
||||
<dd>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{clicksRate}}" aria-valuemin="0" aria-valuemax="100" style="min-width: 6em; width: {{clicksRate}}%;">
|
||||
{{clicks}} ({{clicksRate}}%)
|
||||
</div>
|
||||
</div>
|
||||
</dd>
|
||||
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
</dl>
|
||||
|
@ -321,89 +324,91 @@
|
|||
|
||||
</div>
|
||||
{{#if links}}
|
||||
<div role="tabpanel" class="tab-pane {{#if showLinks}}active{{/if}}" id="links">
|
||||
{{#unless trackingDisabled}}
|
||||
<div role="tabpanel" class="tab-pane {{#if showLinks}}active{{/if}}" id="links">
|
||||
|
||||
<p></p>
|
||||
<p></p>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<th class="col-md-1">
|
||||
#
|
||||
</th>
|
||||
<th>
|
||||
URL
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
Clicks
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
% of clicks
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
% of messages
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#if links}}
|
||||
{{#each links}}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<th class="col-md-1">
|
||||
#
|
||||
</th>
|
||||
<th>
|
||||
URL
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
Clicks
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
% of clicks
|
||||
</th>
|
||||
<th class="col-md-1">
|
||||
% of messages
|
||||
</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#if links}}
|
||||
{{#each links}}
|
||||
<tr>
|
||||
<td>
|
||||
{{index}}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{url}}">{{short}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="pull-right">
|
||||
<a href="/campaigns/clicked/{{../id}}/{{id}}" title="List subscribers who clicked this link"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a>
|
||||
</div>
|
||||
{{clicks}}
|
||||
</td>
|
||||
<td>
|
||||
{{relPercentage}}
|
||||
</td>
|
||||
<td>
|
||||
{{totalPercentage}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
<tr>
|
||||
<td>
|
||||
{{index}}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{url}}">{{short}}</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="pull-right">
|
||||
<a href="/campaigns/clicked/{{../id}}/{{id}}" title="List subscribers who clicked this link"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a>
|
||||
</div>
|
||||
{{clicks}}
|
||||
</td>
|
||||
<td>
|
||||
{{relPercentage}}
|
||||
</td>
|
||||
<td>
|
||||
{{totalPercentage}}
|
||||
<td colspan="5">
|
||||
No data available in table
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{/if}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
No data available in table
|
||||
</td>
|
||||
<th></th>
|
||||
<th>
|
||||
Aggregated clicks
|
||||
</th>
|
||||
<th>
|
||||
<div class="pull-right">
|
||||
<a href="/campaigns/clicked/{{id}}/all" title="List subscribers who clicked on a link"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a>
|
||||
</div>
|
||||
{{clicks}}
|
||||
</th>
|
||||
<th>
|
||||
–
|
||||
</th>
|
||||
<th>
|
||||
CTR {{clicksRate}}%
|
||||
</th>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>
|
||||
Aggregated clicks
|
||||
</th>
|
||||
<th>
|
||||
<div class="pull-right">
|
||||
<a href="/campaigns/clicked/{{id}}/all" title="List subscribers who clicked on a link"><span class="glyphicon glyphicon-zoom-in" aria-hidden="true"></span></a>
|
||||
</div>
|
||||
{{clicks}}
|
||||
</th>
|
||||
<th>
|
||||
–
|
||||
</th>
|
||||
<th>
|
||||
CTR {{clicksRate}}%
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<p class="text-muted">
|
||||
Clicks are counted as unique subscribers that clicked on a specific link or on any link (in aggregated view)
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
Clicks are counted as unique subscribers that clicked on a specific link or on any link (in aggregated view)
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue