haraka/haraka-plugin-known-senders

View on GitHub
index.js

Summary

Maintainability
B
5 hrs
Test Coverage
'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 hook
exports.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
}