/** * The IMAP wire session: one persistent reader per connection, tagged * commands, untagged dispatch, and literals. * * Split out of `imap-client.ts` so that file stays well inside the repository's * per-file line cap now that the client speaks four more commands. Nothing here * knows what a mailbox or a message is, it moves tagged commands and literal * payloads across a socket and hands back lines. * * One reader owns the socket for the connection's lifetime * ──────────────────────────────────────────────────────── * An earlier shape attached `data`/`error`/`close` listeners for the duration * of one command and removed them when it completed. Between commands nothing * was listening, so anything the server sent on its own initiative, which is * every interesting thing a server says, `* n EXISTS` above all, landed in a * buffer that the next command discarded. Worse, the leftover bytes of a * response that arrived in the same TCP segment as the previous one went with * it. * * So the listeners are attached once, in the constructor, and removed once, in * `destroy()`. A single `drain()` turns the byte stream into complete logical * lines and routes each one: * * - a `+ ...` continuation goes to the command waiting for one; * - a ` OK|NO|BAD` line completes the command issued under that tag, * whether or not anybody is awaiting it yet; * - anything else is collected for the command currently in flight, and * every untagged `* ...` line is additionally offered to subscribers. * * Untagged lines are offered to subscribers whether or not a command is in * flight, because that is the only rule that works for IDLE: the untagged * `EXISTS` that IDLE exists to receive arrives while the `IDLE` command itself * is still outstanding. Subscribers therefore see untagged data belonging to * commands they did not issue and must filter for what they care about. * * Reads have a deadline by default and none on request * ──────────────────────────────────────────────────── * Ordinary commands keep the per-operation timeout they always had. A caller * that must wait in silence for half an hour, again, IDLE, passes * `timeoutMs: null` and bounds the wait with an `AbortSignal` instead. There is * no path where a wait is unbounded in both. * * Literals are counted in BYTES * ───────────────────────────── * `{n}` in IMAP is a byte count (RFC 3501 §4.3), and the socket is read with * `setEncoding('utf8')`, so the string this class accumulates has FEWER * characters than the server's byte count whenever the payload is not pure * ASCII. Taking `n` characters would swallow the bytes that follow the literal *, the closing `)` and, after it, the tagged completion line, and the read * would hang until it timed out. `takeUtf8Bytes` walks code points and counts * their UTF-8 width instead, which is what makes reading a message body with * an accented character in it work at all. * * The same arithmetic runs on the way out: `commandWithLiteral` declares * `Buffer.byteLength(payload)`, never `payload.length`. */ import type { Socket } from 'node:net'; /** * Take the longest prefix of `text` that fits in `maxBytes` bytes of UTF-8, * and report how many bytes that prefix actually is. * * A surrogate pair is taken whole or not at all, so the returned prefix is * always valid text. If the very first character is already wider than the * budget, which means the server's byte count fell inside a multi-byte * sequence and cannot be honoured exactly, the character is taken anyway and * the whole remaining budget is reported as consumed, so a read always makes * progress rather than looping on a boundary it can never hit. */ export declare function takeUtf8Bytes(text: string, maxBytes: number): { readonly taken: string; readonly bytes: number; }; /** * Receives one untagged (`* ...`) response line. * * The line is complete: a response whose payload arrived as a `{n}` literal is * delivered with that payload folded onto the end of its own line, exactly as * a tagged command would collect it. */ export type ImapUntaggedListener = (line: string) => void; /** What a caller wants kept while a command is in flight. */ export interface ImapSendOptions { /** * Collect untagged lines into this command's response. Default true, which * is what an ordinary request/response command needs. * * IDLE sets it false: it stays in flight for up to twenty-seven minutes, its * untagged traffic is delivered to subscribers as it arrives, and a second * retained copy would be an array that grows for the whole round with * nothing reading it. The tagged completion is still collected either way. */ readonly retainUntagged?: boolean | undefined; } /** How long a single wait may last, and what may cancel it. */ export interface ImapReadOptions { /** * Deadline in milliseconds. Omitted means the session's per-operation * timeout. `null` means NO deadline: the wait ends when the awaited response * arrives, when the socket fails, or when `signal` aborts, and nothing else. */ readonly timeoutMs?: number | null; /** Cancels the wait. Required in practice whenever `timeoutMs` is null. */ readonly signal?: AbortSignal | undefined; } /** * The wire operations a protocol extension living beside this module needs. * * This exists for IDLE, which cannot be expressed as "send a command, read its * response": it sends `IDLE`, waits for a `+`, then reads untagged responses * for up to twenty-seven minutes, then sends the bare line `DONE`, which is * not a tagged command, and only then collects the completion of the tag it * issued at the start. * * It is deliberately NOT reachable from `ImapClient`'s own method surface and * is not re-exported from `email/index.ts`. Holding a client does not give * anybody a way to put arbitrary bytes on the mailbox connection. */ export interface ImapConnection { /** Subscribe to untagged responses. Returns the unsubscribe function. */ onUntagged(listener: ImapUntaggedListener): () => void; /** Send a tagged command and return its tag without awaiting completion. */ sendCommand(text: string, options?: ImapSendOptions): Promise; /** Write one bare line that is not a tagged command, e.g. IDLE's `DONE`. */ sendRawLine(text: string): Promise; /** Await the `+ ...` continuation request for a command already in flight. */ awaitContinuation(tag: string, options?: ImapReadOptions): Promise; /** Await the tagged completion of a command issued earlier under `tag`. */ awaitTag(tag: string, options?: ImapReadOptions): Promise; /** Await the first untagged line the predicate accepts. */ waitForUntagged(matches: (line: string) => boolean, options?: ImapReadOptions): Promise; } /** * Wraps a Socket with a persistent line reader, tagged command writing, and * untagged response dispatch. One instance per connection, for the life of the * connection. */ export declare class ImapSession implements ImapConnection { private readonly socket; private readonly timeoutMs; private readonly literalCap; private buffer; private tagCounter; private literalBytesRemaining; private literalAccum; private literalOwnerLine; private readonly pending; private readonly untaggedListeners; private readonly untaggedWaiters; private greetingWaiter; private greetingSeen; private greetingLine; private failure; private destroyed; private readonly onData; private readonly onSocketError; private readonly onSocketClose; constructor(socket: Socket, timeoutMs: number, literalCap: number); /** Turn buffered bytes into complete logical lines and route each one. */ private drain; /** Send one complete response line to whoever it belongs to. */ private route; /** The command whose response lines are currently arriving, if any. */ private oldestOpenCommand; private settle; /** Drop the oldest completions nobody ever awaited; see the constant. */ private forgetOldestCompletions; private dispatchUntagged; /** The socket failed or closed. Every wait, present and future, ends here. */ private fail; /** * The byte stream can no longer be parsed, an oversized literal means we do * not know where the payload ends, so nothing after it can be trusted to be * a response. The connection ends with it. */ private failStream; private rejectAll; private wait; /** * Read the server greeting and return it verbatim. * * The line itself is the answer to more than "is it there": most servers * advertise their capabilities inside it, as `* OK [CAPABILITY ...]`, so * discarding it would mean asking again for something already said. * Resolves immediately when the greeting already arrived. */ readGreeting(): Promise; /** Send a tagged IMAP command and collect all response lines through completion. */ command(text: string): Promise; /** * Send a tagged command and hand back its tag without waiting. * * Response lines are collected from the moment the tag is allocated, so a * server that answers before the caller gets round to `awaitTag` loses * nothing. A command that will stay in flight for a long time and read the * stream through `onUntagged` passes `retainUntagged: false`, so its line * array does not grow for the length of the round. */ sendCommand(text: string, options?: ImapSendOptions): Promise; /** * Write one bare line, terminated with CRLF, that is not a tagged command. * * IDLE's `DONE` is the reason this exists: it answers the server's * continuation request and carries no tag of its own, and the tagged * completion that follows belongs to the `IDLE` command issued earlier. */ sendRawLine(text: string): Promise; /** * Await the tagged completion of a command issued earlier under `tag`. * * `NO` and `BAD` reject with the server's own wording, exactly as `command()` * does. Pass `timeoutMs: null` with a `signal` for a wait that may last as * long as the caller's own bound allows. */ awaitTag(tag: string, options?: ImapReadOptions): Promise; /** * Wait for the `+ ...` continuation request for a command in flight. * * A tagged completion arriving instead is the server refusing, and is * reported as the same plain-language failure a normal command would raise. */ awaitContinuation(tag: string, options?: ImapReadOptions): Promise; /** * Wait for the first untagged line the predicate accepts. * * The long-read path: `timeoutMs: null` gives a wait with no deadline at * all, ended by the line arriving, by the socket failing, or by `signal`. */ waitForUntagged(matches: (line: string) => boolean, options?: ImapReadOptions): Promise; /** Subscribe to untagged responses for as long as the connection lives. */ onUntagged(listener: ImapUntaggedListener): () => void; /** * Send a command whose final argument is a literal, e.g. * `APPEND Drafts (\Draft) {N}` followed by N bytes of message. * * N is `Buffer.byteLength(payload)`, not `payload.length`: a subject or body * with any non-ASCII character in it occupies more bytes than characters, and * a short count would leave the tail of the message being parsed as IMAP * commands. Waits for the server's `+` continuation before writing the * payload, and surfaces a `NO`/`BAD` sent instead of the continuation as the * same plain-language failure a normal command would raise. */ commandWithLiteral(commandPrefix: string, payload: string): Promise; private nextTag; private write; private closeSocket; /** Release the socket and end every wait still outstanding. */ destroy(): void; } //# sourceMappingURL=imap-session.d.ts.map