haraka-plugin-headers
Advanced tools
Comparing version
507
index.js
// validate message headers and some fields | ||
const tlds = require('haraka-tld'); | ||
const tlds = require('haraka-tld') | ||
@@ -7,55 +7,66 @@ const phish_targets = [] | ||
exports.register = function () { | ||
this.load_headers_ini() | ||
this.load_headers_ini(); | ||
try { | ||
this.addrparser = require('address-rfc2822') | ||
} catch (e) { | ||
this.logerror( | ||
"unable to load address-rfc2822, try\n\n\t'npm install -g address-rfc2822'\n\n", | ||
) | ||
} | ||
catch (e) { | ||
this.logerror("unable to load address-rfc2822, try\n\n\t'npm install -g address-rfc2822'\n\n") | ||
} | ||
if (this.cfg.check.duplicate_singular) this.register_hook('data_post', 'duplicate_singular') | ||
if (this.cfg.check.missing_required) this.register_hook('data_post', 'missing_required') | ||
if (this.cfg.check.invalid_return_path) this.register_hook('data_post', 'invalid_return_path') | ||
if (this.cfg.check.invalid_date) this.register_hook('data_post', 'invalid_date') | ||
if (this.cfg.check.user_agent) this.register_hook('data_post', 'user_agent') | ||
if (this.cfg.check.direct_to_mx) this.register_hook('data_post', 'direct_to_mx') | ||
if (this.cfg.check.duplicate_singular) | ||
this.register_hook('data_post', 'duplicate_singular') | ||
if (this.cfg.check.missing_required) | ||
this.register_hook('data_post', 'missing_required') | ||
if (this.cfg.check.invalid_return_path) | ||
this.register_hook('data_post', 'invalid_return_path') | ||
if (this.cfg.check.invalid_date) | ||
this.register_hook('data_post', 'invalid_date') | ||
if (this.cfg.check.user_agent) this.register_hook('data_post', 'user_agent') | ||
if (this.cfg.check.direct_to_mx) | ||
this.register_hook('data_post', 'direct_to_mx') | ||
if (this.addrparser) { | ||
if (this.cfg.check.from_match) this.register_hook('data_post', 'from_match') | ||
if (this.cfg.check.delivered_to) this.register_hook('data_post', 'delivered_to') | ||
if (this.cfg.check.from_match) this.register_hook('data_post', 'from_match') | ||
if (this.cfg.check.delivered_to) | ||
this.register_hook('data_post', 'delivered_to') | ||
} | ||
if (this.cfg.check.mailing_list) this.register_hook('data_post', 'mailing_list') | ||
if (this.cfg.check.from_phish) this.register_hook('data_post', 'from_phish') | ||
if (this.cfg.check.mailing_list) | ||
this.register_hook('data_post', 'mailing_list') | ||
if (this.cfg.check.from_phish) this.register_hook('data_post', 'from_phish') | ||
} | ||
exports.load_headers_ini = function () { | ||
const plugin = this; | ||
plugin.cfg = plugin.config.get('headers.ini', { | ||
booleans: [ | ||
'+check.duplicate_singular', | ||
'+check.missing_required', | ||
'+check.invalid_return_path', | ||
'+check.invalid_date', | ||
'+check.user_agent', | ||
'+check.direct_to_mx', | ||
'+check.from_match', | ||
'+check.delivered_to', | ||
'+check.mailing_list', | ||
'+check.from_phish', | ||
const plugin = this | ||
plugin.cfg = plugin.config.get( | ||
'headers.ini', | ||
{ | ||
booleans: [ | ||
'+check.duplicate_singular', | ||
'+check.missing_required', | ||
'+check.invalid_return_path', | ||
'+check.invalid_date', | ||
'+check.user_agent', | ||
'+check.direct_to_mx', | ||
'+check.from_match', | ||
'+check.delivered_to', | ||
'+check.mailing_list', | ||
'+check.from_phish', | ||
'-reject.duplicate_singular', | ||
'-reject.missing_required', | ||
'-reject.invalid_return_path', | ||
'-reject.invalid_date', | ||
'+reject.delivered_to', | ||
'-reject.from_phish', | ||
], | ||
}, () => { | ||
plugin.load_headers_ini() | ||
}) | ||
'-reject.duplicate_singular', | ||
'-reject.missing_required', | ||
'-reject.invalid_return_path', | ||
'-reject.invalid_date', | ||
'+reject.delivered_to', | ||
'-reject.from_phish', | ||
], | ||
}, | ||
() => { | ||
plugin.load_headers_ini() | ||
}, | ||
) | ||
for (const d in plugin.cfg.phish_domains) { | ||
phish_targets.push(new RegExp(d.replace('.','[.]'), 'i')) | ||
phish_targets.push(new RegExp(d.replace('.', '[.]'), 'i')) | ||
} | ||
@@ -66,20 +77,30 @@ // console.log(phish_targets) | ||
exports.duplicate_singular = function (next, connection) { | ||
const plugin = this; | ||
const plugin = this | ||
// RFC 5322 Section 3.6, Headers that MUST be unique if present | ||
const singular = plugin.cfg.main.singular !== undefined ? | ||
plugin.cfg.main.singular.split(',') : [ | ||
'Date', 'From', 'Sender', 'Reply-To', 'To', 'Cc', | ||
'Bcc', 'Message-Id', 'In-Reply-To', 'References', | ||
'Subject' | ||
]; | ||
const singular = | ||
plugin.cfg.main.singular !== undefined | ||
? plugin.cfg.main.singular.split(',') | ||
: [ | ||
'Date', | ||
'From', | ||
'Sender', | ||
'Reply-To', | ||
'To', | ||
'Cc', | ||
'Bcc', | ||
'Message-Id', | ||
'In-Reply-To', | ||
'References', | ||
'Subject', | ||
] | ||
const failures = []; | ||
const failures = [] | ||
for (const name of singular) { | ||
if (connection.transaction.header.get_all(name).length <= 1) { | ||
continue; | ||
continue | ||
} | ||
connection.transaction.results.add(plugin, {fail: `duplicate:${name}`}); | ||
failures.push(name); | ||
connection.transaction.results.add(plugin, { fail: `duplicate:${name}` }) | ||
failures.push(name) | ||
} | ||
@@ -89,24 +110,28 @@ | ||
if (plugin.cfg.reject.duplicate_singular) { | ||
return next(DENY, `Only one ${failures[0]} header allowed. See RFC 5322, Section 3.6`); | ||
return next( | ||
DENY, | ||
`Only one ${failures[0]} header allowed. See RFC 5322, Section 3.6`, | ||
) | ||
} | ||
return next(); | ||
return next() | ||
} | ||
connection.transaction.results.add(plugin, {pass: 'duplicate'}); | ||
next(); | ||
connection.transaction.results.add(plugin, { pass: 'duplicate' }) | ||
next() | ||
} | ||
exports.missing_required = function (next, connection) { | ||
const plugin = this; | ||
const plugin = this | ||
// Enforce RFC 5322 Section 3.6, Headers that MUST be present | ||
const required = plugin.cfg.main.required !== undefined ? | ||
plugin.cfg.main.required.split(',') : | ||
['Date', 'From']; | ||
const required = | ||
plugin.cfg.main.required !== undefined | ||
? plugin.cfg.main.required.split(',') | ||
: ['Date', 'From'] | ||
const failures = []; | ||
const failures = [] | ||
for (const h of required) { | ||
if (connection.transaction.header.get_all(h).length === 0) { | ||
connection.transaction.results.add(plugin, {fail: `missing:${h}`}); | ||
failures.push(h); | ||
connection.transaction.results.add(plugin, { fail: `missing:${h}` }) | ||
failures.push(h) | ||
} | ||
@@ -117,13 +142,13 @@ } | ||
if (plugin.cfg.reject.missing_required) { | ||
return next(DENY, `Required header '${failures[0]}' missing`); | ||
return next(DENY, `Required header '${failures[0]}' missing`) | ||
} | ||
return next(); | ||
return next() | ||
} | ||
connection.transaction.results.add(plugin, {pass: 'missing'}); | ||
next(); | ||
connection.transaction.results.add(plugin, { pass: 'missing' }) | ||
next() | ||
} | ||
exports.invalid_return_path = function (next, connection) { | ||
const plugin = this; | ||
const plugin = this | ||
@@ -137,10 +162,17 @@ // Tests for Return-Path headers that shouldn't be present | ||
// Return-Path, aka Reverse-PATH, Envelope FROM, RFC5321.MailFrom | ||
const rp = connection.transaction.header.get('Return-Path'); | ||
const rp = connection.transaction.header.get('Return-Path') | ||
if (rp) { | ||
if (connection.relaying) { // On messages we originate | ||
connection.transaction.results.add(plugin, {fail: 'Return-Path', emit: true}); | ||
if (connection.relaying) { | ||
// On messages we originate | ||
connection.transaction.results.add(plugin, { | ||
fail: 'Return-Path', | ||
emit: true, | ||
}) | ||
if (plugin.cfg.reject.invalid_return_path) { | ||
return next(DENY, "outgoing mail must not have a Return-Path header (RFC 5321)"); | ||
return next( | ||
DENY, | ||
'outgoing mail must not have a Return-Path header (RFC 5321)', | ||
) | ||
} | ||
return next(); | ||
return next() | ||
} | ||
@@ -152,65 +184,69 @@ | ||
// strip the Return-Path header. | ||
connection.transaction.remove_header('Return-Path'); | ||
connection.transaction.remove_header('Return-Path') | ||
// unless it was added by Haraka. Which at present, doesn't. | ||
} | ||
connection.transaction.results.add(plugin, {pass: 'Return-Path'}); | ||
next(); | ||
connection.transaction.results.add(plugin, { pass: 'Return-Path' }) | ||
next() | ||
} | ||
exports.invalid_date = function (next, connection) { | ||
const plugin = this; | ||
const plugin = this | ||
// Assure Date header value is [somewhat] sane | ||
let msg_date = connection.transaction.header.get_all('Date'); | ||
if (!msg_date || msg_date.length === 0) return next(); | ||
let msg_date = connection.transaction.header.get_all('Date') | ||
if (!msg_date || msg_date.length === 0) return next() | ||
connection.logdebug(plugin, `message date: ${msg_date}`); | ||
msg_date = Date.parse(msg_date); | ||
connection.logdebug(plugin, `message date: ${msg_date}`) | ||
msg_date = Date.parse(msg_date) | ||
const date_future_days = plugin.cfg.main.date_future_days !== undefined ? | ||
plugin.cfg.main.date_future_days : | ||
2; | ||
const date_future_days = | ||
plugin.cfg.main.date_future_days !== undefined | ||
? plugin.cfg.main.date_future_days | ||
: 2 | ||
if (date_future_days > 0) { | ||
const too_future = new Date(); | ||
too_future.setHours(too_future.getHours() + 24 * date_future_days); | ||
const too_future = new Date() | ||
too_future.setHours(too_future.getHours() + 24 * date_future_days) | ||
// connection.logdebug(plugin, "too future: " + too_future); | ||
if (msg_date > too_future) { | ||
connection.transaction.results.add(plugin, {fail: 'invalid_date(future)'}); | ||
connection.transaction.results.add(plugin, { | ||
fail: 'invalid_date(future)', | ||
}) | ||
if (plugin.cfg.reject.invalid_date) { | ||
return next(DENY, "The Date header is too far in the future"); | ||
return next(DENY, 'The Date header is too far in the future') | ||
} | ||
return next(); | ||
return next() | ||
} | ||
} | ||
const date_past_days = plugin.cfg.main.date_past_days !== undefined ? | ||
plugin.cfg.main.date_past_days : | ||
15; | ||
const date_past_days = | ||
plugin.cfg.main.date_past_days !== undefined | ||
? plugin.cfg.main.date_past_days | ||
: 15 | ||
if (date_past_days > 0) { | ||
const too_old = new Date(); | ||
too_old.setHours(too_old.getHours() - 24 * date_past_days); | ||
const too_old = new Date() | ||
too_old.setHours(too_old.getHours() - 24 * date_past_days) | ||
// connection.logdebug(plugin, "too old: " + too_old); | ||
if (msg_date < too_old) { | ||
connection.loginfo(plugin, `date is older than: ${too_old}`); | ||
connection.transaction.results.add(plugin, {fail: 'invalid_date(past)'}); | ||
connection.loginfo(plugin, `date is older than: ${too_old}`) | ||
connection.transaction.results.add(plugin, { fail: 'invalid_date(past)' }) | ||
if (plugin.cfg.reject.invalid_date) { | ||
return next(DENY, "The Date header is too old"); | ||
return next(DENY, 'The Date header is too old') | ||
} | ||
return next(); | ||
return next() | ||
} | ||
} | ||
connection.transaction.results.add(plugin, {pass: 'invalid_date'}); | ||
next(); | ||
connection.transaction.results.add(plugin, { pass: 'invalid_date' }) | ||
next() | ||
} | ||
exports.user_agent = function (next, connection) { | ||
const plugin = this; | ||
const plugin = this | ||
if (!connection.transaction) return next(); | ||
if (!connection.transaction) return next() | ||
let found_ua = 0; | ||
let found_ua = 0 | ||
@@ -226,22 +262,27 @@ // User-Agent: Thunderbird, Squirrelmail, Roundcube, Mutt, MacOutlook, | ||
const headers = [ | ||
'user-agent','x-mailer','x-mua','x-yahoo-newman-property', | ||
'x-ms-has-attach' | ||
]; | ||
'user-agent', | ||
'x-mailer', | ||
'x-mua', | ||
'x-yahoo-newman-property', | ||
'x-ms-has-attach', | ||
] | ||
// for (const h in headers) {} | ||
for (const name of headers) { | ||
const header = connection.transaction.header.get(name); | ||
if (!header) continue; // header not present | ||
found_ua++; | ||
connection.transaction.results.add(plugin, {pass: `UA(${header.substring(0,12)})`}); | ||
const header = connection.transaction.header.get(name) | ||
if (!header) continue // header not present | ||
found_ua++ | ||
connection.transaction.results.add(plugin, { | ||
pass: `UA(${header.substring(0, 12)})`, | ||
}) | ||
} | ||
if (found_ua) return next(); | ||
if (found_ua) return next() | ||
connection.transaction.results.add(plugin, {fail: 'UA'}); | ||
next(); | ||
connection.transaction.results.add(plugin, { fail: 'UA' }) | ||
next() | ||
} | ||
exports.direct_to_mx = function (next, connection) { | ||
const plugin = this; | ||
const plugin = this | ||
if (!connection.transaction) return next(); | ||
if (!connection.transaction) return next() | ||
@@ -252,4 +293,4 @@ // Legit messages normally have at least 2 hops (Received headers) | ||
// User authenticated, so we're likely the first MTA | ||
connection.transaction.results.add(plugin, {skip: 'direct-to-mx(auth)'}); | ||
return next(); | ||
connection.transaction.results.add(plugin, { skip: 'direct-to-mx(auth)' }) | ||
return next() | ||
} | ||
@@ -259,20 +300,22 @@ | ||
const received = connection.transaction.header.get_all('received'); | ||
const received = connection.transaction.header.get_all('received') | ||
if (!received) { | ||
connection.transaction.results.add(plugin, {fail: 'direct-to-mx(none)'}); | ||
return next(); | ||
connection.transaction.results.add(plugin, { fail: 'direct-to-mx(none)' }) | ||
return next() | ||
} | ||
const c = received.length; | ||
const c = received.length | ||
if (c < 2) { | ||
connection.transaction.results.add(plugin, {fail: `direct-to-mx(too few Received(${c}))`}); | ||
return next(); | ||
connection.transaction.results.add(plugin, { | ||
fail: `direct-to-mx(too few Received(${c}))`, | ||
}) | ||
return next() | ||
} | ||
connection.transaction.results.add(plugin, {pass: `direct-to-mx(${c})`}); | ||
next(); | ||
connection.transaction.results.add(plugin, { pass: `direct-to-mx(${c})` }) | ||
next() | ||
} | ||
exports.from_match = function (next, connection) { | ||
const plugin = this; | ||
const plugin = this | ||
@@ -282,147 +325,163 @@ // see if the header From matches the envelope FROM. There are valid | ||
// likely to be spam than ham. This test is useful for heuristics. | ||
if (!connection.transaction) return next(); | ||
if (!connection.transaction) return next() | ||
const env_addr = connection.transaction.mail_from; | ||
const env_addr = connection.transaction.mail_from | ||
if (!env_addr) { | ||
connection.transaction.results.add(plugin, {fail: 'from_match(null)'}); | ||
return next(); | ||
connection.transaction.results.add(plugin, { fail: 'from_match(null)' }) | ||
return next() | ||
} | ||
const hdr_from = connection.transaction.header.get_decoded('From'); | ||
const hdr_from = connection.transaction.header.get_decoded('From') | ||
if (!hdr_from) { | ||
connection.transaction.results.add(plugin, {fail: 'from_match(missing)'}); | ||
return next(); | ||
connection.transaction.results.add(plugin, { fail: 'from_match(missing)' }) | ||
return next() | ||
} | ||
let hdr_addr; | ||
let hdr_addr | ||
try { | ||
hdr_addr = (plugin.addrparser.parse(hdr_from))[0]; | ||
hdr_addr = plugin.addrparser.parse(hdr_from)[0] | ||
} catch (e) { | ||
connection.logwarn( | ||
plugin, | ||
`parsing "${hdr_from.trim()}" with address-rfc2822 plugin returned error: ${e.message}`, | ||
) | ||
connection.transaction.results.add(plugin, { | ||
fail: 'from_match(rfc_violation)', | ||
}) | ||
return next() | ||
} | ||
catch (e) { | ||
connection.logwarn(plugin, `parsing "${hdr_from.trim()}" with address-rfc2822 plugin returned error: ${e.message}`); | ||
connection.transaction.results.add(plugin, {fail: 'from_match(rfc_violation)'}); | ||
return next(); | ||
} | ||
if (!hdr_addr) { | ||
connection.loginfo(plugin, `address at fault is: ${hdr_from}`); | ||
connection.transaction.results.add(plugin, {fail: 'from_match(unparsable)'}); | ||
return next(); | ||
connection.loginfo(plugin, `address at fault is: ${hdr_from}`) | ||
connection.transaction.results.add(plugin, { | ||
fail: 'from_match(unparsable)', | ||
}) | ||
return next() | ||
} | ||
if (env_addr.address().toLowerCase() === hdr_addr.address.toLowerCase()) { | ||
connection.transaction.results.add(plugin, {pass: 'from_match'}); | ||
return next(); | ||
connection.transaction.results.add(plugin, { pass: 'from_match' }) | ||
return next() | ||
} | ||
const extra = ['domain']; | ||
const env_dom = tlds.get_organizational_domain(env_addr.host); | ||
const msg_dom = tlds.get_organizational_domain(hdr_addr.host()); | ||
const extra = ['domain'] | ||
const env_dom = tlds.get_organizational_domain(env_addr.host) | ||
const msg_dom = tlds.get_organizational_domain(hdr_addr.host()) | ||
if (env_dom && msg_dom && env_dom.toLowerCase() === msg_dom.toLowerCase()) { | ||
const fcrdns = connection.results.get('fcrdns'); | ||
if (fcrdns && fcrdns.fcrdns && new RegExp(`${msg_dom }\\b`, 'i').test(fcrdns.fcrdns)) { | ||
extra.push('fcrdns'); | ||
const fcrdns = connection.results.get('fcrdns') | ||
if ( | ||
fcrdns && | ||
fcrdns.fcrdns && | ||
new RegExp(`${msg_dom}\\b`, 'i').test(fcrdns.fcrdns) | ||
) { | ||
extra.push('fcrdns') | ||
} | ||
const helo = connection.results.get('helo.checks'); | ||
const helo = connection.results.get('helo.checks') | ||
if (helo && helo.helo_host && /msg_dom$/.test(helo.helo_host)) { | ||
extra.push('helo'); | ||
extra.push('helo') | ||
} | ||
connection.transaction.results.add(plugin, {pass: `from_match(${extra.join(',')})`}); | ||
return next(); | ||
connection.transaction.results.add(plugin, { | ||
pass: `from_match(${extra.join(',')})`, | ||
}) | ||
return next() | ||
} | ||
connection.transaction.results.add(plugin, {emit: true, | ||
fail: `from_match(${env_dom} / ${msg_dom})` | ||
}); | ||
next(); | ||
connection.transaction.results.add(plugin, { | ||
emit: true, | ||
fail: `from_match(${env_dom} / ${msg_dom})`, | ||
}) | ||
next() | ||
} | ||
exports.delivered_to = function (next, connection) { | ||
const plugin = this; | ||
const plugin = this | ||
const txn = connection.transaction; | ||
if (!txn) return next(); | ||
const del_to = txn.header.get('Delivered-To'); | ||
if (!del_to) return next(); | ||
const txn = connection.transaction | ||
if (!txn) return next() | ||
const del_to = txn.header.get('Delivered-To') | ||
if (!del_to) return next() | ||
const rcpts = connection.transaction.rcpt_to; | ||
const rcpts = connection.transaction.rcpt_to | ||
for (const rcptElement of rcpts) { | ||
const rcpt = rcptElement.address(); | ||
if (rcpt !== del_to) continue; | ||
connection.transaction.results.add(plugin, {emit: true, fail: 'delivered_to'}); | ||
if (!plugin.cfg.reject.delivered_to) continue; | ||
return next(DENY, "Invalid Delivered-To header content"); | ||
const rcpt = rcptElement.address() | ||
if (rcpt !== del_to) continue | ||
connection.transaction.results.add(plugin, { | ||
emit: true, | ||
fail: 'delivered_to', | ||
}) | ||
if (!plugin.cfg.reject.delivered_to) continue | ||
return next(DENY, 'Invalid Delivered-To header content') | ||
} | ||
next(); | ||
next() | ||
} | ||
exports.mailing_list = function (next, connection) { | ||
const plugin = this; | ||
if (!connection.transaction) return next(); | ||
const plugin = this | ||
if (!connection.transaction) return next() | ||
const mlms = { | ||
'Mailing-List' : [ | ||
{ mlm: 'ezmlm', match: 'ezmlm' }, | ||
'Mailing-List': [ | ||
{ mlm: 'ezmlm', match: 'ezmlm' }, | ||
{ mlm: 'yahoogroups', match: 'yahoogroups' }, | ||
{ mlm: 'googlegroups', match: 'googlegroups' }, | ||
], | ||
'Sender' : [ | ||
{ mlm: 'majordomo', start: 'owner-' }, | ||
], | ||
'X-Mailman-Version' : [ { mlm: 'mailman' } ], | ||
'X-Majordomo-Version': [ { mlm: 'majordomo' } ], | ||
'X-Google-Loop' : [ { mlm: 'googlegroups' } ], | ||
}; | ||
Sender: [{ mlm: 'majordomo', start: 'owner-' }], | ||
'X-Mailman-Version': [{ mlm: 'mailman' }], | ||
'X-Majordomo-Version': [{ mlm: 'majordomo' }], | ||
'X-Google-Loop': [{ mlm: 'googlegroups' }], | ||
} | ||
let found_mlm = 0; | ||
const txr = connection.transaction.results; | ||
let found_mlm = 0 | ||
const txr = connection.transaction.results | ||
Object.keys(mlms).forEach(name => { | ||
const header = connection.transaction.header.get(name); | ||
if (!header) { return; } // header not present | ||
Object.keys(mlms).forEach((name) => { | ||
const header = connection.transaction.header.get(name) | ||
if (!header) { | ||
return | ||
} // header not present | ||
for (const j of mlms[name]) { | ||
if (j.start) { | ||
if (header.substring(0,j.start.length) === j.start) { | ||
txr.add(plugin, {pass: `MLM(${j.mlm})`}); | ||
found_mlm++; | ||
continue; | ||
if (header.substring(0, j.start.length) === j.start) { | ||
txr.add(plugin, { pass: `MLM(${j.mlm})` }) | ||
found_mlm++ | ||
continue | ||
} | ||
connection.logdebug(plugin, `mlm start miss: ${name}: ${header}`); | ||
connection.logdebug(plugin, `mlm start miss: ${name}: ${header}`) | ||
} | ||
if (j.match) { | ||
if (header.match(new RegExp(j.match,'i'))) { | ||
txr.add(plugin, {pass: `MLM(${j.mlm})`}); | ||
found_mlm++; | ||
continue; | ||
if (header.match(new RegExp(j.match, 'i'))) { | ||
txr.add(plugin, { pass: `MLM(${j.mlm})` }) | ||
found_mlm++ | ||
continue | ||
} | ||
connection.logdebug(plugin, `mlm match miss: ${name}: ${header}`); | ||
connection.logdebug(plugin, `mlm match miss: ${name}: ${header}`) | ||
} | ||
if (name === 'X-Mailman-Version') { | ||
txr.add(plugin, {pass: `MLM(${j.mlm})`}); | ||
found_mlm++; | ||
continue; | ||
txr.add(plugin, { pass: `MLM(${j.mlm})` }) | ||
found_mlm++ | ||
continue | ||
} | ||
if (name === 'X-Majordomo-Version') { | ||
txr.add(plugin, {pass: `MLM(${j.mlm})`}); | ||
found_mlm++; | ||
continue; | ||
txr.add(plugin, { pass: `MLM(${j.mlm})` }) | ||
found_mlm++ | ||
continue | ||
} | ||
if (name === 'X-Google-Loop') { | ||
txr.add(plugin, {pass: `MLM(${j.mlm})`}); | ||
found_mlm++; | ||
continue; | ||
txr.add(plugin, { pass: `MLM(${j.mlm})` }) | ||
found_mlm++ | ||
continue | ||
} | ||
} | ||
}); | ||
if (found_mlm) return next(); | ||
}) | ||
if (found_mlm) return next() | ||
connection.transaction.results.add(plugin, {msg: 'not MLM'}); | ||
next(); | ||
connection.transaction.results.add(plugin, { msg: 'not MLM' }) | ||
next() | ||
} | ||
exports.from_phish = function (next, connection) { | ||
const plugin = this; | ||
if (!connection.transaction) return next(); | ||
const plugin = this | ||
if (!connection.transaction) return next() | ||
@@ -432,3 +491,3 @@ // check the header From display name for common phish domains | ||
if (!hdr_from) { | ||
connection.transaction.results.add(plugin, {skip: 'from_phish(missing)'}) | ||
connection.transaction.results.add(plugin, { skip: 'from_phish(missing)' }) | ||
return next() | ||
@@ -438,7 +497,9 @@ } | ||
for (const addr of phish_targets) { | ||
if (!addr.test(hdr_from)) continue; // not a sender match | ||
if (!addr.test(hdr_from)) continue // not a sender match | ||
if (!exports.has_auth_match(addr, connection)) { | ||
connection.transaction.results.add(plugin, {fail: `from_phish(${hdr_from}`}) | ||
if (plugin.cfg.reject.from_phish) return next(DENY, `Phishing message detected`) | ||
connection.transaction.results.add(plugin, { | ||
fail: `from_phish(${hdr_from}`, | ||
}) | ||
if (plugin.cfg.reject.from_phish) | ||
return next(DENY, `Phishing message detected`) | ||
return next() | ||
@@ -448,3 +509,3 @@ } | ||
connection.transaction.results.add(plugin, {pass: 'from_phish'}) | ||
connection.transaction.results.add(plugin, { pass: 'from_phish' }) | ||
next() | ||
@@ -456,8 +517,8 @@ } | ||
const spf = conn.transaction.results.get('spf'); // only check mfrom | ||
if (spf && re.test(spf.pass)) return true; | ||
const spf = conn.transaction.results.get('spf') // only check mfrom | ||
if (spf && re.test(spf.pass)) return true | ||
// try DKIM via results | ||
const dkim = conn.transaction.results.get('dkim_verify'); | ||
if (dkim && re.test(dkim.pass)) return true; | ||
const dkim = conn.transaction.results.get('dkim_verify') | ||
if (dkim && re.test(dkim.pass)) return true | ||
@@ -467,3 +528,5 @@ // fallback DKIM via notes | ||
if (dkim_note) { | ||
const passes = dkim_note.filter( r => r.result === 'pass' && re.test(r.domain)) | ||
const passes = dkim_note.filter( | ||
(r) => r.result === 'pass' && re.test(r.domain), | ||
) | ||
if (passes.length) return true | ||
@@ -470,0 +533,0 @@ } |
{ | ||
"name": "haraka-plugin-headers", | ||
"version": "1.0.4", | ||
"version": "1.0.5", | ||
"description": "Haraka plugin that performs tests on email headers", | ||
"main": "index.js", | ||
"files": [ | ||
"config", | ||
"CHANGELOG.md" | ||
], | ||
"scripts": { | ||
"lint": "npx eslint *.js test", | ||
"lintfix": "npx eslint --fix *.js test", | ||
"test": "npx mocha" | ||
"format": "npm run prettier:fix && npm run lint:fix", | ||
"prettier": "npx prettier . --check", | ||
"prettier:fix": "npx prettier . --write --log-level=warn", | ||
"lint": "npx eslint@^8 *.js test", | ||
"lint:fix": "npx eslint@^8 --fix *.js test", | ||
"test": "npx mocha@^10", | ||
"versions": "npx dependency-version-checker check", | ||
"versions:fix": "npx dependency-version-checker update" | ||
}, | ||
@@ -27,12 +36,10 @@ "repository": { | ||
"devDependencies": { | ||
"eslint": ">=8", | ||
"eslint-plugin-haraka": "*", | ||
"haraka-test-fixtures": "*", | ||
"mocha": ">=9" | ||
"@haraka/eslint-config": "^1.1.5", | ||
"haraka-test-fixtures": "^1.3.8" | ||
}, | ||
"dependencies": { | ||
"haraka-tld": "*", | ||
"address-rfc2821": "*", | ||
"address-rfc2822": "*" | ||
"haraka-tld": "^1.2.2", | ||
"address-rfc2821": "^2.1.2", | ||
"address-rfc2822": "^2.2.2" | ||
} | ||
} |
[![CI Tests][ci-img]][ci-url] | ||
[![Code Climate][clim-img]][clim-url] | ||
[![NPM][npm-img]][npm-url] | ||
# haraka-plugin-headers | ||
@@ -34,3 +32,3 @@ | ||
## duplicate\_singular | ||
## duplicate_singular | ||
@@ -42,3 +40,3 @@ Assure that all the singular headers are present only once. The list of | ||
## missing\_required | ||
## missing_required | ||
@@ -50,3 +48,3 @@ Assuring that all the required headers are present. The list of required | ||
## invalid\_return\_path | ||
## invalid_return_path | ||
@@ -56,3 +54,3 @@ Messages arriving via the internet should not have a Return-Path header set. | ||
## invalid\_date | ||
## invalid_date | ||
@@ -68,3 +66,3 @@ Checks the date header and makes sure it's somewhat sane. By default, the date | ||
## user\_agent | ||
## user_agent | ||
@@ -74,3 +72,3 @@ Attempt to determine the User-Agent that generated the email. A UA is | ||
## direct\_to\_mx | ||
## direct_to_mx | ||
@@ -83,3 +81,3 @@ Counts the received headers. If there aren't at least two, then the MUA is | ||
## from\_match | ||
## from_match | ||
@@ -89,3 +87,3 @@ See if the header From domain matches the envelope FROM domain. There are many | ||
## mailing\_list | ||
## mailing_list | ||
@@ -100,3 +98,3 @@ Attempt to determine if this message was sent via an email list. This is very | ||
## from\_phish | ||
## from_phish | ||
@@ -137,4 +135,4 @@ A common form of phishing is spamming the From display name with the domain name of the popular entity whose accounts they're phishing for. This tests the domains in the [phish_domains] configuration section. If that domains appears in the From header, it must also appear in the envelope sender address. | ||
<!-- leave these buried at the bottom of the document --> | ||
<!-- leave these buried at the bottom of the document --> | ||
[ci-img]: https://github.com/haraka/haraka-plugin-headers/actions/workflows/ci.yml/badge.svg | ||
@@ -144,3 +142,1 @@ [ci-url]: https://github.com/haraka/haraka-plugin-headers/actions/workflows/ci.yml | ||
[clim-url]: https://codeclimate.com/github/haraka/haraka-plugin-headers | ||
[npm-img]: https://nodei.co/npm/haraka-plugin-headers.png | ||
[npm-url]: https://www.npmjs.com/package/haraka-plugin-headers |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Wildcard dependency
QualityPackage has a dependency with a floating version range. This can cause issues if the dependency publishes a new major version.
Found 3 instances in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
2
-50%0
-100%0
-100%24581
-38.33%6
-60%449
-37.55%132
-2.94%1
Infinity%Updated
Updated
Updated