🚀 Big News: Socket Acquires Coana to Bring Reachability Analysis to Every Appsec Team.Learn more
Socket
DemoInstallSign in
Socket

haraka-plugin-headers

Package Overview
Dependencies
Maintainers
0
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

haraka-plugin-headers - npm Package Compare versions

Comparing version

to
1.0.5

CHANGELOG.md

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