diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 92bddfb4..60a1b927 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,10 @@ # 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 * Added support for RSS templates diff --git a/lib/models/campaigns.js b/lib/models/campaigns.js index 6c16c27d..52e80c4d 100644 --- a/lib/models/campaigns.js +++ b/lib/models/campaigns.js @@ -177,29 +177,89 @@ module.exports.get = (id, withSegment, callback) => { let campaign = tools.convertKeys(rows[0]); - if (!campaign.segment || !withSegment) { - return callback(null, campaign); - } else { - segments.get(campaign.segment, (err, segment) => { - if (err || !segment) { - // ignore - return callback(null, campaign); - } - segments.subscribers(segment.id, true, (err, subscribers) => { - if (err || !subscribers) { - segment.subscribers = 0; - } else { - segment.subscribers = subscribers; + let handleSegment = () => { + + if (!campaign.segment || !withSegment) { + return callback(null, campaign); + } else { + segments.get(campaign.segment, (err, segment) => { + if (err || !segment) { + // ignore + return callback(null, campaign); } - campaign.segment = segment; - return callback(null, campaign); + segments.subscribers(segment.id, true, (err, subscribers) => { + if (err || !subscribers) { + segment.subscribers = 0; + } else { + segment.subscribers = subscribers; + } + campaign.segment = segment; + return callback(null, campaign); + }); }); - }); + } + }; + + 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) => { campaign = tools.convertKeys(campaign); diff --git a/package.json b/package.json index 552d751e..bb8d893e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mailtrain", "private": true, - "version": "1.4.1", + "version": "1.5.0", "description": "Self hosted email newsletter app", "main": "index.js", "scripts": { diff --git a/public/images/img01.png b/public/images/img01.png new file mode 100644 index 00000000..fc03cdfb Binary files /dev/null and b/public/images/img01.png differ diff --git a/public/images/img01_lists.png b/public/images/img01_lists.png deleted file mode 100644 index f7ff88c5..00000000 Binary files a/public/images/img01_lists.png and /dev/null differ diff --git a/public/images/img02.png b/public/images/img02.png new file mode 100644 index 00000000..7e48f04f Binary files /dev/null and b/public/images/img02.png differ diff --git a/public/images/img02_list.png b/public/images/img02_list.png deleted file mode 100644 index bcd1b312..00000000 Binary files a/public/images/img02_list.png and /dev/null differ diff --git a/public/images/img03.png b/public/images/img03.png new file mode 100644 index 00000000..f9c50e25 Binary files /dev/null and b/public/images/img03.png differ diff --git a/public/images/img03_fields.png b/public/images/img03_fields.png deleted file mode 100644 index 32bc7808..00000000 Binary files a/public/images/img03_fields.png and /dev/null differ diff --git a/public/images/img04.png b/public/images/img04.png new file mode 100644 index 00000000..ba36dc2f Binary files /dev/null and b/public/images/img04.png differ diff --git a/public/images/img04_segment.png b/public/images/img04_segment.png deleted file mode 100644 index 22c6103b..00000000 Binary files a/public/images/img04_segment.png and /dev/null differ diff --git a/public/images/img05.png b/public/images/img05.png new file mode 100644 index 00000000..a5c72c2f Binary files /dev/null and b/public/images/img05.png differ diff --git a/public/images/img05_subscribe.png b/public/images/img05_subscribe.png deleted file mode 100644 index 852939bb..00000000 Binary files a/public/images/img05_subscribe.png and /dev/null differ diff --git a/public/images/img06.png b/public/images/img06.png new file mode 100644 index 00000000..1fdb8c63 Binary files /dev/null and b/public/images/img06.png differ diff --git a/public/images/img07.png b/public/images/img07.png new file mode 100644 index 00000000..a69230cf Binary files /dev/null and b/public/images/img07.png differ diff --git a/public/images/img08.png b/public/images/img08.png new file mode 100644 index 00000000..6096a23d Binary files /dev/null and b/public/images/img08.png differ diff --git a/public/images/img09.png b/public/images/img09.png new file mode 100644 index 00000000..0eb3df83 Binary files /dev/null and b/public/images/img09.png differ diff --git a/routes/campaigns.js b/routes/campaigns.js index e443e9ea..ed5819a2 100644 --- a/routes/campaigns.js +++ b/routes/campaigns.js @@ -271,7 +271,25 @@ router.get('/view/:id', passport.csrfProtection, (req, res) => { campaign.openRate = campaign.delivered ? Math.round((campaign.opened / campaign.delivered) * 100) : 0; campaign.clicksRate = campaign.delivered ? Math.round((campaign.clicks / campaign.delivered) * 100) : 0; - res.render('campaigns/view', campaign); + 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); + }); + }); }); }); diff --git a/routes/lists.js b/routes/lists.js index 3cf12fe5..a48cfcb4 100644 --- a/routes/lists.js +++ b/routes/lists.js @@ -378,7 +378,7 @@ router.post('/subscription/unsubscribe', passport.parseForm, passport.csrfProtec return res.redirect('/lists/view/' + list.id); } - subscriptions.unsubscribe(list.id, subscription.email, err => { + subscriptions.unsubscribe(list.id, subscription.email, false, err => { if (err) { req.flash('danger', err && err.message || err || 'Could not unsubscribe user'); return res.redirect('/lists/subscription/' + list.id + '/edit/' + subscription.cid); diff --git a/services/feedcheck.js b/services/feedcheck.js index 4ffae847..42640b9a 100644 --- a/services/feedcheck.js +++ b/services/feedcheck.js @@ -139,8 +139,7 @@ function checkEntries(parent, entries, callback) { from: parent.from, address: parent.address, subject: entry.title || parent.subject, - list: parent.list, - segment: parent.segment, + list: parent.segment ? parent.list + ':' + parent.segment : parent.list, html }; diff --git a/views/campaigns/edit-rss.hbs b/views/campaigns/edit-rss.hbs index 69e39371..4f57aac9 100644 --- a/views/campaigns/edit-rss.hbs +++ b/views/campaigns/edit-rss.hbs @@ -1,6 +1,9 @@ diff --git a/views/campaigns/edit.hbs b/views/campaigns/edit.hbs index fa656c7f..863cea32 100644 --- a/views/campaigns/edit.hbs +++ b/views/campaigns/edit.hbs @@ -1,6 +1,9 @@ diff --git a/views/campaigns/view.hbs b/views/campaigns/view.hbs index 5ecbb8e2..fbe3ffd1 100644 --- a/views/campaigns/view.hbs +++ b/views/campaigns/view.hbs @@ -1,6 +1,9 @@ @@ -16,250 +19,352 @@
{{{description}}}
{{/if}} -
-
List
-
- {{#if segment}} - + + + +
+
+ +

+ +
+
List
+
+ {{#if segment}} + {{list.name}}: {{segment.name}} - {{else}} - + {{else}} + {{list.name}} - {{/if}} -
- -
List subscribers
-
- {{#if segment}} - {{segment.subscribers}} - {{else}} - {{list.subscribers}} - {{/if}} -
- - {{#if isRss}} -
Feed URL
-
{{sourceUrl}}
-
Last check
-
- {{#if lastCheck}}{{lastCheck}}{{else}} - Not yet checked{{/if}} - {{#unless isActive}}(feed is only checked if RSS campaign is active){{/unless}} -
- {{#if checkStatus}} -
RSS status
-
{{checkStatus}}
- {{/if}} - {{/if}} - -
Email "from name"
-
{{from}}
- -
Email "from" address
-
{{address}}
- -
Email "subject line"
-
{{subject}}
- - {{#if isNormal}} - - {{#unless isIdling}} -
Delivered
-
{{delivered}}
- -
Bounced
-
{{bounced}}
- -
Complaints
-
{{complained}}
- -
Unsubscribed
-
{{unsubscribed}}
- -
Opened
-
-
-
- {{openRate}}% -
-
+ {{/if}}
-
Clicked
+
List subscribers
-
-
- {{clicksRate}}% -
-
-
- - {{/unless}} - {{/if}} -
- -{{#if isNormal}} - -
-
- {{#if isIdling}} -
- - - -
-
-

Delay sending

-
-
-
- -
hours
-
-
-
-
- -
minutes
-
-
-
- - -
- {{/if}} - - {{#if isSending}} - -
- {{#if isScheduled}} -
-
- - - - -
-
-

Sending scheduled {{scheduled}}

+ {{#if segment}} + {{segment.subscribers}} {{else}} -
-
- - + {{list.subscribers}} + {{/if}} +
- - - -

Sending…

+ {{#if isRss}} +
Feed URL
+
{{sourceUrl}}
+
Last check
+
+ {{#if lastCheck}}{{lastCheck}}{{else}} + Not yet checked{{/if}} + {{#unless isActive}}(activate campaign to start checking feed for new messages){{/unless}} +
+ {{#if checkStatus}} +
RSS status
+
{{checkStatus}}
{{/if}} {{/if}} - {{#if isPaused}} -
-
- - - - -
-
-

Sending paused

+ {{#if from}} +
Email "from name"
+
{{from}}
{{/if}} - {{#if isFinished}} -
-
- - -
- -
- - -
- - - - - -
-

All messages sent! Hit "Continue" if you you want to send this campaign to new subscribers

+ {{#if address}} +
Email "from" address
+
{{address}}
{{/if}} - - -{{/if}} - -{{#if isRss}} - -
-
- {{#if isActive}} -
-
- - -
- - -
- - Campaign status: ACTIVE - {{else}} -
-
- - -
- - -
- - Campaign status: INACTIVE + {{#if subject}} +
Email "subject line"
+
{{subject}}
{{/if}} -
-
-
-
- If a new entry is found from campaign feed a new subcampaign is created of that entry and it will be listed here -
- - - - - - - - - -
- # - - Name - - Description - - Status - - Created - -   -
+ {{#if isNormal}} + + {{#unless isIdling}} +
Delivered
+
{{delivered}}
+ +
Bounced
+
{{bounced}}
+ +
Complaints
+
{{complained}}
+ +
Unsubscribed
+
{{unsubscribed}}
+ +
Opened
+
+
+
+ {{openRate}}% +
+
+
+ +
Clicked
+
+
+
+ {{clicksRate}}% +
+
+
+ + {{/unless}} + {{/if}} +
+ + {{#if isNormal}} + +
+
+ {{#if isIdling}} +
+ + + +
+
+

Delay sending

+
+
+
+ +
hours
+
+
+
+
+ +
minutes
+
+
+
+ + +
+ {{/if}} + + {{#if isSending}} + +
+ {{#if isScheduled}} +
+
+ + + + +
+
+

Sending scheduled {{scheduled}}

+ {{else}} +
+
+ + + + +
+
+

Sending…

+ {{/if}} + {{/if}} + + {{#if isPaused}} +
+
+ + + + +
+
+

Sending paused

+ {{/if}} + + {{#if isFinished}} +
+
+ + +
+ +
+ + +
+ + + + + +
+

All messages sent! Hit "Continue" if you you want to send this campaign to new subscribers

+ {{/if}} + +
+
+ {{/if}} + + {{#if isRss}} + +
+
+ {{#if isActive}} +
+
+ + +
+ + +
+ + Campaign status: ACTIVE + {{else}} +
+
+ + +
+ + +
+ + Campaign status: INACTIVE + {{/if}} +
+
+ {{/if}} + -{{/if}} + {{#if links}} + + {{/if}} + + {{#if isRss}} +
+
+ If a new entry is found from campaign feed a new subcampaign is created of that entry and it will be listed here +
+ + + + + + + + + +
+ # + + Name + + Description + + Status + + Created + +   +
+
+ {{/if}} diff --git a/views/index.hbs b/views/index.hbs index 3b04133f..277309dc 100644 --- a/views/index.hbs +++ b/views/index.hbs @@ -33,6 +33,21 @@ +
+
+

RSS Campaigns

+

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.

+
+
+

GPG Encryption

+

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.

+
+
+

Click stats

+

After a campaign is sent, check individual click statistics for every link included in the message.

+
+
+

Open source

@@ -66,19 +81,31 @@