index.js
// spf
const SPF = require('./lib/spf').SPF
const net_utils = require('haraka-net-utils')
const DSN = require('haraka-dsn')
exports.SPF = SPF
exports.register = function () {
// Override logging in SPF module
SPF.prototype.log_debug = (str) => this.logdebug(str)
this.load_spf_ini()
this.register_hook('helo', 'helo_spf')
this.register_hook('ehlo', 'helo_spf')
}
exports.load_spf_ini = function () {
this.nu = net_utils // so tests can set public_ip
this.SPF = SPF
this.cfg = this.config.get(
'spf.ini',
{
booleans: [
'-defer.helo_temperror',
'-defer.mfrom_temperror',
'-defer_relay.helo_temperror',
'-defer_relay.mfrom_temperror',
'-deny.helo_none',
'-deny.helo_softfail',
'-deny.helo_fail',
'-deny.helo_permerror',
'-deny.openspf_text',
'-deny.mfrom_none',
'-deny.mfrom_softfail',
'-deny.mfrom_fail',
'-deny.mfrom_permerror',
'-deny_relay.helo_none',
'-deny_relay.helo_softfail',
'-deny_relay.helo_fail',
'-deny_relay.helo_permerror',
'-deny_relay.mfrom_none',
'-deny_relay.mfrom_softfail',
'-deny_relay.mfrom_fail',
'-deny_relay.mfrom_permerror',
'-deny_relay.openspf_text',
'-skip.relaying',
'-skip.auth',
],
},
() => {
this.load_spf_ini()
},
)
// when set, preserve legacy config settings
for (const phase of ['helo', 'mail']) {
if (this.cfg.main[`${phase}_softfail_reject`]) {
this.cfg.deny[`${phase}_softfail`] = true
}
if (this.cfg.main[`${phase}_fail_reject`]) {
this.cfg.deny[`${phase}_fail`] = true
}
if (this.cfg.main[`${phase}_temperror_defer`]) {
this.cfg.defer[`${phase}_temperror`] = true
}
if (this.cfg.main[`${phase}_permerror_reject`]) {
this.cfg.deny[`${phase}_permerror`] = true
}
}
if (!this.cfg.relay) {
this.cfg.relay = { context: 'sender' } // default/legacy
}
this.cfg.lookup_timeout = this.cfg.main.lookup_timeout || this.timeout - 1
}
exports.helo_spf = async function (next, connection, helo) {
const plugin = this
// bypass auth'ed or relay'ing hosts if told to
const skip_reason = this.skip_hosts(connection)
if (skip_reason) {
connection.results.add(plugin, { skip: `helo(${skip_reason})` })
return next()
}
// Bypass private IPs
if (connection.remote.is_private) {
connection.results.add(plugin, { skip: 'helo(private_ip)' })
return next()
}
// RFC 4408, 2.1: "SPF clients must be prepared for the "HELO"
// identity to be malformed or an IP address literal.
if (net_utils.is_ip_literal(helo)) {
connection.results.add(plugin, { skip: 'helo(ip_literal)' })
return next()
}
// avoid 2nd EHLO evaluation if EHLO host is identical
const results = connection.results.get(plugin)
if (results && results.domain === helo) return next()
let timeout = false
const spf = new SPF()
const timer = setTimeout(() => {
timeout = true
connection.loginfo(plugin, 'timeout')
next()
}, plugin.cfg.lookup_timeout * 1000)
try {
const result = await spf.check_host(connection.remote.ip, helo, null)
if (timer) clearTimeout(timer)
if (timeout) return
const host = connection.hello.host
plugin.log_result(
connection,
'helo',
host,
`postmaster@${host}`,
spf.result(result),
)
connection.notes.spf_helo = result // used between hooks
connection.results.add(plugin, {
scope: 'helo',
result: spf.result(result),
domain: host,
emit: true,
})
if (spf.result(result) === 'Pass')
connection.results.add(plugin, { pass: host })
} catch (err) {
connection.logerror(plugin, err)
}
next()
}
exports.hook_mail = async function (next, connection, params) {
const plugin = this
const txn = connection?.transaction
if (!txn) return next()
// bypass auth'ed or relay'ing hosts if told to
const skip_reason = this.skip_hosts(connection)
if (skip_reason) {
txn.results.add(plugin, { skip: `host(${skip_reason})` })
return next(CONT, `skipped because host(${skip_reason})`)
}
// For messages from private IP space...
if (connection.remote?.is_private) {
if (!connection.relaying) return next()
if (plugin.cfg.relay?.context !== 'myself') {
txn.results.add(plugin, { skip: 'host(private_ip)' })
return next(CONT, 'envelope from private IP space')
}
}
const mfrom = params[0].address()
const host = params[0].host
let spf = new SPF()
let auth_result
if (connection.notes?.spf_helo) {
const h_result = connection.notes.spf_helo
const h_host = connection.hello?.host
plugin.save_to_header(connection, spf, h_result, mfrom, h_host, 'helo')
if (!host) {
// Use results from HELO if the return-path is null
auth_result = spf.result(h_result).toLowerCase()
connection.auth_results(`spf=${auth_result} smtp.helo=${h_host}`)
const sender = `<> via ${h_host}`
return plugin.return_results(
next,
connection,
spf,
'helo',
h_result,
sender,
)
}
}
if (!host) return next() // null-sender
let timeout = false
const timer = setTimeout(() => {
timeout = true
connection.loginfo(plugin, 'timeout')
next()
}, plugin.cfg.lookup_timeout * 1000)
spf.helo = connection.hello?.host
function ch_cb(err, result, ip) {
if (timer) clearTimeout(timer)
if (timeout) return
if (err) {
connection.logerror(plugin, err)
return next()
}
plugin.log_result(
connection,
'mfrom',
host,
mfrom,
spf.result(result),
ip ? ip : connection.remote.ip,
)
plugin.save_to_header(
connection,
spf,
result,
mfrom,
host,
'mailfrom',
ip ? ip : connection.remote.ip,
)
auth_result = spf.result(result).toLowerCase()
connection.auth_results(`spf=${auth_result} smtp.mailfrom=${host}`)
txn.notes.spf_mail_result = spf.result(result)
txn.notes.spf_mail_record = spf.spf_record
txn.results.add(plugin, {
scope: 'mfrom',
result: spf.result(result),
domain: host,
emit: true,
})
if (spf.result(result) === 'Pass')
connection.results.add(plugin, { pass: host })
plugin.return_results(next, connection, spf, 'mfrom', result, mfrom)
}
try {
// Always check the client IP first. A relay could be sending inbound mail
// from a non-local domain, which could case an incorrect SPF Fail result
// if we check the public IP first. Only check the public IP if the
// client IP returns a result other than 'Pass'.
const result = await spf.check_host(connection.remote.ip, host, mfrom)
// typical inbound (!relay)
if (!connection.relaying) return ch_cb(null, result)
// outbound (relaying), context=sender
if (plugin.cfg.relay.context === 'sender') return ch_cb(null, result)
// outbound (relaying), context=myself
const my_public_ip = await net_utils.get_public_ip()
let spf_result
if (result) spf_result = spf.result(result).toLowerCase()
if (spf_result && spf_result !== 'pass') {
if (!my_public_ip) {
return ch_cb(new Error(`failed to discover public IP`))
}
spf = new SPF()
const r = await spf.check_host(my_public_ip, host, mfrom)
return ch_cb(null, r, my_public_ip)
}
ch_cb(null, result, connection.remote.ip)
} catch (err) {
ch_cb(err)
}
}
exports.log_result = function (connection, scope, host, mfrom, result, ip) {
const show_ip = ip ? ip : connection.remote.ip
connection.loginfo(
this,
`identity=${scope} ip=${show_ip} domain="${host}" mfrom=<${mfrom}> result=${result}`,
)
}
exports.return_results = function (
next,
connection,
spf,
scope,
result,
sender,
) {
const msgpre = scope === 'helo' ? `sender ${sender}` : `sender <${sender}>`
const deny = connection.relaying ? 'deny_relay' : 'deny'
const defer = connection.relaying ? 'defer_relay' : 'defer'
const sender_id = scope === 'helo' ? connection.hello_host : sender
let text = DSN.sec_unauthorized(
`http://www.openspf.org/Why?s=${scope}&id=${sender_id}&ip=${connection.remote.ip}`,
)
switch (result) {
case spf.SPF_NONE:
if (this.cfg[deny][`${scope}_none`]) {
text = this.cfg[deny].openspf_text
? text
: `${msgpre} SPF record not found`
return next(DENY, text)
}
return next()
case spf.SPF_NEUTRAL:
case spf.SPF_PASS:
return next()
case spf.SPF_SOFTFAIL:
if (this.cfg[deny][`${scope}_softfail`]) {
text = this.cfg[deny].openspf_text ? text : `${msgpre} SPF SoftFail`
return next(DENY, text)
}
return next()
case spf.SPF_FAIL:
if (this.cfg[deny][`${scope}_fail`]) {
text = this.cfg[deny].openspf_text ? text : `${msgpre} SPF Fail`
return next(DENY, text)
}
return next()
case spf.SPF_TEMPERROR:
if (this.cfg[defer][`${scope}_temperror`]) {
return next(DENYSOFT, `${msgpre} SPF Temporary Error`)
}
return next()
case spf.SPF_PERMERROR:
if (this.cfg[deny][`${scope}_permerror`]) {
return next(DENY, `${msgpre} SPF Permanent Error`)
}
return next()
default:
// Unknown result
connection.logerror(this, `unknown result code=${result}`)
return next()
}
}
exports.save_to_header = (connection, spf, result, mfrom, host, id, ip) => {
// Add a trace header
if (!connection?.transaction) return
const des = result === spf.SPF_PASS ? 'designates' : 'does not designate'
const identity = `identity=${id}; client-ip=${ip ? ip : connection.remote.ip}`
connection.transaction.add_leading_header(
'Received-SPF',
`${spf.result(result)} (${connection.local.host}: domain of ${host} ${des} ${connection.remote.ip} as permitted sender) receiver=${connection.local.host}; ${identity} helo=${connection.hello.host}; envelope-from=<${mfrom}>`,
)
}
exports.skip_hosts = function (connection) {
const skip = this?.cfg?.skip
if (skip) {
if (skip.relaying && connection.relaying) return 'relay'
if (skip.auth && connection.notes.auth_user) return 'auth'
}
}