"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = append; const tools_js_1 = require("../tools.js"); const errors_js_1 = require("../errors.js"); /** * Appends a message to a mailbox. * * @param connection - IMAP connection instance * @param destination - Destination mailbox path * @param content - Message content (RFC 822 format) * @param flags - Message flags to set on the appended message * @param idate - Internal date to set for the message * @returns Append result with UID info if available, or undefined if preconditions not met * @throws {Error} If the APPEND command fails or message exceeds APPENDLIMIT */ async function append(connection, destination, content, flags, idate) { if (!(0, tools_js_1.isAuthenticatedState)(connection) || !destination) { // nothing to do here return; } // The literal size marker is announced from these bytes, so anything that is not // bytes (a Readable, a plain object) is refused here instead of desyncing the session let message; if (typeof content === 'string') { message = Buffer.from(content); } else if (content instanceof Uint8Array) { // a Buffer as is, any other Uint8Array as a view over the same memory, not a copy message = Buffer.isBuffer(content) ? content : Buffer.from(content.buffer, content.byteOffset, content.byteLength); } else { throw (0, errors_js_1.createImapError)('Message content must be a Buffer, Uint8Array or string', 'InvalidMessageContent'); } // 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 < message.length) { let err = new Error('Message content too big for APPENDLIMIT=' + appendLimit); err.serverResponseCode = 'APPENDLIMIT'; throw err; } } destination = (0, tools_js_1.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 selected = (0, tools_js_1.getSelectedMailbox)(connection); // The selected mailbox when appending to it, false otherwise const targetMailbox = selected && (0, tools_js_1.comparePaths)(connection, selected.path, destination) ? selected : false; // Flags are only filtered when a read-write selection of the destination itself says what it // accepts: the permanentFlags of an unrelated mailbox say nothing about the destination, and a // read-only selection reports an empty PERMANENTFLAGS list (Dovecot does), which canUseFlag() // reads as deny-all. Otherwise they go to the server unfiltered and it decides (RFC 9051). const flagSource = targetMailbox && !targetMailbox.readOnly ? targetMailbox : false; const dropped = []; flags = (Array.isArray(flags) ? flags : [].concat(flags || [])) .map(flag => { const formatted = flag && (0, tools_js_1.formatFlag)(flag.toString()); if (!formatted || !(0, tools_js_1.canUseFlag)(flagSource, formatted)) { if (flag) { dropped.push(flag.toString()); } return false; } return formatted; }) .filter((flag) => !!flag); // The stored message simply ends up without them, and the caller is told nothing, so leave // a trail for the flags that never reached the server. if (dropped.length) { connection.log.warn({ msg: 'Dropped flags the mailbox does not accept', cid: connection.id, path: destination, dropped }); } // APPEND command format: APPEND [] [] let attributes = [{ type: 'ATOM', value: (0, tools_js_1.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 ? (0, tools_js_1.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 = message.indexOf(0) >= 0; } attributes.push({ type: 'LITERAL', value: message, 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) => { // The count is written into the live mailbox state below, so it has to clear the // same bar untaggedExists() applies: a digit run long enough to coerce to Infinity // would make resolveRange('*') compile to the literal string "Infinity" and break // every range-based command until the next SELECT. let seq = (0, tools_js_1.parseUintValue)(untagged.command, tools_js_1.MAX_UINT32_DIGITS); if (seq === false) { return; } map.seq = seq; // Update the connection's mailbox state and emit 'exists' event if the // count changed (notifies listeners about the new message). if (targetMailbox) { let prevCount = targetMailbox.exists; if (map.seq !== prevCount) { targetMailbox.exists = map.seq; (0, tools_js_1.emitSafe)(connection, 'exists', { path: targetMailbox.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: targetMailbox ? { EXISTS: handleExistsUpdate } : false }); // UIDPLUS (RFC 4315): the server may include APPENDUID response code in // the tagged OK. Format: [APPENDUID ] let section = response.response.attributes && response.response.attributes[0] && response.response.attributes[0].section; if (section && section.length) { let first = section[0]; let responseCode = first && typeof first.value === 'string' ? first.value : ''; if (responseCode.toUpperCase() === 'APPENDUID') { // Bounded digit runs only: isNaN() also passes '1e5', which BigInt() rejects // with a throw - and this catch rethrows, so the append would reject after the // message was already stored and a retrying caller would duplicate it. let uidValidity = (0, tools_js_1.parseBigIntValue)(section[1] && section[1].value, tools_js_1.MAX_UINT32_DIGITS); let uid = (0, tools_js_1.parseUintValue)(section[2] && section[2].value, tools_js_1.MAX_UINT32_DIGITS); // UIDVALIDITY is only reported next to the UID it was sent with: kept without one, // it would end up paired with the UID the SEARCH fallback below guesses from the // sequence number, and the pair would pass for a server-confirmed identity. if (uid) { map.uid = uid; if (uidValidity) { map.uidValidity = uidValidity; } } } } 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 (targetMailbox && !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. // The message is already stored, so a failed lookup only leaves the UID out: // rejecting here would make a retrying caller append it twice. if (map.seq && !map.uid) { try { let list = await connection.search({ seq: map.seq }, { uid: true }); if (Array.isArray(list) && list.length) { map.uid = list[0]; } } catch (err) { (0, tools_js_1.logConnectionError)(connection, 'Failed to look up the UID of the appended message', err); } } return map; } catch (err) { await (0, tools_js_1.reportCommandError)(connection, err); throw err; } } module.exports = exports.default; Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });