imapflow
Version:
IMAP Client for Node
155 lines (132 loc) • 6.77 kB
JavaScript
;
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;
}
};