mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-11-11 10:09:20 +08:00
636 lines
26 KiB
JavaScript
636 lines
26 KiB
JavaScript
/* eslint new-cap: 0 */
|
|
|
|
'use strict';
|
|
|
|
let imapFormalSyntax = require('./imap-formal-syntax');
|
|
|
|
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 = 'NORMAL';
|
|
|
|
this.processString();
|
|
}
|
|
|
|
getAttributes() {
|
|
let attributes = [],
|
|
branch = attributes;
|
|
|
|
let walk = function (node) {
|
|
let curBranch = branch;
|
|
let elm;
|
|
let partial;
|
|
|
|
if (!node.closed && node.type === 'SEQUENCE' && node.value === '*') {
|
|
node.closed = true;
|
|
node.type = 'ATOM';
|
|
}
|
|
|
|
// If the node was never closed, throw it
|
|
if (!node.closed) {
|
|
throw new Error('Unexpected end of input at position ' + (this.pos + this.str.length - 1));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
node.childNodes.forEach(childNode => walk(childNode));
|
|
branch = curBranch;
|
|
}.bind(this);
|
|
|
|
walk(this.tree);
|
|
|
|
return attributes;
|
|
}
|
|
|
|
createNode(parentNode, startPos) {
|
|
let node = {
|
|
childNodes: [],
|
|
type: false,
|
|
value: '',
|
|
closed: true
|
|
};
|
|
|
|
if (parentNode) {
|
|
node.parentNode = parentNode;
|
|
}
|
|
|
|
if (typeof startPos === 'number') {
|
|
node.startPos = startPos;
|
|
}
|
|
|
|
if (parentNode) {
|
|
parentNode.childNodes.push(node);
|
|
}
|
|
|
|
return node;
|
|
}
|
|
|
|
processString() {
|
|
let chr, i, len,
|
|
checkSP = function () {
|
|
// jump to the next non whitespace pos
|
|
while (this.str.charAt(i + 1) === ' ') {
|
|
i++;
|
|
}
|
|
}.bind(this);
|
|
|
|
for (i = 0, len = this.str.length; i < len; i++) {
|
|
|
|
chr = this.str.charAt(i);
|
|
|
|
switch (this.state) {
|
|
|
|
case 'NORMAL':
|
|
|
|
switch (chr) {
|
|
|
|
// DQUOTE starts a new string
|
|
case '"':
|
|
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
|
this.currentNode.type = 'string';
|
|
this.state = 'STRING';
|
|
this.currentNode.closed = false;
|
|
break;
|
|
|
|
// ( starts a new list
|
|
case '(':
|
|
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
|
this.currentNode.type = 'LIST';
|
|
this.currentNode.closed = false;
|
|
break;
|
|
|
|
// ) closes a list
|
|
case ')':
|
|
if (this.currentNode.type !== 'LIST') {
|
|
throw new Error('Unexpected list terminator ) at position ' + (this.pos + i));
|
|
}
|
|
|
|
this.currentNode.closed = true;
|
|
this.currentNode.endPos = this.pos + i;
|
|
this.currentNode = this.currentNode.parentNode;
|
|
|
|
checkSP();
|
|
break;
|
|
|
|
// ] closes section group
|
|
case ']':
|
|
if (this.currentNode.type !== 'SECTION') {
|
|
throw new Error('Unexpected section terminator ] at position ' + (this.pos + i));
|
|
}
|
|
this.currentNode.closed = 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 = 'ATOM';
|
|
} else {
|
|
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
|
this.currentNode.type = 'PARTIAL';
|
|
this.state = 'PARTIAL';
|
|
this.currentNode.closed = false;
|
|
}
|
|
break;
|
|
|
|
// { starts a new literal
|
|
case '{':
|
|
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
|
this.currentNode.type = 'LITERAL';
|
|
this.state = 'LITERAL';
|
|
this.currentNode.closed = 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.closed = false;
|
|
this.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'].indexOf(this.parent.command.toUpperCase()) >= 0 && 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.closed = false;
|
|
this.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.closed = 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 '' %
|
|
if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== '\\' && chr !== '%') {
|
|
throw new Error('Unexpected char at position ' + (this.pos + i));
|
|
}
|
|
|
|
this.currentNode = this.createNode(this.currentNode, this.pos + i);
|
|
this.currentNode.type = 'ATOM';
|
|
this.currentNode.value = chr;
|
|
this.state = 'ATOM';
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case 'ATOM':
|
|
|
|
// space finishes an atom
|
|
if (chr === ' ') {
|
|
this.currentNode.endPos = this.pos + i - 1;
|
|
this.currentNode = this.currentNode.parentNode;
|
|
this.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.closed = true;
|
|
this.currentNode.endPos = this.pos + i;
|
|
this.currentNode = this.currentNode.parentNode;
|
|
this.state = 'NORMAL';
|
|
|
|
checkSP();
|
|
break;
|
|
}
|
|
|
|
if ((chr === ',' || chr === ':') && this.currentNode.value.match(/^\d+$/)) {
|
|
this.currentNode.type = 'SEQUENCE';
|
|
this.currentNode.closed = true;
|
|
this.state = 'SEQUENCE';
|
|
}
|
|
|
|
// [ starts a section group for this element
|
|
if (chr === '[') {
|
|
// allowed only for selected elements
|
|
if (['BODY', 'BODY.PEEK'].indexOf(this.currentNode.value.toUpperCase()) < 0) {
|
|
throw new Error('Unexpected section start char [ at position ' + this.pos);
|
|
}
|
|
this.currentNode.endPos = this.pos + i;
|
|
this.currentNode = this.createNode(this.currentNode.parentNode, this.pos + i);
|
|
this.currentNode.type = 'SECTION';
|
|
this.currentNode.closed = false;
|
|
this.state = 'NORMAL';
|
|
break;
|
|
}
|
|
|
|
if (chr === '<') {
|
|
throw new Error('Unexpected start of partial at position ' + this.pos);
|
|
}
|
|
|
|
// if the char is not ATOM compatible, throw. Allow \* as an exception
|
|
if (imapFormalSyntax['ATOM-CHAR']().indexOf(chr) < 0 && chr !== ']' && !(chr === '*' && this.currentNode.value === '\\')) {
|
|
throw new Error('Unexpected char at position ' + (this.pos + i));
|
|
} else if (this.currentNode.value === '\\*') {
|
|
throw new Error('Unexpected char at position ' + (this.pos + i));
|
|
}
|
|
|
|
this.currentNode.value += chr;
|
|
break;
|
|
|
|
case 'STRING':
|
|
|
|
// DQUOTE ends the string sequence
|
|
if (chr === '"') {
|
|
this.currentNode.endPos = this.pos + i;
|
|
this.currentNode.closed = true;
|
|
this.currentNode = this.currentNode.parentNode;
|
|
this.state = 'NORMAL';
|
|
|
|
checkSP();
|
|
break;
|
|
}
|
|
|
|
// \ Escapes the following char
|
|
if (chr === '\\') {
|
|
i++;
|
|
if (i >= len) {
|
|
throw new Error('Unexpected end of input at position ' + (this.pos + i));
|
|
}
|
|
chr = this.str.charAt(i);
|
|
}
|
|
|
|
/* // skip this check, otherwise the parser might explode on binary input
|
|
if (imapFormalSyntax['TEXT-CHAR']().indexOf(chr) < 0) {
|
|
throw new Error('Unexpected char at position ' + (this.pos + i));
|
|
}
|
|
*/
|
|
|
|
this.currentNode.value += chr;
|
|
break;
|
|
|
|
case 'PARTIAL':
|
|
if (chr === '>') {
|
|
if (this.currentNode.value.substr(-1) === '.') {
|
|
throw new Error('Unexpected end of partial at position ' + this.pos);
|
|
}
|
|
this.currentNode.endPos = this.pos + i;
|
|
this.currentNode.closed = true;
|
|
this.currentNode = this.currentNode.parentNode;
|
|
this.state = 'NORMAL';
|
|
checkSP();
|
|
break;
|
|
}
|
|
|
|
if (chr === '.' && (!this.currentNode.value.length || this.currentNode.value.match(/\./))) {
|
|
throw new Error('Unexpected partial separator . at position ' + this.pos);
|
|
}
|
|
|
|
if (imapFormalSyntax.DIGIT().indexOf(chr) < 0 && chr !== '.') {
|
|
throw new Error('Unexpected char at position ' + (this.pos + i));
|
|
}
|
|
|
|
if (this.currentNode.value.match(/^0$|\.0$/) && chr !== '.') {
|
|
throw new Error('Invalid partial at position ' + (this.pos + i));
|
|
}
|
|
|
|
this.currentNode.value += chr;
|
|
break;
|
|
|
|
case 'LITERAL':
|
|
if (this.currentNode.started) {
|
|
//if(imapFormalSyntax['CHAR8']().indexOf(chr) < 0){
|
|
if (chr === '\u0000') {
|
|
throw new Error('Unexpected \\x00 at position ' + (this.pos + i));
|
|
}
|
|
this.currentNode.value += chr;
|
|
|
|
if (this.currentNode.value.length >= this.currentNode.literalLength) {
|
|
this.currentNode.endPos = this.pos + i;
|
|
this.currentNode.closed = true;
|
|
this.currentNode = this.currentNode.parentNode;
|
|
this.state = 'NORMAL';
|
|
checkSP();
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (chr === '+' && this.options.literalPlus) {
|
|
this.currentNode.literalPlus = true;
|
|
break;
|
|
}
|
|
|
|
if (chr === '}') {
|
|
if (!('literalLength' in this.currentNode)) {
|
|
throw new Error('Unexpected literal prefix end char } at position ' + (this.pos + i));
|
|
}
|
|
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 {
|
|
throw new Error('Unexpected char at position ' + (this.pos + i));
|
|
}
|
|
this.currentNode.literalLength = Number(this.currentNode.literalLength);
|
|
this.currentNode.started = true;
|
|
|
|
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.closed = true;
|
|
this.currentNode = this.currentNode.parentNode;
|
|
this.state = 'NORMAL';
|
|
checkSP();
|
|
}
|
|
break;
|
|
}
|
|
if (imapFormalSyntax.DIGIT().indexOf(chr) < 0) {
|
|
throw new Error('Unexpected char at position ' + (this.pos + i));
|
|
}
|
|
if (this.currentNode.literalLength === '0') {
|
|
throw new Error('Invalid literal at position ' + (this.pos + i));
|
|
}
|
|
this.currentNode.literalLength = (this.currentNode.literalLength || '') + chr;
|
|
break;
|
|
|
|
case 'SEQUENCE':
|
|
// space finishes the sequence set
|
|
if (chr === ' ') {
|
|
if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
|
|
throw new Error('Unexpected whitespace at position ' + (this.pos + i));
|
|
}
|
|
|
|
if (this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') {
|
|
throw new Error('Unexpected whitespace at position ' + (this.pos + i));
|
|
}
|
|
|
|
this.currentNode.closed = true;
|
|
this.currentNode.endPos = this.pos + i - 1;
|
|
this.currentNode = this.currentNode.parentNode;
|
|
this.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.closed = true;
|
|
this.currentNode.endPos = this.pos + i;
|
|
this.currentNode = this.currentNode.parentNode;
|
|
this.state = 'NORMAL';
|
|
|
|
checkSP();
|
|
break;
|
|
}
|
|
|
|
if (chr === ':') {
|
|
if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
|
|
throw new Error('Unexpected range separator : at position ' + (this.pos + i));
|
|
}
|
|
} else if (chr === '*') {
|
|
if ([',', ':'].indexOf(this.currentNode.value.substr(-1)) < 0) {
|
|
throw new Error('Unexpected range wildcard at position ' + (this.pos + i));
|
|
}
|
|
} else if (chr === ',') {
|
|
if (!this.currentNode.value.substr(-1).match(/\d/) && this.currentNode.value.substr(-1) !== '*') {
|
|
throw new Error('Unexpected sequence separator , at position ' + (this.pos + i));
|
|
}
|
|
if (this.currentNode.value.substr(-1) === '*' && this.currentNode.value.substr(-2, 1) !== ':') {
|
|
throw new Error('Unexpected sequence separator , at position ' + (this.pos + i));
|
|
}
|
|
} else if (!chr.match(/\d/)) {
|
|
throw new Error('Unexpected char at position ' + (this.pos + i));
|
|
}
|
|
|
|
if (chr.match(/\d/) && this.currentNode.value.substr(-1) === '*') {
|
|
throw new Error('Unexpected number at position ' + (this.pos + i));
|
|
}
|
|
|
|
this.currentNode.value += chr;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
class ParserInstance {
|
|
|
|
constructor(input, options) {
|
|
this.input = (input || '').toString();
|
|
this.options = options || {};
|
|
this.remainder = this.input;
|
|
this.pos = 0;
|
|
}
|
|
|
|
getTag() {
|
|
if (!this.tag) {
|
|
this.tag = this.getElement(imapFormalSyntax.tag() + '*+', true);
|
|
}
|
|
return this.tag;
|
|
}
|
|
|
|
getCommand() {
|
|
let responseCode;
|
|
|
|
if (!this.command) {
|
|
this.command = this.getElement(imapFormalSyntax.command());
|
|
}
|
|
|
|
switch ((this.command || '').toString().toUpperCase()) {
|
|
case 'OK':
|
|
case 'NO':
|
|
case 'BAD':
|
|
case 'PREAUTH':
|
|
case 'BYE':
|
|
responseCode = this.remainder.match(/^ \[(?:[^\]]*\])+/);
|
|
if (responseCode) {
|
|
this.humanReadable = this.remainder.substr(responseCode[0].length).trim();
|
|
this.remainder = responseCode[0];
|
|
} else {
|
|
this.humanReadable = this.remainder.trim();
|
|
this.remainder = '';
|
|
}
|
|
break;
|
|
}
|
|
|
|
return this.command;
|
|
}
|
|
|
|
getElement(syntax) {
|
|
let match, element, errPos;
|
|
if (this.remainder.match(/^\s/)) {
|
|
throw new Error('Unexpected whitespace at position ' + this.pos);
|
|
}
|
|
|
|
if ((match = this.remainder.match(/^[^\s]+(?=\s|$)/))) {
|
|
element = match[0];
|
|
|
|
if ((errPos = imapFormalSyntax.verify(element, syntax)) >= 0) {
|
|
throw new Error('Unexpected char at position ' + (this.pos + errPos));
|
|
}
|
|
} else {
|
|
throw new Error('Unexpected end of input at position ' + this.pos);
|
|
}
|
|
|
|
this.pos += match[0].length;
|
|
this.remainder = this.remainder.substr(match[0].length);
|
|
|
|
return element;
|
|
}
|
|
|
|
getSpace() {
|
|
if (!this.remainder.length) {
|
|
throw new Error('Unexpected end of input at position ' + this.pos);
|
|
}
|
|
|
|
if (imapFormalSyntax.verify(this.remainder.charAt(0), imapFormalSyntax.SP()) >= 0) {
|
|
throw new Error('Unexpected char at position ' + this.pos);
|
|
}
|
|
|
|
this.pos++;
|
|
this.remainder = this.remainder.substr(1);
|
|
}
|
|
|
|
getAttributes() {
|
|
if (!this.remainder.length) {
|
|
throw new Error('Unexpected end of input at position ' + this.pos);
|
|
}
|
|
|
|
if (this.remainder.match(/^\s/)) {
|
|
throw new Error('Unexpected whitespace at position ' + this.pos);
|
|
}
|
|
|
|
return new TokenParser(this, this.pos, this.remainder, this.options).getAttributes();
|
|
}
|
|
}
|
|
|
|
module.exports = function (command, options) {
|
|
let parser, response = {};
|
|
|
|
options = options || {};
|
|
|
|
parser = new ParserInstance(command, options);
|
|
|
|
response.tag = parser.getTag();
|
|
parser.getSpace();
|
|
response.command = parser.getCommand();
|
|
|
|
if (['UID', 'AUTHENTICATE'].indexOf((response.command || '').toUpperCase()) >= 0) {
|
|
parser.getSpace();
|
|
response.command += ' ' + parser.getElement(imapFormalSyntax.command());
|
|
}
|
|
|
|
if (parser.remainder.trim().length) {
|
|
parser.getSpace();
|
|
response.attributes = parser.getAttributes();
|
|
}
|
|
|
|
if (parser.humanReadable) {
|
|
response.attributes = (response.attributes || []).concat({
|
|
type: 'TEXT',
|
|
value: parser.humanReadable
|
|
});
|
|
}
|
|
|
|
return response;
|
|
};
|