New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

mailauth

Package Overview
Dependencies
Maintainers
1
Versions
80
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mailauth - npm Package Compare versions

Comparing version 1.0.2 to 1.0.4

lib/arc/index.js

20

examples/authenticate.js
'use strict';
const { authenticate } = require('../lib/mailauth');
//const { dkimSign } = require('../lib/dkim/sign');
const fs = require('fs');
const main = async () => {
let message = fs.createReadStream(__dirname + '/../test/fixtures/authtest.eml');
let message = await fs.promises.readFile(process.argv[2] || __dirname + '/../test/fixtures/message4.eml');
let res = await authenticate(message, {

@@ -19,2 +21,18 @@ ip: '217.146.67.33',

console.log('----');
/*
let signed = await dkimSign(message, {
signTime: Date.now(),
arc: {
instance: 1,
algorithm: 'rsa-sha256',
signingDomain: 'tahvel.info',
selector: 'test.rsa',
privateKey: fs.readFileSync('./test/fixtures/private-rsa.pem')
}
});
console.log(signed);
*/
};

@@ -21,0 +39,0 @@

4

examples/verify-mbox.js

@@ -38,3 +38,3 @@ 'use strict';

if (result.results.some(r => r.status === 'fail')) {
if (result.results.some(r => r.status.result === 'fail')) {
if (dest) {

@@ -46,3 +46,3 @@ let out = Buffer.concat([Buffer.from('X-DGB: ' + JSON.stringify(result.results)), msg]);

if (result.results.some(r => r.status === 'neutral')) {
if (result.results.some(r => r.status.result === 'neutral')) {
if (dest) {

@@ -49,0 +49,0 @@ let out = Buffer.concat([Buffer.from('X-DGB: ' + JSON.stringify(result.results)), msg]);

'use strict';
const { getSignedHeaderLines, formatDKIMHeaderLine } = require('../../lib/tools');
const { getSigningHeaderLines, formatSignatureHeaderLine, defaultDKIMFieldNames, defaultARCFieldNames } = require('../../lib/tools');
const { MessageParser } = require('./message-parser');
const { dkimBody } = require('./body');
const { dkimHeader } = require('./header');
const { generateCanonicalizedHeader } = require('./header');
const crypto = require('crypto');

@@ -13,3 +13,3 @@

let { canonicalization, signTime, headerList, signatureData } = options || {};
let { canonicalization, signTime, headerList, signatureData, arc } = options || {};

@@ -26,5 +26,22 @@ this.canonicalization = canonicalization || 'relaxed/relaxed';

this.signatureData = signatureData;
this.signatureData = [].concat(signatureData || []).map(entry => {
entry.type = 'DKIM';
return entry;
});
this.signatureHeaders = [];
this.arc = Object.assign({}, arc);
if (this.arc && this.arc.instance && this.arc.signingDomain && this.arc.selector && this.arc.privateKey) {
this.arc.set = this.arc.set || {};
this.signatureData.push({
type: 'ARC',
signingDomain: this.arc.signingDomain, // d=
selector: this.arc.selector, // s=
privateKey: this.arc.privateKey,
algorithm: 'rsa-sha256', // fixed for now
instance: this.arc.instance
});
}
this.bodyHashes = new Map();

@@ -35,3 +52,3 @@ this.setupHashes();

setupHashes() {
for (let signatureData of this.signatureData || []) {
for (let signatureData of this.signatureData) {
if (!signatureData.privateKey) {

@@ -98,4 +115,2 @@ continue;

let signedHeaderLines = getSignedHeaderLines(this.headers.parsed, this.headerList);
for (let signatureData of this.signatureData || []) {

@@ -106,2 +121,18 @@ if (!signatureData.privateKey) {

let fieldNames = this.headerList && this.headerList.length ? this.headerList : false;
if (!fieldNames) {
switch (signatureData.type) {
case 'ARC':
fieldNames = defaultARCFieldNames;
break;
case 'DKIM':
default:
fieldNames = defaultDKIMFieldNames;
break;
}
}
let signingHeaderLines = getSigningHeaderLines(this.headers.parsed, this.headerList);
let algorithm = (signatureData.algorithm || '').toLowerCase().trim();

@@ -129,2 +160,3 @@ let signAlgo = algorithm.split('-').shift().toLowerCase().trim() || null;

}
algorithm = `${signAlgo}-${hashAlgo}`;

@@ -153,5 +185,7 @@ } catch (err) {

let { signingHeaders, dkimHeaderOpts } = dkimHeader(
signedHeaderLines,
let { canonicalizedHeader, dkimHeaderOpts } = generateCanonicalizedHeader(
signatureData.type,
signingHeaderLines,
Object.assign({}, signatureData, {
instance: signatureData.instance, // ARC only
algorithm,

@@ -169,3 +203,3 @@ canonicalization: this.canonicalization,

signAlgo === 'rsa' ? algorithm : null,
signingHeaders,
canonicalizedHeader,
signatureData.privateKey

@@ -176,5 +210,18 @@ )

dkimHeaderOpts.b = signature;
this.signatureHeaders.push(formatDKIMHeaderLine(dkimHeaderOpts, true));
const signatureHeaderLine = formatSignatureHeaderLine(signatureData.type, dkimHeaderOpts, true);
switch (signatureData.type) {
case 'ARC':
this.arc.set['arc-message-signature'] = signatureHeaderLine;
break;
case 'DKIM':
default:
this.signatureHeaders.push(signatureHeaderLine);
break;
}
} catch (err) {
this.errors.push({
type: signatureData.type,
algorithm,

@@ -181,0 +228,0 @@ selector: signatureData.selector,

'use strict';
const { getSignedHeaderLines, getPublicKey, parseDkimHeader } = require('../../lib/tools');
const { getSigningHeaderLines, getPublicKey, parseDkimHeaders, formatAuthHeaderRow, getAligment } = require('../../lib/tools');
const { MessageParser } = require('./message-parser');
const { dkimBody } = require('./body');
const { dkimHeader } = require('./header');
const { generateCanonicalizedHeader } = require('./header');
const { getARChain } = require('../arc');
const addressparser = require('nodemailer/lib/addressparser');

@@ -24,2 +25,4 @@ const crypto = require('crypto');

this.envelopeFrom = false;
this.arc = { chain: false };
}

@@ -29,5 +32,21 @@

this.headers = headers;
this.signatureHeaders = headers.parsed.filter(h => h.key === 'dkim-signature').map(h => parseDkimHeader(h.line));
let fromHeaders = headers.parsed.filter(h => h.key === 'from');
try {
this.arc.chain = getARChain(headers);
if (this.arc.chain?.length) {
this.arc.lastEntry = this.arc.chain[this.arc.chain.length - 1];
}
} catch (err) {
this.arc.error = err;
}
this.signatureHeaders = headers.parsed
.filter(h => h.key === 'dkim-signature')
.map(h => {
const value = parseDkimHeaders(h.line);
value.type = 'DKIM';
return value;
});
let fromHeaders = headers?.parsed?.filter(h => h.key === 'from');
for (let fromHeader of fromHeaders) {

@@ -63,8 +82,19 @@ fromHeader = fromHeader.line.toString();

// include newest ARC-Message-Signature as one of the signature headers to check for
if (this.arc.lastEntry) {
const signatureHeader = this.arc.lastEntry['arc-message-signature'];
signatureHeader.type = 'ARC';
this.signatureHeaders.push(signatureHeader);
const sealHeader = this.arc.lastEntry['arc-seal'];
sealHeader.type = 'AS';
this.signatureHeaders.push(sealHeader);
}
for (let signatureHeader of this.signatureHeaders) {
signatureHeader.algorithm = signatureHeader.parsed.a || '';
signatureHeader.algorithm = signatureHeader.parsed?.a?.value || '';
signatureHeader.signAlgo = signatureHeader.algorithm.split('-').shift().toLowerCase().trim();
signatureHeader.hashAlgo = signatureHeader.algorithm.split('-').pop().toLowerCase().trim();
signatureHeader.canonicalization = signatureHeader.parsed.c || '';
signatureHeader.canonicalization = signatureHeader.parsed?.c?.value || '';
signatureHeader.headerCanon = signatureHeader.canonicalization.split('/').shift().toLowerCase().trim();

@@ -74,10 +104,15 @@ // if body canonicalization is not set, then defaults to 'simple'

signatureHeader.signingDomain = signatureHeader.parsed.d || '';
signatureHeader.selector = signatureHeader.parsed.s || '';
signatureHeader.signingDomain = signatureHeader.parsed?.d?.value || '';
signatureHeader.selector = signatureHeader.parsed?.s?.value || '';
const validSignAlgo = ['rsa', 'ed25519'];
const validHeaderAlgo = signatureHeader.type === 'DKIM' ? ['sha256', 'sha1'] : ['sha256'];
const validHeaderCanon = signatureHeader.type === 'DKIM' ? ['relaxed', 'simple'] : ['relaxed'];
const validBodyCanon = signatureHeader.type === 'DKIM' ? ['relaxed', 'simple'] : ['relaxed'];
if (
!['rsa', 'ed25519'].includes(signatureHeader.signAlgo) ||
!['sha256', 'sha1'].includes(signatureHeader.hashAlgo) ||
!['relaxed', 'simple'].includes(signatureHeader.headerCanon) ||
!['relaxed', 'simple'].includes(signatureHeader.bodyCanon) ||
!validSignAlgo.includes(signatureHeader.signAlgo) ||
!validHeaderAlgo.includes(signatureHeader.hashAlgo) ||
!validHeaderCanon.includes(signatureHeader.headerCanon) ||
!validBodyCanon.includes(signatureHeader.bodyCanon) ||
!signatureHeader.signingDomain ||

@@ -93,5 +128,7 @@ !signatureHeader.selector

let maxLength = false;
if (signatureHeader.parsed.l && Number(signatureHeader.parsed.l) > 0 && !isNaN(signatureHeader.parsed.l)) {
maxLength = Number(signatureHeader.parsed.l);
if (signatureHeader.parsed?.l?.value) {
maxLength = signatureHeader.parsed?.l?.value;
}
this.bodyHashes.set(signatureHeader.bodyHashKey, dkimBody(signatureHeader.bodyCanon, signatureHeader.hashAlgo, maxLength));

@@ -124,26 +161,50 @@ }

let signedHeaderLines = getSignedHeaderLines(this.headers.parsed, signatureHeader.parsed.h, true);
let signingHeaderLines = getSigningHeaderLines(this.headers.parsed, signatureHeader.parsed?.h?.value, true);
let { signingHeaders } = dkimHeader(signedHeaderLines, {
dkimHeaderLine: signatureHeader.original,
canonicalization: signatureHeader.canonicalization
let { canonicalizedHeader } = generateCanonicalizedHeader(signatureHeader.type, signingHeaderLines, {
signatureHeaderLine: signatureHeader.original,
canonicalization: signatureHeader.canonicalization,
instance: ['ARC', 'AS'].includes(signatureHeader.type) ? signatureHeader.parsed?.i?.value : false
});
let error;
let publicKey;
let status;
let status = {
result: 'neutral',
comment: false,
// ptype properties
header: {
// signing domain
i: signatureHeader.signingDomain ? `@${signatureHeader.signingDomain}` : false,
// dkim selector
s: signatureHeader.selector,
// algo
a: signatureHeader.parsed?.a?.value,
// signature value
b: signatureHeader.parsed?.b?.value ? `${signatureHeader.parsed?.b?.value.substr(0, 8)}` : false
},
policy: {}
};
if (signatureHeader.type === 'DKIM' && this.headerFrom?.length) {
status.aligned = this.headerFrom?.length ? getAligment(this.headerFrom[0].split('@').pop(), [signatureHeader.signingDomain]) : false;
}
let bodyHash = this.bodyHashes.get(signatureHeader.bodyHashKey);
if (signatureHeader.parsed.bh !== bodyHash) {
status = 'neutral';
error = `body hash did not verify`;
if (signatureHeader.parsed?.bh?.value !== bodyHash) {
status.result = 'neutral';
status.comment = `body hash did not verify`;
} else {
try {
publicKey = await getPublicKey(`${signatureHeader.selector}._domainkey.${signatureHeader.signingDomain}`, this.resolver);
publicKey = await getPublicKey(
signatureHeader.type,
`${signatureHeader.selector}._domainkey.${signatureHeader.signingDomain}`,
this.resolver
);
try {
status = crypto.verify(
status.result = crypto.verify(
signatureHeader.signAlgo === 'rsa' ? signatureHeader.algorithm : null,
signingHeaders,
canonicalizedHeader,
publicKey,
Buffer.from(signatureHeader.parsed.b, 'base64')
Buffer.from(signatureHeader.parsed?.b?.value, 'base64')
)

@@ -153,7 +214,7 @@ ? 'pass'

if (status === 'fail') {
error = 'bad signature';
status.comment = 'bad signature';
}
} catch (err) {
status = 'neutral';
error = err.message;
status.result = 'neutral';
status.comment = err.message;
}

@@ -163,29 +224,29 @@ } catch (err) {

case 'ENOTFOUND':
status = 'neutral';
error = `no key`;
status.result = 'neutral';
status.comment = `no key`;
break;
case 'EINVALIDVER':
status = 'neutral';
error = `unknown key version`;
status.result = 'neutral';
status.comment = `unknown key version`;
break;
case 'EINVALIDTYPE':
status = 'neutral';
error = `unknown key type`;
status.result = 'neutral';
status.comment = `unknown key type`;
break;
case 'EINVALIDVAL':
status = 'neutral';
error = `invalid public key`;
status.result = 'neutral';
status.comment = `invalid public key`;
break;
case 'ESHORTKEY':
status = 'policy';
error = `weak key`;
status.result = 'policy';
status.policy['dkim-rules'] = `weak-key`;
break;
default:
status = 'temperror';
error = `DNS failure. ${err.message}`;
status.result = 'temperror';
status.comment = `DNS failure: ${err.code || err.message}`;
}

@@ -198,7 +259,7 @@ }

selector: signatureHeader.selector,
signature: signatureHeader.parsed.b,
algo: signatureHeader.parsed.a,
format: signatureHeader.parsed.c,
signature: signatureHeader.parsed?.b?.value,
algo: signatureHeader.parsed?.a?.value,
format: signatureHeader.parsed?.c?.value,
bodyHash,
bodyHashExpecting: signatureHeader.parsed.bh,
bodyHashExpecting: signatureHeader.parsed?.bh?.value,
status

@@ -211,7 +272,13 @@ };

if (error) {
result.message = error;
switch (signatureHeader.type) {
case 'ARC':
if (this.arc.lastEntry) {
this.arc.lastEntry.messageSignature = result;
}
break;
case 'DKIM':
default:
this.results.push(result);
break;
}
this.results.push(result);
}

@@ -221,4 +288,6 @@ } finally {

this.results.push({
status: 'none',
message: 'message not signed'
status: {
result: 'none',
comment: 'message not signed'
}
});

@@ -228,11 +297,3 @@ }

this.results.forEach(result => {
result.info = [
`dkim=${result.status}`,
result.message ? `(${result.message})` : '',
result.signingDomain ? `header.i=@${result.signingDomain}` : '',
result.selector ? `header.s=${result.selector}` : '',
result.signature ? `header.b="${result.signature.substr(0, 8)}"` : ''
]
.filter(val => val)
.join(' ');
result.info = formatAuthHeaderRow('dkim', result.status);
});

@@ -239,0 +300,0 @@ }

@@ -6,3 +6,3 @@ 'use strict';

const dkimHeader = (signedHeaderLines, options) => {
const generateCanonicalizedHeader = (type, signingHeaderLines, options) => {
options = options || {};

@@ -12,5 +12,5 @@ let canonicalization = (options.canonicalization || 'relaxed/relaxed').toString().split('/').shift().toLowerCase().trim();

case 'simple':
return simpleHeaders(signedHeaderLines, options);
return simpleHeaders(type, signingHeaderLines, options);
case 'relaxed':
return relaxedHeaders(signedHeaderLines, options);
return relaxedHeaders(type, signingHeaderLines, options);
default:

@@ -21,2 +21,2 @@ throw new Error('Unknown header canonicalization');

module.exports = { dkimHeader };
module.exports = { generateCanonicalizedHeader };
'use strict';
const { formatDKIMHeaderLine } = require('../../../lib/tools');
const { formatSignatureHeaderLine, formatRelaxedLine } = require('../../../lib/tools');
const formatRelaxedLine = (line, suffix) => {
let result =
line
.toString('binary')
// unfold
.replace(/\r?\n/g, '')
// key to lowercase, trim around :
.replace(/^([^:]*):\s*/, (m, k) => k.toLowerCase().trim() + ':')
// single WSP
.replace(/\s+/g, ' ')
.trim() + (suffix ? suffix : '');
return Buffer.from(result, 'binary');
};
// generate headers for signing
const relaxedHeaders = (signedHeaderLines, options) => {
let { dkimHeaderLine, signingDomain, selector, algorithm, canonicalization, bodyHash, signTime, signature } = options || {};
const relaxedHeaders = (type, signingHeaderLines, options) => {
let { signatureHeaderLine, signingDomain, selector, algorithm, canonicalization, bodyHash, signTime, signature, instance } = options || {};
let chunks = [];
for (let signedHeaderLine of signedHeaderLines.headers) {
for (let signedHeaderLine of signingHeaderLines.headers) {
chunks.push(formatRelaxedLine(signedHeaderLine.line, '\r\n'));

@@ -30,3 +16,3 @@ }

if (!dkimHeaderLine) {
if (!signatureHeaderLine) {
opts = {

@@ -37,6 +23,11 @@ a: algorithm,

d: signingDomain,
h: signedHeaderLines.keys,
h: signingHeaderLines.keys,
bh: bodyHash
};
if (instance) {
// ARC only
opts.i = instance;
}
if (signTime) {

@@ -54,3 +45,4 @@ if (typeof signTime === 'string' || typeof signTime === 'number') {

dkimHeaderLine = formatDKIMHeaderLine(
signatureHeaderLine = formatSignatureHeaderLine(
type,
Object.assign(

@@ -69,3 +61,3 @@ {

Buffer.from(
formatRelaxedLine(dkimHeaderLine)
formatRelaxedLine(signatureHeaderLine)
.toString('binary')

@@ -78,5 +70,5 @@ // remove value from b= key

return { signingHeaders: Buffer.concat(chunks), dkimHeaderLine, dkimHeaderOpts: opts };
return { canonicalizedHeader: Buffer.concat(chunks), signatureHeaderLine, dkimHeaderOpts: opts };
};
module.exports = { relaxedHeaders };
'use strict';
const { formatDKIMHeaderLine } = require('../../../lib/tools');
const { formatSignatureHeaderLine } = require('../../../lib/tools');

@@ -10,7 +10,7 @@ const formatSimpleLine = (line, suffix) => {

// generate headers for signing
const simpleHeaders = (signedHeaderLines, options) => {
let { dkimHeaderLine, signingDomain, selector, algorithm, canonicalization, bodyHash, signTime, signature } = options || {};
const simpleHeaders = (type, signingHeaderLines, options) => {
let { signatureHeaderLine, signingDomain, selector, algorithm, canonicalization, bodyHash, signTime, signature, instance } = options || {};
let chunks = [];
for (let signedHeaderLine of signedHeaderLines.headers) {
for (let signedHeaderLine of signingHeaderLines.headers) {
chunks.push(formatSimpleLine(signedHeaderLine.line, '\r\n'));

@@ -21,3 +21,3 @@ }

if (!dkimHeaderLine) {
if (!signatureHeaderLine) {
opts = {

@@ -28,6 +28,11 @@ a: algorithm,

d: signingDomain,
h: signedHeaderLines.keys,
h: signingHeaderLines.keys,
bh: bodyHash
};
if (instance) {
// ARC only (should never happen thoug as simple algo is not allowed)
opts.i = instance;
}
if (signTime) {

@@ -45,3 +50,4 @@ if (typeof signTime === 'string' || typeof signTime === 'number') {

dkimHeaderLine = formatDKIMHeaderLine(
signatureHeaderLine = formatSignatureHeaderLine(
type,
Object.assign(

@@ -60,3 +66,3 @@ {

Buffer.from(
formatSimpleLine(dkimHeaderLine)
formatSimpleLine(signatureHeaderLine)
.toString('binary')

@@ -69,5 +75,5 @@ // remove value from b= key

return { signingHeaders: Buffer.concat(chunks), dkimHeaderLine, dkimHeaderOpts: opts };
return { canonicalizedHeader: Buffer.concat(chunks), signatureHeaderLine, dkimHeaderOpts: opts };
};
module.exports = { simpleHeaders };

@@ -27,5 +27,5 @@ 'use strict';

return { signatures: dkimSigner.signatureHeaders.join('\r\n') + '\r\n', errors: dkimSigner.errors };
return { signatures: dkimSigner.signatureHeaders.join('\r\n') + '\r\n', arc: dkimSigner.arc, errors: dkimSigner.errors };
};
module.exports = { dkimSign };

@@ -9,3 +9,5 @@ 'use strict';

await writeToStream(dkimVerifier, input);
return {
const result = {
//headers: dkimVerifier.headers,
headerFrom: dkimVerifier.headerFrom,

@@ -15,4 +17,20 @@ envelopeFrom: dkimVerifier.envelopeFrom,

};
Object.defineProperty(result, 'headers', {
enumerable: false,
configurable: false,
writable: false,
value: dkimVerifier.headers
});
Object.defineProperty(result, 'arc', {
enumerable: false,
configurable: false,
writable: false,
value: dkimVerifier.arc
});
return result;
};
module.exports = { dkimVerify };

@@ -6,22 +6,5 @@ 'use strict';

const dmarc = async opts => {
let result = await verifyDmarc(opts);
switch (result && result.status) {
case 'pass':
case 'fail':
case 'none':
case 'temperror':
case 'permerror':
{
let p = result.p;
let sp = result.sp || result.p;
let policyInfo = [].concat(p ? `p=${p.toUpperCase()}` : []).concat(sp ? `sp=${sp.toUpperCase()}` : []);
result.info = `dmarc=${result.status}${policyInfo.length ? ` (${policyInfo.join(' ')})` : ''} header.from=${result.domain}`;
}
break;
}
return result;
return await verifyDmarc(opts);
};
module.exports = { dmarc };

@@ -6,2 +6,3 @@ 'use strict';

const psl = require('psl');
const { formatAuthHeaderRow, getAligment } = require('../tools');

@@ -74,38 +75,4 @@ const resolveTxt = async (domain, resolver) => {

const formatDomain = domain => {
domain = domain.toLowerCase().trim();
try {
domain = punycode.toASCII(domain).toLowerCase().trim();
} catch (err) {
// ignore punycode errors
}
return domain;
};
const getAligment = (fromDomain, domainList, strict) => {
domainList = [].concat(domainList || []);
if (strict) {
fromDomain = formatDomain(fromDomain);
for (let domain of domainList) {
domain = formatDomain(psl.get(domain) || domain);
if (formatDomain(domain) === fromDomain) {
return domain;
}
}
}
// match org domains
fromDomain = formatDomain(psl.get(fromDomain) || fromDomain);
for (let domain of domainList) {
domain = formatDomain(psl.get(domain) || domain);
if (domain === fromDomain) {
return domain;
}
}
return false;
};
const verifyDmarc = async opts => {
let { headerFrom, spfDomains, dkimDomains, resolver } = opts;
let { headerFrom, spfDomains, dkimDomains, resolver, arcResult } = opts;

@@ -134,4 +101,18 @@ resolver = resolver || dns.resolve;

let formatResponse = response => {
response.info = formatAuthHeaderRow('dmarc', response.status);
return response;
};
let orgDomain = psl.get(domain);
let status = {
result: 'neutral',
comment: false,
// ptype properties
header: {
from: orgDomain || domain
}
};
let dmarcRecord;

@@ -142,3 +123,4 @@ try {

// temperror?
return { status: 'temperror', domain: orgDomain || domain, error: err.message };
status.result = 'temperror';
return formatResponse({ status, domain: orgDomain || domain, error: err.message });
}

@@ -149,5 +131,12 @@

// none
return { status: 'none', domain: orgDomain || domain };
status.result = 'none';
return formatResponse({ status, domain: orgDomain || domain });
}
status.comment = []
.concat(dmarcRecord.p ? `p=${dmarcRecord.p.toUpperCase()}` : [])
.concat(dmarcRecord.sp ? `sp=${dmarcRecord.sp.toUpperCase()}` : [])
.concat(arcResult?.status?.result ? `arc=${arcResult?.status?.result}` : [])
.join(' ');
// use "sp" if this is a subdomain of an org domain and "sp" is set, otherwise use "p"

@@ -158,10 +147,20 @@ const policy = dmarcRecord.isOrgRecord && dmarcRecord.sp ? dmarcRecord.sp : dmarcRecord.p;

const spfAlignment = getAligment(domain, spfDomains, { strict: dmarcRecord.aspf === 's' });
if (dkimAlignment || spfAlignment) {
// pass
return { status: 'pass', domain: orgDomain || domain, policy, p: dmarcRecord.p, sp: dmarcRecord.sp || dmarcRecord.p };
status.result = 'pass';
} else {
// fail
status.result = 'fail';
}
// fail
return { status: 'fail', domain: orgDomain || domain, policy, p: dmarcRecord.p, sp: dmarcRecord.sp || dmarcRecord.p };
return formatResponse({
status,
domain: orgDomain || domain,
policy,
p: dmarcRecord.p,
sp: dmarcRecord.sp || dmarcRecord.p
});
};
module.exports = verifyDmarc;

@@ -6,2 +6,3 @@ 'use strict';

const { dmarc } = require('./dmarc');
const { arc } = require('./arc');
const libmime = require('libmime');

@@ -32,2 +33,6 @@ const os = require('os');

const arcResult = await arc(dkimResult.arc, {
resolver: opts.resolver
});
let headers = [];

@@ -38,3 +43,3 @@

dkimResult.results.forEach(row => {
arHeader.push(`${libmime.foldLines(row.info)}`);
arHeader.push(`${libmime.foldLines(row.info, 160)}`);
});

@@ -44,12 +49,10 @@ }

if (spfResult) {
arHeader.push(
libmime.foldLines(
`spf=${spfResult.status}${spfResult.info ? ` (${spfResult.info})` : ''}${
spfResult['envelope-from'] ? ` smtp.mailfrom=${spfResult['envelope-from']}` : ''
}`
)
);
arHeader.push(libmime.foldLines(spfResult.info, 160));
headers.push(spfResult.header);
}
if (arcResult && arcResult.info) {
arHeader.push(`${libmime.foldLines(arcResult.info, 160)}`);
}
let dmarcResult;

@@ -59,8 +62,9 @@ if (dkimResult && dkimResult.headerFrom) {

headerFrom: dkimResult.headerFrom,
spfDomains: [].concat((spfResult && spfResult.status === 'pass' && spfResult.domain) || []),
dkimDomains: (dkimResult.results || []).filter(r => r.status === 'pass').map(r => r.signingDomain),
spfDomains: [].concat((spfResult && spfResult.status.result === 'pass' && spfResult.domain) || []),
dkimDomains: (dkimResult.results || []).filter(r => r.status.result === 'pass').map(r => r.signingDomain),
arcResult,
resolver: opts.resolver
});
if (dmarcResult.info) {
arHeader.push(`${libmime.foldLines(dmarcResult.info)}`);
arHeader.push(`${libmime.foldLines(dmarcResult.info, 160)}`);
}

@@ -75,2 +79,3 @@ }

dmarc: dmarcResult,
arc: arcResult,
headers: headers.join('\r\n') + '\r\n'

@@ -77,0 +82,0 @@ };

@@ -9,2 +9,3 @@ 'use strict';

const domainSchema = Joi.string().domain({ allowUnicode: false, tlds: false });
const { formatAuthHeaderRow, escapeCommentValue } = require('../tools');

@@ -14,44 +15,15 @@ const MAX_RESOLVE_COUNT = 50;

const formatHeaders = result => {
let header = `Received-SPF: ${result.status}${result.info ? ` (${result.info})` : ''} client-ip=${result['client-ip']};`;
let header = `Received-SPF: ${result.status.result}${result.status.comment ? ` (${escapeCommentValue(result.status.comment)})` : ''} client-ip=${
result['client-ip']
};`;
return libmime.foldLines(header);
return libmime.foldLines(header, 160);
};
/**
*
* @param {Object} opts
* @param {String} opts.sender Email address
* @param {String} opts.ip Client IP address
* @param {String} [opts.mta] Hostname of the MTA or MX server that processes the message
* @param {String} opts.helo Client EHLO/HELO hostname
*/
const verify = async opts => {
let { sender, ip, mta, helo, resolver, maxResolveCount } = opts || {};
mta = mta || os.hostname();
sender = sender || `postmaster@${helo}`;
// convert mapped IPv6 IP addresses to IPv4
let mappingMatch = (ip || '').toString().match(/^[:A-F]+:((\d+\.){3}\d+)$/i);
if (mappingMatch) {
ip = mappingMatch[1];
}
let atPos = sender.indexOf('@');
if (atPos < 0) {
sender = `postmaster@${sender}`;
} else if (atPos === 0) {
sender = `postmaster${sender}`;
}
let domain = sender.split('@').pop().toLowerCase().trim();
resolver = resolver || dns.promises.resolve;
// DNS resolver method
let limitedResolver = (resolver, maxResolveCount) => {
let resolveCount = 0;
maxResolveCount = maxResolveCount || MAX_RESOLVE_COUNT;
// DNS resolver method
let limitedResolver = async (domain, type) => {
return async (domain, type) => {
// do not allow to make more that MAX_RESOLVE_COUNT DNS requests per SPF check

@@ -117,3 +89,46 @@ resolveCount++;

};
};
/**
*
* @param {Object} opts
* @param {String} opts.sender Email address
* @param {String} opts.ip Client IP address
* @param {String} [opts.mta] Hostname of the MTA or MX server that processes the message
* @param {String} opts.helo Client EHLO/HELO hostname
*/
const verify = async opts => {
let { sender, ip, mta, helo, resolver, maxResolveCount } = opts || {};
mta = mta || os.hostname();
sender = sender || `postmaster@${helo}`;
// convert mapped IPv6 IP addresses to IPv4
let mappingMatch = (ip || '').toString().match(/^[:A-F]+:((\d+\.){3}\d+)$/i);
if (mappingMatch) {
ip = mappingMatch[1];
}
let atPos = sender.indexOf('@');
if (atPos < 0) {
sender = `postmaster@${sender}`;
} else if (atPos === 0) {
sender = `postmaster${sender}`;
}
let domain = sender.split('@').pop().toLowerCase().trim();
resolver = resolver || dns.promises.resolve;
let status = {
result: 'neutral',
comment: false,
// ptype properties
smtp: {
mailfrom: sender,
helo
}
};
let result;

@@ -131,3 +146,11 @@ try {

result = await spfVerify(domain, { sender, ip, mta, helo, resolver: limitedResolver });
result = await spfVerify(domain, {
sender,
ip,
mta,
helo,
// generate DNS handler
resolver: limitedResolver(resolver, maxResolveCount)
});
} catch (err) {

@@ -160,19 +183,19 @@ if (err.spfResult) {

case '+':
response.status = 'pass';
response.info = `${mta}: domain of ${sender} designates ${ip} as permitted sender`;
status.result = 'pass';
status.comment = `${mta}: domain of ${sender} designates ${ip} as permitted sender`;
break;
case '~':
response.status = 'softfail';
response.info = `${mta}: domain of transitioning ${sender} does not designate ${ip} as permitted sender`;
status.result = 'softfail';
status.comment = `${mta}: domain of transitioning ${sender} does not designate ${ip} as permitted sender`;
break;
case '-':
response.status = 'fail';
response.info = `${mta}: domain of ${sender} does not designate ${ip} as permitted sender`;
status.result = 'fail';
status.comment = `${mta}: domain of ${sender} does not designate ${ip} as permitted sender`;
break;
case '?':
response.status = 'neutral';
response.info = `${mta}: ${ip} is neither permitted nor denied by domain of ${sender}`;
status.result = 'neutral';
status.comment = `${mta}: ${ip} is neither permitted nor denied by domain of ${sender}`;
break;

@@ -182,9 +205,9 @@

case 'none':
response.status = 'none';
response.info = `${mta}: ${domain} does not designate permitted sender hosts`;
status.result = 'none';
status.comment = `${mta}: ${domain} does not designate permitted sender hosts`;
break;
case 'permerror':
response.status = 'permerror';
response.info = `${mta}: permanent error in processing during lookup of ${sender}${result.text ? `: ${result.text}` : ''}`;
status.result = 'permerror';
status.comment = `${mta}: permanent error in processing during lookup of ${sender}${result.text ? `: ${result.text}` : ''}`;
break;

@@ -194,8 +217,11 @@

default:
response.status = 'temperror';
response.info = `${mta}: error in processing during lookup of ${sender}${result.text ? `: ${result.text}` : ''}`;
status.result = 'temperror';
status.comment = `${mta}: error in processing during lookup of ${sender}${result.text ? `: ${result.text}` : ''}`;
break;
}
response.status = status;
response.header = formatHeaders(response);
response.info = formatAuthHeaderRow('spf', status);
return response;

@@ -202,0 +228,0 @@ };

@@ -28,3 +28,3 @@ 'use strict';

let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `invalid address definition: ${val}` };
err.spfResult = { error: 'permerror', text: `invalid address definition: ${val}` };
throw err;

@@ -41,3 +41,3 @@ }

let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `invalid cidr definition: ${val}` };
err.spfResult = { error: 'permerror', text: `invalid cidr definition: ${val}` };
throw err;

@@ -80,3 +80,3 @@ }

let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `multiple SPF records found for ${domain}` };
err.spfResult = { error: 'permerror', text: `multiple SPF records found for ${domain}` };
throw err;

@@ -90,3 +90,3 @@ }

let err = new Error('SPF failure');
err.spfResult = { error: 'none', message: `no SPF records found for ${domain}` };
err.spfResult = { error: 'none', text: `no SPF records found for ${domain}` };
throw err;

@@ -103,3 +103,3 @@ }

let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `invalid modifier ${part}` };
err.spfResult = { error: 'permerror', text: `invalid modifier ${part}` };
throw err;

@@ -118,7 +118,7 @@ }

let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `Empty modifier value for ${modifier}` };
err.spfResult = { error: 'permerror', text: `Empty modifier value for ${modifier}` };
throw err;
} else if (modifier === 'redirect' && !/^([\x21-\x2D\x2f-\x7e]+\.)+[a-z]+[a-z\-0-9]*$/.test(value)) {
let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `Invalid redirect target ${value}` };
err.spfResult = { error: 'permerror', text: `Invalid redirect target ${value}` };
throw err;

@@ -140,3 +140,3 @@ }

let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `Unknown mechanism ${mechanism}` };
err.spfResult = { error: 'permerror', text: `Unknown mechanism ${mechanism}` };
throw err;

@@ -149,3 +149,3 @@ }

let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `more than 1 redirect found` };
err.spfResult = { error: 'permerror', text: `more than 1 redirect found` };
throw err;

@@ -200,3 +200,3 @@ }

let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `unexpected empty value` };
err.spfResult = { error: 'permerror', text: `unexpected empty value` };
throw err;

@@ -227,3 +227,3 @@ }

let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `unexpected extension for all` };
err.spfResult = { error: 'permerror', text: `unexpected extension for all` };
throw err;

@@ -265,3 +265,3 @@ }

let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `bare IP address` };
err.spfResult = { error: 'permerror', text: `bare IP address` };
throw err;

@@ -285,3 +285,3 @@ }

let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `invalid CIDR for IP` };
err.spfResult = { error: 'permerror', text: `invalid CIDR for IP` };
throw err;

@@ -301,3 +301,3 @@ }

let err = new Error('SPF failure');
err.spfResult = { error: 'permerror', message: `invalid IP address` };
err.spfResult = { error: 'permerror', text: `invalid IP address` };
throw err;

@@ -304,0 +304,0 @@ }

@@ -0,1 +1,3 @@

/* eslint no-control-regex: 0 */
'use strict';

@@ -10,4 +12,6 @@

const packageData = require('../package');
const parseDkimHeaders = require('./parse-dkim-headers');
const psl = require('psl');
const defaultFieldNames =
const defaultDKIMFieldNames =
'From:Sender:Reply-To:Subject:Date:Message-ID:To:' +

@@ -20,2 +24,9 @@ 'Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID:' +

const defaultARCFieldNames = `DKIM-Signature:Delivered-To:${defaultDKIMFieldNames}`;
const defaultASFieldNames = `ARC-Authentication-Results:ARC-Message-Signature:ARC-Seal`;
const keyOrderingDKIM = ['v', 'a', 'c', 'd', 'h', 'i', 'l', 'q', 's', 't', 'x', 'z', 'bh', 'b'];
const keyOrderingARC = ['i', 'a', 'c', 'd', 'h', 'l', 'q', 's', 't', 'x', 'z', 'bh', 'b'];
const keyOrderingAS = ['i', 'a', 't', 'cv', 'd', 's', 'b'];
const writeToStream = async (stream, input, chunkSize) => {

@@ -91,4 +102,4 @@ chunkSize = chunkSize || 64 * 1024;

const getSignedHeaderLines = (parsedHeaders, fieldNames, verify) => {
fieldNames = (fieldNames || defaultFieldNames)
const getSigningHeaderLines = (parsedHeaders, fieldNames, verify) => {
fieldNames = (fieldNames || defaultDKIMFieldNames)
.split(':')

@@ -131,10 +142,51 @@ .map(key => key.trim().toLowerCase())

*/
const formatDKIMHeaderLine = (values, folded) => {
values = Object.assign({ v: 1, t: Math.round(Date.now() / 1000), q: 'dns/txt' }, values);
let keyOrdering = ['v', 'a', 'c', 'd', 'h', 'i', 'l', 'q', 's', 't', 'x', 'z', 'bh', 'b'];
const formatSignatureHeaderLine = (type, values, folded) => {
type = (type || '').toString().toUpperCase();
let header =
'DKIM-Signature: ' +
let keyOrdering, headerKey;
switch (type) {
case 'DKIM':
headerKey = 'DKIM-Signature';
keyOrdering = keyOrderingDKIM;
values = Object.assign(
{
v: 1,
t: Math.round(Date.now() / 1000),
q: 'dns/txt'
},
values
);
break;
case 'ARC':
headerKey = 'ARC-Message-Signature';
keyOrdering = keyOrderingARC;
values = Object.assign(
{
t: Math.round(Date.now() / 1000),
q: 'dns/txt'
},
values
);
break;
case 'AS':
headerKey = 'ARC-Seal';
keyOrdering = keyOrderingAS;
values = Object.assign(
{
t: Math.round(Date.now() / 1000)
},
values
);
break;
default:
throw new Error('Unknown Signature type');
}
const header =
`${headerKey}: ` +
Object.keys(values)
.filter(key => values[key] !== false && typeof values[key] !== 'undefined' && values.key !== null)
.filter(key => values[key] !== false && typeof values[key] !== 'undefined' && values.key !== null && keyOrdering.includes(key))
.sort((a, b) => keyOrdering.indexOf(a) - keyOrdering.indexOf(b))

@@ -157,3 +209,3 @@ .map(key => {

if (key === 'i') {
if (key === 'i' && type === 'DKIM') {
let atPos = val.indexOf('@');

@@ -183,53 +235,7 @@ if (atPos >= 0) {

const parseDkimHeader = buf => {
let line = (buf || '').toString().trim();
let splitterPos = line.indexOf(':');
if (splitterPos >= 0) {
line = line.substr(splitterPos + 1).trim();
}
return {
parsed: Object.fromEntries(
line
.replace(/\r?\n/g, '')
.split(/\s*;\s*/)
.map(entry => {
let splitterPos = entry.indexOf('=');
let key, value;
if (splitterPos < 0) {
key = 'default';
value = entry.replace(/\s+/g, ' ').trim();
} else {
key = entry.substr(0, splitterPos);
value = entry
.substr(splitterPos + 1)
.trim()
.replace(/\s+/g, ' ')
.trim();
}
key = key.toLowerCase().trim();
value = value.trim();
switch (key) {
case 'bh':
case 'b':
case 'p':
value = value.replace(/\s+/g, '');
break;
}
return [key, value.trim()];
})
),
original: buf
};
};
const getPublicKey = async (rr, resolver) => {
const getPublicKey = async (type, rr, resolver) => {
resolver = resolver || dns.resolve;
let list = await resolver(rr, 'TXT');
let entry =
let row =
list &&

@@ -241,12 +247,7 @@ []

if (entry) {
entry = parseDkimHeader(entry);
if (row) {
// prefix value for parsing as there is no default value
let entry = parseDkimHeaders(`DNS: TXT;${row}`);
if (entry.parsed.v && (entry.parsed.v || '').toString().toLowerCase().trim() !== 'dkim1') {
let err = new Error('Unknown key version');
err.code = 'EINVALIDVER';
throw err;
}
let pubKey = entry.parsed && entry.parsed.p;
let pubKey = entry?.parsed?.p?.value;
if (!pubKey) {

@@ -258,6 +259,12 @@ let err = new Error('Missing key value');

if (type === 'DKIM' && entry?.parsed?.v && (entry?.parsed?.v?.value || '').toString().toLowerCase().trim() !== 'dkim1') {
let err = new Error('Unknown key version');
err.code = 'EINVALIDVER';
throw err;
}
pubKey = Buffer.from(`-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`);
let keyType = crypto.createPublicKey({ key: pubKey, format: 'pem' }).asymmetricKeyType;
if (!['rsa', 'ed25519'].includes(keyType) || (entry.parsed.k && entry.parsed.k.toLowerCase() !== keyType)) {
if (!['rsa', 'ed25519'].includes(keyType) || (entry?.parsed?.k && entry?.parsed?.k?.value?.toLowerCase() !== keyType)) {
let err = new Error('Unknown key type');

@@ -320,10 +327,120 @@ err.code = 'EINVALIDTYPE';

const escapePropValue = value => {
value = (value || '')
.toString()
.replace(/[\x00-\x1F]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (!/[\s\x00-\x1F\x7F-\uFFFF()<>,;:\\"/[\]?=]/.test(value)) {
// return token value
return value;
}
// return quoted string with escaped quotes
return `"${value.replace(/["\\]/g, c => `\\${c}`)}"`;
};
const escapeCommentValue = value => {
value = (value || '')
.toString()
.replace(/[\x00-\x1F]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
return `${value.replace(/[\\)]/g, c => `\\${c}`)}`;
};
const formatAuthHeaderRow = (method, status) => {
status = status || {};
let parts = [];
parts.push(`${method}=${status.result || 'none'}`);
if (status.comment) {
parts.push(`(${escapeCommentValue(status.comment)})`);
}
for (let ptype of ['policy', 'smtp', 'body', 'header']) {
if (!status[ptype] || typeof status[ptype] !== 'object') {
continue;
}
for (let prop of Object.keys(status[ptype])) {
if (status[ptype][prop]) {
parts.push(`${ptype}.${prop}=${escapePropValue(status[ptype][prop])}`);
}
}
}
return parts.join(' ');
};
const formatRelaxedLine = (line, suffix) => {
let result =
line
.toString('binary')
// unfold
.replace(/\r?\n/g, '')
// key to lowercase, trim around :
.replace(/^([^:]*):\s*/, (m, k) => k.toLowerCase().trim() + ':')
// single WSP
.replace(/\s+/g, ' ')
.trim() + (suffix ? suffix : '');
return Buffer.from(result, 'binary');
};
const formatDomain = domain => {
domain = domain.toLowerCase().trim();
try {
domain = punycode.toASCII(domain).toLowerCase().trim();
} catch (err) {
// ignore punycode errors
}
return domain;
};
const getAligment = (fromDomain, domainList, strict) => {
domainList = [].concat(domainList || []);
if (strict) {
fromDomain = formatDomain(fromDomain);
for (let domain of domainList) {
domain = formatDomain(psl.get(domain) || domain);
if (formatDomain(domain) === fromDomain) {
return domain;
}
}
}
// match org domains
fromDomain = formatDomain(psl.get(fromDomain) || fromDomain);
for (let domain of domainList) {
domain = formatDomain(psl.get(domain) || domain);
if (domain === fromDomain) {
return domain;
}
}
return false;
};
module.exports = {
writeToStream,
parseHeaders,
getSignedHeaderLines,
formatDKIMHeaderLine,
parseDkimHeader,
defaultDKIMFieldNames,
defaultARCFieldNames,
defaultASFieldNames,
getSigningHeaderLines,
formatSignatureHeaderLine,
parseDkimHeaders,
getPublicKey,
fetch
formatAuthHeaderRow,
escapeCommentValue,
fetch,
getAligment,
formatRelaxedLine
};
{
"name": "mailauth",
"version": "1.0.2",
"version": "1.0.4",
"description": "Email authentication library for Node.js",

@@ -27,12 +27,12 @@ "main": "lib/mailauth.js",

"chai": "4.2.0",
"eslint": "7.11.0",
"eslint": "7.12.1",
"eslint-config-nodemailer": "1.2.0",
"eslint-config-prettier": "6.12.0",
"eslint-config-prettier": "6.15.0",
"js-yaml": "3.14.0",
"mbox-reader": "1.1.5",
"mocha": "8.1.3"
"mocha": "8.2.0"
},
"dependencies": {
"ipaddr.js": "2.0.0",
"joi": "17.2.1",
"joi": "17.3.0",
"libmime": "5.0.0",

@@ -39,0 +39,0 @@ "node-forge": "0.10.0",

@@ -9,4 +9,4 @@ # mailauth

- [x] DMARC verification
- [ ] ARC signing
- [ ] ARC verification
- [x] ARC verification
- [ ] ARC sealing
- [ ] MTA-STS resolver

@@ -62,3 +62,3 @@

Validate DKIM signatures, SPF and DMARC for an email.
Validate DKIM signatures, SPF, DMARC and ARC for an email.

@@ -86,8 +86,8 @@ ```js

```
Received-SPF: pass (mx.ethereal.email: domain of andris@ekiri.ee designates
217.146.67.33 as permitted sender) client-ip=217.146.67.33;
Received-SPF: pass (mx.ethereal.email: domain of andris@ekiri.ee designates 217.146.67.33 as permitted sender) client-ip=217.146.67.33;
Authentication-Results: mx.ethereal.email;
dkim=pass header.i=@ekiri.ee header.s=default header.b="1VSEye1n"
spf=pass (mx.ethereal.email: domain of andris@ekiri.ee designates
217.146.67.33 as permitted sender) smtp.mailfrom=andris@ekiri.ee;
dkim=pass header.i=@ekiri.ee header.s=default header.a=rsa-sha256 header.b=TXuCNlsq;
spf=pass (mx.ethereal.email: domain of andris@ekiri.ee designates 217.146.67.33 as permitted sender) smtp.mailfrom=andris@ekiri.ee
smtp.helo=uvn-67-33.tll01.zonevs.eu;
arc=pass (i=2 spf=neutral dkim=pass dkdomain=ekiri.ee);
dmarc=none header.from=ekiri.ee

@@ -163,3 +163,3 @@ From: ...

dkim=pass header.i=@tahvel.info header.s=test.rsa header.b="BrEgDN4A"
dkim=policy (weak key) header.i=@tahvel.info header.s=test.small header.b="d0jjgPun"
dkim=policy policy.dkim-rules=weak-key header.i=@tahvel.info header.s=test.small header.b="d0jjgPun"
```

@@ -166,0 +166,0 @@

@@ -142,5 +142,5 @@ /* eslint no-unused-expressions:0 */

if (Array.isArray(testdata.result)) {
expect(testdata.result).to.include(result.status);
expect(testdata.result).to.include(result?.status?.result);
} else {
expect(result.status).to.equal(testdata.result);
expect(testdata.result).to.equal(result?.status?.result);
}

@@ -147,0 +147,0 @@ });

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc