├── .gitignore ├── .npmignore ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ ├── test.yml │ └── release.yaml ├── .prettierrc.js ├── .ncurc.js ├── CHANGELOG.md ├── Gruntfile.js ├── test ├── fixtures │ ├── test_public.pem │ ├── test_private.pem │ ├── test_private.key │ ├── test_private_1024bit.key │ └── test_private_2048bit.key └── nodemailer-openpgp-test.js ├── package.json ├── README.md ├── LICENSE └── lib └── nodemailer-openpgp.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | .eslintrc.js 3 | Gruntfile.js 4 | test 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "nodemailer", 3 | "parserOptions": { 4 | "ecmaVersion": 2020 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [andris9] # enable once enrolled 4 | custom: ['https://www.paypal.me/nodemailer'] 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 160, 3 | tabWidth: 4, 4 | singleQuote: true, 5 | endOfLine: 'lf', 6 | trailingComma: 'none', 7 | arrowParens: 'avoid' 8 | }; 9 | -------------------------------------------------------------------------------- /.ncurc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | upgrade: true, 3 | reject: [ 4 | // api changes, check and fix 5 | 'eslint', 6 | 'grunt-eslint', 7 | 'chai' 8 | ] 9 | }; 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.2.1](https://github.com/nodemailer/nodemailer-openpgp/compare/v2.2.0...v2.2.1) (2024-05-26) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **deploy:** Added autodeployment, bumped dependencies ([fa6ff1b](https://github.com/nodemailer/nodemailer-openpgp/commit/fa6ff1bf638903550ae433e90158e7ea4cec2c89)) 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | node: [16.x, 18.x, 20.x] 12 | os: [ubuntu-latest] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js ${{ matrix.node }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | eslint: { 8 | all: ['lib/*.js', 'test/*.js', 'Gruntfile.js', '.eslintrc.js'] 9 | }, 10 | 11 | mochaTest: { 12 | all: { 13 | options: { 14 | reporter: 'spec' 15 | }, 16 | src: ['test/*-test.js'] 17 | } 18 | } 19 | }); 20 | 21 | // Load the plugin(s) 22 | grunt.loadNpmTasks('grunt-eslint'); 23 | grunt.loadNpmTasks('grunt-mocha-test'); 24 | 25 | // Tasks 26 | grunt.registerTask('default', ['eslint', 'mochaTest']); 27 | }; 28 | -------------------------------------------------------------------------------- /test/fixtures/test_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: BCPG C# v1.6.1.0 3 | 4 | mQENBFcST/wBCACGhyFVJ2B6sqlYXDW2KCtPw/wZQ6CS4W+tLYx9ufg5liC8Zpf3 5 | VXWyKqCx8c0YlW3Cx9JD20eITHwxc4l4RLB4NKVXqq5rlNTcpsDzM+9TT7+Q74eE 6 | 8jj+mbHS/XcgHnbuoIlH2AYckmXeX0nrRISEz5R1Nt+7xYQPVbzMTYkXdvRG2oIn 7 | 7lezAVcy9Tc+x0GPJA4kwX43qD1i2T+ttQVFJlfQpmR1x1Wj9y2zVMTz9vipCFjn 8 | tsKJjDqEuLUJ0jpJFnlWjK5f+2Ks6ZnGJrJIkB6JQe6O5kAJGOOJDusBvjHr8c1l 9 | 5nm8+E9246gnNOUVwUUyKn8EUmQ+Pu6LA4M3ABEBAAG0G2RvLW5vdC10cnVzdEBu 10 | b2RlbWFpbGVyLmNvbYkBHAQQAQIABgUCVxJP/AAKCRDloRhfvnt1lftfB/9n54/7 11 | 6LbsroMwyr74W/oyqegQV9NOKno0V672VdwKBvZ2+5QbriufbupmnYePOVmrPr0R 12 | tlz5FvnZmeu0UGvKpSJYIcgmLEA4OmyMod850pEm6uuJ9vuslLziaXAIgJRb5sdT 13 | mEL/9lBnF7wySyIikkctBKUyarJQlneQpUIl6zlqSAMdnG/J4sn/+3mKBDECZdB+ 14 | lS9FJQ2anm8883n6ItTgtzaUnocdV7xQTE68kOqQ8ETkKSCZNIJ8QvDvCTY8yA2a 15 | fYekdcNF9/HUBtiUd90ubm1edD6gB8V3aVdJgqf10Lh5BasOHEaEs7klotaBaa2+ 16 | nwIdVPgpkDuKuOhu 17 | =s48h 18 | -----END PGP PUBLIC KEY BLOCK----- 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodemailer-openpgp", 3 | "version": "2.2.1", 4 | "description": "Encrypt Nodemailer messages with PGP", 5 | "main": "lib/nodemailer-openpgp", 6 | "scripts": { 7 | "test": "grunt", 8 | "update": "rm -rf node_modules package-lock.json && ncu -u && npm install" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git://github.com/nodemailer/nodemailer-openpgp.git" 13 | }, 14 | "keywords": [ 15 | "DKIM", 16 | "Nodemailer" 17 | ], 18 | "author": "Andris Reinman", 19 | "license": "LGPL-3.0", 20 | "bugs": { 21 | "url": "https://github.com/nodemailer/nodemailer-openpgp/issues" 22 | }, 23 | "homepage": "https://github.com/nodemailer/nodemailer-openpgp", 24 | "engines": { 25 | "node": ">=14" 26 | }, 27 | "dependencies": { 28 | "openpgp": "5.11.1" 29 | }, 30 | "devDependencies": { 31 | "chai": "4.4.1", 32 | "eslint-config-nodemailer": "1.2.0", 33 | "grunt": "1.6.1", 34 | "grunt-cli": "1.4.3", 35 | "grunt-eslint": "24.3.0", 36 | "grunt-mocha-test": "0.13.3", 37 | "mocha": "10.4.0", 38 | "nodemailer": "6.9.13", 39 | "nodemailer-stub-transport": "1.1.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | id-token: write 10 | 11 | name: release 12 | jobs: 13 | release-please: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: google-github-actions/release-please-action@v3 17 | id: release 18 | with: 19 | release-type: node 20 | package-name: ${{vars.NPM_MODULE_NAME}} 21 | pull-request-title-pattern: 'chore${scope}: release ${version} [skip-ci]' 22 | # The logic below handles the npm publication: 23 | - uses: actions/checkout@v3 24 | # these if statements ensure that a publication only occurs when 25 | # a new release is created: 26 | if: ${{ steps.release.outputs.release_created }} 27 | - uses: actions/setup-node@v3 28 | with: 29 | node-version: 20 30 | registry-url: 'https://registry.npmjs.org' 31 | if: ${{ steps.release.outputs.release_created }} 32 | - run: npm ci 33 | if: ${{ steps.release.outputs.release_created }} 34 | - run: npm publish --provenance --access public 35 | env: 36 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 37 | if: ${{ steps.release.outputs.release_created }} 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenPGP module for Nodemailer 2 | 3 | This module allows you to send PGP encrypted and/or signed messages using Nodemailer. 4 | Generated messages are in PGP/MIME format. 5 | 6 | [![Build Status](https://travis-ci.org/nodemailer/nodemailer-openpgp.svg?branch=master)](https://travis-ci.org/nodemailer/nodemailer-openpgp) 7 | 8 | ## Install 9 | 10 | Install from npm 11 | 12 | npm install nodemailer-openpgp --save 13 | 14 | ## Usage 15 | 16 | Load the `openpgpEncrypt` function 17 | 18 | ```javascript 19 | var openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt; 20 | ``` 21 | 22 | Attach it as a 'stream' handler for a nodemailer transport object 23 | 24 | ```javascript 25 | transporter.use('stream', openpgpEncrypt(options)); 26 | ``` 27 | 28 | Where 29 | 30 | * **options** includes the following optional options for signing messages 31 | * **signingKey** is an optional PGP private key for signing the (encrypted) message. If this value is not given then messages are not signed 32 | * **passphrase** is the optional passphrase for the signing key in case it is encrypted 33 | 34 | To encrypt outgoing messages add `encryptionKeys` array that holds the public keys used to encrypt the message. 35 | To not sign an outgoing message set `shouldSign` to `false`. 36 | 37 | ## Example 38 | 39 | ```javascript 40 | var nodemailer = require('nodemailer'); 41 | var transporter = nodemailer.createTransport(); 42 | var openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt; 43 | transporter.use('stream', openpgpEncrypt()); 44 | transporter.sendMail({ 45 | from: 'sender@address', 46 | to: 'receiver@address', 47 | subject: 'hello', 48 | text: 'hello world!', 49 | encryptionKeys: ['-----BEGIN PGP PUBLIC KEY BLOCK-----…'], 50 | shouldSign: true 51 | }, function(err, response) { 52 | console.log(err || response); 53 | }); 54 | ``` 55 | 56 | ## License 57 | 58 | **LGPL-3.0** 59 | -------------------------------------------------------------------------------- /test/fixtures/test_private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | Version: BCPG C# v1.6.1.0 3 | 4 | lQOsBFcST/wBCACGhyFVJ2B6sqlYXDW2KCtPw/wZQ6CS4W+tLYx9ufg5liC8Zpf3 5 | VXWyKqCx8c0YlW3Cx9JD20eITHwxc4l4RLB4NKVXqq5rlNTcpsDzM+9TT7+Q74eE 6 | 8jj+mbHS/XcgHnbuoIlH2AYckmXeX0nrRISEz5R1Nt+7xYQPVbzMTYkXdvRG2oIn 7 | 7lezAVcy9Tc+x0GPJA4kwX43qD1i2T+ttQVFJlfQpmR1x1Wj9y2zVMTz9vipCFjn 8 | tsKJjDqEuLUJ0jpJFnlWjK5f+2Ks6ZnGJrJIkB6JQe6O5kAJGOOJDusBvjHr8c1l 9 | 5nm8+E9246gnNOUVwUUyKn8EUmQ+Pu6LA4M3ABEBAAH/AwMCzF2GRxXMeg9goneT 10 | PfUa5ZbwoFgrJ1DRXFzUq/x7p7MG+GwGdm9881gwpHJbSWAvdtpXnxI9CfyoEWsV 11 | Mkw8LlCHvmYl7Yxdo21XMcYDRJDM48RL0NVNgDJo3OzjQ5Ac1F0ry86lxE8r7PNx 12 | 86BWbos4+KMS8RgrX9FletF1SKLydnb1unW+Z2Largv5lrGQlQ4n+6n0zhVPiyLC 13 | 6nhMbo4gEJ2Chx/w9tfBCwthc0/Uo18oH4mvUGJWuErbltbG7YLNiCPyf44TELUc 14 | ytQMLn91tAsVYhnecuLa7RWaRDO7l5iuCZ4llZAcaA81wyixSTWhgWfC9F98z9cz 15 | vGqCPneif7+WHqFeT4tyodFzd5bENLyfb5LHtpsH1FGa1bbdcNztJ+dg5CtcNtBE 16 | 4rDrEmaoZUr/rM+xDbN7tH5gslOKfXzXHi0pjby2w8ZJYi/ykwiREPRzT6DENyFU 17 | 3lpVbj2n+IvSzDGWXyy2XOUkcsLtSlWO5SteoYzq0Xa7R2doJYQrCvbd05Abu6Dr 18 | Gd+hu3CylbD+ep8G25+8UUB17nPMNSavJJuVYPJNQRP1Ka4TKZY1piAkMjp7neQb 19 | 2hWDnpyI2o9kWnZ1IQFC5z2EF4epJikC4dUIH+gT+wvA99ePp+wJ0i8ClKzBPjfJ 20 | 3jjiNyn04q6iomRMI6i3ELd6Tp3LlhuFBHnrL3dNM/32AwIdv8m7iMIrrzFAQH4G 21 | YarkQkNHh3tesX98dtgBrOQRhmN5Raw1gJKTQ0NC5b78qWTq95iUx/WNaByo6iVi 22 | b8tv/6yxOM5UbveSXZL5H11VIPp/cnPBfuWfErastApBNN6WIJYKUzsOwyS/R4uM 23 | QCdjWT6gPr7HdY9KhmNUZfaD+gtXO7pwapKfunCXobQbZG8tbm90LXRydXN0QG5v 24 | ZGVtYWlsZXIuY29tiQEcBBABAgAGBQJXEk/8AAoJEOWhGF++e3WV+18H/2fnj/vo 25 | tuyugzDKvvhb+jKp6BBX004qejRXrvZV3AoG9nb7lBuuK59u6madh485Was+vRG2 26 | XPkW+dmZ67RQa8qlIlghyCYsQDg6bIyh3znSkSbq64n2+6yUvOJpcAiAlFvmx1OY 27 | Qv/2UGcXvDJLIiKSRy0EpTJqslCWd5ClQiXrOWpIAx2cb8niyf/7eYoEMQJl0H6V 28 | L0UlDZqebzzzefoi1OC3NpSehx1XvFBMTryQ6pDwROQpIJk0gnxC8O8JNjzIDZp9 29 | h6R1w0X38dQG2JR33S5ubV50PqAHxXdpV0mCp/XQuHkFqw4cRoSzuSWi1oFprb6f 30 | Ah1U+CmQO4q46G4= 31 | =fgp5 32 | -----END PGP PRIVATE KEY BLOCK----- 33 | -------------------------------------------------------------------------------- /test/fixtures/test_private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | Version: GnuPG v2.0.19 (GNU/Linux) 3 | 4 | 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 37 | =lw5e 38 | -----END PGP PRIVATE KEY BLOCK----- -------------------------------------------------------------------------------- /test/fixtures/test_private_1024bit.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | Version: GnuPG v2.0.19 (GNU/Linux) 3 | 4 | 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 37 | =lw5e 38 | -----END PGP PRIVATE KEY BLOCK----- -------------------------------------------------------------------------------- /test/fixtures/test_private_2048bit.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | 3 | 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 58 | =P+r0 59 | -----END PGP PRIVATE KEY BLOCK----- -------------------------------------------------------------------------------- /test/nodemailer-openpgp-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions, no-invalid-this */ 2 | 3 | 'use strict'; 4 | 5 | const chai = require('chai'); 6 | const nodemailerOpenpgp = require('../lib/nodemailer-openpgp'); 7 | const stubTransport = require('nodemailer-stub-transport'); 8 | const nodemailer = require('nodemailer'); 9 | const fs = require('fs'); 10 | 11 | const expect = chai.expect; 12 | chai.config.includeStack = true; 13 | 14 | describe('nodemailer-openpgp tests', () => { 15 | it('should add an encrypt message', done => { 16 | let mail = 'From: andris@node.ee\r\nTo:andris@kreata.ee\r\nSubject:\r\n Hello!\r\nContent-Type: text/plain\r\n\r\nHello world!'; 17 | 18 | let signer = new nodemailerOpenpgp.Encrypter({ 19 | signingKey: fs.readFileSync(__dirname + '/fixtures/test_private_2048bit.key'), 20 | passphrase: 'hello world', 21 | encryptionKeys: [].concat(fs.readFileSync(__dirname + '/fixtures/test_public.pem') || []) 22 | }); 23 | 24 | let chunks = []; 25 | 26 | signer.on('data', chunk => { 27 | chunks.push(chunk); 28 | }); 29 | 30 | signer.on('err', err => { 31 | expect(err).to.not.exist; 32 | }); 33 | 34 | signer.on('end', () => { 35 | let message = Buffer.concat(chunks).toString('utf-8'); 36 | expect(message).to.exist; 37 | expect(message.indexOf('This is an OpenPGP/MIME encrypted message')).to.be.gte(0); 38 | done(); 39 | }); 40 | 41 | signer.end(mail); 42 | }); 43 | 44 | it('should use keys provided by mail options', done => { 45 | let transport = nodemailer.createTransport(stubTransport()); 46 | let openpgpEncrypt = nodemailerOpenpgp.openpgpEncrypt; 47 | transport.use( 48 | 'stream', 49 | openpgpEncrypt({ 50 | signingKey: fs.readFileSync(__dirname + '/fixtures/test_private_2048bit.key'), 51 | passphrase: 'hello world' 52 | }) 53 | ); 54 | 55 | let mailOptions = { 56 | from: 'sender@example.com', 57 | to: 'receiver@example.com', 58 | subject: 'hello world!', 59 | text: 'Hello text!', 60 | html: 'Hello html!', 61 | encryptionKeys: [].concat(fs.readFileSync(__dirname + '/fixtures/test_public.pem') || []) 62 | }; 63 | 64 | transport.sendMail(mailOptions, (err, info) => { 65 | expect(err).to.not.exist; 66 | expect(info.response).to.exist; 67 | expect(info.response.toString().indexOf('This is an OpenPGP/MIME encrypted message')).to.be.gte(0); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('should not encrypt if no keys provided', done => { 73 | let transport = nodemailer.createTransport(stubTransport()); 74 | let openpgpEncrypt = nodemailerOpenpgp.openpgpEncrypt; 75 | transport.use('stream', openpgpEncrypt({})); 76 | 77 | let mailOptions = { 78 | from: 'sender@example.com', 79 | to: 'receiver@example.com', 80 | subject: 'hello world!', 81 | text: 'Hello text!', 82 | html: 'Hello html!' 83 | }; 84 | 85 | transport.sendMail(mailOptions, (err, info) => { 86 | expect(err).to.not.exist; 87 | expect(info.response).to.exist; 88 | expect(info.response.toString().indexOf('This is an OpenPGP/MIME encrypted message')).to.be.lte(0); 89 | done(); 90 | }); 91 | }); 92 | 93 | it('should sign only', done => { 94 | let transport = nodemailer.createTransport(stubTransport()); 95 | let openpgpEncrypt = nodemailerOpenpgp.openpgpEncrypt; 96 | transport.use( 97 | 'stream', 98 | openpgpEncrypt({ 99 | signingKey: fs.readFileSync(__dirname + '/fixtures/test_private_2048bit.key'), 100 | passphrase: 'hello world' 101 | }) 102 | ); 103 | 104 | let mailOptions = { 105 | from: 'sender@example.com', 106 | to: 'receiver@example.com', 107 | subject: 'hello world!', 108 | text: 'Hello text!', 109 | html: 'Hello html!' 110 | }; 111 | 112 | transport.sendMail(mailOptions, (err, info) => { 113 | expect(err).to.not.exist; 114 | expect(info.response).to.exist; 115 | expect(info.response.toString().indexOf('Content-Description: OpenPGP signed message')).to.be.gte(0); 116 | done(); 117 | }); 118 | }); 119 | 120 | it('should disable sign', done => { 121 | let transport = nodemailer.createTransport(stubTransport()); 122 | let openpgpEncrypt = nodemailerOpenpgp.openpgpEncrypt; 123 | transport.use( 124 | 'stream', 125 | openpgpEncrypt({ 126 | signingKey: fs.readFileSync(__dirname + '/fixtures/test_private_2048bit.key'), 127 | passphrase: 'hello world' 128 | }) 129 | ); 130 | 131 | let mailOptions = { 132 | from: 'sender@example.com', 133 | to: 'receiver@example.com', 134 | subject: 'hello world!', 135 | text: 'Hello text!', 136 | html: 'Hello html!', 137 | shouldSign: false 138 | }; 139 | 140 | transport.sendMail(mailOptions, (err, info) => { 141 | expect(err).to.not.exist; 142 | expect(info.response).to.exist; 143 | expect(info.response.toString().indexOf('Content-Description: OpenPGP signed message')).to.be.lte(0); 144 | done(); 145 | }); 146 | }); 147 | 148 | it('should reject weak keys', done => { 149 | let transport = nodemailer.createTransport(stubTransport()); 150 | let openpgpEncrypt = nodemailerOpenpgp.openpgpEncrypt; 151 | transport.use( 152 | 'stream', 153 | openpgpEncrypt({ 154 | signingKey: fs.readFileSync(__dirname + '/fixtures/test_private_1024bit.key'), 155 | passphrase: 'hello world' 156 | }) 157 | ); 158 | 159 | let mailOptions = { 160 | from: 'sender@example.com', 161 | to: 'receiver@example.com', 162 | subject: 'hello world!', 163 | text: 'Hello text!', 164 | html: 'Hello html!', 165 | encryptionKeys: [].concat(fs.readFileSync(__dirname + '/fixtures/test_public.pem') || []) 166 | }; 167 | 168 | transport.sendMail(mailOptions, err => { 169 | expect(err).to.exist; 170 | expect(err.toString().indexOf('RSA keys shorter than 2047 bits are considered too weak')).to.be.gte(0); 171 | done(); 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | This version of the GNU Lesser General Public License incorporates 9 | the terms and conditions of version 3 of the GNU General Public 10 | License, supplemented by the additional permissions listed below. 11 | 12 | 0. Additional Definitions. 13 | 14 | As used herein, "this License" refers to version 3 of the GNU Lesser 15 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 16 | General Public License. 17 | 18 | "The Library" refers to a covered work governed by this License, 19 | other than an Application or a Combined Work as defined below. 20 | 21 | An "Application" is any work that makes use of an interface provided 22 | by the Library, but which is not otherwise based on the Library. 23 | Defining a subclass of a class defined by the Library is deemed a mode 24 | of using an interface provided by the Library. 25 | 26 | A "Combined Work" is a work produced by combining or linking an 27 | Application with the Library. The particular version of the Library 28 | with which the Combined Work was made is also called the "Linked 29 | Version". 30 | 31 | The "Minimal Corresponding Source" for a Combined Work means the 32 | Corresponding Source for the Combined Work, excluding any source code 33 | for portions of the Combined Work that, considered in isolation, are 34 | based on the Application, and not on the Linked Version. 35 | 36 | The "Corresponding Application Code" for a Combined Work means the 37 | object code and/or source code for the Application, including any data 38 | and utility programs needed for reproducing the Combined Work from the 39 | Application, but excluding the System Libraries of the Combined Work. 40 | 41 | 1. Exception to Section 3 of the GNU GPL. 42 | 43 | You may convey a covered work under sections 3 and 4 of this License 44 | without being bound by section 3 of the GNU GPL. 45 | 46 | 2. Conveying Modified Versions. 47 | 48 | If you modify a copy of the Library, and, in your modifications, a 49 | facility refers to a function or data to be supplied by an Application 50 | that uses the facility (other than as an argument passed when the 51 | facility is invoked), then you may convey a copy of the modified 52 | version: 53 | 54 | a) under this License, provided that you make a good faith effort to 55 | ensure that, in the event an Application does not supply the 56 | function or data, the facility still operates, and performs 57 | whatever part of its purpose remains meaningful, or 58 | 59 | b) under the GNU GPL, with none of the additional permissions of 60 | this License applicable to that copy. 61 | 62 | 3. Object Code Incorporating Material from Library Header Files. 63 | 64 | The object code form of an Application may incorporate material from 65 | a header file that is part of the Library. You may convey such object 66 | code under terms of your choice, provided that, if the incorporated 67 | material is not limited to numerical parameters, data structure 68 | layouts and accessors, or small macros, inline functions and templates 69 | (ten or fewer lines in length), you do both of the following: 70 | 71 | a) Give prominent notice with each copy of the object code that the 72 | Library is used in it and that the Library and its use are 73 | covered by this License. 74 | 75 | b) Accompany the object code with a copy of the GNU GPL and this license 76 | document. 77 | 78 | 4. Combined Works. 79 | 80 | You may convey a Combined Work under terms of your choice that, 81 | taken together, effectively do not restrict modification of the 82 | portions of the Library contained in the Combined Work and reverse 83 | engineering for debugging such modifications, if you also do each of 84 | the following: 85 | 86 | a) Give prominent notice with each copy of the Combined Work that 87 | the Library is used in it and that the Library and its use are 88 | covered by this License. 89 | 90 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 91 | document. 92 | 93 | c) For a Combined Work that displays copyright notices during 94 | execution, include the copyright notice for the Library among 95 | these notices, as well as a reference directing the user to the 96 | copies of the GNU GPL and this license document. 97 | 98 | d) Do one of the following: 99 | 100 | 0) Convey the Minimal Corresponding Source under the terms of this 101 | License, and the Corresponding Application Code in a form 102 | suitable for, and under terms that permit, the user to 103 | recombine or relink the Application with a modified version of 104 | the Linked Version to produce a modified Combined Work, in the 105 | manner specified by section 6 of the GNU GPL for conveying 106 | Corresponding Source. 107 | 108 | 1) Use a suitable shared library mechanism for linking with the 109 | Library. A suitable mechanism is one that (a) uses at run time 110 | a copy of the Library already present on the user's computer 111 | system, and (b) will operate properly with a modified version 112 | of the Library that is interface-compatible with the Linked 113 | Version. 114 | 115 | e) Provide Installation Information, but only if you would otherwise 116 | be required to provide such information under section 6 of the 117 | GNU GPL, and only to the extent that such information is 118 | necessary to install and execute a modified version of the 119 | Combined Work produced by recombining or relinking the 120 | Application with a modified version of the Linked Version. (If 121 | you use option 4d0, the Installation Information must accompany 122 | the Minimal Corresponding Source and Corresponding Application 123 | Code. If you use option 4d1, you must provide the Installation 124 | Information in the manner specified by section 6 of the GNU GPL 125 | for conveying Corresponding Source.) 126 | 127 | 5. Combined Libraries. 128 | 129 | You may place library facilities that are a work based on the 130 | Library side by side in a single library together with other library 131 | facilities that are not Applications and are not covered by this 132 | License, and convey such a combined library under terms of your 133 | choice, if you do both of the following: 134 | 135 | a) Accompany the combined library with a copy of the same work based 136 | on the Library, uncombined with any other library facilities, 137 | conveyed under the terms of this License. 138 | 139 | b) Give prominent notice with the combined library that part of it 140 | is a work based on the Library, and explaining where to find the 141 | accompanying uncombined form of the same work. 142 | 143 | 6. Revised Versions of the GNU Lesser General Public License. 144 | 145 | The Free Software Foundation may publish revised and/or new versions 146 | of the GNU Lesser General Public License from time to time. Such new 147 | versions will be similar in spirit to the present version, but may 148 | differ in detail to address new problems or concerns. 149 | 150 | Each version is given a distinguishing version number. If the 151 | Library as you received it specifies that a certain numbered version 152 | of the GNU Lesser General Public License "or any later version" 153 | applies to it, you have the option of following the terms and 154 | conditions either of that published version or of any later version 155 | published by the Free Software Foundation. If the Library as you 156 | received it does not specify a version number of the GNU Lesser 157 | General Public License, you may choose any version of the GNU Lesser 158 | General Public License ever published by the Free Software Foundation. 159 | 160 | If the Library as you received it specifies that a proxy can decide 161 | whether future versions of the GNU Lesser General Public License shall 162 | apply, that proxy's public statement of acceptance of any version is 163 | permanent authorization for you to choose that version for the 164 | Library. 165 | -------------------------------------------------------------------------------- /lib/nodemailer-openpgp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const openpgp = require('openpgp'); 4 | const Transform = require('stream').Transform; 5 | const crypto = require('crypto'); 6 | 7 | /** 8 | * Creates a Transform stream for signing messages 9 | * 10 | * @constructor 11 | * @param {Object} options DKIM options 12 | */ 13 | class Encrypter extends Transform { 14 | constructor(options) { 15 | super(options); 16 | this.options = options || {}; 17 | 18 | this._messageChunks = []; 19 | this._messageLength = 0; 20 | } 21 | 22 | /** 23 | * Caches all input 24 | */ 25 | _transform(chunk, encoding, done) { 26 | if (encoding !== 'buffer') { 27 | chunk = Buffer.from(chunk, encoding); 28 | } 29 | this._message += chunk; 30 | this._messageChunks.push(chunk); 31 | this._messageLength += chunk.length; 32 | done(); 33 | } 34 | 35 | /** 36 | * Signs and emits the entire cached input at once 37 | */ 38 | _flush(done) { 39 | (async () => { 40 | try { 41 | let message = Buffer.concat(this._messageChunks, this._messageLength); 42 | 43 | let privKey = false; 44 | let pubKeys = await Promise.all((this.options.encryptionKeys || []).map(armoredKey => openpgp.readKey({ armoredKey: armoredKey.toString() }))); 45 | const shouldSign = this.options.shouldSign === false ? false : true; 46 | 47 | if (shouldSign && this.options.signingKey) { 48 | privKey = await openpgp.readPrivateKey({ armoredKey: this.options.signingKey.toString() }); 49 | 50 | if (this.options.passphrase) { 51 | privKey = await openpgp.decryptKey({ 52 | privateKey: privKey, 53 | passphrase: this.options.passphrase 54 | }); 55 | } 56 | } 57 | 58 | if (!pubKeys.length && (!shouldSign || !privKey)) { 59 | // dont sign or encrypt 60 | this.push(message); 61 | return done(); 62 | } 63 | 64 | let messageParts = message.toString().split('\r\n\r\n'); 65 | let header = messageParts.shift(); 66 | let headers = []; 67 | let bodyHeaders = []; 68 | let lastHeader = false; 69 | let boundary = 'nm_' + crypto.randomBytes(14).toString('hex'); 70 | header.split('\r\n').forEach((line, i) => { 71 | if (!i || !lastHeader || !/^\s/.test(line)) { 72 | lastHeader = [line]; 73 | if (/^(content-type|content-transfer-encoding):/i.test(line)) { 74 | bodyHeaders.push(lastHeader); 75 | } else { 76 | headers.push(lastHeader); 77 | } 78 | } else { 79 | lastHeader.push(line); 80 | } 81 | }); 82 | 83 | if (!pubKeys.length) { 84 | // sign only 85 | headers.push( 86 | ['Content-Type: multipart/signed; protocol="application/pgp-signature"; micalg=pgp-sha512;'], 87 | [' boundary="' + boundary + '"'] 88 | ); 89 | headers.push(['Content-Description: OpenPGP signed message']); 90 | 91 | headers = headers.map(line => line.join('\r\n')).join('\r\n'); 92 | bodyHeaders = bodyHeaders.map(line => line.join('\r\n')).join('\r\n'); 93 | 94 | let body = messageParts.join('\r\n\r\n'); 95 | const data = `${bodyHeaders}\r\n\r\n${body}`; 96 | 97 | const signature = await openpgp.sign({ 98 | message: await openpgp.createMessage({ text: data }), 99 | signingKeys: privKey, 100 | detached: true 101 | }); 102 | const mailBody = 103 | '--' + 104 | boundary + 105 | '\r\n' + 106 | data + 107 | '\r\n' + 108 | '--' + 109 | boundary + 110 | '\r\n' + 111 | 'Content-Type: application/pgp-signature\r\n' + 112 | 'Content-Disposition: inline; filename=signature.asc\r\n' + 113 | '\r\n' + 114 | signature + 115 | '\r\n--' + 116 | boundary + 117 | '--\r\n'; 118 | 119 | this.push(Buffer.from(`${headers}\r\n\r\n${mailBody}`)); 120 | return done(); 121 | } else { 122 | // encrypt (and sign) 123 | headers.push(['Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";'], [' boundary="' + boundary + '"']); 124 | 125 | headers.push(['Content-Description: OpenPGP encrypted message']); 126 | headers.push(['Content-Transfer-Encoding: 7bit']); 127 | 128 | headers = headers.map(line => line.join('\r\n')).join('\r\n'); 129 | bodyHeaders = bodyHeaders.map(line => line.join('\r\n')).join('\r\n'); 130 | 131 | let body = messageParts.join('\r\n\r\n'); 132 | const data = `${bodyHeaders}\r\n\r\n${body}`; 133 | 134 | let options = { 135 | message: await openpgp.createMessage({ text: data }), 136 | encryptionKeys: pubKeys 137 | }; 138 | 139 | if (shouldSign && privKey) { 140 | options.signingKeys = privKey; 141 | } 142 | 143 | const encrypted = await openpgp.encrypt(options); 144 | const mailBody = 145 | 'This is an OpenPGP/MIME encrypted message\r\n\r\n' + 146 | '--' + 147 | boundary + 148 | '\r\n' + 149 | 'Content-Type: application/pgp-encrypted\r\n' + 150 | 'Content-Transfer-Encoding: 7bit\r\n' + 151 | '\r\n' + 152 | 'Version: 1\r\n' + 153 | '\r\n' + 154 | '--' + 155 | boundary + 156 | '\r\n' + 157 | 'Content-Type: application/octet-stream; name=encrypted.asc\r\n' + 158 | 'Content-Disposition: inline; filename=encrypted.asc\r\n' + 159 | 'Content-Transfer-Encoding: 7bit\r\n' + 160 | '\r\n' + 161 | encrypted + 162 | '\r\n--' + 163 | boundary + 164 | '--\r\n'; 165 | 166 | this.push(Buffer.from(`${headers}\r\n\r\n${mailBody}`)); 167 | return done(); 168 | } 169 | } catch (err) { 170 | return done(err); 171 | } 172 | })(); 173 | } 174 | } 175 | 176 | /** 177 | * Nodemailer plugin for the 'stream' event. Caches the entire message to memory, 178 | * signes it and passes on 179 | * 180 | * @param {Object} [options] Optional options object 181 | * @returns {Function} handler for 'stream' 182 | */ 183 | module.exports.openpgpEncrypt = function (options) { 184 | return function (mail, callback) { 185 | if ( 186 | (!(options && options.signingKey) || mail.data.shouldSign === false) && 187 | (!mail.data.encryptionKeys || (Array.isArray(mail.data.encryptionKeys) && !mail.data.encryptionKeys.length)) 188 | ) { 189 | return setImmediate(callback); 190 | } 191 | mail.message.transform( 192 | () => 193 | new Encrypter({ 194 | signingKey: options && options.signingKey, 195 | passphrase: options && options.passphrase, 196 | encryptionKeys: mail.data.encryptionKeys, 197 | shouldSign: mail.data.shouldSign 198 | }) 199 | ); 200 | setImmediate(callback); 201 | }; 202 | }; 203 | 204 | // Expose for testing only 205 | module.exports.Encrypter = Encrypter; 206 | --------------------------------------------------------------------------------