UNPKG

mina-attestations

Version:
324 lines (268 loc) 7.31 kB
/** * This file was copied and modified from zk-email-verify, which copied and modified from mailauth: * https://github.com/postalsys/mailauth */ // NB! fails to properly parse nested comments (should be rare enough though) export { parseDkimHeaders }; type Part = { [key: string]: string }; function parseValue(str: string) { let line = str.replace(/\s+/g, ' ').trim(); let parts: Part[] = []; let lastState: string | boolean = false; const createPart = () => { let part: Part = { key: '', value: '', }; parts.push(part); return part; }; const parse = () => { let state = 'key'; let escaped; let quote; let curPart = createPart(); for (let i = 0; i < line.length; i++) { let c = line.charAt(i); switch (state) { // @ts-ignore case 'key': if (c === '=') { state = 'value'; break; } // falls through case 'value': { if (escaped === true) { curPart[state] += c; break; } switch (c) { case ' ': // start new part curPart = createPart(); state = 'key'; break; case '\\': escaped = true; break; case '"': case "'": lastState = state; state = 'quoted'; quote = c; break; default: curPart[state] += c; break; } break; } case 'quoted': if (escaped === true && typeof lastState === 'string') { curPart[lastState] += c; break; } switch (c) { case '\\': escaped = true; break; case quote: state = lastState as string; break; default: if (typeof lastState === 'string') { curPart[lastState] += c; } break; } break; } } let result: { [key: string]: any } = { value: parts[0]!.key, }; parts.slice(1).forEach((part) => { if (part.key || part.value) { let path = part.key!.split('.'); let curRes = result; let final = path.pop(); for (let p of path) { if (typeof curRes[p] !== 'object' || !curRes[p]) { curRes[p] = {}; } curRes = curRes[p]; } curRes[final ?? ''] = part.value; } }); return result; }; return parse(); } function parseDkimHeaders(buf: Buffer | string) { let line = (buf || '').toString().trim(); let splitterPos = line.indexOf(':'); let headerKey: string; if (splitterPos >= 0) { headerKey = line.substr(0, splitterPos).trim().toLowerCase(); line = line.substr(splitterPos + 1).trim(); } let parts: { [key: string]: any }[] = []; let lastState: string | boolean = false; const createPart = (): { [key: string]: string | boolean } => { let part = { key: '', value: '', comment: '', hasValue: false, }; parts.push(part); return part; }; const parse = () => { let state = 'key'; let escaped; let quote; let curPart = createPart(); for (let i = 0; i < line.length; i++) { let c = line.charAt(i); switch (state) { // @ts-ignore case 'key': if (c === '=') { state = 'value'; curPart.hasValue = true; break; } // falls through case 'value': { if (escaped === true) { curPart[state] += c; } switch (c) { case ';': // start new part curPart = createPart(); state = 'key'; break; case '\\': escaped = true; break; case '(': lastState = state; state = 'comment'; break; case '"': case "'": lastState = state; curPart[state] += c; state = 'quoted'; quote = c; break; default: curPart[state] += c; break; } break; } case 'comment': switch (c) { case '\\': escaped = true; break; case ')': state = lastState as string; break; default: curPart[state] += c; break; } break; case 'quoted': switch (c) { case '\\': escaped = true; break; // @ts-ignore case quote: state = lastState as string; // falls through default: if (typeof lastState === 'string') { curPart[lastState] += c; } break; } break; } } for (let i = parts.length - 1; i >= 0; i--) { for (let key of Object.keys(parts[i]!)) { if (typeof parts[i]![key] === 'string') { parts[i]![key] = parts[i]![key].replace(/\s+/g, ' ').trim(); } } parts[i]!.key = parts[i]!.key.toLowerCase(); if (!parts[i]!.key) { // remove empty value parts.splice(i, 1); } else if (['bh', 'b', 'p', 'h'].includes(parts[i]!.key)) { // remove unneeded whitespace parts[i]!.value = parts[i]!.value?.replace(/\s+/g, ''); } else if ( ['l', 'v', 't'].includes(parts[i]!.key) && !isNaN(parts[i]!.value) ) { parts[i]!.value = Number(parts[i]!.value); } else if (parts[i]!.key === 'i' && /^arc-/i.test(headerKey)) { parts[i]!.value = Number(parts[i]!.value); } } let result: { [key: string]: any } = { header: headerKey, }; for (let i = 0; i < parts.length; i++) { // find the first entry with key only and use it as the default value if (parts[i]!.key && !parts[i]!.hasValue) { result.value = parts[i]!.key; parts.splice(i, 1); break; } } parts.forEach((part) => { let entry: { [key: string]: any } = { value: part.value, }; if ( ['arc-authentication-results', 'authentication-results'].includes( headerKey ) && typeof part.value === 'string' ) { // parse value into subparts as well entry = Object.assign(entry, parseValue(entry.value)); } if (part.comment) { entry.comment = part.comment; } if ( ['arc-authentication-results', 'authentication-results'].includes( headerKey ) && part.key === 'dkim' ) { if (!result[part.key]) { result[part.key] = []; } if (Array.isArray(result[part.key])) { result[part.key].push(entry); } } else { result[part.key] = entry; } }); return result; }; return { parsed: parse(), original: buf }; }