From 6ec72aef066631b5d7eba3074be7f95e5d9e89c8 Mon Sep 17 00:00:00 2001 From: Tankred Hase Date: Sat, 19 Aug 2017 17:06:36 +0800 Subject: [PATCH] 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 --- package.json | 1 - src/email/email.js | 87 ++++++++++++++++------------------ src/email/templates.js | 13 +++++ src/email/templates.json | 12 ----- src/service/public-key.js | 2 +- test/integration/email-test.js | 6 +-- 6 files changed, 57 insertions(+), 64 deletions(-) create mode 100644 src/email/templates.js delete mode 100644 src/email/templates.json diff --git a/package.json b/package.json index b526d4c..2b17853 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "koa-static": "^4.0.1", "mongodb": "^2.2.31", "nodemailer": "^2.4.2", - "nodemailer-openpgp": "^1.0.2", "openpgp": "^2.3.0", "winston": "^2.3.1", "winston-papertrail": "^1.0.5" diff --git a/src/email/email.js b/src/email/email.js index 86ee2a8..8e8c2a4 100644 --- a/src/email/email.js +++ b/src/email/email.js @@ -19,8 +19,8 @@ const log = require('winston'); const util = require('../service/util'); +const openpgp = require('openpgp'); const nodemailer = require('nodemailer'); -const openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt; /** * A simple wrapper around Nodemailer to send verification emails @@ -44,70 +44,63 @@ class Email { secure: (tls !== undefined) ? util.isTrue(tls) : true, requireTLS: (starttls !== undefined) ? util.isTrue(starttls) : true, }); - if (util.isTrue(pgp)) { - this._transport.use('stream', openpgpEncrypt()); - } + this._usePGPEncryption = util.isTrue(pgp); this._sender = sender; } /** * Send the verification email to the user using a template. - * @param {Object} template the email template to use - * @param {Object} userId user id document + * @param {Object} template the email template function to use + * @param {Object} userId recipient user id object: { name:'Jon Smith', email:'j@smith.com', publicKeyArmored:'...' } * @param {string} keyId key id of public key * @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}) { - const message = { - from: this._sender, - to: userId, - subject: template.subject, - text: template.text, - html: template.html, - params: { - name: userId.name, - baseUrl: util.url(origin), - keyId, - nonce: userId.nonce - } + const compiled = template({ + name: userId.name, + baseUrl: util.url(origin), + keyId, + 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. - * @param {Object} from sender user id object: { name:'Jon Smith', email:'j@smith.com' } - * @param {Object} to recipient 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 object: { name:'Jon Smith', address:'j@smith.com' } * @param {string} subject message subject - * @param {string} text message plaintext body template - * @param {string} html message html body template - * @param {Object} params (optional) nodermailer template parameters + * @param {string} text message text body + * @param {string} html message html body * @yield {Object} reponse object containing SMTP info */ - async _sendHelper({from, to, subject, text, html, params = {}}) { - 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 - } - }; - + async _sendHelper(sendOptions) { try { - const sendFn = this._transport.templateSender(template, sender); - const info = await sendFn(recipient, params); + const info = await this._transport.sendMail(sendOptions); if (!this._checkResponse(info)) { log.warn('email', 'Message may not have been received.', info); } diff --git a/src/email/templates.js b/src/email/templates.js new file mode 100644 index 0000000..db9f8b0 --- /dev/null +++ b/src/email/templates.js @@ -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: `

Hello ${name},

please click here to verify your key.

` +}); + +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: `

Hello ${name},

please click here to verify the removal of your key.

` +}); diff --git a/src/email/templates.json b/src/email/templates.json deleted file mode 100644 index 533a707..0000000 --- a/src/email/templates.json +++ /dev/null @@ -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": "

Hello {{name}},

please click here to verify your key.

" - }, - "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": "

Hello {{name}},

please click here to verify the removal of your key.

" - } -} \ No newline at end of file diff --git a/src/service/public-key.js b/src/service/public-key.js index 4f78949..a311081 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -19,7 +19,7 @@ const config = require('config'); const util = require('./util'); -const tpl = require('../email/templates.json'); +const tpl = require('../email/templates'); /** * Database documents have the format: diff --git a/test/integration/email-test.js b/test/integration/email-test.js index e3afc2d..9540565 100644 --- a/test/integration/email-test.js +++ b/test/integration/email-test.js @@ -2,7 +2,7 @@ const config = require('config'); const Email = require('../../src/email/email'); -const tpl = require('../../src/email/templates.json'); +const tpl = require('../../src/email/templates'); describe('Email Integration Tests', function() { this.timeout(20000); @@ -38,8 +38,8 @@ describe('Email Integration Tests', function() { describe("_sendHelper", () => { it('should work', async () => { const mailOptions = { - from: email._sender, - to: recipient, + from: {name: email._sender.name, address: email._sender.email}, + to: {name: recipient.name, address: recipient.email}, subject: 'Hello ✔', // Subject line text: 'Hello world 🐴', // plaintext body html: 'Hello world 🐴' // html body