UNPKG

imapflow

Version:

IMAP Client for Node

226 lines (199 loc) 9.23 kB
/* eslint new-cap: 0 */ 'use strict'; const imapFormalSyntax = require('./imap-formal-syntax'); const { TokenParser } = require('./token-parser'); /** * Parses a single IMAP response line into its structural components: tag, command, * and attributes. Handles status responses (OK, NO, BAD, PREAUTH, BYE) with their * human-readable text and response codes, as well as continuation responses ("+"). */ class ParserInstance { /** * Creates a new ParserInstance for parsing an IMAP response line. * * @param {Buffer|string} input - The raw IMAP response line to parse. * @param {Object} [options] - Parser options passed through to the TokenParser for attribute parsing. * @param {boolean} [options.literalPlus] - Whether the LITERAL+ extension is in use. * @param {Array<Buffer>} [options.literals] - Pre-parsed literal values from the stream. */ constructor(input, options) { this.input = (input || '').toString(); this.options = options || {}; this.remainder = this.input; this.pos = 0; } /** * Extracts and returns the IMAP tag from the beginning of the response. * The tag is typically "*" for untagged responses, "+" for continuation requests, * or a client-assigned command tag like "A1". * * @returns {Promise<string>} The parsed tag string. * @throws {Error} If the tag contains invalid characters. */ async getTag() { if (!this.tag) { this.tag = await this.getElement(imapFormalSyntax.tag() + '*+', true); } return this.tag; } /** * Extracts and returns the IMAP command or response name from the input. * For continuation responses (tag "+"), returns an empty string and stores * the remainder as human-readable text. For status responses (OK, NO, BAD, * PREAUTH, BYE), separates the optional response code from the human-readable text. * * @returns {Promise<string>} The parsed command string. * @throws {Error} If the command contains invalid characters or input ends unexpectedly. */ 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()); } // Status responses have the format: TAG OK/NO/BAD [response-code] human-readable text // Example: * OK [CAPABILITY IMAP4rev1] Server ready // Example: A1 NO [AUTHENTICATIONFAILED] Invalid credentials // We need to separate the optional [response-code] from the human-readable text. 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; } /** * Extracts the next whitespace-delimited element from the input and validates it * against the given syntax character set. Advances the parser position past the element. * * @param {string} syntax - A string of allowed characters for the element (as returned by imap-formal-syntax methods). * @returns {Promise<string>} The extracted element string. * @throws {Error} If the element contains characters not in the syntax set, or if input ends unexpectedly. */ async getElement(syntax) { let match, element, errPos; if (/^\s/.test(this.remainder)) { 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.') { // Microsoft Exchange sometimes sends a non-standard response // "Server Unavailable." instead of a proper IMAP tagged/untagged response. // We detect this specific pattern and convert it into a synthetic BAD response // so the rest of the parser can handle it gracefully. 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; } /** * Consumes a single space character from the current position in the input. * Advances the parser position by one. * * @returns {Promise<void>} * @throws {Error} If the current character is not a space, or if input has ended unexpectedly. */ 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); } /** * Parses the remaining input as IMAP attributes using the TokenParser. * This handles complex structures including nested lists, literals, strings, * atoms, sections, sequences, and partial ranges. * * @returns {Promise<Array>} A promise that resolves to an array of parsed attribute objects. * @throws {Error} If the input contains unexpected whitespace, invalid characters, or ends unexpectedly. */ 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 (/^\s/.test(this.remainder)) { 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;