|
@ -1,5 +1,10 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 1.5.0 2016-05-05
|
||||||
|
|
||||||
|
* Fixed a bug in unsubscribing through the admin interface
|
||||||
|
* Added individual link click stats
|
||||||
|
|
||||||
## 1.4.1 2016-05-04
|
## 1.4.1 2016-05-04
|
||||||
|
|
||||||
* Added support for RSS templates
|
* Added support for RSS templates
|
||||||
|
|
|
@ -177,6 +177,8 @@ module.exports.get = (id, withSegment, callback) => {
|
||||||
|
|
||||||
let campaign = tools.convertKeys(rows[0]);
|
let campaign = tools.convertKeys(rows[0]);
|
||||||
|
|
||||||
|
let handleSegment = () => {
|
||||||
|
|
||||||
if (!campaign.segment || !withSegment) {
|
if (!campaign.segment || !withSegment) {
|
||||||
return callback(null, campaign);
|
return callback(null, campaign);
|
||||||
} else {
|
} else {
|
||||||
|
@ -196,8 +198,66 @@ module.exports.get = (id, withSegment, callback) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!campaign.parent) {
|
||||||
|
return handleSegment();
|
||||||
|
}
|
||||||
|
|
||||||
|
db.getConnection((err, connection) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
connection.query('SELECT `id`, `cid`, `name` FROM campaigns WHERE id=?', [campaign.parent], (err, rows) => {
|
||||||
|
connection.release();
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rows || !rows.length) {
|
||||||
|
return handleSegment();
|
||||||
|
}
|
||||||
|
|
||||||
|
campaign.parent = tools.convertKeys(rows[0]);
|
||||||
|
return handleSegment();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.getLinks = (id, callback) => {
|
||||||
|
id = Number(id) || 0;
|
||||||
|
|
||||||
|
if (id < 1) {
|
||||||
|
return callback(new Error('Missing Campaign ID'));
|
||||||
|
}
|
||||||
|
|
||||||
|
db.getConnection((err, connection) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = 'SELECT `id`, `url`, `clicks` FROM links WHERE `campaign`=? LIMIT 1000';
|
||||||
|
connection.query(query, [id], (err, rows) => {
|
||||||
|
connection.release();
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rows || !rows.length) {
|
||||||
|
return callback(null, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
let links = rows.map(
|
||||||
|
row => tools.convertKeys(row)
|
||||||
|
).sort((a, b) => (
|
||||||
|
a.url.replace(/^https?:\/\/(www.)?/, '').toLowerCase()).localeCompare(b.url.replace(/^https?:\/\/(www.)?/, '').toLowerCase()));
|
||||||
|
|
||||||
|
return callback(null, links);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.create = (campaign, opts, callback) => {
|
module.exports.create = (campaign, opts, callback) => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "mailtrain",
|
"name": "mailtrain",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.4.1",
|
"version": "1.5.0",
|
||||||
"description": "Self hosted email newsletter app",
|
"description": "Self hosted email newsletter app",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
BIN
public/images/img01.png
Normal file
After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 71 KiB |
BIN
public/images/img02.png
Normal file
After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 107 KiB |
BIN
public/images/img03.png
Normal file
After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 88 KiB |
BIN
public/images/img04.png
Normal file
After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 82 KiB |
BIN
public/images/img05.png
Normal file
After Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 50 KiB |
BIN
public/images/img06.png
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
public/images/img07.png
Normal file
After Width: | Height: | Size: 99 KiB |
BIN
public/images/img08.png
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
public/images/img09.png
Normal file
After Width: | Height: | Size: 101 KiB |
|
@ -271,8 +271,26 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => {
|
||||||
campaign.openRate = campaign.delivered ? Math.round((campaign.opened / campaign.delivered) * 100) : 0;
|
campaign.openRate = campaign.delivered ? Math.round((campaign.opened / campaign.delivered) * 100) : 0;
|
||||||
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0;
|
campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 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 = true;
|
||||||
res.render('campaigns/view', campaign);
|
res.render('campaigns/view', campaign);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -378,7 +378,7 @@ router.post('/subscription/unsubscribe', passport.parseForm, passport.csrfProtec
|
||||||
return res.redirect('/lists/view/' + list.id);
|
return res.redirect('/lists/view/' + list.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions.unsubscribe(list.id, subscription.email, err => {
|
subscriptions.unsubscribe(list.id, subscription.email, false, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
req.flash('danger', err && err.message || err || 'Could not unsubscribe user');
|
req.flash('danger', err && err.message || err || 'Could not unsubscribe user');
|
||||||
return res.redirect('/lists/subscription/' + list.id + '/edit/' + subscription.cid);
|
return res.redirect('/lists/subscription/' + list.id + '/edit/' + subscription.cid);
|
||||||
|
|
|
@ -139,8 +139,7 @@ function checkEntries(parent, entries, callback) {
|
||||||
from: parent.from,
|
from: parent.from,
|
||||||
address: parent.address,
|
address: parent.address,
|
||||||
subject: entry.title || parent.subject,
|
subject: entry.title || parent.subject,
|
||||||
list: parent.list,
|
list: parent.segment ? parent.list + ':' + parent.segment : parent.list,
|
||||||
segment: parent.segment,
|
|
||||||
html
|
html
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="/">Home</a></li>
|
<li><a href="/">Home</a></li>
|
||||||
<li><a href="/campaigns">Campaigns</a></li>
|
<li><a href="/campaigns">Campaigns</a></li>
|
||||||
|
{{#if parent}}
|
||||||
|
<li><a href="/campaigns/view/{{parent.id}}">{{parent.name}}</a></li>
|
||||||
|
{{/if}}
|
||||||
<li><a href="/campaigns/view/{{id}}">{{name}}</a></li>
|
<li><a href="/campaigns/view/{{id}}">{{name}}</a></li>
|
||||||
<li class="active">Edit RSS Campaign</li>
|
<li class="active">Edit RSS Campaign</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="/">Home</a></li>
|
<li><a href="/">Home</a></li>
|
||||||
<li><a href="/campaigns">Campaigns</a></li>
|
<li><a href="/campaigns">Campaigns</a></li>
|
||||||
|
{{#if parent}}
|
||||||
|
<li><a href="/campaigns/view/{{parent.id}}">{{parent.name}}</a></li>
|
||||||
|
{{/if}}
|
||||||
<li><a href="/campaigns/view/{{id}}">{{name}}</a></li>
|
<li><a href="/campaigns/view/{{id}}">{{name}}</a></li>
|
||||||
<li class="active">Edit Campaign</li>
|
<li class="active">Edit Campaign</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="/">Home</a></li>
|
<li><a href="/">Home</a></li>
|
||||||
<li><a href="/campaigns">Campaigns</a></li>
|
<li><a href="/campaigns">Campaigns</a></li>
|
||||||
|
{{#if parent}}
|
||||||
|
<li><a href="/campaigns/view/{{parent.id}}">{{parent.name}}</a></li>
|
||||||
|
{{/if}}
|
||||||
<li class="active">{{name}}</li>
|
<li class="active">{{name}}</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
@ -16,7 +19,20 @@
|
||||||
<div class="well well-sm">{{{description}}}</div>
|
<div class="well well-sm">{{{description}}}</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<dl class="dl-horizontal">
|
<!-- Nav tabs -->
|
||||||
|
<ul class="nav nav-tabs" role="tablist">
|
||||||
|
<li role="presentation" class="{{#if showOverview}}active{{/if}}"><a href="#overview" aria-controls="overview" role="tab" data-toggle="tab">Overview</a></li>
|
||||||
|
{{#if links}}
|
||||||
|
<li role="presentation" class="{{#if showLinks}}active{{/if}}"><a href="#links" aria-controls="links" role="tab" data-toggle="tab">Links</a></li>
|
||||||
|
{{/if}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<div role="tabpanel" class="tab-pane {{#if showOverview}}active{{/if}}" id="overview">
|
||||||
|
|
||||||
|
<p></p>
|
||||||
|
|
||||||
|
<dl class="dl-horizontal">
|
||||||
<dt>List</dt>
|
<dt>List</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{#if segment}}
|
{{#if segment}}
|
||||||
|
@ -46,7 +62,7 @@
|
||||||
<dd>
|
<dd>
|
||||||
{{#if lastCheck}}<span class="datestring" data-date="{{lastCheck}}" title="{{lastCheck}}">{{lastCheck}}</span>{{else}}
|
{{#if lastCheck}}<span class="datestring" data-date="{{lastCheck}}" title="{{lastCheck}}">{{lastCheck}}</span>{{else}}
|
||||||
Not yet checked{{/if}}
|
Not yet checked{{/if}}
|
||||||
{{#unless isActive}}<span class="text-muted">(feed is only checked if RSS campaign is active)</span>{{/unless}}
|
{{#unless isActive}}<span class="text-muted">(activate campaign to start checking feed for new messages)</span>{{/unless}}
|
||||||
</dd>
|
</dd>
|
||||||
{{#if checkStatus}}
|
{{#if checkStatus}}
|
||||||
<dt>RSS status</dt>
|
<dt>RSS status</dt>
|
||||||
|
@ -54,14 +70,20 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if from}}
|
||||||
<dt>Email "from name"</dt>
|
<dt>Email "from name"</dt>
|
||||||
<dd>{{from}}</dd>
|
<dd>{{from}}</dd>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if address}}
|
||||||
<dt>Email "from" address</dt>
|
<dt>Email "from" address</dt>
|
||||||
<dd>{{address}}</dd>
|
<dd>{{address}}</dd>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if subject}}
|
||||||
<dt>Email "subject line"</dt>
|
<dt>Email "subject line"</dt>
|
||||||
<dd>{{subject}}</dd>
|
<dd>{{subject}}</dd>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if isNormal}}
|
{{#if isNormal}}
|
||||||
|
|
||||||
|
@ -98,9 +120,9 @@
|
||||||
|
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{{#if isNormal}}
|
{{#if isNormal}}
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
@ -201,9 +223,9 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if isRss}}
|
{{#if isRss}}
|
||||||
|
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
|
@ -234,7 +256,90 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{#if links}}
|
||||||
|
<div role="tabpanel" class="tab-pane {{#if showLinks}}active{{/if}}" id="links">
|
||||||
|
|
||||||
|
<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}}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{index}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{url}}">{{short}}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{clicks}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{relPercentage}}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{totalPercentage}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
{{else}}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5">
|
||||||
|
No data available in table
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{/if}}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>
|
||||||
|
Aggregated clicks
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{{clicks}}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
–
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
CTR {{clicksRate}}%
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isRss}}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<div class="well text-info">
|
<div class="well text-info">
|
||||||
If a new entry is found from campaign feed a new subcampaign is created of that entry and it will be listed here
|
If a new entry is found from campaign feed a new subcampaign is created of that entry and it will be listed here
|
||||||
|
@ -262,4 +367,4 @@
|
||||||
</thead>
|
</thead>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -33,6 +33,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h2>RSS Campaigns</h2>
|
||||||
|
<p>Setup Mailtrain to track RSS feeds and if a new entry is detected in a feed then Mailtrain auto-generates a new campaign using entry data as message contents and sends it to selected subscribers.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h2>GPG Encryption</h2>
|
||||||
|
<p>If a list includes a custom field for a GPG Public Key then subscribers can upload their GPG public keys when signing up or managing preferences. Subscriber that have a key set get all messages from the list in an encrypted form.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<h2>Click stats</h2>
|
||||||
|
<p>After a campaign is sent, check individual click statistics for every link included in the message.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<h2>Open source</h2>
|
<h2>Open source</h2>
|
||||||
|
@ -66,19 +81,31 @@
|
||||||
<!-- Wrapper for slides -->
|
<!-- Wrapper for slides -->
|
||||||
<div class="carousel-inner" role="listbox">
|
<div class="carousel-inner" role="listbox">
|
||||||
<div class="item active">
|
<div class="item active">
|
||||||
<img src="/images/img01_lists.png" alt="Lists">
|
<img src="/images/img01.png">
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<img src="/images/img02_list.png" alt="List">
|
<img src="/images/img02.png">
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<img src="/images/img03_fields.png" alt="Custom fields">
|
<img src="/images/img03.png">
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<img src="/images/img04_segment.png" alt="List segments">
|
<img src="/images/img04.png">
|
||||||
</div>
|
</div>
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<img src="/images/img05_subscribe.png" alt="List segments">
|
<img src="/images/img05.png">
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<img src="/images/img06.png">
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<img src="/images/img07.png">
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<img src="/images/img08.png">
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<img src="/images/img09.png">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|