Projektstart

This commit is contained in:
2026-01-22 15:49:12 +01:00
parent 7212eb6f7a
commit 57e5f652f8
10637 changed files with 2598792 additions and 64 deletions

View File

@@ -0,0 +1,190 @@
/* eslint no-console: 0, new-cap: 0 */
'use strict';
const imapFormalSyntax = require('./imap-formal-syntax');
const formatRespEntry = (entry, returnEmpty) => {
if (typeof entry === 'string') {
return Buffer.from(entry);
}
if (typeof entry === 'number') {
return Buffer.from(entry.toString());
}
if (Buffer.isBuffer(entry)) {
return entry;
}
if (returnEmpty) {
return null;
}
return Buffer.alloc(0);
};
/**
* Compiles an input object into
*/
module.exports = async (response, options) => {
let { asArray, isLogging, literalPlus, literalMinus } = options || {};
const respParts = [];
let resp = [].concat(formatRespEntry(response.tag, true) || []).concat(response.command ? formatRespEntry(' ' + response.command) : []);
let val;
let lastType;
let walk = async (node, options) => {
options = options || {};
let lastRespEntry = resp.length && resp[resp.length - 1];
let lastRespByte = (lastRespEntry && lastRespEntry.length && lastRespEntry[lastRespEntry.length - 1]) || '';
if (typeof lastRespByte === 'number') {
lastRespByte = String.fromCharCode(lastRespByte);
}
if (lastType === 'LITERAL' || (!['(', '<', '['].includes(lastRespByte) && resp.length)) {
if (options.subArray) {
// ignore separator
} else {
resp.push(formatRespEntry(' '));
}
}
if (node && node.buffer && !Buffer.isBuffer(node)) {
// mongodb binary
node = node.buffer;
}
if (Array.isArray(node)) {
lastType = 'LIST';
resp.push(formatRespEntry('('));
// check if we need to skip separator WS between two arrays
let subArray = node.length > 1 && Array.isArray(node[0]);
for (let child of node) {
if (subArray && !Array.isArray(child)) {
subArray = false;
}
await walk(child, { subArray });
}
resp.push(formatRespEntry(')'));
return;
}
if (!node && typeof node !== 'string' && typeof node !== 'number' && !Buffer.isBuffer(node)) {
resp.push(formatRespEntry('NIL'));
return;
}
if (typeof node === 'string' || Buffer.isBuffer(node)) {
if (isLogging && node.length > 100) {
resp.push(formatRespEntry('"(* ' + node.length + 'B string *)"'));
} else {
resp.push(formatRespEntry(JSON.stringify(node.toString())));
}
return;
}
if (typeof node === 'number') {
resp.push(formatRespEntry(Math.round(node) || 0)); // Only integers allowed
return;
}
lastType = node.type;
if (isLogging && node.sensitive) {
resp.push(formatRespEntry('"(* value hidden *)"'));
return;
}
switch (node.type.toUpperCase()) {
case 'LITERAL':
if (isLogging) {
resp.push(formatRespEntry('"(* ' + node.value.length + 'B literal *)"'));
} else {
let literalLength = !node.value ? 0 : Math.max(node.value.length, 0);
let canAppend = !asArray || literalPlus || (literalMinus && literalLength <= 4096);
let usePlus = canAppend && (literalMinus || literalPlus);
resp.push(formatRespEntry(`${node.isLiteral8 ? '~' : ''}{${literalLength}${usePlus ? '+' : ''}}\r\n`));
if (canAppend) {
if (node.value && node.value.length) {
resp.push(formatRespEntry(node.value));
}
} else {
respParts.push(resp);
resp = [].concat(formatRespEntry(node.value, true) || []);
}
}
break;
case 'STRING':
if (isLogging && node.value.length > 100) {
resp.push(formatRespEntry('"(* ' + node.value.length + 'B string *)"'));
} else {
resp.push(formatRespEntry(JSON.stringify((node.value || '').toString())));
}
break;
case 'TEXT':
case 'SEQUENCE':
if (node.value) {
resp.push(formatRespEntry(node.value));
}
break;
case 'NUMBER':
resp.push(formatRespEntry(node.value || 0));
break;
case 'ATOM':
case 'SECTION':
val = (node.value || '').toString();
if (!node.section || val) {
if (node.value === '' || imapFormalSyntax.verify(val.charAt(0) === '\\' ? val.substr(1) : val, imapFormalSyntax['ATOM-CHAR']()) >= 0) {
val = JSON.stringify(val);
}
resp.push(formatRespEntry(val));
}
if (node.section) {
resp.push(formatRespEntry('['));
for (let child of node.section) {
await walk(child);
}
resp.push(formatRespEntry(']'));
}
if (node.partial) {
resp.push(formatRespEntry(`<${node.partial.join('.')}>`));
}
break;
}
};
if (response.attributes) {
let attributes = Array.isArray(response.attributes) ? response.attributes : [].concat(response.attributes);
for (let child of attributes) {
await walk(child);
}
}
if (resp.length) {
respParts.push(resp);
}
for (let i = 0; i < respParts.length; i++) {
respParts[i] = Buffer.concat(respParts[i]);
}
return asArray ? respParts : respParts.flatMap(entry => entry);
};

