/** * Opening a connection: what it turned out to be able to do, the ways it can * fail to become readable, and what the owner is told when it does. * * Split out of `imap-client.ts` to keep that file under the repository's * per-file line cap, and because these types are what a caller reasons about * BEFORE it has a usable client, the failure it has to classify and the * capability record it has to read. */ import type { ImapMailboxStatus } from './imap-headers.js'; import type { ImapConnection, ImapSession } from './imap-session.js'; import type { ImapClient } from './imap-client.js'; /** * What a caller is told when it reads before connecting. Named rather than * inlined because two places raise it and one test asserts on it. */ export declare const NOT_OPEN_MESSAGE = "The IMAP connection is not open. Call open() before reading from the mailbox."; /** * Why a connection could not be opened, as four distinct facts. * * They are distinct because they call for different responses, and a caller * that cannot tell them apart necessarily gets some of them wrong: * * - `authentication-rejected`, the credential was REFUSED as a credential, * or could not be put on the wire at all. TERMINAL. Retrying a rejected * password on a backoff loop is how an account gets locked; the operator * has to change something before this can succeed. * - `mailbox-unavailable`, the credential worked and the named mailbox does * not exist. TERMINAL for the same reason: reconnecting does not create a * folder. Authenticated is not readable, and this is the case that says so. * - `server-unavailable`, the server said no for a reason that is about the * SERVER, not the account: a connection limit, a capacity refusal, a * temporary fault. NOT terminal. This exists because a refusal at the * login step is not necessarily about the login: Gmail answers * `NO [LIMIT] Too many simultaneous connections` right there, and it * clears in seconds. Classifying that as a rejected credential stops a * watcher permanently, and the symptom is a mailbox that looks quiet while * mail piles up behind it, which is the failure this whole capability * exists to end. We reach it routinely on our own account, because * `EmailService` opens a fresh connection per request on top of the one a * watcher holds permanently, and Gmail allows fifteen at once. * - `connection-failed`, the socket, the greeting or the timing. Transient. * * When the server gives no response code and its wording is ambiguous, the * classification is deliberately the NON-terminal one. The asymmetry is not * close: guessing terminal stops mail delivery until a human notices, guessing * transient costs a retry. */ export type ImapOpenFailureReason = 'authentication-rejected' | 'mailbox-unavailable' | 'server-unavailable' | 'connection-failed'; /** * What a server refusal actually means, read from the refusal itself. * * The phase a refusal arrived in is a hint, not the answer. A `NO` at the * login step is only an authentication failure when it says something about * authentication; when it says `[LIMIT]` it is about the server, and treating * the two the same is how a transient condition becomes permanent silence. * `phaseReason` is used only when the refusal itself is ambiguous AND the * phase's own reason is not the terminal guess. */ export declare function classifyServerRefusal(serverMessage: string, phaseReason: ImapOpenFailureReason): ImapOpenFailureReason; /** * An `open()` that did not reach a readable mailbox, with the reason named. * * The message is composed so it still contains the underlying wording, the * server's own text where the server gave any, because "IMAP command failed" * with no further detail is what made these three indistinguishable before. */ export declare class ImapOpenError extends Error { readonly reason: ImapOpenFailureReason; /** The server's own words, or the original failure text. '' when neither. */ readonly serverMessage: string; /** The mailbox this attempt was for. */ readonly mailbox: string; /** True only when retrying cannot help; something has to change first. */ readonly terminal: boolean; /** * The routable record. A supervisor reads this and delivers it; it does not * have to re-derive from a message string what the failure was, and a * terminal failure therefore cannot end as a log line nobody reads. */ readonly notice: EmailCapabilityFailureNotice; constructor(input: { readonly reason: ImapOpenFailureReason; readonly summary: string; readonly serverMessage: string; readonly mailbox: string; }); } /** * What a connection turned out to be able to do, established at open time. * * Returned by `open()` rather than assumed, because "the socket connected and * the password was accepted" answers neither "can I read this mailbox" nor * "can this connection be held open with IDLE", and a caller that treats it as * though it did has no way to find out it was wrong except by getting nothing. */ export interface ImapConnectionReport { /** * Capability atoms the server volunteered, upper-cased. Empty means it * volunteered none, ask `capabilities()`, which will request them. */ readonly advertisedCapabilities: readonly string[]; /** * Whether `IDLE` (RFC 2177) was advertised, as two cases, not three values. * * A tri-state whose third value can be read as falsy looks careful and * behaves carelessly: `if (report.supportsIdle)` would compile and would * quietly mean "poll forever against a server that supports push". This * shape does not permit that. `.supported` does not exist until `.known` has * been narrowed to true, so a caller either handles "the server said * nothing" or does not compile. * * The way to handle it is `resolveIdleSupport(client)`, which answers the * unknown case by actually asking. */ readonly idle: ImapIdleSupport; /** The mailbox that was EXAMINEd, and what the server said about it. */ readonly mailbox: ImapMailboxStatus & { readonly name: string; }; } /** * What the server said about IDLE, in a shape that cannot be read as a boolean * by accident. * * Two cases, not three values: either the server told us (`known: true`, with * the answer) or it told us nothing (`known: false`, with no answer to read). * `supported` is deliberately absent from the second case rather than present * and undefined, present-and-undefined is falsy, which is the exact mistake * this shape exists to make impossible. */ export type ImapIdleSupport = { readonly known: true; readonly supported: boolean; } | { readonly known: false; }; /** Build the IDLE case from a capability set, empty meaning "said nothing". */ export declare function idleSupportFrom(capabilities: readonly string[]): ImapIdleSupport; /** Whether IDLE can be used, and how that was established. */ export interface ImapIdleDecision { readonly supported: boolean; /** * `advertised`, the server named IDLE. * `not-advertised`, the server listed its capabilities and IDLE was not one. * `server-would-not-say`, it never listed them, even when asked. Polling is * the right fallback, and the reason belongs in the surfaced status so the * owner can see WHY it is polling rather than assume the provider cannot * do better. */ readonly reason: 'advertised' | 'not-advertised' | 'server-would-not-say'; } /** * Resolve IDLE support, asking the server when it volunteered nothing. * * This is the accessor the watcher goes through. It exists so that "the server * said nothing" is answered by a `CAPABILITY` command rather than by a * shrug, an unknown resolved into a real answer, or into a named reason for * not having one. */ export declare function resolveIdleSupport(client: Pick): Promise; /** * Why a mailbox capability is unavailable, in a form a supervisor can route. * * `credential-unavailable` sits alongside the three open failures because it * is the same fact from one step earlier: there is nothing to sign in with. * It is called out separately because its fix is different from a rejected * password, the secret is missing rather than wrong. * * `bodies-unfetchable` sits alongside them from one step LATER: the credential * worked, the mailbox opened, and the server will not hand over what is inside * a message. It is deliberately not `mailbox-unavailable` and deliberately not * a refused fetch, the server answered, and answered with nothing, because * its remedy is different from both: the account's access rights, not the * folder name and not the password. */ export type EmailCapabilityFailureReason = ImapOpenFailureReason | 'credential-unavailable' | 'bodies-unfetchable'; /** * A terminal failure that must reach the owner, not merely a log line. * * A watcher that stops permanently has to say so somewhere authoritative and * name the step that fixes it. Silence is the failure this whole capability * exists to end: an inbox that looks quiet while mail piles up in it is * indistinguishable, from the outside, from an inbox with no mail in it. */ export interface EmailCapabilityFailureNotice { readonly reason: EmailCapabilityFailureReason; /** True when retrying cannot help; something has to change first. */ readonly terminal: boolean; /** The mailbox this was about, or '' when it was not about one. */ readonly mailbox: string; /** One or two sentences for the owner, naming the step that fixes it. */ readonly ownerMessage: string; /** The server's own words, or the underlying failure text. '' when neither. */ readonly serverMessage: string; } /** The owner-facing sentence for each reason, naming the step that fixes it. */ export declare function ownerMessageForFailure(reason: EmailCapabilityFailureReason, mailbox: string): string; /** * Read the routable notice off a failure, whatever threw it. * * Structural rather than `instanceof`, so a credential failure raised before * any socket exists, in a module this one must not import, is routed by the * same path as an open failure. */ export declare function describeEmailCapabilityFailure(error: unknown): EmailCapabilityFailureNotice | null; /** * The wire connection of an OPEN client, for protocol work that cannot be * expressed as "send a command, read its response". * * IDLE is why this exists: it sends `IDLE`, waits for a `+`, reads untagged * responses for up to twenty-seven minutes, sends the bare line `DONE`, and * only then collects the completion of the tag it issued at the start. * * Deliberately a free function, and deliberately not re-exported from * `email/index.ts`: it is reachable from the modules that sit beside this one * and from nowhere else. */ export declare function imapConnection(client: ImapClient): ImapConnection; /** * Compose a named open failure, keeping the underlying wording. * * `refusedReason` is what the failing phase means when the SERVER said no. A * phase that timed out or lost the socket instead did not get an answer at * all, and calling that a rejected credential would mark a transient network * stall terminal and stop a watcher from ever retrying it. So the * classification is made on what actually happened, not on which phase it * happened in. */ export declare function composeOpenFailure(input: { readonly refusedReason: ImapOpenFailureReason; readonly refusedSummary: string; readonly error: unknown; readonly mailbox: string; }): ImapOpenError; /** Record the live session of a client that has just connected. */ export declare function rememberConnection(client: ImapClient, session: ImapSession): void; /** Forget a client's session; it is no longer usable. */ export declare function forgetConnection(client: ImapClient): void; //# sourceMappingURL=imap-open.d.ts.map