mailauth
Version:
Email authentication library for Node.js
301 lines (244 loc) • 8.57 kB
JavaScript
'use strict';
// NB! fails to properly parse nested comments (should be rare enough though)
const valueParser = str => {
let line = str.replace(/\s+/g, ' ').trim();
let parts = [];
let lastState = false;
const createPart = () => {
let 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) {
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) {
curPart[lastState] += c;
break;
}
switch (c) {
case '\\':
escaped = true;
break;
case quote:
state = lastState;
break;
default:
curPart[lastState] += c;
break;
}
break;
}
}
let result = {
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();
};
const headerParser = buf => {
let line = (buf || '').toString().trim();
let splitterPos = line.indexOf(':');
let headerKey;
if (splitterPos >= 0) {
headerKey = line.substr(0, splitterPos).trim().toLowerCase();
line = line.substr(splitterPos + 1).trim();
}
let parts = [];
let lastState = false;
const createPart = () => {
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) {
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;
break;
default:
curPart[state] += c;
break;
}
break;
case 'quoted':
switch (c) {
case '\\':
escaped = true;
break;
case quote:
state = lastState;
// falls through
default:
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', 'x'].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 = {
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 = {
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, valueParser(entry.value));
}
if (part.comment) {
entry.comment = part.comment;
}
if (part.key && !['__proto__', 'constructor'].includes(part.key)) {
if (['arc-authentication-results', 'authentication-results'].includes(headerKey) && part.key === 'dkim') {
if (!result[part.key]) {
result[part.key] = [];
}
result[part.key].push(entry);
} else {
result[part.key] = entry;
}
}
});
return result;
};
return { parsed: parse(), original: buf };
};
module.exports = headerParser;