├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── package.json ├── src └── nodemailer-dkim.js └── test ├── fixtures ├── test_private.pem └── test_public.pem └── nodemailer-dkim-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent": 4, 3 | "node": true, 4 | "globalstrict": true, 5 | "evil": true, 6 | "unused": true, 7 | "undef": true, 8 | "newcap": true, 9 | "esnext": true, 10 | "curly": true, 11 | "eqeqeq": true, 12 | "expr": true, 13 | 14 | "predef": [ 15 | "describe", 16 | "it" 17 | ] 18 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .travis.yml 2 | .jshintrc 3 | test -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "0.10" 5 | - 0.12 6 | - 4 7 | - 5 8 | 9 | notifications: 10 | email: 11 | recipients: 12 | - andris@kreata.ee 13 | on_success: change 14 | on_failure: change 15 | webhooks: 16 | urls: 17 | - https://webhooks.gitter.im/e/0ed18fd9b3e529b3c2cc 18 | on_success: change # options: [always|never|change] default: always 19 | on_failure: always # options: [always|never|change] default: always 20 | on_start: false # default: false 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.0.4 2016-03-02 4 | 5 | Bumped dependencies to fix too long lines issue 6 | 7 | ## v1.0.3 2015-05-13 8 | 9 | Fixed an issue with long public keys (https://github.com/andris9/nodemailer-dkim/pull/4) 10 | 11 | ## v1.0.2 2015-01-13 12 | 13 | Trim whitespace around keys and values in the TXT record when verifying DKIM setup 14 | 15 | ## v1.0.0 2014-09-12 16 | 17 | Set transform stream as a callable function, this allows to use the same mail object several times (new DKIMSigner is created for every instance). 18 | 19 | ## v0.2.0 2014-08-06 20 | 21 | Added new method `verifyKeys` to verify the DKIM settings. The method fetches the public key from DNS and tries to verify some data signed with the private key. 22 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | jshint: { 8 | all: ['src/*.js', 'test/*.js'], 9 | options: { 10 | jshintrc: '.jshintrc' 11 | } 12 | }, 13 | 14 | mochaTest: { 15 | all: { 16 | options: { 17 | reporter: 'spec' 18 | }, 19 | src: ['test/*-test.js'] 20 | } 21 | } 22 | }); 23 | 24 | // Load the plugin(s) 25 | grunt.loadNpmTasks('grunt-contrib-jshint'); 26 | grunt.loadNpmTasks('grunt-mocha-test'); 27 | 28 | // Tasks 29 | grunt.registerTask('default', ['jshint', 'mochaTest']); 30 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2016 Andris Reinman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodemailer-dkim 2 | 3 | ![Nodemailer](https://raw.githubusercontent.com/nodemailer/nodemailer/master/assets/nm_logo_200x136.png) 4 | 5 | DKIM Signer plugin for Nodemailer 6 | 7 | See [Nodemailer homepage](https://nodemailer.com/dkim/) for documentation and terms of using DKIM. 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodemailer-dkim", 3 | "version": "1.0.5", 4 | "description": "Sign Nodemailer DKIM headers", 5 | "main": "src/nodemailer-dkim", 6 | "scripts": { 7 | "test": "grunt" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/andris9/nodemailer-dkim.git" 12 | }, 13 | "keywords": [ 14 | "DKIM", 15 | "Nodemailer" 16 | ], 17 | "author": "Andris Reinman", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/andris9/nodemailer-dkim/issues" 21 | }, 22 | "homepage": "https://github.com/andris9/nodemailer-dkim", 23 | "dependencies": { 24 | "dkim-signer": "^0.2.2" 25 | }, 26 | "devDependencies": { 27 | "chai": "~3.5.0", 28 | "grunt": "~0.4.5", 29 | "grunt-cli": "^0.1.13", 30 | "grunt-contrib-jshint": "~1.0.0", 31 | "grunt-mocha-test": "~0.12.7", 32 | "mocha": "^2.4.5", 33 | "sinon": "^1.17.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/nodemailer-dkim.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var dkimSign = require("dkim-signer").DKIMSign; 4 | var Transform = require('stream').Transform; 5 | var util = require('util'); 6 | var dns = require('dns'); 7 | var crypto = require('crypto'); 8 | var punycode = require('punycode'); 9 | 10 | /** 11 | * Nodemailer plugin for the 'stream' event. Caches the entire message to memory, 12 | * signes it and passes on 13 | * 14 | * @param {Object} options DKIM options 15 | * @returns {Function} handler for 'stream' 16 | */ 17 | module.exports.signer = function(options) { 18 | return function(mail, callback) { 19 | mail.message.transform(function() { 20 | return new DKIMSigner(options); 21 | }); 22 | setImmediate(callback); 23 | }; 24 | }; 25 | 26 | module.exports.verifyKeys = verifyKeys; 27 | 28 | // Expose for testing only 29 | module.exports.DKIMSigner = DKIMSigner; 30 | 31 | /** 32 | * Creates a Transform stream for signing messages 33 | * 34 | * @constructor 35 | * @param {Object} options DKIM options 36 | */ 37 | function DKIMSigner(options) { 38 | this.options = options || {}; 39 | Transform.call(this, this.options); 40 | 41 | this._message = ''; 42 | } 43 | util.inherits(DKIMSigner, Transform); 44 | 45 | /** 46 | * Caches all input 47 | */ 48 | DKIMSigner.prototype._transform = function(chunk, encoding, done) { 49 | chunk = (chunk || '').toString('utf-8'); 50 | this._message += chunk; 51 | done(); 52 | }; 53 | 54 | /** 55 | * Signs and emits the entire cached input at once 56 | */ 57 | DKIMSigner.prototype._flush = function(done) { 58 | var signature = dkimSign(this._message, this.options); 59 | this.push(new Buffer([].concat(signature || []).concat(this._message || []).join('\r\n'), 'utf-8')); 60 | done(); 61 | }; 62 | 63 | /** 64 | * Verifies if selected settings are valid (private key matches the publick key listed in DNS) 65 | * 66 | * @param {Function} callback Callback function with the result 67 | */ 68 | function verifyKeys(options, callback) { 69 | var domain = punycode.toASCII([options.keySelector, '_domainkey', options.domainName].join('.')); 70 | dns.resolveTxt(domain, function(err, result) { 71 | if (err) { 72 | return callback(err); 73 | } 74 | 75 | if (!result || !result.length) { 76 | return callback(new Error('Selector not found (%s)', domain)); 77 | } 78 | 79 | var data = {}; 80 | [].concat(result[0] || []).join('').split(/;/).forEach(function(row) { 81 | var key, val; 82 | row = row.split('='); 83 | key = (row.shift() || '').toString().trim(); 84 | val = (row.join('=') || '').toString().trim(); 85 | data[key] = val; 86 | }); 87 | 88 | if (!data.p) { 89 | return callback(new Error('DNS TXT record does not seem to be a DKIM value', domain)); 90 | } 91 | 92 | var pubKey = '-----BEGIN PUBLIC KEY-----\n' + data.p.replace(/.{78}/g, '$&\n') + '\n-----END PUBLIC KEY-----'; 93 | 94 | try { 95 | var sign = crypto.createSign('RSA-SHA256'); 96 | sign.update('nodemailer'); 97 | var signature = sign.sign(options.privateKey, 'hex'); 98 | var verifier = crypto.createVerify('RSA-SHA256'); 99 | verifier.update('nodemailer'); 100 | 101 | if (verifier.verify(pubKey, signature, 'hex')) { 102 | return callback(null, true); 103 | } else { 104 | return callback(new Error('Verification failed, keys do not match')); 105 | } 106 | } catch (E) { 107 | callback(E); 108 | } 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /test/fixtures/test_private.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIBywIBAAJhANCx7ncKUfQ8wBUYmMqq6ky8rBB0NL8knBf3+uA7q/CSxpX6sQ8N 3 | dFNtEeEd7gu7BWEM7+PkO1P0M78eZOvVmput8BP9R44ARpgHY4V0qSCdUt4rD32n 4 | wfjlGbh8p5ua5wIDAQABAmAm+uUQpQPTu7kg95wqVqw2sxLsa9giT6M8MtxQH7Uo 5 | 1TF0eAO0TQ4KOxgY1S9OT5sGPVKnag258m3qX7o5imawcuyStb68DQgAUg6xv7Af 6 | AqAEDfYN5HW6xK+X81jfOUECMQDr7XAS4PERATvgb1B3vRu5UEbuXcenHDYgdoyT 7 | 3qJFViTbep4qeaflF0uF9eFveMcCMQDic10rJ8fopGD7/a45O4VJb0+lRXVdqZxJ 8 | QzAp+zVKWqDqPfX7L93SQLzOGhdd7OECMQDeQyD7WBkjSQNMy/GF7I1qxrscIxNN 9 | VqGTcbu8Lti285Hjhx/sqhHHHGwU9vB7oM8CMQDKTS3Kw/s/xrot5O+kiZwFgr+w 10 | cmDrj/7jJHb+ykFNb7GaEkiSYqzUjKkfpweBDYECMFJUyzuuFJAjq3BXmGJlyykQ 11 | TweUw+zMVdSXjO+FCPcYNi6CP1t1KoESzGKBVoqA/g== 12 | -----END RSA PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /test/fixtures/test_public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MHwwDQYJKoZIhvcNAQEBBQADawAwaAJhANCx7ncKUfQ8wBUYmMqq6ky8rBB0NL8k 3 | nBf3+uA7q/CSxpX6sQ8NdFNtEeEd7gu7BWEM7+PkO1P0M78eZOvVmput8BP9R44A 4 | RpgHY4V0qSCdUt4rD32nwfjlGbh8p5ua5wIDAQAB 5 | -----END PUBLIC KEY----- 6 | -------------------------------------------------------------------------------- /test/nodemailer-dkim-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var dkim = require('../src/nodemailer-dkim'); 5 | var fs = require('fs'); 6 | var sinon = require('sinon'); 7 | 8 | var expect = chai.expect; 9 | chai.Assertion.includeStack = true; 10 | 11 | describe('nodemailer-dkim tests', function() { 12 | it('should add valid signature', function(done) { 13 | var mail = 'From: andris@node.ee\r\nTo:andris@kreata.ee\r\n\r\nHello world!'; 14 | 15 | var signer = new dkim.DKIMSigner({ 16 | domainName: 'node.ee', 17 | keySelector: 'dkim', 18 | privateKey: fs.readFileSync(__dirname + '/fixtures/test_private.pem') 19 | }); 20 | 21 | var chunks = []; 22 | 23 | signer.on('data', function(chunk) { 24 | chunks.push(chunk); 25 | }); 26 | 27 | signer.on('end', function() { 28 | // unwrap all lines 29 | var message = Buffer.concat(chunks).toString('utf-8').replace(/\r?\n +/g, ' '); 30 | // normalize first line by removing spaces 31 | message = message.replace(/^.*$/m, function(str) { 32 | return str.replace(/ /g, ''); 33 | }); 34 | expect(message).to.exist; 35 | expect(message).to.equal('DKIM-Signature:v=1;a=rsa-sha256;c=relaxed/relaxed;d=node.ee;q=dns/txt;s=dkim;bh=z6TUz85EdYrACGMHYgZhJGvVy5oQI0dooVMKa2ZT7c4=;h=from:to;b=pVd+Dp+EjmYBcc1AWlBAP4ESpuAJ2WMS4gbxWLoeUZ1vZRodVN7K9UXvcCsLuqjJktCZMN2+8dyEUaYW2VIcxg4sVBCS1wqB/tqYZ/gxXLnG2/nZf4fyD2vxltJP4pDL\r\n' + mail); 36 | done(); 37 | }); 38 | 39 | signer.end(mail); 40 | }); 41 | 42 | it('should verify valid keys', function(done) { 43 | var dns = require('dns'); 44 | sinon.stub(dns, 'resolveTxt').yields(null, [ [' p = MHwwDQYJKoZIhvcNAQEBBQADawAwaAJhANCx7ncKUfQ8wBUYmMqq6ky8rBB0NL8knBf3+uA7q/CSxpX6sQ8NdFNtEeEd7gu7BWEM7+PkO1P0M78eZOvVmput8BP9R44ARpgHY4V0qSCdUt4rD32nwfjlGbh8p5ua5wIDAQAB'] ]); 45 | 46 | dkim.verifyKeys({ 47 | domainName: 'node.ee', 48 | keySelector: 'dkim', 49 | privateKey: fs.readFileSync(__dirname + '/fixtures/test_private.pem') 50 | }, function(err, success) { 51 | expect(err).to.not.exist; 52 | expect(success).to.be.true; 53 | dns.resolveTxt.restore(); 54 | done(); 55 | }); 56 | }); 57 | 58 | it('should not verify missing keys', function(done) { 59 | var dns = require('dns'); 60 | sinon.stub(dns, 'resolveTxt').yields(null, []); 61 | 62 | dkim.verifyKeys({ 63 | domainName: 'node.ee', 64 | keySelector: 'dkim', 65 | privateKey: fs.readFileSync(__dirname + '/fixtures/test_private.pem') 66 | }, function(err) { 67 | expect(err).to.exist; 68 | dns.resolveTxt.restore(); 69 | done(); 70 | }); 71 | }); 72 | 73 | it('should not verify non matching keys', function(done) { 74 | var dns = require('dns'); 75 | sinon.stub(dns, 'resolveTxt').yields(null, [ ['p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDFDiKg3O4hdG5iehr5MNxMgrJNMUh6hgWekILDZg2I8WGERJTFZpnspUT1wgoVRziVzGB7ORbVOZEPdZy7noNSTpx5hDgHie/8cO1Q9O/IIX6Mx4qfQL21m0d1zZRbCo6wdO/cwXMoqOZN6ijpFsLFBMNanJ7AysIXiu6GeYLxwQIDAQAB'] ]); 76 | 77 | dkim.verifyKeys({ 78 | domainName: 'node.ee', 79 | keySelector: 'dkim', 80 | privateKey: fs.readFileSync(__dirname + '/fixtures/test_private.pem') 81 | }, function(err) { 82 | expect(err).to.exist; 83 | dns.resolveTxt.restore(); 84 | done(); 85 | }); 86 | }); 87 | }); 88 | --------------------------------------------------------------------------------