Implement Email DAO for sending verification mails
This commit is contained in:
parent
c805371f0e
commit
d3cce89b06
7
config/integration.js
Normal file
7
config/integration.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
log: {
|
||||||
|
level: "warn"
|
||||||
|
}
|
||||||
|
};
|
@ -11,7 +11,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js",
|
"start": "node index.js",
|
||||||
"test": "grunt test"
|
"test": "export NODE_ENV=integration && grunt test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"addressparser": "^1.0.1",
|
"addressparser": "^1.0.1",
|
||||||
|
10
src/app.js
10
src/app.js
@ -96,9 +96,9 @@ app.on('error', (error, ctx) => {
|
|||||||
function injectDependencies() {
|
function injectDependencies() {
|
||||||
let credentials = readCredentials();
|
let credentials = readCredentials();
|
||||||
mongo = new Mongo({
|
mongo = new Mongo({
|
||||||
uri: process.env.MONGO_URI || credentials.mongoUri,
|
uri: process.env.MONGO_URI || credentials.mongo.uri,
|
||||||
user: process.env.MONGO_USER || credentials.mongoUser,
|
user: process.env.MONGO_USER || credentials.mongo.user,
|
||||||
password: process.env.MONGO_PASS || credentials.mongoPass
|
password: process.env.MONGO_PASS || credentials.mongo.pass
|
||||||
});
|
});
|
||||||
email = new Email(nodemailer);
|
email = new Email(nodemailer);
|
||||||
userId = new UserId(mongo);
|
userId = new UserId(mongo);
|
||||||
@ -123,14 +123,14 @@ if (!global.testing) { // don't automatically start server in tests
|
|||||||
co(function *() {
|
co(function *() {
|
||||||
let app = yield init();
|
let app = yield init();
|
||||||
app.listen(config.server.port);
|
app.listen(config.server.port);
|
||||||
log.verbose('app', 'Ready to rock! Listening on http://localhost:' + config.server.port);
|
log.info('app', 'Ready to rock! Listening on http://localhost:' + config.server.port);
|
||||||
}).catch(err => log.error('app', 'Initialization failed!', err));
|
}).catch(err => log.error('app', 'Initialization failed!', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
function *init() {
|
function *init() {
|
||||||
log.level = config.log.level; // set log level depending on process.env.NODE_ENV
|
log.level = config.log.level; // set log level depending on process.env.NODE_ENV
|
||||||
injectDependencies();
|
injectDependencies();
|
||||||
log.verbose('app', 'Connecting to MongoDB ...');
|
log.info('app', 'Connecting to MongoDB ...');
|
||||||
yield mongo.connect();
|
yield mongo.connect();
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
123
src/dao/email.js
123
src/dao/email.js
@ -17,6 +17,9 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const log = require('npmlog');
|
||||||
|
const util = require('../service/util');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple wrapper around Nodemailer to send verification emails
|
* A simple wrapper around Nodemailer to send verification emails
|
||||||
*/
|
*/
|
||||||
@ -30,21 +33,131 @@ class Email {
|
|||||||
this._mailer = mailer;
|
this._mailer = mailer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of the reusable nodemailer SMTP transport.
|
||||||
|
* @param {string} host The SMTP server's hostname e.g. 'smtp.gmail.com'
|
||||||
|
* @param {Object} auth Auth credential e.g. { user:'user@gmail.com', pass:'pass' }
|
||||||
|
* @param {Object} sender The message 'FROM' field e.g. { name:'Your Support', email:'noreply@exmple.com' }
|
||||||
|
* @param {string} port (optional) The SMTP server's SMTP port. Defaults to 465.
|
||||||
|
* @param {boolean} secure (optional) If TSL should be used. Defaults to true.
|
||||||
|
* @param {boolean} requireTLS (optional) If TSL is mandatory. Defaults to true.
|
||||||
|
*/
|
||||||
|
init(options) {
|
||||||
|
this._transport = this._mailer.createTransport({
|
||||||
|
host: options.host,
|
||||||
|
port: options.port || 465,
|
||||||
|
auth: options.auth,
|
||||||
|
secure: options.secure || true,
|
||||||
|
requireTLS: options.requireTLS || true
|
||||||
|
});
|
||||||
|
this._sender = options.sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic method to send an email message via nodemail.
|
||||||
|
* @param {Object} from The sender user id object e.g. { name:'Jon Smith', email:'j@smith.com' }
|
||||||
|
* @param {Object} to The recipient user id object e.g. { name:'Jon Smith', email:'j@smith.com' }
|
||||||
|
* @param {string} subject The message subject
|
||||||
|
* @param {string} text The message plaintext body
|
||||||
|
* @param {string} html The message html body
|
||||||
|
* @yield {Object} The reponse object containing SMTP info
|
||||||
|
*/
|
||||||
|
*send(options) {
|
||||||
|
let mailOptions = {
|
||||||
|
from: {
|
||||||
|
name: options.from.name,
|
||||||
|
address: options.from.email
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
name: options.to.name,
|
||||||
|
address: options.to.email
|
||||||
|
},
|
||||||
|
subject: options.subject,
|
||||||
|
text: options.text,
|
||||||
|
html: options.html
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
let info = yield this._transport.sendMail(mailOptions);
|
||||||
|
log.silly('email', 'Email sent.', info);
|
||||||
|
return info;
|
||||||
|
} catch(error) {
|
||||||
|
log.error('email', 'Sending email failed.', error, options);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send the verification email to the user to verify email address
|
* Send the verification email to the user to verify email address
|
||||||
* ownership. If the primary email address is provided, only one email
|
* ownership. If the primary email address is provided, only one email
|
||||||
* will be sent out. Otherwise all of the PGP key's user IDs will be
|
* will be sent out. Otherwise all of the PGP key's user IDs will be
|
||||||
* verified, resulting in an email sent per user ID.
|
* verified, resulting in an email sent per user ID.
|
||||||
* @param {Array} options.userIds The user id documents containing the nonces
|
* @param {Array} userIds The user id documents containing the nonces
|
||||||
* @param {Array} options.primaryEmail (optional) The user's primary email address
|
* @param {Array} primaryEmail (optional) The user's primary email address
|
||||||
|
* @param {Object} origin Required for links to the keyserver e.g. { protocol:'https', host:'openpgpkeys@example.com' }
|
||||||
* @yield {undefined}
|
* @yield {undefined}
|
||||||
*/
|
*/
|
||||||
sendVerification() {
|
*sendVerification(options) {
|
||||||
return Promise.resolve();
|
let primaryEmail = options.primaryEmail, userIds = options.userIds, origin = options.origin;
|
||||||
|
let primaryUserId = userIds.find(uid => uid.email === primaryEmail);
|
||||||
|
if (primaryUserId) { // send only one email to the primary user id
|
||||||
|
return yield this._sendVerificationHelper(primaryUserId, origin);
|
||||||
|
}
|
||||||
|
for (let uid of userIds) {
|
||||||
|
yield this._sendVerificationHelper(uid, origin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
send() {
|
/**
|
||||||
|
* Help method to send a verification message
|
||||||
|
* @param {Object} userId The user id document
|
||||||
|
* @param {Object} origin The origin of the server
|
||||||
|
* @yield {Object} The send response from the SMTP server
|
||||||
|
*/
|
||||||
|
*_sendVerificationHelper(userId, origin) {
|
||||||
|
let message = this._createVerifyMessage(userId, origin);
|
||||||
|
try {
|
||||||
|
let info = yield this.send(message);
|
||||||
|
if (!this._checkResponse(info)) {
|
||||||
|
log.warn('email', 'Verification mail may not have been received.', info);
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
} catch(e) {
|
||||||
|
util.throw(500, 'Sending verification email failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create a verification message object.
|
||||||
|
* @param {Object} userId The user id document
|
||||||
|
* @param {Object} origin The origin of the server
|
||||||
|
* @return {Object} The message object
|
||||||
|
*/
|
||||||
|
_createVerifyMessage(userId, origin) {
|
||||||
|
let verifyLink = origin.protocol + '://' + origin.host +
|
||||||
|
'/api/v1/verify/?keyid=' + encodeURIComponent(userId.keyid) +
|
||||||
|
'&nonce=' + encodeURIComponent(userId.nonce);
|
||||||
|
let text = `Hey${userId.name ? ' ' + userId.name : ''},
|
||||||
|
|
||||||
|
please click here to verify your key: ${verifyLink}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: this._sender,
|
||||||
|
to: userId,
|
||||||
|
subject: 'Verify Your Key',
|
||||||
|
text: text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the message was sent successfully according to SMTP
|
||||||
|
* reply codes: http://www.supermailer.de/smtp_reply_codes.htm
|
||||||
|
* @param {Object} info The info object return from nodemailer
|
||||||
|
* @return {boolean} If the message was received by the user
|
||||||
|
*/
|
||||||
|
_checkResponse(info) {
|
||||||
|
return /^2/.test(info.response);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -40,10 +40,12 @@ class HKP {
|
|||||||
*/
|
*/
|
||||||
*add(ctx) {
|
*add(ctx) {
|
||||||
let body = yield parse.form(ctx, { limit: '1mb' });
|
let body = yield parse.form(ctx, { limit: '1mb' });
|
||||||
if (!util.validatePublicKey(body.keytext)) {
|
let publicKeyArmored = body.keytext;
|
||||||
|
if (!util.validatePublicKey(publicKeyArmored)) {
|
||||||
ctx.throw(400, 'Invalid request!');
|
ctx.throw(400, 'Invalid request!');
|
||||||
}
|
}
|
||||||
yield this._publicKey.put({ publicKeyArmored:body.keytext });
|
let origin = util.getOrigin(ctx);
|
||||||
|
yield this._publicKey.put({ publicKeyArmored, origin });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,12 +38,15 @@ class REST {
|
|||||||
* @param {Object} ctx The koa request/response context
|
* @param {Object} ctx The koa request/response context
|
||||||
*/
|
*/
|
||||||
*create(ctx) {
|
*create(ctx) {
|
||||||
let pk = yield parse.json(ctx, { limit: '1mb' });
|
let body = yield parse.json(ctx, { limit: '1mb' });
|
||||||
if ((pk.primaryEmail && !util.validateAddress(pk.primaryEmail)) ||
|
let primaryEmail = body.primaryEmail;
|
||||||
!util.validatePublicKey(pk.publicKeyArmored)) {
|
let publicKeyArmored = body.publicKeyArmored;
|
||||||
|
if ((primaryEmail && !util.validateAddress(primaryEmail)) ||
|
||||||
|
!util.validatePublicKey(publicKeyArmored)) {
|
||||||
ctx.throw(400, 'Invalid request!');
|
ctx.throw(400, 'Invalid request!');
|
||||||
}
|
}
|
||||||
yield this._publicKey(pk);
|
let origin = util.getOrigin(ctx);
|
||||||
|
yield this._publicKey({ publicKeyArmored, primaryEmail, origin });
|
||||||
}
|
}
|
||||||
|
|
||||||
*verify(ctx) {
|
*verify(ctx) {
|
||||||
|
@ -50,13 +50,14 @@ class PublicKey {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist a new public key
|
* Persist a new public key
|
||||||
* @param {String} options.publicKeyArmored The ascii armored pgp key block
|
* @param {String} publicKeyArmored The ascii armored pgp key block
|
||||||
* @param {String} options.primaryEmail (optional) The key's primary email address
|
* @param {String} primaryEmail (optional) The key's primary email address
|
||||||
|
* @param {Object} origin Required for links to the keyserver e.g. { protocol:'https', host:'openpgpkeys@example.com' }
|
||||||
* @yield {undefined}
|
* @yield {undefined}
|
||||||
*/
|
*/
|
||||||
*put(options) {
|
*put(options) {
|
||||||
// parse key block
|
// parse key block
|
||||||
let publicKeyArmored = options.publicKeyArmored;
|
let publicKeyArmored = options.publicKeyArmored, primaryEmail = options.primaryEmail, origin = options.origin;
|
||||||
let params = this.parseKey(publicKeyArmored);
|
let params = this.parseKey(publicKeyArmored);
|
||||||
// check for existing verfied key by id or email addresses
|
// check for existing verfied key by id or email addresses
|
||||||
let verified = yield this._userid.getVerfied(params);
|
let verified = yield this._userid.getVerfied(params);
|
||||||
@ -73,7 +74,7 @@ class PublicKey {
|
|||||||
// persist new user ids
|
// persist new user ids
|
||||||
let userIds = yield this._userid.batch(params);
|
let userIds = yield this._userid.batch(params);
|
||||||
// send mails to verify user ids (send only one if primary email is provided)
|
// send mails to verify user ids (send only one if primary email is provided)
|
||||||
yield this._email.sendVerification({ userIds, primaryEmail:options.primaryEmail });
|
yield this._email.sendVerification({ userIds, primaryEmail, origin });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -107,4 +107,19 @@ exports.throw = function(status, message) {
|
|||||||
err.status = status;
|
err.status = status;
|
||||||
err.expose = true; // display message to the client
|
err.expose = true; // display message to the client
|
||||||
throw err;
|
throw err;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the server's own origin host and protocol. Required for sending
|
||||||
|
* verification links via email. If the PORT environmane variable
|
||||||
|
* is set, we assume the protocol to be 'https', since the AWS loadbalancer
|
||||||
|
* speaks 'https' externally but 'http' between the LB and the server.
|
||||||
|
* @param {Object} ctx The koa request/repsonse context
|
||||||
|
* @return {Object} The server origin
|
||||||
|
*/
|
||||||
|
exports.getOrigin = function(ctx) {
|
||||||
|
return {
|
||||||
|
protocol: process.env.PORT ? 'https' : ctx.protocol,
|
||||||
|
host: ctx.host
|
||||||
|
};
|
||||||
};
|
};
|
70
test/integration/email-test.js
Normal file
70
test/integration/email-test.js
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('co-mocha')(require('mocha')); // monkey patch mocha for generators
|
||||||
|
|
||||||
|
const expect = require('chai').expect;
|
||||||
|
const log = require('npmlog');
|
||||||
|
const config = require('config');
|
||||||
|
const Email = require('../../src/dao/email');
|
||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
|
||||||
|
log.level = config.log.level;
|
||||||
|
|
||||||
|
describe('Email Integration Tests', function() {
|
||||||
|
this.timeout(20000);
|
||||||
|
|
||||||
|
let email, credentials;
|
||||||
|
|
||||||
|
before(function() {
|
||||||
|
try {
|
||||||
|
credentials = require('../../credentials.json');
|
||||||
|
} catch(e) {
|
||||||
|
log.warn('email-test', 'No credentials.json found ... skipping tests.');
|
||||||
|
this.skip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
email = new Email(nodemailer);
|
||||||
|
email.init({
|
||||||
|
host: credentials.smtp.host,
|
||||||
|
auth: {
|
||||||
|
user: credentials.smtp.user,
|
||||||
|
pass: credentials.smtp.pass
|
||||||
|
},
|
||||||
|
sender: credentials.sender
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("send", function() {
|
||||||
|
it('should work', function *() {
|
||||||
|
let mailOptions = {
|
||||||
|
from: credentials.sender,
|
||||||
|
to: credentials.sender,
|
||||||
|
subject: 'Hello ✔', // Subject line
|
||||||
|
text: 'Hello world 🐴', // plaintext body
|
||||||
|
html: '<b>Hello world 🐴</b>' // html body
|
||||||
|
};
|
||||||
|
let info = yield email.send(mailOptions);
|
||||||
|
expect(info).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sendVerification", function() {
|
||||||
|
it('should work', function *() {
|
||||||
|
let options = {
|
||||||
|
userIds: [{
|
||||||
|
name: credentials.sender.name,
|
||||||
|
email: credentials.sender.email,
|
||||||
|
keyid: '0123456789ABCDF0',
|
||||||
|
nonce: 'qwertzuioasdfghjkqwertzuio'
|
||||||
|
}],
|
||||||
|
primaryEmail: credentials.sender.email,
|
||||||
|
origin: {
|
||||||
|
protocol: 'http',
|
||||||
|
host: 'localhost:' + config.server.port
|
||||||
|
}
|
||||||
|
};
|
||||||
|
yield email.sendVerification(options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@ -20,9 +20,9 @@ describe('Mongo Integration Tests', function() {
|
|||||||
log.info('mongo-test', 'No credentials.json found ... using environment vars.');
|
log.info('mongo-test', 'No credentials.json found ... using environment vars.');
|
||||||
}
|
}
|
||||||
mongo = new Mongo({
|
mongo = new Mongo({
|
||||||
uri: process.env.MONGO_URI || credentials.mongoUri,
|
uri: process.env.MONGO_URI || credentials.mongo.uri,
|
||||||
user: process.env.MONGO_USER || credentials.mongoUser,
|
user: process.env.MONGO_USER || credentials.mongo.user,
|
||||||
password: process.env.MONGO_PASS || credentials.mongoPass
|
password: process.env.MONGO_PASS || credentials.mongo.pass
|
||||||
});
|
});
|
||||||
yield mongo.connect();
|
yield mongo.connect();
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user