View File

@@ -0,0 +1,147 @@
/* eslint object-shorthand:0, new-cap: 0, no-useless-concat: 0 */
'use strict';
// IMAP Formal Syntax
// http://tools.ietf.org/html/rfc3501#section-9
function expandRange(start, end) {
let chars = [];
for (let i = start; i <= end; i++) {
chars.push(i);
}
return String.fromCharCode(...chars);
}
function excludeChars(source, exclude) {
let sourceArr = Array.prototype.slice.call(source);
for (let i = sourceArr.length - 1; i >= 0; i--) {
if (exclude.indexOf(sourceArr[i]) >= 0) {
sourceArr.splice(i, 1);
}
}
return sourceArr.join('');
}
module.exports = {
CHAR() {
let value = expandRange(0x01, 0x7f);
this.CHAR = function () {
return value;
};
return value;
},
CHAR8() {
let value = expandRange(0x01, 0xff);
this.CHAR8 = function () {
return value;
};
return value;
},
SP() {
return ' ';
},
CTL() {
let value = expandRange(0x00, 0x1f) + '\x7F';
this.CTL = function () {
return value;
};
return value;
},
DQUOTE() {
return '"';
},
ALPHA() {
let value = expandRange(0x41, 0x5a) + expandRange(0x61, 0x7a);
this.ALPHA = function () {
return value;
};
return value;
},
DIGIT() {
let value = expandRange(0x30, 0x39);
this.DIGIT = function () {
return value;
};
return value;
},
'ATOM-CHAR'() {
let value = excludeChars(this.CHAR(), this['atom-specials']());
this['ATOM-CHAR'] = function () {
return value;
};
return value;
},
'ASTRING-CHAR'() {
let value = this['ATOM-CHAR']() + this['resp-specials']();
this['ASTRING-CHAR'] = function () {
return value;
};
return value;
},
'TEXT-CHAR'() {
let value = excludeChars(this.CHAR(), '\r\n');
this['TEXT-CHAR'] = function () {
return value;
};
return value;
},
'atom-specials'() {
let value = '(' + ')' + '{' + this.SP() + this.CTL() + this['list-wildcards']() + this['quoted-specials']() + this['resp-specials']();
this['atom-specials'] = function () {
return value;
};
return value;
},
'list-wildcards'() {
return '%' + '*';
},
'quoted-specials'() {
let value = this.DQUOTE() + '\\';
this['quoted-specials'] = function () {
return value;
};
return value;
},
'resp-specials'() {
return ']';
},
tag() {
let value = excludeChars(this['ASTRING-CHAR'](), '+');
this.tag = function () {
return value;
};
return value;
},
command() {
let value = this.ALPHA() + this.DIGIT() + '-';
this.command = function () {
return value;
};
return value;
},
verify(str, allowedChars) {
for (let i = 0, len = str.length; i < len; i++) {
if (allowedChars.indexOf(str.charAt(i)) < 0) {
return i;
}
}
return -1;
}
};

View File

@@ -0,0 +1,9 @@
'use strict';
const parser = require('./imap-parser');
const compiler = require('./imap-compiler');
module.exports = {
parser,
compiler
};

View File

@@ -0,0 +1,67 @@
'use strict';
const imapFormalSyntax = require('./imap-formal-syntax');
const { ParserInstance } = require('./parser-instance');
module.exports = async (command, options) => {
options = options || {};
let nullBytesRemoved = 0;
// special case with a buggy IMAP server where responses are padded with zero bytes
if (command[0] === 0) {
// find the first non null byte and trim
let firstNonNull = -1;
for (let i = 0; i < command.length; i++) {
if (command[i] !== 0) {
firstNonNull = i;
break;
}
}
if (firstNonNull === -1) {
// All bytes are null
return { tag: '*', command: 'BAD', attributes: [] };
}
command = command.slice(firstNonNull);
nullBytesRemoved = firstNonNull;
}
const parser = new ParserInstance(command, options);
const response = {};
try {
response.tag = await parser.getTag();
await parser.getSpace();
response.command = await parser.getCommand();
if (nullBytesRemoved) {
response.nullBytesRemoved = nullBytesRemoved;
}
if (['UID', 'AUTHENTICATE'].indexOf((response.command || '').toUpperCase()) >= 0) {
await parser.getSpace();
response.command += ' ' + (await parser.getElement(imapFormalSyntax.command()));
}
if (parser.remainder.trim().length) {
await parser.getSpace();
response.attributes = await parser.getAttributes();
}
if (parser.humanReadable) {
response.attributes = (response.attributes || []).concat({
type: 'TEXT',
value: parser.humanReadable
});
}
} catch (err) {
if (err.code === 'ParserErrorExchange' && err.parserContext && err.parserContext.value) {
return err.parserContext.value;
}
throw err;
}
return response;
};

