UNPKG

imapflow

Version:

IMAP Client for Node

155 lines (132 loc) 6.77 kB
'use strict'; const { formatFlag, canUseFlag, formatDateTime, normalizePath, encodePath, comparePaths, enhanceCommandError } = require('../tools.js'); /** * Appends a message to a mailbox. * * @param {Object} connection - IMAP connection instance * @param {string} destination - Destination mailbox path * @param {Buffer|string} content - Message content (RFC 822 format) * @param {string|string[]} [flags] - Message flags to set on the appended message * @param {Date|string} [idate] - Internal date to set for the message * @returns {Promise<{destination: string, path?: string, uid?: number, uidValidity?: BigInt, seq?: number}|undefined>} Append result with UID info if available, or undefined if preconditions not met * @throws {Error} If the APPEND command fails or message exceeds APPENDLIMIT */ module.exports = async (connection, destination, content, flags, idate) => { if (![connection.states.AUTHENTICATED, connection.states.SELECTED].includes(connection.state) || !destination) { // nothing to do here return; } if (typeof content === 'string') { content = Buffer.from(content); } // APPENDLIMIT capability (RFC 7889): server may advertise the maximum message // size it accepts. Check before sending to avoid a wasted round-trip. if (connection.capabilities.has('APPENDLIMIT')) { let appendLimit = connection.capabilities.get('APPENDLIMIT'); if (typeof appendLimit === 'number' && appendLimit < content.length) { let err = new Error('Message content too big for APPENDLIMIT=' + appendLimit); err.serverResponseCode = 'APPENDLIMIT'; throw err; } } destination = normalizePath(connection, destination); // If appending to the currently selected mailbox, we can listen for the // untagged EXISTS response to capture the new message's sequence number. let expectExists = comparePaths(connection, connection.mailbox.path, destination); // Validate and format flags. Only flags allowed by the mailbox's permanentFlags are included. flags = (Array.isArray(flags) ? flags : [].concat(flags || [])) .map(flag => flag && formatFlag(flag.toString())) .filter(flag => flag && canUseFlag(connection.mailbox, flag)); // APPEND command format: APPEND <mailbox> [<flags>] [<date-time>] <literal> let attributes = [{ type: 'ATOM', value: encodePath(connection, destination) }]; // Internal date: the date the server should record for this message. // Must be quoted (STRING type) per the IMAP date-time grammar. idate = idate ? formatDateTime(idate) : false; // Flags and date are optional; flags must come before date if both are present if (flags.length || idate) { attributes.push(flags.map(flag => ({ type: 'ATOM', value: flag }))); } if (idate) { attributes.push({ type: 'STRING', value: idate }); } // BINARY extension (RFC 3516): if the message content contains NUL bytes, // use literal8 syntax (~{size}\r\n) instead of regular literal ({size}\r\n). // Regular literals cannot contain NUL bytes per the IMAP grammar. let isLiteral8 = false; if (connection.capabilities.has('BINARY') && !connection.disableBinary) { isLiteral8 = content.indexOf(Buffer.from([0])) >= 0; } attributes.push({ type: 'LITERAL', value: content, isLiteral8 }); let map = { destination }; if (connection.mailbox && connection.mailbox.path) { map.path = connection.mailbox.path; } // Handler for untagged EXISTS: captures the new message count which gives // us the sequence number of the appended message (it's the latest message). const handleExistsUpdate = untagged => { map.seq = Number(untagged.command); // Update the connection's mailbox state and emit 'exists' event if the // count changed (notifies listeners about the new message). if (expectExists) { let prevCount = connection.mailbox.exists; if (map.seq !== prevCount) { connection.mailbox.exists = map.seq; connection.emit('exists', { path: connection.mailbox.path, count: map.seq, prevCount }); } } }; let response; try { response = await connection.exec('APPEND', attributes, { // Only listen for EXISTS if we're appending to the currently selected mailbox untagged: expectExists ? { EXISTS: handleExistsUpdate } : false }); // UIDPLUS (RFC 4315): the server may include APPENDUID response code in // the tagged OK. Format: [APPENDUID <uidValidity> <uid>] let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section; if (section && section.length) { let responseCode = section[0] && typeof section[0].value === 'string' ? section[0].value : ''; if (responseCode.toUpperCase() === 'APPENDUID') { let uidValidity = section[1] && typeof section[1].value === 'string' && !isNaN(section[1].value) ? BigInt(section[1].value) : false; let uid = section[2] && typeof section[2].value === 'string' && !isNaN(section[2].value) ? Number(section[2].value) : false; if (uidValidity !== false) { map.uidValidity = uidValidity; } if (uid) { map.uid = uid; } } } response.next(); // If we didn't get an EXISTS during APPEND (some servers don't send it // until the next command), issue a NOOP to flush pending notifications. if (expectExists && !map.seq) { try { response = await connection.exec('NOOP', false, { untagged: { EXISTS: handleExistsUpdate }, comment: 'Sequence not found from APPEND output' }); response.next(); } catch (err) { connection.log.warn({ err, cid: connection.id }); } } // If we have a sequence number but no UID (server doesn't support UIDPLUS), // look up the UID via SEARCH to provide a consistent result to the caller. if (map.seq && !map.uid) { let list = await connection.search({ seq: map.seq }, { uid: true }); if (list && list.length) { map.uid = list[0]; } } return map; } catch (err) { await enhanceCommandError(err); connection.log.warn({ err, cid: connection.id }); throw err; } };