442 lines
No EOL
13 KiB
JavaScript
442 lines
No EOL
13 KiB
JavaScript
import fs from 'node:fs';
|
|
|
|
const validMime = fs.readFileSync("/home/rallias/Dev/andrewpietiladotcom/messageStore/1.eml", "utf-8");
|
|
|
|
// TODO: Does this actually need to throw internally?
|
|
class ABNFNoMatchError extends Error {}
|
|
|
|
class ABNFResult {
|
|
constructor(value, subTokens, type, startChar, endChar) {
|
|
this._value = value;
|
|
this._subTokens = subTokens;
|
|
this._type = type;
|
|
this._startChar = startChar;
|
|
this._endChar = endChar;
|
|
}
|
|
}
|
|
|
|
function parseCRLF(chars, ptr) {
|
|
// RFC 5234 B.1
|
|
// CRLF = CR LF
|
|
// ; Internet standard newline
|
|
if ( chars[ptr] === String.fromCharCode(0x0D) && chars[ptr+1] === String.fromCharCode(0x0A) ) {
|
|
return new ABNFResult(chars[ptr]+chars[ptr+1], null, "CRLF", ptr, ptr+1);
|
|
} else {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
}
|
|
|
|
function parseWSP(chars, ptr) {
|
|
// RFC 5234 B.1
|
|
// WSP = SP / HTAB
|
|
// ; white space
|
|
if ( chars[ptr] === String.fromCharCode(0x20) ) {
|
|
return new ABNFResult(chars[ptr], null, "WSP", ptr, ptr);
|
|
} else if ( chars[ptr] === String.fromCharCode(0x09)) {
|
|
return new ABNFResult(chars[ptr], null, "WSP", ptr, ptr);
|
|
} else {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
}
|
|
|
|
function parseQuotedPair(chars, ptr) {
|
|
// RFC 5322 3.2.1
|
|
// quoted-pair = ("\" (VCHAR / WSP)) / obs-qp
|
|
|
|
// TODO: Handle obs-qp
|
|
if ( chars[ptr] === "\\" && chars[ptr+1] === "\"" ) {
|
|
return new ABNFResult("\\\"", null, "quoted-pair", ptr, ptr+1);
|
|
} else {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
}
|
|
|
|
function parseFWS(chars, ptr) {
|
|
// RFC 5322 3.2.2
|
|
// FWS = ([*WSP CRLF] 1*WSP) / obs-FWS
|
|
// ; Folding white space
|
|
|
|
// TODO: Handle obs-FWS
|
|
var startChar = ptr;
|
|
var value = "";
|
|
var subTokens = [];
|
|
var inFWS = true;
|
|
while(inFWS) {
|
|
try {
|
|
var wsp = parseWSP(chars, ptr);
|
|
subTokens.push(wsp);
|
|
value += wsp._value;
|
|
ptr = wsp._endChar + 1;
|
|
} catch(e) {
|
|
try {
|
|
var crlf = parseCRLF(chars, ptr);
|
|
subTokens.push(crlf);
|
|
value += crlf._value;
|
|
ptr = crlf._endChar + 1;
|
|
} catch(e) {
|
|
inFWS = false;
|
|
}
|
|
}
|
|
if ( inFWS && subTokens.length > 1 && !(subTokens[subTokens.length-1]._type === "CRLF" && subTokens[subTokens.length-2]._type === "CRLF")) {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
}
|
|
if ( subTokens[subTokens.length-1]._type === "CRLF" ) {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
return new ABNFResult(value, subTokens, "FWS", startChar, ptr);
|
|
}
|
|
|
|
const ctextChars = "!\"#$%&'*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~".split("");
|
|
|
|
function parseCtext(chars, ptr) {
|
|
// RFC 5322 3.2.2
|
|
// ctext = %d33-39 / ; Printable US-ASCII
|
|
// %d42-91 / ; characters not including
|
|
// %d93-126 / ; "(", ")", or "\"
|
|
// obs-ctext
|
|
|
|
// TODO: handle obs-ctext
|
|
if ( ctextChars.includes(chars[ptr]) ) {
|
|
return new ABNFResult(chars[ptr], null, "ctext", ptr, ptr);
|
|
} else {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
}
|
|
|
|
// Help the typescript parser understand heads from tails.
|
|
/**
|
|
* @param {String[]} chars
|
|
* @param {Number} ptr
|
|
* @returns {ABNFResult}
|
|
* @throws {ABNFNoMatchError}
|
|
*/
|
|
function parseCcontent(chars, ptr) {
|
|
// RFC 5322 3.2.2
|
|
// ccontent = ctext / quoted-pair / comment
|
|
try {
|
|
var ctext = parseCtext(chars, ptr);
|
|
return new ABNFResult(ctext._value, ctext, "ccontent", ctext._startChar, ctext._endChar);
|
|
} catch(e) {
|
|
try {
|
|
var quotedPair = parseQuotedPair(chars, ptr);
|
|
return new ABNFResult(quotedPair._value, quotedPair, "ccontent", quotedPair._startChar, quotedPair._endChar);
|
|
} catch(e) {
|
|
try {
|
|
var comment = parseComment(chars, ptr);
|
|
return new ABNFResult(comment._value, comment, "ccontent", comment._startChar, comment._endChar);
|
|
} catch(e) {
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
|
|
function parseComment(chars, ptr) {
|
|
// RFC 5322 3.2.2
|
|
// comment = "(" *([FWS] ccontent) [FWS] ")"
|
|
var startChar = ptr;
|
|
var value = "";
|
|
var subTokens = [];
|
|
if ( chars[ptr] === "(" ) {
|
|
value += chars[ptr];
|
|
ptr++;
|
|
var inLoop = true;
|
|
while(inLoop) {
|
|
try {
|
|
var fws = parseFWS(chars, ptr);
|
|
subTokens.push(fws);
|
|
value += fws._value;
|
|
ptr = fws._endChar + 1;
|
|
} catch(e) {
|
|
try {
|
|
var ccontent = parseCcontent(chars, ptr);
|
|
subTokens.push(ccontent);
|
|
value += ccontent._value;
|
|
ptr = ccontent._endChar + 1;
|
|
} catch(e) {
|
|
inLoop = false;
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
var fws = parseFWS(chars, ptr);
|
|
subTokens.push(fws);
|
|
value += fws._value;
|
|
ptr = fws._endChar + 1;
|
|
} catch(e) {
|
|
// FWS is optional.
|
|
}
|
|
if ( chars[ptr] === ")" ) {
|
|
value += chars[ptr];
|
|
return new ABNFResult(value, subTokens, "comment", startChar, ptr);
|
|
}
|
|
}
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
|
|
function parseCFWS(chars, ptr) {
|
|
// RFC 5322 3.2.2
|
|
// CFWS = (1*([FWS] comment) [FWS]) / FWS
|
|
|
|
var startChar = ptr;
|
|
var value = "";
|
|
var subTokens = [];
|
|
var inCFWS = true;
|
|
while(inCFWS) {
|
|
try {
|
|
var fws = parseFWS(chars, ptr);
|
|
subTokens.push(fws);
|
|
value += fws._value;
|
|
ptr = fws._endChar + 1;
|
|
} catch(e) {
|
|
try {
|
|
var comment = parseComment(chars, ptr);
|
|
subTokens.push(comment);
|
|
value += comment._value;
|
|
ptr = comment._endChar + 1;
|
|
} catch(e) {
|
|
inCFWS = false;
|
|
}
|
|
}
|
|
}
|
|
if ( subTokens.length >= 1 ) {
|
|
return new ABNFResult(value, subTokens, "CFWS", startChar, ptr);
|
|
}
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
|
|
const atextChars = "ABCDEFGHIJKLMNOPQRSTUVWXYabcdefghijklmnopqrstuvwxy012345678!#$%&'*+-/=?^_`{|}~".split('');
|
|
|
|
function parseAtext(chars, ptr) {
|
|
// RFC 5322 3.2.3
|
|
// atext = ALPHA / DIGIT / ; Printable US-ASCII
|
|
// "!" / "#" / ; characters not including
|
|
// "$" / "%" / ; specials. Used for atoms.
|
|
// "&" / "'" /
|
|
// "*" / "+" /
|
|
// "-" / "/" /
|
|
// "=" / "?" /
|
|
// "^" / "_" /
|
|
// "`" / "{" /
|
|
// "|" / "}" /
|
|
// "~"
|
|
|
|
if ( atextChars.includes(chars[ptr]) ) {
|
|
return new ABNFResult(chars[ptr], null, "atext", ptr, ptr);
|
|
} else {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
}
|
|
|
|
function parseAtom(chars, ptr) {
|
|
// RFC 5322 3.2.3
|
|
// atom = [CFWS] 1*atext [CFWS]
|
|
|
|
var startChar = ptr;
|
|
var value = "";
|
|
var subTokens = [];
|
|
var inAtom = true;
|
|
try {
|
|
var cfws = parseCFWS(chars, ptr);
|
|
subTokens.push(cfws);
|
|
value += cfws._value;
|
|
ptr = cfws._endChar + 1;
|
|
} catch(e) {
|
|
// Optional.
|
|
}
|
|
|
|
while (inAtom) {
|
|
try {
|
|
var atext = parseAtext(chars, ptr);
|
|
subTokens.push(atext);
|
|
value += atext._value;
|
|
ptr = atext._endChar + 1;
|
|
} catch(e) {
|
|
inAtom = false;
|
|
}
|
|
}
|
|
if ( subTokens[subTokens.length-1]._type != "atext" ) {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
try {
|
|
var cfws = parseCFWS(chars, ptr);
|
|
subTokens.push(cfws);
|
|
value += cfws._value;
|
|
ptr = cfws._endChar + 1;
|
|
} catch(e) {
|
|
// Optional.
|
|
}
|
|
return new ABNFResult(value, subTokens, "atom", startChar, ptr);
|
|
}
|
|
|
|
function parseDotAtomText(chars, ptr) {
|
|
// RFC 5322 3.2.3
|
|
// dot-atom-text = 1*atext *("." 1*atext)
|
|
var startChar = ptr;
|
|
var value = "";
|
|
var subTokens = [];
|
|
var inDAT = true;
|
|
|
|
while (inDAT) {
|
|
try {
|
|
var atext = parseAtext(chars, ptr);
|
|
subTokens.push(atext);
|
|
value += atext._value;
|
|
ptr = atext._endChar + 1;
|
|
} catch(e) {
|
|
// We fish later.
|
|
}
|
|
if ( chars[ptr] === "." && value === "" ) {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
if ( chars[ptr] === "." ) {
|
|
value += ".";
|
|
ptr++;
|
|
}
|
|
}
|
|
if ( value.endsWith(".") ) {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
|
|
// TODO: Do we care with granularity about the dots and their positions?
|
|
return new ABNFResult(value, subTokens, "dot-atom-text", startChar, ptr);
|
|
}
|
|
|
|
function parseDotAtom(chars, ptr) {
|
|
// RFC 5322 3.2.3
|
|
// dot-atom = [CFWS] dot-atom-text [CFWS]
|
|
var startChar = ptr;
|
|
var value = "";
|
|
var subTokens = [];
|
|
try {
|
|
var cfws = parseCFWS(chars, ptr);
|
|
subTokens.push(cfws);
|
|
value += cfws._value;
|
|
ptr = cfws._endChar + 1;
|
|
} catch(e) {
|
|
|
|
}
|
|
try {
|
|
var dat = parseDotAtomText(chars, ptr);
|
|
subTokens.push(dat);
|
|
value += dat._value;
|
|
ptr = dat._endChar + 1;
|
|
} catch(e) {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
try {
|
|
const cfws = parseCFWS(chars, ptr);
|
|
subTokens.push(cfws);
|
|
value += cfws._value;
|
|
ptr = cfws._endChar + 1;
|
|
} catch(e) {
|
|
|
|
}
|
|
return new ABNFResult(value, subTokens, "dot-atom", startChar, ptr);
|
|
}
|
|
|
|
const qtextChars = "!#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~".split('');
|
|
|
|
function parseQtext(chars, ptr) {
|
|
// RFC 5322 3.2.4
|
|
// qtext = %d33 / ; Printable US-ASCII
|
|
// %d35-91 / ; characters not including
|
|
// %d93-126 / ; "\" or the quote character
|
|
// obs-qtext
|
|
|
|
if ( qtextChars.includes(chars[ptr]) ) {
|
|
return new ABNFResult(chars[ptr], null, "qtext", ptr, ptr);
|
|
} else {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
}
|
|
|
|
function parseQcontent(chars, ptr) {
|
|
// RFC 5322 3.2.4
|
|
try {
|
|
const qtext = parseQtext(chars, ptr);
|
|
return new ABNFResult(qtext._value, qtext, "qcontent", qtext._startChar, qtext._endChar);
|
|
} catch(e) {
|
|
try {
|
|
const quotedPair = parseQuotedPair(chars, ptr);
|
|
return new ABNFResult(quotedPair._value, quotedPair, "qcontent", quotedPair._startChar, quotedPair._endChar);
|
|
} catch(e) {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
}
|
|
}
|
|
|
|
function parseDQUOTE(chars, ptr) {
|
|
// RFC 5234 B.1
|
|
// DQUOTE = %x22
|
|
// ; " (Double Quote)
|
|
if ( chars[ptr] === String.fromCharCode(0x22) ) {
|
|
return new ABNFResult(chars[ptr], null, "DQUOTE", ptr, ptr);
|
|
} else {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
}
|
|
|
|
function parseQuotedString(chars, ptr) {
|
|
// RFC 5322 3.2.4
|
|
// quoted-string = [CFWS]
|
|
// DQUOTE *([FWS] qcontent) [FWS] DQUOTE
|
|
// [CFWS]
|
|
var startChar = ptr;
|
|
var value = "";
|
|
var subTokens = [];
|
|
try {
|
|
const cfws = parseCFWS(chars, ptr);
|
|
subTokens.push(cfws);
|
|
value += cfws._value;
|
|
ptr = cfws._endChar + 1;
|
|
} catch(e) {
|
|
|
|
}
|
|
try {
|
|
const dquote = parseDQUOTE(chars, ptr);
|
|
subTokens.push(dquote);
|
|
value += dquote._value;
|
|
ptr = dquote._endChar + 1;
|
|
} catch(e) {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
var inDquot = true;
|
|
while (inDquot) {
|
|
try {
|
|
const fws = parseFWS(chars, ptr);
|
|
subTokens.push(fws);
|
|
value += fws._value;
|
|
ptr = fws._endChar + 1;
|
|
} catch(e) {
|
|
try {
|
|
const qcontent = parseQcontent(chars, ptr);
|
|
subTokens.push(qcontent);
|
|
value += qcontent._value;
|
|
ptr = qcontent._endChar + 1;
|
|
} catch(e) {
|
|
inDquot = false;
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
const dquote = parseDQUOTE(chars, ptr);
|
|
subTokens.push(dquote);
|
|
value += dquote._value;
|
|
ptr = dquote._endChar + 1;
|
|
} catch(e) {
|
|
throw new ABNFNoMatchError();
|
|
}
|
|
try {
|
|
const cfws = parseCFWS(chars, ptr);
|
|
subTokens.push(cfws);
|
|
value += cfws._value;
|
|
ptr = cfws._endChar + 1;
|
|
} catch(e) {
|
|
|
|
}
|
|
return new ABNFResult(value, subTokens, "quoted-string", startChar, ptr);
|
|
} |