View File

@@ -0,0 +1,257 @@
'use strict';
const Transform = require('stream').Transform;
const logger = require('../logger');
const LINE = 0x01;
const LITERAL = 0x02;
const LF = 0x0a;
const CR = 0x0d;
const NUM_0 = 0x30;
const NUM_9 = 0x39;
const CURLY_OPEN = 0x7b;
const CURLY_CLOSE = 0x7d;
// Maximum allowed literal size: 1GB (1073741824 bytes)
const MAX_LITERAL_SIZE = 1024 * 1024 * 1024;
class ImapStream extends Transform {
constructor(options) {
super({
//writableHighWaterMark: 3,
readableObjectMode: true,
writableObjectMode: false
});
this.options = options || {};
this.cid = this.options.cid;
this.log =
this.options.logger && typeof this.options.logger === 'object'
? this.options.logger
: logger.child({
component: 'imap-connection',
cid: this.cid
});
this.readBytesCounter = 0;
this.state = LINE;
this.literalWaiting = 0;
this.inputBuffer = []; // lines
this.lineBuffer = []; // current line
this.literalBuffer = [];
this.literals = [];
this.compress = false;
this.secureConnection = this.options.secureConnection;
this.processingInput = false;
this.inputQueue = []; // unprocessed input chunks
}
checkLiteralMarker(line) {
if (!line || !line.length) {
return false;
}
let pos = line.length - 1;
if (line[pos] === LF) {
pos--;
} else {
return false;
}
if (pos >= 0 && line[pos] === CR) {
pos--;
}
if (pos < 0) {
return false;
}
if (!pos || line[pos] !== CURLY_CLOSE) {
return false;
}
pos--;
let numBytes = [];
for (; pos > 0; pos--) {
let c = line[pos];
if (c >= NUM_0 && c <= NUM_9) {
numBytes.unshift(c);
continue;
}
if (c === CURLY_OPEN && numBytes.length) {
const literalSize = Number(Buffer.from(numBytes).toString());
if (literalSize > MAX_LITERAL_SIZE) {
const err = new Error(`Literal size ${literalSize} exceeds maximum allowed size of ${MAX_LITERAL_SIZE} bytes`);
err.code = 'LiteralTooLarge';
err.literalSize = literalSize;
err.maxSize = MAX_LITERAL_SIZE;
this.emit('error', err);
return false;
}
this.state = LITERAL;
this.literalWaiting = literalSize;
return true;
}
return false;
}
return false;
}
async processInputChunk(chunk, startPos) {
startPos = startPos || 0;
if (startPos >= chunk.length) {
return;
}
switch (this.state) {
case LINE: {
let lineStart = startPos;
for (let i = startPos, len = chunk.length; i < len; i++) {
if (chunk[i] === LF) {
// line end found
this.lineBuffer.push(chunk.slice(lineStart, i + 1));
lineStart = i + 1;
let line = Buffer.concat(this.lineBuffer);
this.inputBuffer.push(line);
this.lineBuffer = [];
// try to detect if this is a literal start
if (this.checkLiteralMarker(line)) {
// switch into line mode and start over
return await this.processInputChunk(chunk, lineStart);
}
// reached end of command input, emit it
let payload = this.inputBuffer.length === 1 ? this.inputBuffer[0] : Buffer.concat(this.inputBuffer);
let literals = this.literals;
this.inputBuffer = [];
this.literals = [];
if (payload.length) {
// remove final line terminator
let skipBytes = 0;
if (payload.length >= 1 && payload[payload.length - 1] === LF) {
skipBytes++;
if (payload.length >= 2 && payload[payload.length - 2] === CR) {
skipBytes++;
}
}
if (skipBytes) {
payload = payload.slice(0, payload.length - skipBytes);
}
if (payload.length) {
await new Promise(resolve => {
this.push({ payload, literals, next: resolve });
});
}
}
}
}
if (lineStart < chunk.length) {
this.lineBuffer.push(chunk.slice(lineStart));
}
break;
}
case LITERAL: {
// exactly until end of chunk
if (chunk.length === startPos + this.literalWaiting) {
if (!startPos) {
this.literalBuffer.push(chunk);
} else {
this.literalBuffer.push(chunk.slice(startPos));
}
this.literalWaiting -= chunk.length;
this.literals.push(Buffer.concat(this.literalBuffer));
this.literalBuffer = [];
this.state = LINE;
return;
} else if (chunk.length > startPos + this.literalWaiting) {
let partial = chunk.slice(startPos, startPos + this.literalWaiting);
this.literalBuffer.push(partial);
startPos += partial.length;
this.literalWaiting -= partial.length;
this.literals.push(Buffer.concat(this.literalBuffer));
this.literalBuffer = [];
this.state = LINE;
return await this.processInputChunk(chunk, startPos);
} else {
let partial = chunk.slice(startPos);
this.literalBuffer.push(partial);
startPos += partial.length;
this.literalWaiting -= partial.length;
return;
}
}
}
}
async processInput() {
let data;
let processedCount = 0;
while ((data = this.inputQueue.shift())) {
await this.processInputChunk(data.chunk);
// mark chunk as processed
data.next();
// Yield to event loop every 10 chunks to prevent CPU blocking
processedCount++;
if (processedCount % 10 === 0) {
await new Promise(resolve => setImmediate(resolve));
}
}
}
_transform(chunk, encoding, next) {
if (typeof chunk === 'string') {
chunk = Buffer.from(chunk, encoding);
}
if (!chunk || !chunk.length) {
return next();
}
this.readBytesCounter += chunk.length;
if (this.options.logRaw) {
this.log.trace({
src: 's',
msg: 'read from socket',
data: chunk.toString('base64'),
compress: !!this.compress,
secure: !!this.secureConnection,
cid: this.cid
});
}
if (chunk && chunk.length) {
this.inputQueue.push({ chunk, next });
}
if (!this.processingInput) {
this.processingInput = true;
this.processInput()
.catch(err => this.emit('error', err))
.finally(() => (this.processingInput = false));
}
}
_flush(next) {
next();
}
}
module.exports.ImapStream = ImapStream;

