Projektstart
This commit is contained in:
190
backend/node_modules/imapflow/lib/handler/imap-compiler.js
generated
vendored
Normal file
190
backend/node_modules/imapflow/lib/handler/imap-compiler.js
generated
vendored
Normal 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);
|
||||
};
|
||||
147
backend/node_modules/imapflow/lib/handler/imap-formal-syntax.js
generated
vendored
Normal file
147
backend/node_modules/imapflow/lib/handler/imap-formal-syntax.js
generated
vendored
Normal 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;
|
||||
}
|
||||
};
|
||||
9
backend/node_modules/imapflow/lib/handler/imap-handler.js
generated
vendored
Normal file
9
backend/node_modules/imapflow/lib/handler/imap-handler.js
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const parser = require('./imap-parser');
|
||||
const compiler = require('./imap-compiler');
|
||||
|
||||
module.exports = {
|
||||
parser,
|
||||
compiler
|
||||
};
|
||||
67
backend/node_modules/imapflow/lib/handler/imap-parser.js
generated
vendored
Normal file
67
backend/node_modules/imapflow/lib/handler/imap-parser.js
generated
vendored
Normal 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;
|
||||
};
|
||||
257
backend/node_modules/imapflow/lib/handler/imap-stream.js
generated
vendored
Normal file
257
backend/node_modules/imapflow/lib/handler/imap-stream.js
generated
vendored
Normal 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;
|
||||
165
backend/node_modules/imapflow/lib/handler/parser-instance.js
generated
vendored
Normal file
165
backend/node_modules/imapflow/lib/handler/parser-instance.js
generated
vendored
Normal 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;
|
||||
654
backend/node_modules/imapflow/lib/handler/token-parser.js
generated
vendored
Normal file
654
backend/node_modules/imapflow/lib/handler/token-parser.js
generated
vendored
Normal 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;
|
||||
Reference in New Issue
Block a user