├── .eslintrc.json ├── .codeclimate.yml ├── .travis.yml ├── Changes.md ├── appveyor.yml ├── .gitignore ├── run_tests ├── package.json ├── LICENSE ├── test └── redis.js ├── README.md └── index.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "haraka" 4 | ], 5 | "extends": ["eslint:recommended", "plugin:haraka/recommended"], 6 | "rules": { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | eslint: 3 | enabled: true 4 | channel: "eslint-3" 5 | config: 6 | config: ".eslintrc" 7 | 8 | ratings: 9 | paths: 10 | - "**.js" 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4" 4 | - "6" 5 | 6 | services: 7 | - redis-server 8 | 9 | before_script: 10 | 11 | script: 12 | - npm run lint 13 | - npm test 14 | 15 | after_success: 16 | - npm install istanbul codecov 17 | - npm run cover 18 | - ./node_modules/.bin/codecov 19 | 20 | sudo: false 21 | -------------------------------------------------------------------------------- /Changes.md: -------------------------------------------------------------------------------- 1 | 2 | # 1.0.6 - 2017-06-16 3 | 4 | - eslint 4 compat 5 | 6 | # 1.0.5 - 2017-06-09 7 | 8 | - disconnect per-connection redis client upon punsubscribe 9 | 10 | # 1.0.4 - 2017-02-06 11 | 12 | - remove retry_strategy, redis client now does The Right Thing w/o it 13 | 14 | # 1.0.3 - 2017-02-06 15 | 16 | - don't break when no [redis] config exists 17 | 18 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | 3 | environment: 4 | nodejs_version: "4" 5 | 6 | # Install scripts. (runs after repo cloning) 7 | install: 8 | - ps: Install-Product node $env:nodejs_version 9 | - npm install 10 | - choco install redis-64 11 | - redis-server --service-install 12 | - redis-server --service-start 13 | 14 | build: off 15 | 16 | before_test: 17 | - node --version 18 | - npm --version 19 | 20 | test_script: 21 | - node run_tests 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /run_tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | try { 6 | var reporter = require('nodeunit').reporters.default; 7 | } 8 | catch(e) { 9 | console.log("Error: " + e.message); 10 | console.log(""); 11 | console.log("Cannot find nodeunit module."); 12 | console.log("Please run the following:"); 13 | console.log(""); 14 | console.log(" npm install"); 15 | console.log(""); 16 | process.exit(); 17 | } 18 | 19 | process.chdir(__dirname); 20 | 21 | if (process.argv[2]) { 22 | console.log("Running tests: ", process.argv.slice(2)); 23 | reporter.run(process.argv.slice(2), undefined, function (err) { 24 | process.exit(((err) ? 1 : 0)); 25 | }); 26 | } 27 | else { 28 | reporter.run([ 29 | 'test', 30 | ], undefined, function (err) { 31 | process.exit(((err) ? 1 : 0)); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haraka-plugin-redis", 3 | "version": "1.0.6", 4 | "description": "Redis plugin for Haraka & other plugins to inherit from", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "dependencies": { 10 | "redis": "^2.6.5" 11 | }, 12 | "devDependencies": { 13 | "eslint": "*", 14 | "eslint-plugin-haraka": "*", 15 | "haraka-test-fixtures": "*", 16 | "nodeunit": "*" 17 | }, 18 | "scripts": { 19 | "lint": "./node_modules/.bin/eslint *.js test/*.js", 20 | "lintfix": "./node_modules/.bin/eslint --fix *.js test/*.js", 21 | "test": "./run_tests" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/haraka/haraka-plugin-redis.git" 26 | }, 27 | "keywords": [ 28 | "haraka", 29 | "mail", 30 | "smtp", 31 | "redis" 32 | ], 33 | "author": "Matt Simerson ", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/haraka/haraka-plugin-redis/issues" 37 | }, 38 | "homepage": "https://github.com/haraka/haraka-plugin-redis#readme" 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Haraka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/redis.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fixtures = require('haraka-test-fixtures'); 4 | 5 | var _set_up_redis = function (done) { 6 | 7 | this.plugin = new fixtures.plugin('index'); 8 | this.plugin.register(); 9 | 10 | done(); 11 | }; 12 | 13 | var retry = function (options) { 14 | if (options.error) { 15 | console.error(options.error); 16 | } 17 | return undefined; 18 | }; 19 | 20 | exports.redis = { 21 | setUp : _set_up_redis, 22 | 'loads' : function (test) { 23 | test.expect(1); 24 | test.equal(this.plugin.name, 'index'); 25 | test.done(); 26 | }, 27 | 'config defaults' : function (test) { 28 | test.expect(2); 29 | test.equal(this.plugin.redisCfg.server.host, '127.0.0.1'); 30 | test.equal(this.plugin.redisCfg.server.port, 6379); 31 | test.done(); 32 | }, 33 | 'connects' : function (test) { 34 | test.expect(1); 35 | var redis = this.plugin.get_redis_client({ 36 | host: this.plugin.redisCfg.server.host, 37 | port: this.plugin.redisCfg.server.port, 38 | retry_strategy: retry, 39 | }, 40 | function () { 41 | test.ok(redis.connected); 42 | test.done(); 43 | }); 44 | }, 45 | 'populates plugin.cfg.redis when asked' : function (test) { 46 | test.expect(2); 47 | test.equal(this.plugin.cfg, undefined); 48 | this.plugin.merge_redis_ini(); 49 | test.deepEqual(this.plugin.cfg.redis, { host: '127.0.0.1', port: '6379', db: undefined }); 50 | test.done(); 51 | }, 52 | 'connects to a different redis db' : function (test) { 53 | test.expect(2); 54 | this.plugin.merge_redis_ini(); 55 | this.plugin.cfg.redis.db = 2; 56 | this.plugin.cfg.redis.retry_strategy = retry; 57 | var client = this.plugin.get_redis_client(this.plugin.cfg.redis, function () { 58 | test.expect(2); 59 | // console.log(client); 60 | test.equal(client.connected, true); 61 | test.equal(client.selected_db, 2); 62 | test.done(); 63 | }); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # haraka-plugin-redis 2 | 3 | [![Build Status][ci-img]][ci-url] 4 | [![Code Climate][clim-img]][clim-url] 5 | [![Windows Build status][apv-img]][apv-url] 6 | [![Greenkeeper badge][gk-img]][gk-url] 7 | 8 | Connects to a redis instance. By default it stores a `redis` 9 | connection handle at `server.notes.redis`. See below to get a custom DB handle 10 | attached to another database. 11 | 12 | ## Config 13 | 14 | The `redis.ini` file has the following sections (defaults shown): 15 | 16 | ### [server] 17 | 18 | ; host=127.0.0.1 19 | ; port=6379 20 | ; db=0 21 | 22 | ### [pubsub] 23 | 24 | ; host=127.0.0.1 25 | ; port=6379 26 | 27 | Publish & Subscribe are DB agnostic and thus have no db setting. If host and port and not defined, they default to the same as [server] settings. 28 | 29 | ### [opts] 30 | 31 | ; see https://www.npmjs.com/package/redis#overloading 32 | 33 | 34 | ## Usage (shared redis) 35 | 36 | Use redis in your plugin like so: 37 | 38 | if (server.notes.redis) { 39 | server.notes.redis.hgetall(...); 40 | // or any other redis command 41 | } 42 | 43 | ## Publish/Subscribe Usage 44 | 45 | In your plugin: 46 | 47 | exports.results_init = function (next, connection) { 48 | var plugin = this; 49 | plugin.redis_subscribe(connection, function () { 50 | connection.notes.redis.on('pmessage', function (pattern, channel, message) { 51 | plugin.do_something_with_message(message, ...); 52 | }); 53 | next(); 54 | }); 55 | } 56 | // be nice to redis and disconnect 57 | exports.hook_disconnect = function (next, connection) { 58 | this.redis_unsubscribe(connection); 59 | } 60 | 61 | ## Custom Usage 62 | 63 | This variation lets your plugin establish its own Redis connection, 64 | optionally with a redis db ID. 65 | 66 | exports.register = function () { 67 | var plugin = this; 68 | plugin.inherits('redis'); 69 | 70 | plugin.cfg = plugin.config.get('my-plugin.ini'); 71 | 72 | // populate plugin.cfg.redis with defaults from redis.ini 73 | plugin.merge_redis_ini(); 74 | 75 | // cluster aware redis connection(s) 76 | plugin.register_hook('init_master', 'init_redis_plugin'); 77 | plugin.register_hook('init_child', 'init_redis_plugin'); 78 | } 79 | 80 | When a db ID is specified in the [redis] section of a redis inheriting plugin, log messages like these will be emitted when Haraka starts: 81 | 82 | [INFO] [-] [redis] connected to redis://172.16.15.16:6379 v3.2.6 83 | [INFO] [-] [limit] connected to redis://172.16.15.16:6379/1 v3.2.6 84 | [INFO] [-] [karma] connected to redis://172.16.15.16:6379/2 v3.2.6 85 | [INFO] [-] [known-senders] connected to redis://172.16.15.16:6379/3 v3.2.6 86 | 87 | Notice the database ID numbers appended to each plugins redis connection 88 | message. 89 | 90 | 91 | `[![Coverage Status][cov-img]][cov-url]` nyet 92 | 93 | 94 | [ci-img]: https://travis-ci.org/haraka/haraka-plugin-redis.svg 95 | [ci-url]: https://travis-ci.org/haraka/haraka-plugin-redis 96 | [cov-img]: https://codecov.io/github/haraka/haraka-plugin-redis/coverage.svg 97 | [cov-url]: https://codecov.io/github/haraka/haraka-plugin-redis?branch=master 98 | [clim-img]: https://codeclimate.com/github/haraka/haraka-plugin-redis/badges/gpa.svg 99 | [clim-url]: https://codeclimate.com/github/haraka/haraka-plugin-redis 100 | [apv-img]: https://ci.appveyor.com/api/projects/status/fxk78f25n61nq3lx?svg=true 101 | [apv-url]: https://ci.appveyor.com/project/msimerson/haraka-plugin-redis 102 | [gk-img]: https://badges.greenkeeper.io/haraka/haraka-plugin-redis.svg 103 | [gk-url]: https://greenkeeper.io/ 104 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global server */ 3 | 4 | var redis = require('redis'); 5 | 6 | exports.register = function () { 7 | var plugin = this; 8 | 9 | plugin.load_redis_ini(); 10 | 11 | // some other plugin doing: inherits('haraka-plugin-redis') 12 | if (plugin.name !== 'redis') return; 13 | 14 | // do register these when 'redis' is declared in config/plugins 15 | plugin.register_hook('init_master', 'init_redis_shared'); 16 | plugin.register_hook('init_child', 'init_redis_shared'); 17 | }; 18 | 19 | exports.load_redis_ini = function () { 20 | var plugin = this; 21 | 22 | plugin.redisCfg = plugin.config.get('redis.ini', function () { 23 | plugin.load_redis_ini(); 24 | }); 25 | 26 | if (!plugin.redisCfg.server) plugin.redisCfg.server = {}; 27 | var s = plugin.redisCfg.server; 28 | if (s.ip && !s.host) s.host = s.ip; 29 | if (!s.host) s.host = '127.0.0.1'; 30 | if (!s.port) s.port = '6379'; 31 | 32 | if (!plugin.redisCfg.pubsub) { 33 | plugin.redisCfg.pubsub = JSON.parse(JSON.stringify(s)); 34 | } 35 | var ps = plugin.redisCfg.pubsub; 36 | if (!ps.host) ps.host = s.host; 37 | if (!ps.port) ps.port = s.port; 38 | 39 | if (!plugin.redisCfg.opts) plugin.redisCfg.opts = {}; 40 | }; 41 | 42 | exports.merge_redis_ini = function () { 43 | var plugin = this; 44 | 45 | if (!plugin.cfg) plugin.cfg = {}; // no .ini loaded? 46 | 47 | if (!plugin.cfg.redis) { // no [redis] in .ini file 48 | plugin.cfg.redis = {}; 49 | } 50 | 51 | if (!plugin.redisCfg) plugin.load_redis_ini(); 52 | 53 | ['host', 'port', 'db'].forEach(function (k) { 54 | if (plugin.cfg.redis[k]) return; // property already set 55 | plugin.cfg.redis[k] = plugin.redisCfg.server[k]; 56 | }); 57 | } 58 | 59 | exports.init_redis_shared = function (next, server) { 60 | var plugin = this; 61 | 62 | var calledNext = false; 63 | function nextOnce () { 64 | if (calledNext) return; 65 | calledNext = true; 66 | next(); 67 | } 68 | 69 | // this is the server-wide redis, shared by plugins that don't 70 | // specificy a db ID. 71 | if (server.notes.redis) { 72 | server.notes.redis.ping(function (err, res) { 73 | if (err) { 74 | plugin.logerror(err); 75 | return nextOnce(err); 76 | } 77 | plugin.loginfo('already connected'); 78 | nextOnce(); // connection is good 79 | }); 80 | } 81 | else { 82 | var opts = plugin.redisCfg.opts; 83 | opts.host = plugin.redisCfg.server.host; 84 | opts.port = plugin.redisCfg.server.port; 85 | server.notes.redis = plugin.get_redis_client(opts, nextOnce); 86 | } 87 | }; 88 | 89 | exports.init_redis_plugin = function (next, server) { 90 | var plugin = this; 91 | 92 | // this function is called by plugins at init_*, to establish their 93 | // shared or unique redis db handle. 94 | 95 | var calledNext=false; 96 | function nextOnce () { 97 | if (calledNext) return; 98 | calledNext = true; 99 | next(); 100 | } 101 | 102 | // tests that do not load config 103 | if (!plugin.cfg) plugin.cfg = { redis: {} }; 104 | if (!server) server = { notes: {} }; 105 | 106 | // use server-wide redis connection when using default DB id 107 | if (!plugin.cfg.redis.db) { 108 | if (server.notes.redis) { 109 | server.loginfo(plugin, 'using server.notes.redis'); 110 | plugin.db = server.notes.redis; 111 | return nextOnce(); 112 | } 113 | } 114 | 115 | plugin.db = plugin.get_redis_client(plugin.cfg.redis, nextOnce); 116 | }; 117 | 118 | exports.shutdown = function () { 119 | if (this.db) { 120 | this.db.quit(); 121 | } 122 | if (server && server.notes && server.notes.redis) { 123 | server.notes.redis.quit(); 124 | } 125 | } 126 | 127 | exports.redis_ping = function (done) { 128 | var plugin = this; 129 | var nope = function (err) { 130 | plugin.redis_pings=false; 131 | done(err); 132 | }; 133 | 134 | if (!plugin.db) { 135 | return nope(new Error('redis not initialized')); 136 | } 137 | 138 | plugin.db.ping(function (err, res) { 139 | if (err ) { return nope(err); } 140 | if (res !== 'PONG') { return nope(new Error('not PONG')); } 141 | plugin.redis_pings=true; 142 | done(err, true); 143 | }); 144 | }; 145 | 146 | exports.get_redis_client = function (opts, next) { 147 | var plugin = this; 148 | 149 | var client = redis.createClient(opts) 150 | .on('error', function (error) { 151 | plugin.logerror('Redis error: ' + error.message); 152 | next(); 153 | }) 154 | .on('ready', function () { 155 | var msg = 'connected to redis://' + opts.host + ':' + opts.port; 156 | if (opts.db) msg += '/' + opts.db; 157 | if (client.server_info && client.server_info.redis_version) { 158 | msg += ' v' + client.server_info.redis_version; 159 | } 160 | plugin.loginfo(plugin, msg); 161 | next(); 162 | }) 163 | .on('end', function () { 164 | if (arguments.length) console.log(arguments); 165 | // plugin.logerror('Redis error: ' + error.message); 166 | next(); 167 | }); 168 | 169 | return client; 170 | }; 171 | 172 | exports.get_redis_pub_channel = function (conn) { 173 | return 'result-' + conn.transaction ? conn.transaction.uuid : conn.uuid; 174 | }; 175 | 176 | exports.get_redis_sub_channel = function (conn) { 177 | return 'result-' + conn.uuid + '*'; 178 | }; 179 | 180 | exports.redis_subscribe_pattern = function (pattern, next) { 181 | var plugin = this; 182 | if (plugin.redis) { 183 | // already subscribed? 184 | return next(); 185 | } 186 | 187 | plugin.redis = require('redis').createClient({ 188 | host: plugin.redisCfg.pubsub.host, 189 | port: plugin.redisCfg.pubsub.port, 190 | password: plugin.redisCfg.pubsub.password, 191 | }) 192 | .on('psubscribe', function (pattern2, count) { 193 | plugin.logdebug(plugin, 'psubscribed to ' + pattern2); 194 | next(); 195 | }) 196 | .on('punsubscribe', function (pattern3, count) { 197 | plugin.logdebug(plugin, 'unsubsubscribed from ' + pattern3); 198 | }); 199 | plugin.redis.psubscribe(pattern); 200 | }; 201 | 202 | exports.redis_subscribe = function (connection, next) { 203 | var plugin = this; 204 | 205 | if (connection.notes.redis) { 206 | // another plugin has already called this. Do nothing 207 | return next(); 208 | } 209 | 210 | connection.notes.redis = require('redis').createClient({ 211 | host: plugin.redisCfg.pubsub.host, 212 | port: plugin.redisCfg.pubsub.port, 213 | password: plugin.redisCfg.pubsub.password, 214 | }) 215 | .on('psubscribe', function (pattern, count) { 216 | connection.logdebug(plugin, 'psubscribed to ' + pattern); 217 | next(); 218 | }) 219 | .on('punsubscribe', function (pattern, count) { 220 | connection.logdebug(plugin, 'unsubsubscribed from ' + pattern); 221 | connection.notes.redis.quit(); 222 | }); 223 | connection.notes.redis.psubscribe(plugin.get_redis_sub_channel(connection)); 224 | }; 225 | 226 | exports.redis_unsubscribe = function (connection) { 227 | if (!connection.notes.redis) return; 228 | connection.notes.redis.punsubscribe(this.get_redis_sub_channel(connection)); 229 | }; 230 | --------------------------------------------------------------------------------