View File

@@ -0,0 +1,165 @@
/* eslint new-cap: 0 */
'use strict';
const imapFormalSyntax = require('./imap-formal-syntax');
const { TokenParser } = require('./token-parser');
class ParserInstance {
constructor(input, options) {
this.input = (input || '').toString();
this.options = options || {};
this.remainder = this.input;
this.pos = 0;
}
async getTag() {
if (!this.tag) {
this.tag = await this.getElement(imapFormalSyntax.tag() + '*+', true);
}
return this.tag;
}
async getCommand() {
if (this.tag === '+') {
// special case
this.humanReadable = this.remainder.trim();
this.remainder = '';
return '';
}
if (!this.command) {
this.command = await this.getElement(imapFormalSyntax.command());
}
switch ((this.command || '').toString().toUpperCase()) {
case 'OK':
case 'NO':
case 'BAD':
case 'PREAUTH':
case 'BYE':
{
let match = this.remainder.match(/^\s+\[/);
if (match) {
let nesting = 1;
for (let i = match[0].length; i <= this.remainder.length; i++) {
let c = this.remainder[i];
if (c === '[') {
nesting++;
} else if (c === ']') {
nesting--;
}
if (!nesting) {
this.humanReadable = this.remainder.substring(i + 1).trim();
this.remainder = this.remainder.substring(0, i + 1);
break;
}
}
} else {
this.humanReadable = this.remainder.trim();
this.remainder = '';
}
}
break;
}
return this.command;
}
async getElement(syntax) {
let match, element, errPos;
if (this.remainder.match(/^\s/)) {
let error = new Error(`Unexpected whitespace at position ${this.pos} [E1]`);
error.code = 'ParserError1';
error.parserContext = { input: this.input, pos: this.pos };
throw error;
}
if ((match = this.remainder.match(/^\s*[^\s]+(?=\s|$)/))) {
element = match[0];
if ((errPos = imapFormalSyntax.verify(element, syntax)) >= 0) {
if (this.tag === 'Server' && element === 'Unavailable.') {
// Exchange error
let error = new Error(`Server returned an error: ${this.input}`);
error.code = 'ParserErrorExchange';
error.parserContext = {
input: this.input,
element,
pos: this.pos,
value: {
tag: '*',
command: 'BAD',
attributes: [{ type: 'TEXT', value: this.input }]
}
};
throw error;
}
let error = new Error(`Unexpected char at position ${this.pos + errPos} [E2: ${JSON.stringify(element.charAt(errPos))}]`);
error.code = 'ParserError2';
error.parserContext = { input: this.input, element, pos: this.pos };
throw error;
}
} else {
let error = new Error(`Unexpected end of input at position ${this.pos} [E3]`);
error.code = 'ParserError3';
error.parserContext = { input: this.input, pos: this.pos };
throw error;
}
this.pos += match[0].length;
this.remainder = this.remainder.substr(match[0].length);
return element;
}
async getSpace() {
if (!this.remainder.length) {
if (this.tag === '+' && this.pos === 1) {
// special case, empty + response
return;
}
let error = new Error(`Unexpected end of input at position ${this.pos} [E4]`);
error.code = 'ParserError4';
error.parserContext = { input: this.input, pos: this.pos };
throw error;
}
if (imapFormalSyntax.verify(this.remainder.charAt(0), imapFormalSyntax.SP()) >= 0) {
let error = new Error(`Unexpected char at position ${this.pos} [E5: ${JSON.stringify(this.remainder.charAt(0))}]`);
error.code = 'ParserError5';
error.parserContext = { input: this.input, element: this.remainder, pos: this.pos };
throw error;
}
this.pos++;
this.remainder = this.remainder.substr(1);
}
async getAttributes() {
if (!this.remainder.length) {
let error = new Error(`Unexpected end of input at position ${this.pos} [E6]`);
error.code = 'ParserError6';
error.parserContext = { input: this.input, pos: this.pos };
throw error;
}
if (this.remainder.match(/^\s/)) {
let error = new Error(`Unexpected whitespace at position ${this.pos} [E7]`);
error.code = 'ParserError7';
error.parserContext = { input: this.input, element: this.remainder, pos: this.pos };
throw error;
}
const tokenParser = new TokenParser(this, this.pos, this.remainder, this.options);
return await tokenParser.getAttributes();
}
}
module.exports.ParserInstance = ParserInstance;

View File

@@ -0,0 +1,654 @@
/* eslint new-cap: 0 */
'use strict';
const imapFormalSyntax = require('./imap-formal-syntax');
const STATE_ATOM = 0x001;
const STATE_LITERAL = 0x002;
const STATE_NORMAL = 0x003;
const STATE_PARTIAL = 0x004;
const STATE_SEQUENCE = 0x005;
const STATE_STRING = 0x006;
const STATE_TEXT = 0x007;
const RE_DIGITS = /^\d+$/;
const RE_SINGLE_DIGIT = /^\d$/;
const MAX_NODE_DEPTH = 25;
class TokenParser {
constructor(parent, startPos, str, options) {
this.str = (str || '').toString();
this.options = options || {};
this.parent = parent;
this.tree = this.currentNode = this.createNode();
this.pos = startPos || 0;
this.currentNode.type = 'TREE';
this.state = STATE_NORMAL;
}
async getAttributes() {
await this.processString();
const attributes = [];
let branch = attributes;
let walk = async node => {
let curBranch = branch;
let elm;
let partial;
if (!node.isClosed && node.type === 'SEQUENCE' && node.value === '*') {
node.isClosed = true;
node.type = 'ATOM';
}
// If the node was never closed, throw it
if (!node.isClosed) {
let error = new Error(`Unexpected end of input at position ${this.pos + this.str.length - 1} [E9]`);
error.code = 'ParserError9';
error.parserContext = { input: this.str, pos: this.pos + this.str.length - 1 };
throw error;
}
let type = (node.type || '').toString().toUpperCase();
switch (type) {
case 'LITERAL':
case 'STRING':
case 'SEQUENCE':
elm = {
type: node.type.toUpperCase(),
value: node.value
};
branch.push(elm);
break;
case 'ATOM':
if (node.value.toUpperCase() === 'NIL') {
branch.push(null);
break;
}
elm = {
type: node.type.toUpperCase(),
value: node.value
};
branch.push(elm);
break;
case 'SECTION':
branch = branch[branch.length - 1].section = [];
break;
case 'LIST':
elm = [];
branch.push(elm);
branch = elm;
break;
case 'PARTIAL':
partial = node.value.split('.').map(Number);
branch[branch.length - 1].partial = partial;
break;
}
for (let childNode of node.childNodes) {
await walk(childNode);
}
branch = curBranch;
};
await walk(this.tree);
return attributes;
}
createNode(parentNode, startPos) {
let node = {
childNodes: [],
type: false,
value: '',
isClosed: true
};
if (parentNode) {
node.parentNode = parentNode;
node.depth = parentNode.depth + 1;
} else {
node.depth = 0;
}
if (node.depth > MAX_NODE_DEPTH) {
let error = new Error('Too much nesting in IMAP string');
error.code = 'MAX_IMAP_NESTING_REACHED';
error._imapStr = this.str;
throw error;
}
if (typeof startPos === 'number') {
node.startPos = startPos;
}
if (parentNode) {
parentNode.childNodes.push(node);
}
return node;
}
async processString() {
let chr, i, len;
const checkSP = () => {
// jump to the next non whitespace pos
while (this.str.charAt(i + 1) === ' ') {
i++;
}
};
for (i = 0, len = this.str.length; i < len; i++) {
chr = this.str.charAt(i);
switch (this.state) {
case STATE_NORMAL:
switch (chr) {
// DQUOTE starts a new string
case '"':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'string';
this.state = STATE_STRING;
this.currentNode.isClosed = false;
break;
// ( starts a new list
case '(':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'LIST';
this.currentNode.isClosed = false;
break;
// ) closes a list
case ')':
if (this.currentNode.type !== 'LIST') {
let error = new Error(`Unexpected list terminator ) at position ${this.pos + i} [E10]`);
error.code = 'ParserError10';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.isClosed = true;
this.currentNode.endPos = this.pos + i;
this.currentNode = this.currentNode.parentNode;
checkSP();
break;
// ] closes section group
case ']':
if (this.currentNode.type !== 'SECTION') {
let error = new Error(`Unexpected section terminator ] at position ${this.pos + i} [E11]`);
error.code = 'ParserError11';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.isClosed = true;
this.currentNode.endPos = this.pos + i;
this.currentNode = this.currentNode.parentNode;
checkSP();
break;
// < starts a new partial
case '<':
if (this.str.charAt(i - 1) !== ']') {
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'ATOM';
this.currentNode.value = chr;
this.state = STATE_ATOM;
} else {
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'PARTIAL';
this.state = STATE_PARTIAL;
this.currentNode.isClosed = false;
}
break;
// binary literal8
case '~': {
let nextChr = this.str.charAt(i + 1);
if (nextChr !== '{') {
if (imapFormalSyntax['ATOM-CHAR']().indexOf(nextChr) >= 0) {
// treat as ATOM
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'ATOM';
this.currentNode.value = chr;
this.state = STATE_ATOM;
break;
}
let error = new Error(`Unexpected literal8 marker at position ${this.pos + i} [E12]`);
error.code = 'ParserError12';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.expectedLiteralType = 'literal8';
break;
}
// { starts a new literal
case '{':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'LITERAL';
this.currentNode.literalType = this.expectedLiteralType || 'literal';
this.expectedLiteralType = false;
this.state = STATE_LITERAL;
this.currentNode.isClosed = false;
break;
// * starts a new sequence
case '*':
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'SEQUENCE';
this.currentNode.value = chr;
this.currentNode.isClosed = false;
this.state = STATE_SEQUENCE;
break;
// normally a space should never occur
case ' ':
// just ignore
break;
// [ starts section
case '[':
// If it is the *first* element after response command, then process as a response argument list
if (['OK', 'NO', 'BAD', 'BYE', 'PREAUTH'].includes(this.parent.command.toUpperCase()) && this.currentNode === this.tree) {
this.currentNode.endPos = this.pos + i;
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'ATOM';
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'SECTION';
this.currentNode.isClosed = false;
this.state = STATE_NORMAL;
// RFC2221 defines a response code REFERRAL whose payload is an
// RFC2192/RFC5092 imapurl that we will try to parse as an ATOM but
// fail quite badly at parsing. Since the imapurl is such a unique
// (and crazy) term, we just specialize that case here.
if (this.str.substr(i + 1, 9).toUpperCase() === 'REFERRAL ') {
// create the REFERRAL atom
this.currentNode = this.createNode(this.currentNode, this.pos + i + 1);
this.currentNode.type = 'ATOM';
this.currentNode.endPos = this.pos + i + 8;
this.currentNode.value = 'REFERRAL';
this.currentNode = this.currentNode.parentNode;
// eat all the way through the ] to be the IMAPURL token.
this.currentNode = this.createNode(this.currentNode, this.pos + i + 10);
// just call this an ATOM, even though IMAPURL might be more correct
this.currentNode.type = 'ATOM';
// jump i to the ']'
i = this.str.indexOf(']', i + 10);
this.currentNode.endPos = this.pos + i - 1;
this.currentNode.value = this.str.substring(this.currentNode.startPos - this.pos, this.currentNode.endPos - this.pos + 1);
this.currentNode = this.currentNode.parentNode;
// close out the SECTION
this.currentNode.isClosed = true;
this.currentNode = this.currentNode.parentNode;
checkSP();
}
break;
}
/* falls through */
default:
// Any ATOM supported char starts a new Atom sequence, otherwise throw an error
// Allow \ as the first char for atom to support system flags
// Allow % to support LIST '' %
// Allow 8bit characters (presumably unicode)
if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== '\\' && chr !== '%' && chr.charCodeAt(0) < 0x80) {
let error = new Error(`Unexpected char at position ${this.pos + i} [E13: ${JSON.stringify(chr)}]`);
error.code = 'ParserError13';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode = this.createNode(this.currentNode, this.pos + i);
this.currentNode.type = 'ATOM';
this.currentNode.value = chr;
this.state = STATE_ATOM;
break;
}
break;
case STATE_ATOM:
// space finishes an atom
if (chr === ' ') {
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
break;
}
//
if (
this.currentNode.parentNode &&
((chr === ')' && this.currentNode.parentNode.type === 'LIST') || (chr === ']' && this.currentNode.parentNode.type === 'SECTION'))
) {
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
this.currentNode.isClosed = true;
this.currentNode.endPos = this.pos + i;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
break;
}
if ((chr === ',' || chr === ':') && RE_DIGITS.test(this.currentNode.value)) {
this.currentNode.type = 'SEQUENCE';
this.currentNode.isClosed = true;
this.state = STATE_SEQUENCE;
}
// [ starts a section group for this element
// Allowed only for selected elements, otherwise falls through to regular ATOM processing
if (chr === '[' && ['BODY', 'BODY.PEEK', 'BINARY', 'BINARY.PEEK'].indexOf(this.currentNode.value.toUpperCase()) >= 0) {
this.currentNode.endPos = this.pos + i;
this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i);
this.currentNode.type = 'SECTION';
this.currentNode.isClosed = false;
this.state = STATE_NORMAL;
break;
}
// if the char is not ATOM compatible, throw. Allow \* as an exception
if (
imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 &&
chr.charCodeAt(0) < 0x80 && // allow 8bit (presumably unicode) bytes
chr !== ']' &&
!(chr === '*' && this.currentNode.value === '\\') &&
(!this.parent || !this.parent.command || !['NO', 'BAD', 'OK'].includes(this.parent.command))
) {
let error = new Error(`Unexpected char at position ${this.pos + i} [E16: ${JSON.stringify(chr)}]`);
error.code = 'ParserError16';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
} else if (this.currentNode.value === '\\*') {
let error = new Error(`Unexpected char at position ${this.pos + i} [E17: ${JSON.stringify(chr)}]`);
error.code = 'ParserError17';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.value += chr;
break;
case STATE_STRING:
// DQUOTE ends the string sequence
if (chr === '"') {
this.currentNode.endPos = this.pos + i;
this.currentNode.isClosed = true;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
break;
}
// \ Escapes the following char
if (chr === '\\') {
i++;
if (i >= len) {
let error = new Error(`Unexpected end of input at position ${this.pos + i} [E18]`);
error.code = 'ParserError18';
error.parserContext = { input: this.str, pos: this.pos + i };
throw error;
}
chr = this.str.charAt(i);
}
this.currentNode.value += chr;
break;
case STATE_PARTIAL:
if (chr === '>') {
if (this.currentNode.value.at(-1) === '.') {
let error = new Error(`Unexpected end of partial at position ${this.pos + i} [E19]`);
error.code = 'ParserError19';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.endPos = this.pos + i;
this.currentNode.isClosed = true;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
break;
}
if (chr === '.' && (!this.currentNode.value.length || this.currentNode.value.match(/\./))) {
let error = new Error(`Unexpected partial separator . at position ${this.pos + i} [E20]`);
error.code = 'ParserError20';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (imapFormalSyntax.DIGIT().indexOf(chr) < 0 && chr !== '.') {
let error = new Error(`Unexpected char at position ${this.pos + i} [E21: ${JSON.stringify(chr)}]`);
error.code = 'ParserError21';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (this.currentNode.value.match(/^0$|\.0$/) && chr !== '.') {
let error = new Error(`Invalid partial at position ${this.pos + i} [E22: ${JSON.stringify(chr)}]`);
error.code = 'ParserError22';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.value += chr;
break;
case STATE_LITERAL:
if (this.currentNode.started) {
// only relevant if literals are not already parsed out from input
// Disabled NULL byte check
// See https://github.com/emailjs/emailjs-imap-handler/commit/f11b2822bedabe492236e8263afc630134a3c41c
/*
if (chr === '\u0000') {
throw new Error('Unexpected \\x00 at position ' + (this.pos + i));
}
*/
this.currentNode.chBuffer[this.currentNode.chPos++] = chr.charCodeAt(0);
if (this.currentNode.chPos >= this.currentNode.literalLength) {
this.currentNode.endPos = this.pos + i;
this.currentNode.isClosed = true;
this.currentNode.value = this.currentNode.chBuffer.toString('binary');
this.currentNode.chBuffer = Buffer.alloc(0);
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
}
break;
}
if (chr === '+' && this.options.literalPlus) {
this.currentNode.literalPlus = true;
break;
}
if (chr === '}') {
if (!('literalLength' in this.currentNode)) {
let error = new Error(`Unexpected literal prefix end char } at position ${this.pos + i} [E23]`);
error.code = 'ParserError23';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (this.str.charAt(i + 1) === '\n') {
i++;
} else if (this.str.charAt(i + 1) === '\r' && this.str.charAt(i + 2) === '\n') {
i += 2;
} else {
let error = new Error(`Unexpected char at position ${this.pos + i} [E24: ${JSON.stringify(chr)}]`);
error.code = 'ParserError24';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.literalLength = Number(this.currentNode.literalLength);
if (!this.currentNode.literalLength) {
// special case where literal content length is 0
// close the node right away, do not wait for additional input
this.currentNode.endPos = this.pos + i;
this.currentNode.isClosed = true;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
} else if (this.options.literals) {
// use the next precached literal values
this.currentNode.value = this.options.literals.shift();
// only APPEND arguments are kept as Buffers
/*
if ((this.parent.command || '').toString().toUpperCase() !== 'APPEND') {
this.currentNode.value = this.currentNode.value.toString('binary');
}
*/
this.currentNode.endPos = this.pos + i + this.currentNode.value.length;
this.currentNode.started = false;
this.currentNode.isClosed = true;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
} else {
this.currentNode.started = true;
// Allocate expected size buffer. Max size check is already performed
// Maybe should use allocUnsafe instead?
this.currentNode.chBuffer = Buffer.alloc(this.currentNode.literalLength);
this.currentNode.chPos = 0;
}
break;
}
if (imapFormalSyntax.DIGIT().indexOf(chr) < 0) {
let error = new Error(`Unexpected char at position ${this.pos + i} [E25: ${JSON.stringify(chr)}]`);
error.code = 'ParserError25';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (this.currentNode.literalLength === '0') {
let error = new Error(`Invalid literal at position ${this.pos + i} [E26]`);
error.code = 'ParserError26';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.literalLength = (this.currentNode.literalLength || '') + chr;
break;
case STATE_SEQUENCE:
// space finishes the sequence set
if (chr === ' ') {
if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
let error = new Error(`Unexpected whitespace at position ${this.pos + i} [E27]`);
error.code = 'ParserError27';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (this.currentNode.value !== '*' && this.currentNode.value.at(-1) === '*' && this.currentNode.value.at(-2) !== ':') {
let error = new Error(`Unexpected whitespace at position ${this.pos + i} [E28]`);
error.code = 'ParserError28';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.isClosed = true;
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
break;
} else if (this.currentNode.parentNode && chr === ']' && this.currentNode.parentNode.type === 'SECTION') {
this.currentNode.endPos = this.pos + i - 1;
this.currentNode = this.currentNode.parentNode;
this.currentNode.isClosed = true;
this.currentNode.endPos = this.pos + i;
this.currentNode = this.currentNode.parentNode;
this.state = STATE_NORMAL;
checkSP();
break;
}
if (chr === ':') {
if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
let error = new Error(`Unexpected range separator : at position ${this.pos + i} [E29]`);
error.code = 'ParserError29';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
} else if (chr === '*') {
if ([',', ':'].indexOf(this.currentNode.value.at(-1)) < 0) {
let error = new Error(`Unexpected range wildcard at position ${this.pos + i} [E30]`);
error.code = 'ParserError30';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
} else if (chr === ',') {
if (!RE_SINGLE_DIGIT.test(this.currentNode.value.at(-1)) && this.currentNode.value.at(-1) !== '*') {
let error = new Error(`Unexpected sequence separator , at position ${this.pos + i} [E31]`);
error.code = 'ParserError31';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (this.currentNode.value.at(-1) === '*' && this.currentNode.value.at(-2) !== ':') {
let error = new Error(`Unexpected sequence separator , at position ${this.pos + i} [E32]`);
error.code = 'ParserError32';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
} else if (!RE_SINGLE_DIGIT.test(chr)) {
let error = new Error(`Unexpected char at position ${this.pos + i} [E33: ${JSON.stringify(chr)}]`);
error.code = 'ParserError33';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
if (RE_SINGLE_DIGIT.test(chr) && this.currentNode.value.at(-1) === '*') {
let error = new Error(`Unexpected number at position ${this.pos + i} [E34: ${JSON.stringify(chr)}]`);
error.code = 'ParserError34';
error.parserContext = { input: this.str, pos: this.pos + i, chr };
throw error;
}
this.currentNode.value += chr;
break;
case STATE_TEXT:
this.currentNode.value += chr;
break;
}
}
}
}
module.exports.TokenParser = TokenParser;