This commit is contained in:
Andris Reinman 2016-05-12 19:21:56 +03:00
parent bc902f8db7
commit c26f8b15d7
6 changed files with 127 additions and 81 deletions

View file

@ -1,5 +1,9 @@
# Changelog
## 1.7.0 2016-05-11
* Updated API, added new option **REQUIRE_CONFIRMATION** for subscriptions to send confirmation email before subscribing
## 1.6.0 2016-05-07
* Added simple API support for adding and removing list subscriptions

View file

@ -6,6 +6,10 @@ let tools = require('../tools');
let fields = require('./fields');
let geoip = require('geoip-ultralight');
let segments = require('./segments');
let settings = require('./settings');
let mailer = require('../mailer');
let urllib = require('url');
let log = require('npmlog');
module.exports.list = (listId, start, limit, callback) => {
listId = Number(listId) || 0;
@ -131,7 +135,7 @@ module.exports.filter = (listId, request, columns, segmentId, callback) => {
};
module.exports.addConfirmation = (listId, email, data, callback) => {
module.exports.addConfirmation = (list, email, data, callback) => {
let cid = shortid.generate();
tools.validateEmail(email, false, err => {
@ -145,12 +149,64 @@ module.exports.addConfirmation = (listId, email, data, callback) => {
}
let query = 'INSERT INTO confirmations (cid, list, email, data) VALUES (?,?,?,?)';
connection.query(query, [cid, listId, email, JSON.stringify(data || {})], (err, result) => {
connection.query(query, [cid, list.id, email, JSON.stringify(data || {})], (err, result) => {
connection.release();
if (err) {
return callback(err);
}
return callback(null, result && cid || false);
if (!result || !result.affectedRows) {
return callback(null, false);
}
fields.list(list.id, (err, fieldList) => {
if (err) {
return callback(err);
}
let encryptionKeys = [];
fields.getRow(fieldList, data).forEach(field => {
if (field.type === 'gpg' && field.value) {
encryptionKeys.push(field.value.trim());
}
});
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'serviceUrl'], (err, configItems) => {
if (err) {
return callback(err);
}
setImmediate(() => {
mailer.sendMail({
from: {
name: configItems.defaultFrom,
address: configItems.defaultAddress
},
to: {
name: [].concat(data.firstName || []).concat(data.lastName || []).join(' '),
address: email
},
subject: list.name + ': Please Confirm Subscription',
encryptionKeys
}, {
html: 'emails/confirm-html.hbs',
text: 'emails/confirm-text.hbs',
data: {
title: list.name,
contactAddress: configItems.defaultAddress,
confirmUrl: urllib.resolve(configItems.serviceUrl, '/subscription/subscribe/' + cid)
}
}, err => {
if (err) {
log.error('Subscription', err.stack);
}
});
});
return callback(null, cid);
});
});
});
});
});
@ -263,7 +319,7 @@ module.exports.insert = (listId, meta, subscription, callback) => {
return callback(err);
}
let query = 'SELECT id, status FROM `subscription__' + listId + '` WHERE email=? OR cid=? LIMIT 1';
let query = 'SELECT `id`, `status`, `cid` FROM `subscription__' + listId + '` WHERE `email`=? OR `cid`=? LIMIT 1';
connection.query(query, [meta.email, meta.cid], (err, rows) => {
if (err) {
return connection.rollback(() => {
@ -277,6 +333,7 @@ module.exports.insert = (listId, meta, subscription, callback) => {
let existing = rows && rows[0] || false;
let entryId = existing ? existing.id : false;
meta.cid = existing ? rows[0].cid : meta.cid;
meta.status = meta.status || (existing ? existing.status : 1);
let statusChange = !existing || existing.status !== meta.status;
@ -327,6 +384,7 @@ module.exports.insert = (listId, meta, subscription, callback) => {
connection.release();
return callback(null, {
entryId,
cid: meta.cid,
inserted: !existing
});
});
@ -342,6 +400,7 @@ module.exports.insert = (listId, meta, subscription, callback) => {
connection.release();
return callback(null, {
entryId,
cid: meta.cid,
inserted: !existing
});
});

View file

@ -1,7 +1,7 @@
{
"name": "mailtrain",
"private": true,
"version": "1.6.0",
"version": "1.7.0",
"description": "Self hosted email newsletter app",
"main": "index.js",
"scripts": {
@ -30,7 +30,7 @@
},
"dependencies": {
"bcrypt-nodejs": "0.0.3",
"body-parser": "^1.15.0",
"body-parser": "^1.15.1",
"bounce-handler": "^7.3.2-fork.0",
"compression": "^1.6.1",
"config": "^1.20.1",
@ -50,16 +50,16 @@
"humanize": "0.0.9",
"is-url": "^1.2.1",
"isemail": "^2.1.0",
"jsdom": "^8.5.0",
"juice": "^1.10.0",
"moment-timezone": "^0.5.3",
"jsdom": "^9.0.0",
"juice": "^1.11.0",
"moment-timezone": "^0.5.4",
"morgan": "^1.7.0",
"multer": "^1.1.0",
"mysql": "^2.10.2",
"nodemailer": "^2.3.2",
"nodemailer": "^2.4.1",
"nodemailer-openpgp": "^1.0.2",
"npmlog": "^2.0.3",
"openpgp": "^2.2.2",
"openpgp": "^2.3.0",
"passport": "^0.3.2",
"passport-local": "^1.0.0",
"request": "^2.72.0",

View file

@ -114,26 +114,43 @@ router.post('/subscribe/:listId', (req, res) => {
partial: true
};
if (input.FORCE_SUBSCRIBE === 'yes') {
if (/^(yes|true|1)$/i.test(input.FORCE_SUBSCRIBE)) {
meta.status = 1;
}
subscriptions.insert(list.id, meta, subscription, (err, response) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
}
res.status(200);
res.json({
data: {
id: response.entryId,
subscribed: true
if (/^(yes|true|1)$/i.test(input.REQUIRE_CONFIRMATION)) {
subscriptions.addConfirmation(list, input.EMAIL, subscription, (err, cid) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
}
res.status(200);
res.json({
data: {
id: cid
}
});
});
});
} else {
subscriptions.insert(list.id, meta, subscription, (err, response) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
}
res.status(200);
res.json({
data: {
id: response.cid
}
});
});
}
});
});
});

