haraka-results
Version:
Haraka results store for connections and transactions
260 lines (215 loc) • 7.05 kB
JavaScript
// results - programmatic handling of plugin results
'use strict'
const config = require('haraka-config')
const { types } = require('node:util')
// see docs in docs/Results.md
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype'])
const append_lists = ['msg', 'pass', 'fail', 'skip', 'err']
const overwrite_lists = ['hide', 'order']
const log_opts = ['emit', 'human', 'human_html']
const all_opts = [...append_lists, ...overwrite_lists, ...log_opts]
let cfg
class ResultStore {
constructor(conn) {
this.conn = conn
this.store = Object.create(null)
cfg = config.get('results.ini', {
booleans: ['+main.redis_publish'],
})
}
has(plugin, list, search) {
const name = this.resolve_plugin_name(plugin)
const result = this.store[name]
if (!result || !result[list]) return false
if (typeof result[list] === 'string') {
return this._has_string(result[list], search)
}
if (Array.isArray(result[list])) {
return this._has_array(result[list], search)
}
return false
}
_has_string(msg, search) {
if (typeof search === 'string') return search === msg
if (search instanceof RegExp) return search.test(msg)
return false
}
_has_array(msg, search) {
for (const item of msg) {
switch (typeof search) {
case 'string':
case 'number':
case 'boolean':
if (search === item) return true
break
case 'object':
if (search instanceof RegExp && search.test(item)) return true
break
}
}
return false
}
redis_publish(name, obj) {
if (!cfg.main.redis_publish) return
if (!this.conn.server?.notes?.redis) return
const channel = `result-${this.conn.transaction?.uuid ?? this.conn.uuid}`
this.conn.server.notes.redis
.publish(channel, JSON.stringify({ plugin: name, result: obj }))
.catch((err) => {
const msg = err?.message ?? String(err)
this.conn.logerror('results', `redis publish failed: ${msg}`)
})
}
add(plugin, obj) {
const name = this.resolve_plugin_name(plugin)
let result = this.store[name]
if (!result) {
result = default_result()
this.store[name] = result
}
this.redis_publish(name, obj)
// these are arrays each invocation appends to
for (const key of append_lists) {
if (!Object.hasOwn(obj, key)) continue
let val = obj[key]
if (val === undefined) continue
if (key === 'err') {
if (Array.isArray(val))
val = val.map((e) => (types.isNativeError(e) ? e.message : e))
else if (types.isNativeError(val)) val = val.message
}
result[key] = this._append_to_array(result[key], val)
}
// these arrays are overwritten when passed
for (const key of overwrite_lists) {
if (!Object.hasOwn(obj, key)) continue
if (obj[key] === undefined) continue
result[key] = obj[key]
}
// anything else is an arbitrary key/val to store
for (const [key, val] of Object.entries(obj)) {
if (all_opts.includes(key)) continue // weed out our keys
if (UNSAFE_KEYS.has(key)) continue
if (val === undefined) continue // ignore keys w/undef value
result[key] = val
}
return this._log(plugin, result, obj)
}
_append_to_array(array, item) {
if (Array.isArray(item)) return array.concat(item)
array.push(item)
return array
}
incr(plugin, obj) {
const name = this.resolve_plugin_name(plugin)
let result = this.store[name]
if (!result) {
result = default_result()
this.store[name] = result
}
const pub = {}
for (const [key, raw] of Object.entries(obj)) {
if (UNSAFE_KEYS.has(key)) continue
const val = parseFloat(raw) || 0
if (isNaN(result[key])) result[key] = 0
result[key] = parseFloat(result[key]) + val
pub[key] = result[key]
}
this.redis_publish(name, pub)
}
push(plugin, obj) {
const name = this.resolve_plugin_name(plugin)
let result = this.store[name]
if (!result) {
result = default_result()
this.store[name] = result
}
this.redis_publish(name, obj)
for (const [key, val] of Object.entries(obj)) {
if (UNSAFE_KEYS.has(key)) continue
if (!result[key]) result[key] = []
result[key] = this._append_to_array(result[key], val)
}
return this._log(plugin, result, obj)
}
collate(plugin) {
const name = this.resolve_plugin_name(plugin)
const result = this.store[name]
if (!result) return
return this.private_collate(result, name).join(', ')
}
get(plugin_or_name) {
return this.store[this.resolve_plugin_name(plugin_or_name)]
}
resolve_plugin_name(thing) {
if (typeof thing === 'string') return thing
if (typeof thing === 'object' && thing?.name) return thing.name
return
}
get_all() {
return this.store
}
private_collate(result, name) {
const r = []
const order = this._get_order(cfg[name])
const hide = this._get_hide(cfg[name])
// anything not predefined in the result was purposeful, show it first
for (const key in result) {
if (!this._pre_defined(key, result[key], hide)) continue
r.push(`${key}: ${result[key]}`)
}
// and then supporting information
let array = append_lists // default
if (order.length) array = order // config file
if (result.order?.length) array = result.order // caller
for (const key of array) {
if (!result[key]) continue
if (!result[key].length) continue
if (hide.length && hide.includes(key)) continue
r.push(`${key}:${result[key].join(', ')}`)
}
return r
}
_pre_defined(key, res, hide) {
if (key[0] === '_') return false // 'private' keys
if (all_opts.includes(key)) return false // these get shown later
if (hide.length && hide.includes(key)) return false
if (Array.isArray(res)) return res.length > 0
if (typeof res === 'object') return false
return true
}
_get_order(c) {
if (!c?.order) return []
return c.order.trim().split(/[,; ]+/)
}
_get_hide(c) {
if (!c?.hide) return []
return c.hide.trim().split(/[,; ]+/)
}
_log(plugin, result, obj) {
const name = this.resolve_plugin_name(plugin)
// collate results
result.human = obj.human
if (!result.human) {
const r = this.private_collate(result, name)
result.human = r.join(', ')
result.human_html = r.join(', \t ')
}
// logging results
if (obj.emit) this.conn.loginfo(plugin, result.human) // by request
if (obj.err) {
const errMsg = types.isNativeError(obj.err) ? obj.err.message : obj.err
this.conn.logerror(plugin, errMsg)
}
if (!obj.emit && !obj.err) {
// by config
const pic = cfg[name]
if (pic?.debug) this.conn.logdebug(plugin, result.human)
}
return result.human
}
}
function default_result() {
return { pass: [], fail: [], msg: [], err: [], skip: [] }
}
module.exports = ResultStore