imapflow
Version:
IMAP Client for Node
89 lines (75 loc) • 3.58 kB
JavaScript
;
const imapFormalSyntax = require('./imap-formal-syntax');
const { ParserInstance } = require('./parser-instance');
/**
* Parses a raw IMAP command or response buffer into a structured object.
* Handles edge cases such as null-byte-padded responses from buggy servers and
* multi-word commands like UID and AUTHENTICATE.
*
* @param {Buffer|string} command - The raw IMAP command or response data to parse.
* @param {Object} [options] - Parser options passed through to the underlying ParserInstance and TokenParser.
* @param {boolean} [options.literalPlus] - Whether the LITERAL+ extension is in use.
* @param {Array<Buffer>} [options.literals] - Pre-parsed literal values extracted from the input stream.
* @returns {Promise<Object>} A promise that resolves to a parsed response object.
* @returns {string} return.tag - The IMAP tag (e.g., "*", "+", or a command tag like "A1").
* @returns {string} return.command - The IMAP command or response name (e.g., "OK", "FETCH").
* @returns {Array} [return.attributes] - Parsed attributes of the response.
* @returns {number} [return.nullBytesRemoved] - Number of leading null bytes removed, if any.
*/
module.exports = async (command, options) => {
options = options || {};
let nullBytesRemoved = 0;
// Workaround for buggy IMAP servers that pad responses with leading NUL (\x00) bytes.
// Some servers (observed in the wild) prepend null bytes to their output, which would
// cause parsing to fail. We strip them and note how many were removed for diagnostics.
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 -- treat as a BAD response
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;
}
// Some IMAP commands are multi-word: "UID FETCH", "UID STORE", "UID COPY",
// "UID MOVE", "UID SEARCH", "UID EXPUNGE", and "AUTHENTICATE PLAIN", etc.
// For these, the first word is consumed as the command, then we read the
// subcommand and concatenate them (e.g., "UID" + " " + "FETCH" -> "UID FETCH").
if (['UID', 'AUTHENTICATE'].includes((response.command || '').toUpperCase())) {
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;
};