Send email message with PGP inline not PGP/MIME

* Use OpenPGP.js directly instead of nodemailer-openpgp plugin
* Use native ES6 string templates instead of nodemailer template engine
This commit is contained in:
Tankred Hase 2017-08-19 17:06:36 +08:00 committed by Martin Hauck
parent 656aa9b6ed
commit 6ec72aef06
No known key found for this signature in database
GPG Key ID: 16A77ADCADE027B1
6 changed files with 57 additions and 64 deletions

View File

@ -28,7 +28,6 @@
"koa-static": "^4.0.1", "koa-static": "^4.0.1",
"mongodb": "^2.2.31", "mongodb": "^2.2.31",
"nodemailer": "^2.4.2", "nodemailer": "^2.4.2",
"nodemailer-openpgp": "^1.0.2",
"openpgp": "^2.3.0", "openpgp": "^2.3.0",
"winston": "^2.3.1", "winston": "^2.3.1",
"winston-papertrail": "^1.0.5" "winston-papertrail": "^1.0.5"

View File

@ -19,8 +19,8 @@
const log = require('winston'); const log = require('winston');
const util = require('../service/util'); const util = require('../service/util');
const openpgp = require('openpgp');
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt;
/** /**
* A simple wrapper around Nodemailer to send verification emails * A simple wrapper around Nodemailer to send verification emails
@ -44,70 +44,63 @@ class Email {
secure: (tls !== undefined) ? util.isTrue(tls) : true, secure: (tls !== undefined) ? util.isTrue(tls) : true,
requireTLS: (starttls !== undefined) ? util.isTrue(starttls) : true, requireTLS: (starttls !== undefined) ? util.isTrue(starttls) : true,
}); });
if (util.isTrue(pgp)) { this._usePGPEncryption = util.isTrue(pgp);
this._transport.use('stream', openpgpEncrypt());
}
this._sender = sender; this._sender = sender;
} }
/** /**
* Send the verification email to the user using a template. * Send the verification email to the user using a template.
* @param {Object} template the email template to use * @param {Object} template the email template function to use
* @param {Object} userId user id document * @param {Object} userId recipient user id object: { name:'Jon Smith', email:'j@smith.com', publicKeyArmored:'...' }
* @param {string} keyId key id of public key * @param {string} keyId key id of public key
* @param {Object} origin origin of the server * @param {Object} origin origin of the server
* @yield {Object} send response from the SMTP server * @yield {Object} reponse object containing SMTP info
*/ */
async send({template, userId, keyId, origin}) { async send({template, userId, keyId, origin}) {
const message = { const compiled = template({
from: this._sender,
to: userId,
subject: template.subject,
text: template.text,
html: template.html,
params: {
name: userId.name, name: userId.name,
baseUrl: util.url(origin), baseUrl: util.url(origin),
keyId, keyId,
nonce: userId.nonce nonce: userId.nonce
});
if (this._usePGPEncryption && userId.publicKeyArmored) {
compiled.text = await this._pgpEncrypt(compiled.text, userId.publicKeyArmored);
} }
const sendOptions = {
from: {name: this._sender.name, address: this._sender.email},
to: {name: userId.name, address: userId.email},
subject: compiled.subject,
text: compiled.text
}; };
return this._sendHelper(message); return this._sendHelper(sendOptions);
}
/**
* Encrypt the message body using OpenPGP.js
* @param {string} plaintext the plaintex message body
* @param {string} publicKeyArmored the recipient's public key
* @return {string} the encrypted PGP message block
*/
async _pgpEncrypt(plaintext, publicKeyArmored) {
const ciphertext = await openpgp.encrypt({
data: plaintext,
publicKeys: openpgp.key.readArmored(publicKeyArmored).keys,
});
return ciphertext.data;
} }
/** /**
* A generic method to send an email message via nodemailer. * A generic method to send an email message via nodemailer.
* @param {Object} from sender user id object: { name:'Jon Smith', email:'j@smith.com' } * @param {Object} from sender object: { name:'Jon Smith', address:'j@smith.com' }
* @param {Object} to recipient user id object: { name:'Jon Smith', email:'j@smith.com' } * @param {Object} to recipient object: { name:'Jon Smith', address:'j@smith.com' }
* @param {string} subject message subject * @param {string} subject message subject
* @param {string} text message plaintext body template * @param {string} text message text body
* @param {string} html message html body template * @param {string} html message html body
* @param {Object} params (optional) nodermailer template parameters
* @yield {Object} reponse object containing SMTP info * @yield {Object} reponse object containing SMTP info
*/ */
async _sendHelper({from, to, subject, text, html, params = {}}) { async _sendHelper(sendOptions) {
const template = {
subject,
text,
html,
encryptionKeys: [to.publicKeyArmored]
};
const sender = {
from: {
name: from.name,
address: from.email
}
};
const recipient = {
to: {
name: to.name,
address: to.email
}
};
try { try {
const sendFn = this._transport.templateSender(template, sender); const info = await this._transport.sendMail(sendOptions);
const info = await sendFn(recipient, params);
if (!this._checkResponse(info)) { if (!this._checkResponse(info)) {
log.warn('email', 'Message may not have been received.', info); log.warn('email', 'Message may not have been received.', info);
} }

13
src/email/templates.js Normal file
View File

@ -0,0 +1,13 @@
'use strict';
exports.verifyKey = ({name, baseUrl, keyId, nonce}) => ({
subject: `Verify Your Key`,
text: `Hello ${name},\n\nplease click here to verify your key:\n\n${baseUrl}/api/v1/key?op=verify&keyId=${keyId}&nonce=${nonce}`,
html: `<p>Hello ${name},</p><p>please <a href=\"${baseUrl}/api/v1/key?op=verify&keyId=${keyId}&nonce=${nonce}\">click here to verify</a> your key.</p>`
});
exports.verifyRemove = ({name, baseUrl, keyId, nonce}) => ({
subject: `Verify Key Removal`,
text: `Hello ${name},\n\nplease click here to verify the removal of your key:\n\n${baseUrl}/api/v1/key?op=verifyRemove&keyId=${keyId}&nonce=${nonce}`,
html: `<p>Hello ${name},</p><p>please <a href=\"${baseUrl}/api/v1/key?op=verifyRemove&keyId=${keyId}&nonce=${nonce}\">click here to verify</a> the removal of your key.</p>`
});

View File

@ -1,12 +0,0 @@
{
"verifyKey": {
"subject": "Verify Your Key",
"text": "Hello {{name}},\n\nplease click here to verify your key:\n\n{{baseUrl}}/api/v1/key?op=verify&keyId={{keyId}}&nonce={{nonce}}",
"html": "<p>Hello {{name}},</p><p>please <a href=\"{{baseUrl}}/api/v1/key?op=verify&keyId={{keyId}}&nonce={{nonce}}\">click here to verify</a> your key.</p>"
},
"verifyRemove": {
"subject": "Verify Key Removal",
"text": "Hello {{name}},\n\nplease click here to verify the removal of your key:\n\n{{baseUrl}}/api/v1/key?op=verifyRemove&keyId={{keyId}}&nonce={{nonce}}",
"html": "<p>Hello {{name}},</p><p>please <a href=\"{{baseUrl}}/api/v1/key?op=verifyRemove&keyId={{keyId}}&nonce={{nonce}}\">click here to verify</a> the removal of your key.</p>"
}
}

View File

@ -19,7 +19,7 @@
const config = require('config'); const config = require('config');
const util = require('./util'); const util = require('./util');
const tpl = require('../email/templates.json'); const tpl = require('../email/templates');
/** /**
* Database documents have the format: * Database documents have the format:

View File

@ -2,7 +2,7 @@
const config = require('config'); const config = require('config');
const Email = require('../../src/email/email'); const Email = require('../../src/email/email');
const tpl = require('../../src/email/templates.json'); const tpl = require('../../src/email/templates');
describe('Email Integration Tests', function() { describe('Email Integration Tests', function() {
this.timeout(20000); this.timeout(20000);
@ -38,8 +38,8 @@ describe('Email Integration Tests', function() {
describe("_sendHelper", () => { describe("_sendHelper", () => {
it('should work', async () => { it('should work', async () => {
const mailOptions = { const mailOptions = {
from: email._sender, from: {name: email._sender.name, address: email._sender.email},
to: recipient, to: {name: recipient.name, address: recipient.email},
subject: 'Hello ✔', // Subject line subject: 'Hello ✔', // Subject line
text: 'Hello world 🐴', // plaintext body text: 'Hello world 🐴', // plaintext body
html: '<b>Hello world 🐴</b>' // html body html: '<b>Hello world 🐴</b>' // html body