index.js
'use strict' const tlds = require('haraka-tld') exports.register = function () { this.inherits('haraka-plugin-redis') this.load_sender_ini() this.register_hook('init_master', 'init_redis_plugin') this.register_hook('init_child', 'init_redis_plugin') this.register_hook('mail', 'is_authenticated') this.register_hook('rcpt_ok', 'check_recipient') this.register_hook('queue_ok', 'update_sender') this.register_hook('data_post', 'is_dkim_authenticated')} exports.load_sender_ini = function () { const plugin = this plugin.cfg = plugin.config.get('known-senders.ini', function () { plugin.load_sender_ini() }) if (plugin.cfg.ignored_ods === undefined) plugin.cfg.ignored_ods = {} plugin.merge_redis_ini()} /* * Outbound Processing * * Identify and save to Redis domains the local users send email to * * Context: these functions run after a message has been queued. * */ Function `update_sender` has 36 lines of code (exceeds 25 allowed). Consider refactoring.exports.update_sender = async function (next, connection, params) { const plugin = this // queue_ok arguments: next, connection, msg // ok 1390590369 qp 634 (F82E2DD5-9238-41DC-BC95-9C3A02716AD2.1) let sender_od let rcpt_domains function errNext(err) { connection.logerror(plugin, `update_sender: ${err}`) next(null, null, sender_od, rcpt_domains) } // connection.loginfo(this, params); if (!connection) return errNext('no connection') if (!connection.transaction) return errNext('no transaction') if (!connection.relaying) return next() const txn = connection.transaction sender_od = this.get_sender_domain_by_txn(txn) if (!sender_od) return errNext('no sender domain') if (sender_od in plugin.cfg.ignored_ods) return errNext(`ignored(${sender_od})`) rcpt_domains = this.get_recipient_domains_by_txn(txn) if (rcpt_domains.length === 0) { return errNext(`no rcpt ODs for ${sender_od}`) } // within this function, the sender is a local domain // and the recipient is an external domain try { const multi = this.db.multi() for (const rcptDomain of rcpt_domains) { multi.hIncrBy(sender_od, rcptDomain, 1) } const replies = await multi.exec() for (let i = 0; i < rcpt_domains.length; i++) { connection.loginfo( this, `saved ${sender_od} : ${rcpt_domains[i]} : ${replies[i]}`, ) } next(null, null, sender_od, rcpt_domains) } catch (err) { connection.logerror(this, err) next() }} exports.get_sender_domain_by_txn = function (txn) { if (!txn.mail_from || !txn.mail_from.host) return const sender_od = tlds.get_organizational_domain(txn.mail_from.host) if (txn.mail_from.host !== sender_od) { this.logdebug(`sender: ${txn.mail_from.host} -> ${sender_od}`) } return sender_od} exports.get_recipient_domains_by_txn = function (txn) { const plugin = this const rcpt_domains = [] if (!txn.rcpt_to) return rcpt_domains for (const element of txn.rcpt_to) { if (!element.host) continue const rcpt_od = tlds.get_organizational_domain(element.host) if (element.host !== rcpt_od) { plugin.loginfo(`rcpt: ${element.host} -> ${rcpt_od}`) } if (rcpt_domains.indexOf(rcpt_od) === -1) { // not a duplicate, add to the list rcpt_domains.push(rcpt_od) } } return rcpt_domains} /* * Inbound Processing * * Look for sender domains we can validate against something. Anything.. * FCrDNS, SPF, DKIM, verified TLS host name, etc.. * * When verified / validated sender domains are found, check to see if * their recipients have ever sent mail to their domain. */ // early checks, on the mail hookexports.is_authenticated = function (next, connection, params) { // only validate inbound messages if (connection.relaying) return next() const sender_od = this.get_sender_domain_by_txn(connection.transaction) if (sender_od in this.cfg.ignored_ods) return next() if (this.has_fcrdns_match(sender_od, connection)) { connection.logdebug(this, `+fcrdns: ${sender_od}`) return next(null, null, sender_od) } if (this.has_spf_match(sender_od, connection)) { connection.logdebug(this, `+spf: ${sender_od}`) return next(null, null, sender_od) } // Maybe: TLS verified domain? if (connection.tls.verified) { // TODO: get the CN and Subject Alternative Names of the cert // then look for match with sender_od connection.logdebug(this, `+tls: ${sender_od}`) // return next(null, null, sender_od); } next()} exports.get_validated_sender_od = function (connection) { if (!connection || !connection.transaction) return const txn_res = connection.transaction.results.get(this.name) if (!txn_res) return return txn_res.sender} exports.get_rcpt_ods = function (connection) { if (!connection) return [] if (!connection.transaction) return [] const txn_r = connection.transaction.results.get(this.name) if (!txn_r) return [] return txn_r.rcpt_ods} exports.already_matched = function (connection) { const res = connection.transaction.results.get(this) if (!res) return false return res.pass && res.pass.length ? true : false} exports.check_recipient = async function (next, connection, rcpt) { // rcpt is a valid local email address. Some rcpt_to.* plugin has // accepted it. // inbound only if (connection.relaying) return next() function errNext(err) { connection.logerror(this, `check_recipient: ${err}`) next() } if (!rcpt.host) return errNext('rcpt.host unset?') // reduce the host portion of the email address to an OD const rcpt_od = tlds.get_organizational_domain(rcpt.host) if (!rcpt_od) return errNext(`no rcpt od for ${rcpt.host}`) connection.transaction.results.push(this, { rcpt_ods: rcpt_od }) // if no validated sender domain, there's nothing to do...yet const sender_od = this.get_validated_sender_od(connection) if (!sender_od) return next() if (sender_od in this.cfg.ignored_ods) return errNext(`ignored(${sender_od})`) // The sender OD is validated, check Redis for a match try { const reply = await this.db.hGet(rcpt_od, sender_od) connection.logdebug(this, `${rcpt_od} : ${sender_od} : ${reply}`) if (reply) { connection.transaction.results.add(this, { pass: rcpt_od, count: reply }) } next(null, null, rcpt_od) } catch (err) { this.logerror(err) next() }} Function `is_dkim_authenticated` has 49 lines of code (exceeds 25 allowed). Consider refactoring.
Function `is_dkim_authenticated` has a Cognitive Complexity of 18 (exceeds 10 allowed). Consider refactoring.exports.is_dkim_authenticated = async function (next, connection) { const plugin = this if (connection.relaying) return next() let rcpt_ods = [] function errNext(err) { connection.logerror(plugin, `is_dkim_authenticated: ${err}`) next(null, null, rcpt_ods) } function infoNext(msg) { connection.loginfo(plugin, `is_dkim_authenticated: ${msg}`) next(null, null, rcpt_ods) } if (this.already_matched(connection)) return infoNext('already matched') const sender_od = this.get_validated_sender_od(connection) if (!sender_od) return errNext('no sender_od') if (sender_od in this.cfg.ignored_ods) return infoNext(`ignored(${sender_od})`) rcpt_ods = this.get_rcpt_ods(connection) if (!rcpt_ods || !rcpt_ods.length) return errNext('no rcpt_ods') const dkim = connection.transaction.results.get('dkim_verify') if (!dkim) return infoNext('no dkim_verify results') if (!dkim.pass || !dkim.pass.length) return infoNext('no dkim pass') try { const multi = this.db.multi() for (const pas of dkim.pass) { const dkim_od = tlds.get_organizational_domain(pas) if (dkim_od === sender_od) { connection.transaction.results.add(this, { sender: sender_od, auth: 'dkim', }) for (const rcptOd of rcpt_ods) { multi.hGet(rcptOd, sender_od) } } } const replies = await multi.exec() for (let j = 0; j < rcpt_ods.length; j++) { if (!replies[j]) continue connection.transaction.results.add(this, { pass: rcpt_ods[j], count: replies[j], emit: true, }) } next(null, null, rcpt_ods) } catch (err) { connection.logerror(this, err) errNext(err) }} exports.has_fcrdns_match = function (sender_od, connection) { const fcrdns = connection.results.get('fcrdns') if (!fcrdns) return false if (!fcrdns.fcrdns) return false connection.logdebug(this, fcrdns.fcrdns) let mail_host = fcrdns.fcrdns if (Array.isArray(mail_host)) mail_host = fcrdns.fcrdns[0] const fcrdns_od = tlds.get_organizational_domain(mail_host) if (fcrdns_od !== sender_od) return false connection.transaction.results.add(this, { sender: sender_od, auth: 'fcrdns', emit: true, }) return true} exports.has_spf_match = function (sender_od, connection) { let spf = connection.results.get('spf') if (spf && spf.domain && spf.result === 'Pass') { // scope=helo (HELO/EHLO) if (tlds.get_organizational_domain(spf.domain) === sender_od) { connection.transaction.results.add(this, { sender: sender_od }) return true } } spf = connection.transaction.results.get('spf') if (spf && spf.domain && spf.result === 'Pass') { // scope=mfrom (HELO/EHLO) if (tlds.get_organizational_domain(spf.domain) === sender_od) { connection.transaction.results.add(this, { sender: sender_od, auth: 'spf', emit: true, }) return true } } return false}