View file

@ -230,7 +230,7 @@ router.post('/:cid/subscribe', passport.parseForm, passport.csrfProtection, (req
});
data = tools.convertKeys(data);
subscriptions.addConfirmation(list.id, email, data, (err, confirmCid) => {
subscriptions.addConfirmation(list, email, data, (err, confirmCid) => {
if (!err && !confirmCid) {
err = new Error('Could not store confirmation data');
}
@ -239,51 +239,7 @@ router.post('/:cid/subscribe', passport.parseForm, passport.csrfProtection, (req
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
}
fields.list(list.id, (err, fieldList) => {
if (err) {
return next(err);
}
let encryptionKeys = [];
fields.getRow(fieldList, data).forEach(field => {
if (field.type === 'gpg' && field.value) {
encryptionKeys.push(field.value.trim());
}
});
settings.list(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'serviceUrl'], (err, configItems) => {
if (err) {
return next(err);
}
res.redirect('/subscription/' + req.params.cid + '/confirm-notice');
mailer.sendMail({
from: {
name: configItems.defaultFrom,
address: configItems.defaultAddress
},
to: {
name: [].concat(data.firstName || []).concat(data.lastName || []).join(' '),
address: email
},
subject: list.name + ': Please Confirm Subscription',
encryptionKeys
}, {
html: 'emails/confirm-html.hbs',
text: 'emails/confirm-text.hbs',
data: {
title: list.name,
contactAddress: configItems.defaultAddress,
confirmUrl: urllib.resolve(configItems.serviceUrl, '/subscription/subscribe/' + confirmCid)
}
}, err => {
if (err) {
log.error('Subscription', err.stack);
}
});
});
});
res.redirect('/subscription/' + req.params.cid + '/confirm-notice');
});
});
});

View file

@ -60,14 +60,24 @@
<strong>POST</strong> arguments
</p>
<ul>
<li><strong>EMAIL</strong> subscriber's email address (<em>required</em>)
<li><strong>FIRST_NAME</strong> subscriber's first name</li>
<li><strong>LAST_NAME</strong> subscriber's last name</li>
<li><strong>TIMEZONE</strong> subscriber's timezone (eg. "Europe/Tallinn", "PST" or "UTC"). If not set defaults to "UTC"</li>
<li><strong>MERGE_TAG_VALUE</strong> custom field value. Use yes/no for option group values (checkboxes, radios, drop downs)</li>
<li>
<strong>FORCE_SUBSCRIBE</strong> set to "yes" if you want to make sure the email is marked as subscribed even if it was previously marked as unsubscribed. By default if the email was already unsubscribed then subscription status is not changed.
</li>
<li><strong>EMAIL</strong> subscriber's email address (<em>required</em>)</li>
<li><strong>FIRST_NAME</strong> subscriber's first name</li>
<li><strong>LAST_NAME</strong> subscriber's last name</li>
<li><strong>TIMEZONE</strong> subscriber's timezone (eg. "Europe/Tallinn", "PST" or "UTC"). If not set defaults to "UTC"</li>
<li><strong>MERGE_TAG_VALUE</strong> custom field value. Use yes/no for option group values (checkboxes, radios, drop downs)</li>
</ul>
<p>
Additional <strong>POST</strong> arguments:
</p>
<ul>
<li>
<strong>FORCE_SUBSCRIBE</strong> set to "yes" if you want to make sure the email is marked as subscribed even if it was previously marked as unsubscribed. If the email was already unsubscribed/blocked then subscription status is not changed by default.
</li>
<li>
<strong>REQUIRE_CONFIRMATION</strong> set to "yes" if you want to send confirmation email to the subscriber before actually marking as subscribed
</li>
</ul>
<p>
@ -75,7 +85,7 @@
</p>
<pre>curl -XPOST {{serviceUrl}}api/subscribe/B16uVTdW?access_token={{accessToken}}\
--data 'EMAIL=test@example.com&MERGE_CHECKBOX=yes'</pre>
--data 'EMAIL=test@example.com&amp;MERGE_CHECKBOX=yes&amp;REQUIRE_CONFIRMATION=yes'</pre>
<h3>POST /api/unsubscribe/:listId Remove subscription</h3>