/** * One mailbox, watched: the connection lifecycle the IDLE and poll loops sit * inside. * * `idle-watcher.ts` knows how to hold an IDLE and `poll-loop.ts` knows how to * find what arrived. Neither knows what to do when the socket dies, when the * credential is refused, or when the server turns out not to support push, * and those are the cases that decide whether this capability is worth having, * because they are the ones that end with the owner hearing nothing. * * What this file guarantees * ───────────────────────── * **No message is lost across a reconnect.** Recovery is not "resume the * stream"; it is "ask what is above the persisted cursor". Whatever arrived * while the socket was down is above the cursor, so the first thing a fresh * connection does, before IDLE, before polling, is drain the delta. The * cursor advances only behind completed work, so a crash mid-message * re-delivers rather than skips, and dedup absorbs the duplicate. * * **"Cannot" and "not yet" stay different.** A watcher waiting out a backoff * is `degraded`, never `insufficient`. Nothing is lost by waiting; the delta * is still above the cursor. `insufficient` is reserved for a verdict about * capability, a refused credential, an unopenable mailbox, a server that will * not hand over message data, and in that state the watcher does not run, * says so once, and re-probes on a timer so that fixing the cause does not * need a restart. * * **A terminal failure is surfaced, not filed.** A rejected credential ends * with `terminalFailure` on the observer carrying the exact remedial step, so * the supervisor can route it somewhere authoritative. Silent permanent death * is the failure this whole round exists to eliminate; recording it in a * status object nobody opens is the same failure with a paper trail. * * **A connection limit is not an outage.** Gmail permits fifteen concurrent * IMAP connections and `EmailService` opens a fresh one per request on top of * the one held here, so hitting it is an expected Tuesday. It backs off on a * longer ceiling and reports `degraded` in the provider's own words, because * asking again sooner cannot clear a limit that our own asking is part of. */ /** * The watcher's constructor arguments, its published status, and the two retry * ceilings, declared in `watcher-types.ts` and re-exported here, because * `watcher.ts` is the name every consumer imports. */ export type { InboundMailboxWatcherDeps, InboundMailboxWatcherStatus, } from './watcher-types.js'; import type { InboundMailboxWatcherDeps, InboundMailboxWatcherStatus } from './watcher-types.js'; export declare class InboundMailboxWatcher { private readonly deps; private readonly settings; private readonly tracker; /** Escalates on failed connections; reset when a connection works. */ private readonly connectBackoff; /** Escalates on refused deliveries; reset when a drain completes. */ private readonly deliveryBackoff; private shutdown; private runner; private recheckWake; private cursor; private mode; private terminal; private authRetried; private connectionCount; private lastBodyProbe; /** Set when a verdict only a change can clear was reported this pass. */ private pendingRecheck; /** Unexpected throws in a row, with no completed drain in between. */ private localFailures; /** Drains ending in an unreadable answer in a row, with none completed in between. */ private unreadableDrains; constructor(deps: InboundMailboxWatcherDeps); get status(): InboundMailboxWatcherStatus; /** * Begin watching. Idempotent; a second call while running does nothing. * * The runner is cleared when the loop ends, whatever ended it. `status.running` * is read off this field, and a field that stays set after the loop returned * is the exact lie this round exists to remove: a dead watcher reporting that * it is running. */ start(): void; /** Stop watching and wait for the loop to unwind. Safe to call twice. */ stop(): Promise; /** * Re-probe now instead of at the next scheduled check. * * Called when configuration changed. An owner who has just fixed a password * should not wait out an hour-long timer to find out whether it worked, and * a restart to pick up a setting is a restart the daemon should not need. */ recheckNow(): void; /** Await the run loop. Test seam; production uses `start()` / `stop()`. */ whenSettled(): Promise; private run; /** * A throw that no reporting path claimed. * * Classified rather than logged: a full disk is a wait, an unwritable state * directory is a decision, and the two need different answers. Either way the * loop stays alive and the verdict stops saying healthy, the failure this * catch exists to prevent is not the throw, it is the silence after it. */ private handleUnexpectedFailure; /** * Wait out the capability re-check, when the last attempt ended in a verdict * only a change can clear. * * Deliberately outside the `finally` that closes the connection, and this * placement is the whole point of the method. An `insufficient` watcher does * not run, and one that sat on an open IMAP connection for an hour while * refusing to read from it would still be holding one of the provider's * simultaneous-connection slots. Gmail allows fifteen and `EmailService` * takes a fresh one per request, so spending one on a mailbox we have * already decided we cannot read is the same limit pressure with none of * the benefit, and it would make our own `server-unavailable` verdict more * likely on every other mailbox. */ private settleTerminal; /** * One connection's working life: reconcile the cursor, decide push or poll, * drain what arrived while we were away, then hold the chosen loop. */ private serve; private holdIdleLoop; private holdPollLoop; private drainOnce; /** * A drain finished the work it was for. Everything that counts consecutive * failures clears here, and ONLY here. * * One place rather than three, because these three counters answer the same * question, "has anything actually worked lately?", and three copies of * that answer is three chances for one of them to keep counting through a * mailbox that has been healthy for a week. The escalations they feed are * about CONSECUTIVE failures for exactly this reason: one bad minute a week * apart must never add up to a permanent verdict. * * `connectBackoff` belongs in this set and used to sit on the connection * instead. A completed drain is the only evidence that a connection is good * for the thing connections are for. */ private noteDrainCompleted; private drain; /** * A drain that did not complete. * * A refused FETCH means the mailbox opens and its contents are withheld, * arrival can be observed and never read, which is a capability verdict and * not something reconnecting fixes. A dead socket is a reconnect. A sink * that refused a message is neither: the message is still above the cursor * and will be handed over again, so this pauses on its own escalation rather * than re-labelling the connection. */ private handleDrainFailure; /** * A drain that stopped because the server's answer could not be read. * * Retried, and bounded. Retrying is right: the cursor is below the message, * the message is still in the mailbox, and one torn response on one socket * does clear. Bounding it is right for the opposite reason: the response * shape is the server's and the parser is ours, and neither changes between * attempts, so a batch that will not read has no next time in which to read. * * Between the two lies the failure this method exists to make impossible, * a mailbox retrying an unreadable batch forever, delivering nothing, * reporting `degraded`, and opening a connection every few hundred * milliseconds against a provider that counts them. The owner is told once, * with what could not be read, and the watcher stops rather than becoming * the outage. */ private handleUnreadableDrain; private handleOpenFailure; /** * Report a condition only a change can clear, and arm the wait for it. * * Three things happen and all three matter: the state goes `insufficient` * (so the watcher stops rather than looking armed), the observer is told * ONCE for this transition (so an hourly re-probe does not become an hourly * alarm), and the wait is interruptible (so `recheckNow()` after a config * change retries immediately). * * The waiting itself is left to `settleTerminal`, which runs after the * connection has been released. */ private reportTerminal; /** * Wait out the capability re-probe interval, or until somebody says the * configuration changed. * * Deliberately not a tight loop: re-probing a refused credential every * second is how an account gets locked, and re-probing it once an hour is * enough for "the owner fixed it and did not restart the daemon" to just work. */ private waitForRecheck; private pauseBeforeReconnect; private sleep; private requireCursor; private note; } //# sourceMappingURL=watcher.d.ts.map