This commit is contained in:
Andris Reinman 2016-09-08 14:39:41 +03:00
parent 6c34091634
commit 95379f731f
17 changed files with 187 additions and 99 deletions

View file

@ -1,5 +1,11 @@
# Changelog # 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 ## 1.17.0 2016-08-29
* Added new custom field for JSON data that is rendered using Handlebars when included in an email * Added new custom field for JSON data that is rendered using Handlebars when included in an email

View file

@ -13,7 +13,7 @@ let log = require('npmlog');
let mailer = require('../mailer'); let mailer = require('../mailer');
let caches = require('../caches'); 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) => { module.exports.list = (start, limit, callback) => {
db.getConnection((err, connection) => { db.getConnection((err, connection) => {
@ -417,6 +417,8 @@ module.exports.create = (campaign, opts, callback) => {
campaign = tools.convertKeys(campaign); campaign = tools.convertKeys(campaign);
let name = (campaign.name || '').toString().trim(); let name = (campaign.name || '').toString().trim();
campaign.trackingDisabled = campaign.trackingDisabled ? 1 : 0;
opts = opts || {}; opts = opts || {};
if (/^\d+:\d+$/.test(campaign.list)) { if (/^\d+:\d+$/.test(campaign.list)) {
@ -631,6 +633,8 @@ module.exports.update = (id, updates, callback) => {
let campaign = tools.convertKeys(updates); let campaign = tools.convertKeys(updates);
let name = (campaign.name || '').toString().trim(); let name = (campaign.name || '').toString().trim();
campaign.trackingDisabled = campaign.trackingDisabled ? 1 : 0;
if (!name) { if (!name) {
return callback(new Error('Campaign Name must be set')); return callback(new Error('Campaign Name must be set'));
} }

View file

@ -39,9 +39,11 @@ module.exports.countClick = (remoteIp, campaignCid, listCid, subscriptionCid, li
if (err) { if (err) {
return callback(err); return callback(err);
} }
if(!data){
if (!data || data.campaign.trackingDisabled) {
return callback(null, false); return callback(null, false);
} }
db.getConnection((err, connection) => { db.getConnection((err, connection) => {
if (err) { if (err) {
return callback(err); return callback(err);
@ -154,6 +156,10 @@ module.exports.countOpen = (remoteIp, campaignCid, listCid, subscriptionCid, cal
return callback(err); return callback(err);
} }
if (!data || data.campaign.trackingDisabled) {
return callback(null, false);
}
db.getConnection((err, connection) => { db.getConnection((err, connection) => {
if (err) { if (err) {
return callback(err); return callback(err);
@ -260,6 +266,10 @@ module.exports.add = (url, campaignId, callback) => {
}; };
module.exports.updateLinks = (campaign, list, subscription, serviceUrl, message, 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 re = /(<a[^>]* href\s*=[\s"']*)(http[^"'>\s]+)/gi;
let urls = new Set(); let urls = new Set();
(message || '').replace(re, (match, prefix, url) => { (message || '').replace(re, (match, prefix, url) => {
@ -272,7 +282,7 @@ module.exports.updateLinks = (campaign, list, subscription, serviceUrl, message,
// insert tracking image // insert tracking image
let inserted = false; let inserted = false;
let imgUrl = urllib.resolve(serviceUrl, util.format('/links/%s/%s/%s', campaign.cid, list.cid, encodeURIComponent(subscription.cid))); 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 => { message = message.replace(/<\/body\b/i, match => {
inserted = true; inserted = true;
return img + match; return img + match;

View file

@ -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(); let cid = shortid.generate();
tools.validateEmail(email, false, err => { tools.validateEmail(email, false, err => {
@ -185,8 +185,8 @@ module.exports.addConfirmation = (list, email, data, callback) => {
return callback(err); return callback(err);
} }
let query = 'INSERT INTO confirmations (cid, list, email, data) VALUES (?,?,?,?)'; let query = 'INSERT INTO confirmations (cid, list, email, opt_in_ip, data) VALUES (?,?,?,?,?)';
connection.query(query, [cid, list.id, email, JSON.stringify(data || {})], (err, result) => { connection.query(query, [cid, list.id, email, optInIp, JSON.stringify(data || {})], (err, result) => {
connection.release(); connection.release();
if (err) { if (err) {
return callback(err); return callback(err);

View file

@ -1,3 +1,3 @@
{ {
"schemaVersion": 17 "schemaVersion": 18
} }

View file

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

View file

@ -122,7 +122,7 @@ router.post('/subscribe/:listId', (req, res) => {
} }
if (/^(yes|true|1)$/i.test(input.REQUIRE_CONFIRMATION)) { 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) { if (err) {
log.error('API', err); log.error('API', err);
res.status(500); res.status(500);

View file

@ -233,7 +233,6 @@ router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
req.flash('danger', err.message || err); req.flash('danger', err.message || err);
return res.redirect('/'); return res.redirect('/');
} }
campaign.mergeTags = mergeTags; campaign.mergeTags = mergeTags;
res.render(view, campaign); res.render(view, campaign);
}); });

View file

@ -234,7 +234,7 @@ router.post('/:cid/subscribe', passport.parseForm, passport.csrfProtection, (req
}); });
data = tools.convertKeys(data); data = tools.convertKeys(data);
subscriptions.addConfirmation(list, email, data, (err, confirmCid) => { subscriptions.addConfirmation(list, email, req.ip, data, (err, confirmCid) => {
if (!err && !confirmCid) { if (!err && !confirmCid) {
err = new Error('Could not store confirmation data'); err = new Error('Could not store confirmation data');
} }

View 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;

View file

@ -81,6 +81,14 @@
</div> </div>
</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 /> <hr />
<div class="form-group"> <div class="form-group">

View file

@ -100,6 +100,15 @@
<input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="Keep it relevant and non-spammy" required> <input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="Keep it relevant and non-spammy" required>
</div> </div>
</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 /> <hr />
<div class="form-group"> <div class="form-group">

View file

@ -100,6 +100,15 @@
<input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="Keep it relevant and non-spammy" required> <input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="Keep it relevant and non-spammy" required>
</div> </div>
</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 /> <hr />
<div class="form-group"> <div class="form-group">

View file

@ -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> <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> </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> </fieldset>
<hr /> <hr />

View file

@ -99,6 +99,15 @@
<input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="Keep it relevant and non-spammy" required> <input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="Keep it relevant and non-spammy" required>
</div> </div>
</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> </fieldset>
</div> </div>
<div role="tabpanel" class="tab-pane {{#if showTemplate}}active{{/if}}" id="template"> <div role="tabpanel" class="tab-pane {{#if showTemplate}}active{{/if}}" id="template">

View file

@ -98,6 +98,14 @@
<input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="Keep it relevant and non-spammy" required> <input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="Keep it relevant and non-spammy" required>
</div> </div>
</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> </fieldset>
</div> </div>
<div role="tabpanel" class="tab-pane {{#if showTemplate}}active{{/if}}" id="template"> <div role="tabpanel" class="tab-pane {{#if showTemplate}}active{{/if}}" id="template">

View file

@ -145,24 +145,27 @@
</div> </div>
</dd> </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> {{#unless trackingDisabled}}
<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}}&nbsp;({{openRate}}%)
</div>
</div>
</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> <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> <dd>
<div class="progress"> <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}}%;"> <div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="{{openRate}}" aria-valuemin="0" aria-valuemax="100" style="min-width: 6em; width: {{openRate}}%;">
{{clicks}}&nbsp;({{clicksRate}}%) {{opened}}&nbsp;({{openRate}}%)
</div>
</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}}&nbsp;({{clicksRate}}%)
</div>
</div>
</dd>
{{/unless}}
{{/unless}} {{/unless}}
{{/unless}} {{/unless}}
</dl> </dl>
@ -321,89 +324,91 @@
</div> </div>
{{#if links}} {{#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"> <div class="table-responsive">
<table class="table table-bordered table-hover"> <table class="table table-bordered table-hover">
<thead> <thead>
<th class="col-md-1"> <th class="col-md-1">
# #
</th> </th>
<th> <th>
URL URL
</th> </th>
<th class="col-md-1"> <th class="col-md-1">
Clicks Clicks
</th> </th>
<th class="col-md-1"> <th class="col-md-1">
% of clicks % of clicks
</th> </th>
<th class="col-md-1"> <th class="col-md-1">
% of messages % of messages
</th> </th>
</thead> </thead>
<tbody> <tbody>
{{#if links}} {{#if links}}
{{#each 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> <tr>
<td> <td colspan="5">
{{index}} No data available in table
</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> </td>
</tr> </tr>
{{/each}} {{/if}}
{{else}} </tbody>
<tfoot>
<tr> <tr>
<td colspan="5"> <th></th>
No data available in table <th>
</td> 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> </tr>
{{/if}} </tfoot>
</tbody> </table>
<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>
<p class="text-muted"> <p class="text-muted">
Clicks are counted as unique subscribers that clicked on a specific link or on any link (in aggregated view) Clicks are counted as unique subscribers that clicked on a specific link or on any link (in aggregated view)
</p> </p>
</div>
</div> </div>
</div> {{/unless}}
{{/if}} {{/if}}
</div> </div>