haraka/haraka-results

View on GitHub
README.md

Summary

Maintainability
Test Coverage
# haraka-results

[![Build Status][ci-img]][ci-url]
[![Code Coverage][cov-img]][cov-url]
[![Code Climate][clim-img]][clim-url]
[![NPM][npm-img]][npm-url]

Add, log, retrieve, and share the results of plugin tests.

## Synopsis

Results is a structured way of storing results from plugins across a
session, allowing those results to be retrieved later or by other plugins.

Results objects are present on every Haraka connection _and_ transaction. When
in a SMTP transaction, results from _both_ are applicable to that transaction.

## Usage

Use results in your plugins like so:

```js
exports.my_first_hook = function (next, connection) {

    // run a test
    ......

    // store the results
    connection.results.add(this, {pass: 'my great test' })

    // run another test
    .....

    // store the results
    connection.results.add(this, {fail: 'gotcha!', msg: 'show this'})
}
```

Store the results in the transaction (vs connection):

```js
   connection.transaction.results.add(this, {...});`
```

### Config options

Each plugin can have custom settings in results.ini to control results logging.
There are three options available: hide, order, and debug.

- hide - a comma separated list of results to hide from the output
- order - a comman separated list, specifing the order of items in the output
- debug - log debug messages every time results are called

```ini
; put this in config/results.ini
[plugin_name]
hide=skip
order=msg,pass,fail
debug=0
```

### Results Functions

#### add

Store information. Most calls to `results` will append data to the lists
in the connection. The following lists are available:

    pass  - names of tests that passed
    fail  - names of tests that failed
    skip  - names of tests that were skipped (with a why, if you wish)
    err   - error messages encountered during processing
    msg   - arbitratry messages

    human - a custom summary to return (bypass collate)
    emit  - log an INFO summary

When err results are received, a logerror is automatically emitted, saving the
need to specify {emit: true} with the request.

Examples:

```js
    const results = connection.results
    results.add(this, {pass: 'null_sender'})
    results.add(this, {fail: 'single_recipient'})
    results.add(this, {skip: 'valid_bounce'}
    results.add(this, {err: 'timed out looking in couch cushions'})
    results.add(this, {msg: 'I found a nickel!', emit: true})
```

In addition to appending values to the predefined lists, arbitrary results
can be stored in the cache:

```js
results.add(this, { my_result: 'anything I want' })
```

When arbirary values are stored, they are listed first in the log output. Their
display can be suppressed with the **hide** option in results.ini.

#### incr

Increment counters. The argument to incr is an object with counter names and
increment values. Examples:

```js
results.incr(this, { unrecognized_commands: 1 })

results.incr(this, { karma: -1 })
results.incr(this, { karma: 2 })
```

#### push

Append items onto arrays. The argument to push is an object with array names and
the new value to be appended to the array. Examples:

```js
results.push(this, { dns_recs: 'name1' })
results.push(this, { dns_recs: 'name2' })
```

#### collate

```js
const summary = results.collate(this)
```

Formats the contents of the result cache and returns them. This function is
called internally by `add()` after each update.

#### get

Retrieve the stored results as an object. The only argument is the name of the
plugin whose results are desired.

```js
    const geoip = results.get('geoip')
    if (geoip && geoip.distance && geoip.distance > 2000) {
        ....
    }
```

Keep in mind that plugins also store results in the transaction. Example:

```js
    const sa = connection.transaction.results.get('spamassassin')
    if (sa && sa.score > 5) {
        ....
    }
```

#### has

Check result contents for string or pattern matches.

Syntax:

```js
results.has('plugin_name', 'result_name', 'search_term')
```

- result_name: the name of an array or string in the result object
- search_term: a string or RegExp object

### More Examples

#### Store Results:

```js
results.add(this, { pass: 'some_test' })
results.add(this, { pass: 'some_test(with reason)' })
```

#### Retrieve exact match with **get**:

```js
if (results.get('plugin_name').pass.indexOf('some_test') !== -1) {
  // some_test passed (1x)
}
```

#### Retrieve a string match with **has**

```js
if (results.has('plugin_name', 'pass', 'some_test')) {
  // some_test passed (1x)
}
```

The syntax for using **has** is a little more pleasant.

Both options require one to check for each reason which is unpleasant when
and all we really want to know is if some_test passed or not.

#### Retrieve a matching pattern:

```js
if (results.has('plugin_name', 'pass', /^some_test/)) {
  // some_test passed (2x)
}
```

### Private Results

To store structured data in results that are hidden from the human and
human_html output, prefix the name of the key with an underscore.

Example:

```js
results.add(this, { _hidden: 'some data' })
```

## Redis Pub/Sub

If a redis client is found on server.notes.redis, then new results are JSON
encoded and published to Redis on the channel named `result-${UUID}`. This
feature can be disabled by setting `[main]redis_publish=false` in results.ini.
Plugins can recieve the events by psubscribing (pattern subscribe) to the
channel named `result-${UUID}*` where ${UUID} is the connection UUID.

This is from the karma plugin subscribing on the `connect_init` hook:

```js
exports.register = function (next, server) {
  this.inherits('redis')

  register_hook('connect_init', 'redis_subscribe')
  register_hook('disconnect', 'redis_unsubscribe')
}

exports.redis_subscribe = function (next, connection) {
  this.redis_subscribe(connection, function () {
    connection.notes.redis.on('pmessage', (pattern, channel, message) => {
      // do stuff with messages that look like this
      // {"plugin":"karma","result":{"fail":"spamassassin.hits"}}
      // {"plugin":"geoip","result":{"country":"CN"}}
    })
    next()
  })
}
exports.redis_unsubscribe = function (next, connection) {
  this.redis_unsubscribe(connection)
}
```

[ci-img]: https://github.com/haraka/haraka-results/actions/workflows/ci.yml/badge.svg
[ci-url]: https://github.com/haraka/haraka-results/actions/workflows/ci.yml
[cov-img]: https://codecov.io/github/haraka/haraka-results/coverage.svg
[cov-url]: https://codecov.io/github/haraka/haraka-results
[clim-img]: https://codeclimate.com/github/haraka/haraka-results/badges/gpa.svg
[clim-url]: https://codeclimate.com/github/haraka/haraka-results
[npm-img]: https://nodei.co/npm/haraka-results.png
[npm-url]: https://www.npmjs.com/package/haraka-results