mina-attestations
Version:
Private Attestations on Mina
324 lines (268 loc) • 7.31 kB
text/typescript
/**
* 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 };
}