/**
 * @bobfrankston/mailx-imap
 * Multi-account IMAP management wrapping iflow.
 * Syncs messages to local store, emits events for new mail.
 */
import type { TransportFactory } from "@bobfrankston/tcp-transport";
import { FileMessageStore, Store } from "@bobfrankston/mailx-store";
import type { AccountConfig, MessageEnvelope, Folder } from "@bobfrankston/mailx-types";
import { EventEmitter } from "node:events";
/** Events emitted by the IMAP manager */
export interface ImapManagerEvents {
    newMessage: (accountId: string, folderId: number, envelope: MessageEnvelope) => void;
    syncProgress: (accountId: string, phase: string, progress: number) => void;
    syncComplete: (accountId: string) => void;
    syncError: (accountId: string, error: string) => void;
    folderCountsChanged: (accountId: string, counts: Record<number, {
        total: number;
        unread: number;
    }>) => void;
    accountError: (accountId: string, error: string, hint: string, isOAuth: boolean) => void;
    configChanged: (filename: string) => void;
    /** Fired after a message body has been written to the local store — lets
     *  the UI flip a row's "not-downloaded" indicator without re-rendering. */
    bodyCached: (accountId: string, uid: number) => void;
    syncActionFailed: (accountId: string, action: string, uid: number, error: string) => void;
    /** Fired whenever the outbox queue depth or state changes (file added,
     *  file sent and removed, retry attempted). Lets the UI show a persistent
     *  queue-status indicator without polling. Aggregate status across all
     *  accounts is included so the listener doesn't have to reassemble it. */
    outboxStatus: (status: OutboxStatus) => void;
    /** Per-account health counter update. Fires when an inactivity timeout,
     *  connection-cap hit, or rate-limit wait happens. */
    diagnostics: (accountId: string, snapshot: AccountDiagnostics) => void;
}
/** Per-account diagnostic counters — tracks "something's wrong" events the
 *  user should be able to see without opening the log. */
export interface AccountDiagnostics {
    accountId: string;
    inactivityTimeouts: number;
    connCapHits: number;
    rateLimitWaits: number;
    lastTimeoutAt: number;
    lastCommand: string;
    lastError: string;
}
/** Per-account outbox queue breakdown, plus totals for the UI. */
export interface OutboxStatus {
    total: number;
    retrying: number;
    claimed: number;
    oldestAgeSec: number;
    maxAttempts: number;
    perAccount: Record<string, {
        total: number;
        retrying: number;
        claimed: number;
    }>;
}
export declare class ImapManager extends EventEmitter {
    private configs;
    private watchers;
    private watcherClients;
    /** Accounts whose IDLE watcher is mid-establish. `watchers` only gains an
     *  entry AFTER connect+login+SELECT+IDLE completes (seconds), so two
     *  overlapping startWatching() sweeps both see "no watcher" and both open
     *  a socket — a duplicate IDLE connection per account. Bob 2026-08-05:
     *  boot's startWatching was still in flight when the 60s deadman fired,
     *  giving two watchers each for bobma/gmail and pushing aol into
     *  "[LIMIT] LOGIN Rate limit hit". This set closes that window. */
    private watchStarting;
    /** Accounts whose IDLE watcher was refused for a CREDENTIAL reason, with
     *  the error text. Retrying those every 60s forever is pure waste: the
     *  outlook account has failed `AUTHENTICATE failed.` on every boot for
     *  weeks (its mail syncs over Graph, so nothing is lost — but each attempt
     *  burns a connection slot and buries the log). Suppressed until the
     *  account re-authenticates, which clears the entry. Transient refusals
     *  (rate limits, network) are NOT recorded here — they keep the normal
     *  60s deadman cadence. */
    private watchAuthFailed;
    /** Accounts the SERVER has told us to back off, with when they may be
     *  tried again and how many consecutive refusals we've had.
     *
     *  AOL/Yahoo answer an over-quota LOGIN with `[LIMIT] LOGIN Rate limit
     *  hit.` — an explicit instruction, not a transient blip. mailx used to
     *  treat it as transient and keep the 60 s deadman cadence, so every
     *  refusal was followed by another LOGIN a minute later: measured on
     *  2026-08-09, four separate runs of 4–22 minutes, one failed attempt per
     *  minute throughout, every one of them spending quota that keeps the
     *  limiter hot. Backing off is not politeness, it is how the account
     *  becomes usable again sooner. */
    private rateLimited;
    private fetchClients;
    /** The Store is the architectural nexus — owner of MailxDB +
     *  FileMessageStore + the event bus. This package (mailx-imap) is a
     *  *consumer* of the Store. `this.db` and `this.bodyStore` are getters
     *  onto Store-owned objects, kept as fields here for source-diff
     *  compatibility during the inversion (so the ~120 existing
     *  `this.db.X(...)` callsites don't all need touching at once). Future:
     *  every mutation moves to `this.store.X(...)` with bus-emit baked in;
     *  these fields shrink to read-only convenience refs. */
    private store;
    private get db();
    private get bodyStore();
    private syncIntervals;
    /** Track which accounts have already shown an error banner — only emit once per session */
    private accountErrorShown;
    private syncing;
    private inboxSyncing;
    /** Wall-clock of ImapManager construction. Used by the lazy-folder-sync
     *  gate so non-priority folders defer their first-sync until ~3 min
     *  past startup, when the event loop has quieted down. Each restart
     *  resets this; the gate auto-lifts as time passes. */
    private _startupAt;
    /** Use native IMAP client instead of imapflow. Set to true to enable. */
    useNativeClient: boolean;
    /** Per-account health counters. Incremented when the server misbehaves
     *  in ways that suggest a problem the user should know about (inactivity
     *  timeouts, connection-cap hits, rate-limit waits). Surfaced via a
     *  `diagnostics` event + `getDiagnostics` IPC so the UI can show a ⚠
     *  badge instead of burying the issue in the log. */
    private diagnostics;
    private getDiagnosticsEntry;
    /** Classify an error message and bump the relevant counter; emit the
     *  updated diagnostics snapshot. Call this from every catch in the sync
     *  paths so the UI can count "something's wrong" in real time. */
    private recordError;
    /** Public read for the IPC surface: snapshot of all account diagnostics. */
    getDiagnosticsSnapshot(): AccountDiagnostics[];
    private transportFactory;
    constructor(store: Store, transportFactory: TransportFactory);
    /** Get OAuth access token for an account (for SMTP auth) */
    getOAuthToken(accountId: string): Promise<string | null>;
    /** Accounts currently re-authenticating — all operations skip these */
    private reauthenticating;
    /** Last reauth attempt timestamp per account — prevents reauth loops (5 min cooldown) */
    private lastReauthAttempt;
    /** Force re-authentication for an OAuth account — deletes cached IMAP token, triggers browser consent */
    reauthenticate(accountId: string): Promise<boolean>;
    /** Delete a message directly on the IMAP server (for stuck outbox messages not in local DB) */
    deleteOnServer(accountId: string, folderPath: string, uid: number): Promise<void>;
    /** Search messages on the IMAP server — returns matching UIDs */
    searchOnServer(accountId: string, mailboxPath: string, criteria: any): Promise<number[]>;
    /** Server-side search that also materializes any UIDs we don't yet have
     *  locally. Returns the full result after upsert, so the caller can
     *  render hits that fall outside the history window. The fetch loop
     *  can be long for big hit-sets, so this runs on the slow lane and
     *  yields between chunks (each chunk is a separate withConnection)
     *  so an interactive body fetch can interleave. */
    searchAndFetchOnServer(accountId: string, folderId: number, mailboxPath: string, criteria: any): Promise<number[]>;
    /** Create a fresh IMAP client for an account (public access for API endpoints) */
    createPublicClient(accountId: string): Promise<any>;
    /** Create `fullPath` on the server, upsert the local folder row, re-sync
     *  the folder list, and report whether the folder actually exists after. */
    createFolderViaServer(accountId: string, fullPath: string, name: string, delimiter: string): Promise<boolean>;
    /** Rename `oldPath` → `newPath` on the server and re-sync folders. */
    renameFolderViaServer(accountId: string, oldPath: string, newPath: string): Promise<void>;
    /** Special-use roles a folder may NOT be renamed/reparented out of. Renaming
     *  INBOX/Sent/Drafts/Trash/Junk/Archive would break the special-folder
     *  mapping (compose's draft auto-save, the trash target, sent-copy append),
     *  and most servers reject it outright — so we refuse before touching the
     *  server. */
    private static readonly PROTECTED_ROLES;
    /** Rename and/or reparent a folder. Both operations are a single server-side
     *  mailbox rename (IMAP RENAME / Gmail label PATCH / Graph move+patch) —
     *  messages never move, no copy-and-delete. Mirrors moveMessage's plumbing:
     *  validate + branch API-vs-IMAP + commit locally.
     *
     *  - `newName` is the new LEAF name.
     *  - `newParentId` (optional) reparents under that folder; omit for an
     *    in-place rename. Reparenting and renaming share one path computation:
     *    newPath = (newParent ? newParent.path + delim : oldParentPrefix) + newName.
     *
     *  Guard rails (all throw a UI-surfaced error): protected special-use
     *  folders, empty/whitespace names, names containing the hierarchy
     *  delimiter, a destination that already exists, and reparenting a folder
     *  under itself or its own descendant (which would orphan the subtree). */
    renameFolder(accountId: string, folderId: number, newName: string, newParentId?: number): Promise<void>;
    /** Delete `path` on the server (tolerating already-gone). Local folder-row
     *  removal stays with the caller (it knows the folderId). */
    deleteFolderViaServer(accountId: string, path: string): Promise<void>;
    /** Expunge every message in `path` on the server. */
    emptyFolderViaServer(accountId: string, path: string): Promise<void>;
    /** Move a folder into Trash by IMAP RENAME, falling back to spilling its
     *  messages into Trash root + deleting the empty folder when the server
     *  forbids nesting under Trash. Self-contained on the worker. */
    moveFolderToTrashViaServer(accountId: string, folderId: number, folderPath: string, targetPath: string, trashId: number, trashPath: string, delim: string): Promise<void>;
    private spillFolderToTrashServer;
    /** Per-workload connection lanes. Each (lane, account) gets its OWN
     *  persistent IMAP connection so a slow op on one lane can't block another:
     *  a 90s `SELECT Sent` on "outbox" or a giant `UID FETCH` on "prefetch" no
     *  longer wedges INBOX sync/backfill on "sync". This is the root fix for
     *  the "whole sync worker goes silent" wedge (Bob 2026-06-20): before, all
     *  background work shared ONE slow connection and serialized behind the
     *  slowest command (a 90s SELECT Sent / a giant prefetch FETCH stalled
     *  INBOX backfill behind it). Lanes in use:
     *    fast     — interactive: body fetch on click, flag toggle, move
     *    slow     — folder sync + set-diff backfill (the `{slow:true}` default)
     *    prefetch — background body download (heaviest, longest)
     *    outbox   — send + sent-sweep + Outbox/Sent SELECT/APPEND
     *  Per account that's ≤4 persistent sockets + 1 IDLE, under Dovecot's 20.
     *  laneName → (accountId → persistent client). */
    private laneClients;
    /** Per-account, per-lane FIFO task queues. accountId → (laneName → queue).
     *  All lanes for an account drain concurrently (each on its own socket);
     *  within a lane, strictly sequential — no SELECT race. */
    private laneQueues;
    /** Per-host semaphore — caps simultaneous IMAP socket opens to one server.
     *  Defensive guardrail for the multi-account-on-one-host case (e.g. bobma +
     *  bobma2 both on imap.iecc.com). NOTE: the permit is held for the
     *  connection's LIFETIME (released on logout/destroy), not just the open —
     *  so this also bounds the number of live persistent connections per host.
     *  Each account now keeps up to 4 persistent lanes (fast/slow/prefetch/
     *  outbox) + 1 IDLE (IDLE bypasses the semaphore). 8 permits covers two
     *  accounts' lanes with headroom, still far under Dovecot's ~20/user+IP. */
    private hostSemaphores;
    private static readonly HOST_PERMITS;
    /** The (accountId → client) map for a lane, created on first use. */
    private laneClientMap;
    private getLaneClient;
    /** Backwards-compat shim — callers outside the queue still ask for the
     *  ops client by historical name. New code should prefer withConnection
     *  with `slow:true` for slow operations. */
    private getOpsClient;
    /** Run an operation against one of the account's per-workload lane
     *  connections. Tasks queue strictly sequentially WITHIN a lane (one IMAP
     *  command in flight, no SELECT race) but lanes run CONCURRENTLY, each on
     *  its own socket — so a slow op on one lane never blocks another.
     *
     *  Pick the lane via `opts.lane` (e.g. "sync", "prefetch", "outbox").
     *  Default is "fast" — interactive ops (body fetch on click, flag toggle,
     *  move). `slow: true` is a legacy alias for the "slow" misc lane; prefer
     *  a named lane so distinct workloads don't serialize behind each other.
     *
     *  The running task always finishes — IMAP can't preempt a command
     *  mid-flight; the wall-clock timeout below bounds how long that can be. */
    withConnection<T>(accountId: string, fn: (client: any) => Promise<T>, opts?: {
        slow?: boolean;
        lane?: string;
        timeoutMs?: number;
    }): Promise<T>;
    /** Drain the next queued task on EVERY lane for the account. Lanes run
     *  concurrently — each on its own connection — and the per-lane running
     *  flag prevents reentrant draining of the same lane. */
    private drainOpsQueue;
    /** Acquire one slot of the per-host connection semaphore. Returns a release
     *  function — call exactly once when the socket is closed. Used by
     *  newClient to cap simultaneous IMAP connections to a single server
     *  across all mailx accounts. */
    private acquireHostSlot;
    /** Open IMAP clients per account, used to trace who's opening sockets
     *  when we hit the Dovecot per-user+IP connection cap. */
    private openClients;
    /** Create a new IMAP client (internal — callers use getOpsClient or withConnection).
     *  Acquires one slot of the per-host semaphore before constructing the
     *  client; the slot is released when logout() or destroy() runs.
     *  `purpose` is a short tag printed alongside the `[conn+]` log so we can
     *  tell which code path (ops/idle/etc.) opened each connection. */
    /** Remaining cooldown in ms for an account the server rate-limited, or 0. */
    rateLimitRemaining(accountId: string): number;
    /** Record a server rate-limit refusal and widen the cooldown. 5 → 10 → 20
     *  → 30 min (capped): long enough to actually clear AOL's window, short
     *  enough that a mistake costs one poll cycle rather than an afternoon. */
    private noteRateLimited;
    /** Clear the cooldown after a connection succeeds. */
    private noteRateLimitCleared;
    private newClient;
    /** Force-close every IMAP socket for an account — both lane clients
     *  (ops + fast) plus any lingering ones in openClients (e.g. an IDLE
     *  watcher in flight). Used during account removal and disconnectOps
     *  so the server's connection slots free immediately rather than
     *  waiting for socket idle timeouts. */
    closeAllClients(accountId: string): Promise<void>;
    /** Disconnect the persistent operational connections (both lanes) for
     *  an account. */
    disconnectOps(accountId: string): Promise<void>;
    /** Legacy entry: returns the shared persistent ops client. Most callers
     *  should be using `withConnection()` instead — that gives proper
     *  queueing and lets fast operations jump ahead of slow ones. */
    createClientWithLimit(accountId: string): Promise<any>;
    /** Disposable fresh client — only used by the IDLE watcher, which holds
     *  its own socket so the fast/slow ops queue isn't blocked by IDLE
     *  parking the connection in a wait-for-server state. */
    private createClient;
    private trackLogout;
    /** Number of registered IMAP accounts */
    getAccountCount(): number;
    /** Register an account */
    addAccount(account: AccountConfig): Promise<void>;
    /** Check if an account uses Gmail (should use API instead of IMAP) */
    private isGmailAccount;
    /** Get a Gmail API provider for an account (reuses tokenProvider from IMAP config) */
    private getGmailProvider;
    /** Check if an account is Outlook.com / Microsoft 365 (should use Graph API
     *  instead of IMAP). Detection mirrors isGmailAccount: host or email domain.
     *  office365 IMAP front-ends and consumer outlook.com both route here. */
    private isOutlookAccount;
    /** True for any account that syncs via a REST provider (Gmail or Outlook)
     *  rather than IMAP. The branch points that used to ask isGmailAccount to
     *  mean "API, not IMAP" ask this instead. */
    private isApiAccount;
    /** Get a Microsoft Graph provider for an account (reuses the tokenProvider —
     *  which mints Graph-scoped tokens for Outlook accounts, see addAccount). */
    private getOutlookProvider;
    /** Get the right REST provider (Gmail or Outlook) for an API account.
     *  Both implement MailProvider plus the setFlags/trashMessage/moveMessage/
     *  fetchBodiesBatch surface the dispatcher uses, so callers stay generic. */
    private getApiProvider;
    /** Convert ProviderMessage to the shape expected by storeMessages/upsertMessage */
    private providerMsgToLocal;
    /** Sync folder list for an account */
    syncFolders(accountId: string, client?: any): Promise<Folder[]>;
    /** Store a batch of messages to DB immediately — used by onChunk for incremental sync */
    private storeMessages;
    /** Insert a local row for a message we just APPENDed (Sent / Drafts).
     *  Uses the source bytes we already have plus the UID returned by the
     *  server's APPENDUID response to build the local row directly — no
     *  IMAP SELECT/SEARCH/FETCH round-trip required. This is the user-
     *  visible fix for "I sent a message but it doesn't show up in mailx
     *  Sent until the broad sync finally completes" (which on slow servers
     *  can be minutes — and which Thunderbird sidesteps entirely because
     *  it inserts its own row at APPEND time too).
     *
     *  Fires the same emits as a normal sync so the UI updates. */
    insertLocalRowFromSource(accountId: string, folder: {
        id: number;
        path: string;
    }, uid: number, source: string, flags: string[]): Promise<void>;
    /** Sync messages for a specific folder */
    /** Per-(accountId,folderId) sync lock. Multiple paths can call syncFolder
     *  for the same folder concurrently — `syncOne` (full sync), `syncInbox`
     *  (5-min fast poll), `quickInboxCheckAccount` (startup quick check),
     *  `syncInboxNewOnly` (IDLE callback). Each path takes a different
     *  connection, so withConnection can't serialize them. The DB layer
     *  enforces "one transaction per connection" but doesn't notice the
     *  concurrent UID set being mutated underneath. Symptom: two
     *  `[sync-enter]` log lines for the same folder within ms (Bob 2026-05-08
     *  21:47:52), `cannot start a transaction within a transaction` SQLite
     *  errors, and prefetch SELECT/FETCH races when sync's SELECT runs
     *  between prefetch's SELECT and FETCH.
     *
     *  This per-folder mutex means the second concurrent caller waits
     *  rather than silently racing. The waiter still gets the lock when
     *  the first caller finishes — important for outbox flushes that
     *  expect their syncFolder for `Sent` to actually run. */
    private syncFolderLocks;
    /** Per-folder timestamp of the last set-difference reconcile. Drives the
     *  throttle for the QRESYNC-path gap backfill (see fetchServerOnlyUids /
     *  syncFolder QRESYNC branch). Empty at boot, so the first sync of each
     *  folder after a restart always runs a reconcile — which is exactly when
     *  a daemon-was-down gap needs to be healed. */
    private lastReconcileMs;
    /**
     * Set-difference backfill. Fetch every server UID (within the history
     * window) that we don't already have locally and aren't fetching this
     * cycle. This is the ONLY mechanism that recovers a message which is on
     * the server but missing locally — the high-water-mark fetch
     * (fetchMessagesSinceUid) cannot, because it only looks above the local
     * highest UID. Gaps it heals: a message that arrived while the daemon was
     * down (so it sits below the watermark once the next poll advances it), or
     * one dropped mid-stream by a response desync. Returns the recovered
     * messages (caller stores them) plus the server UID list it fetched, so the
     * caller's deletion-reconcile can reuse it instead of paying for a second
     * UID SEARCH round-trip.
     */
    private fetchServerOnlyUids;
    syncFolder(accountId: string, folderId: number, client?: any): Promise<number>;
    private _syncFolderImpl;
    /** Sync all folders for all accounts */
    syncAll(): Promise<void>;
    private _syncAll;
    /** Sync a single account — manages its own connection lifecycle */
    private syncAccount;
    /** Sync a Gmail or Outlook account via REST API — no IMAP connections */
    private syncAccountViaApi;
    /** Sync a single folder via Gmail/Outlook API */
    private syncFolderViaApi;
    /** Store API-fetched messages to DB */
    private storeApiMessages;
    /** Kill and recreate the persistent connections for an account — drops the
     *  client on every lane so the next task on each reconnects fresh. */
    private reconnectOps;
    /** Handle sync errors — classify and emit appropriate UI events.
     *  The connection-cap branch was removed: with the unified ops queue +
     *  per-host semaphore, mailx alone can't exceed the server cap. If the
     *  cap *is* hit, that means another client (Thunderbird, phone, sibling
     *  process) is holding slots — punishing mailx with a multi-minute
     *  blackout doesn't help the user, the next sync tick will retry. */
    private handleSyncError;
    /** Fetch ONLY new messages above highestUid for one account's INBOX —
     *  the IDLE callback's hot path. Skips gap detection, backfill, and the
     *  server reconcile (each of which fetches a full UID list — multi-second
     *  on a large mailbox). The 5-minute STATUS poll path still runs full
     *  `syncFolder` so deletions and gaps eventually reconcile. */
    syncInboxNewOnly(accountId: string): Promise<void>;
    /** Sync just INBOX for each account (fast check for new mail) */
    syncInbox(): Promise<void>;
    /** Quick inbox check — per-account lightweight probe.
     *  If the probe value changed since last time, triggers an inbox sync.
     *  The marker is only advanced after a successful sync so that a failed
     *  sync doesn't eat the "new mail" signal and make us stop retrying. */
    private lastInboxMarker;
    private quickCheckRunning;
    /** Shared quick-check skeleton: probe → compare → sync-if-changed → advance marker.
     *  `probe` returns the current marker value; `sync` runs only when it differs
     *  from the previously stored value. Marker is advanced only after sync resolves. */
    private quickCheck;
    /** Check a single account's inbox — uses its own connection, never blocked by sync */
    quickInboxCheckAccount(accountId: string): Promise<void>;
    /** Lightweight inbox refresh for a generic API account (Outlook). Unlike
     *  quickGmailCheck there's no cheap "top message id" probe — a single
     *  incremental syncFolderViaApi is itself cheap (Graph delta-ish page), so
     *  we just run it and recalc counts. */
    private quickApiCheck;
    private quickImapCheck;
    private quickGmailCheck;
    /** Check all accounts (used by legacy callers) */
    quickInboxCheck(): Promise<void>;
    /** Start periodic sync */
    startPeriodicSync(intervalMinutes: number): void;
    /** One-shot full sync + IDLE restart. Public so callers (Reconciler,
     *  user-initiated "Sync now") can fire it on demand. The original body
     *  that lived inline in startPeriodicSync was lifted verbatim. */
    runFullSync(): Promise<void>;
    /** Stop periodic sync */
    stopPeriodicSync(): void;
    /** Check if an account is OAuth (Gmail/Outlook — generous connection limits) */
    isOAuthAccount(accountId: string): boolean;
    /** Start an IMAP IDLE watcher per account on INBOX. Other folders are
     *  not special-cased here — sync runs uniformly on every folder via the
     *  periodic full-sync timer (STATUS-before-SELECT optimization is the
     *  right way to make that cheap, not piling per-folder IDLE sockets). */
    startWatching(): Promise<void>;
    /** Stop all IDLE watchers */
    stopWatching(): Promise<void>;
    /** Unlink the on-disk body file for a message by reading its `body_path`
     *  from the DB. Safe to call either before or after `db.deleteMessage`
     *  — read body_path first, store it, then unlink whenever. */
    /** Per-(account, folder, uid) deferred-delete timer. Reconcile populates
     *  this; the timer fires 60s later and re-checks whether the row at that
     *  exact (folder, uid) still exists. If yes → really delete (server
     *  expunged it). If no → skip (move-detect rebound it elsewhere; the
     *  row already moved to a new folder/uid in the same DB row, so the
     *  original key no longer matches anything). Net effect: server-side
     *  moves preserve UUID + body cache + flags. */
    private deferredDeletes;
    /** Grace window for reconcile-delete. Set to 30 minutes because the
     *  full-folder sync loop walks ~96 folders sequentially and each one
     *  takes 1-30 seconds; a server-side move from INBOX to a folder that
     *  syncs late (alphabetically far from INBOX) can take 10+ minutes
     *  before move-detect fires in the destination. 60s was way too short
     *  in production — the grace expired and rows were committed-deleted
     *  before _Spam / Sent / archive folders had a chance to rebind. */
    private static readonly RECONCILE_DELETE_GRACE_MS;
    private scheduleDeferredReconcileDelete;
    /** Delete local rows whose uid >= the server's UIDNEXT for this folder.
     *  Such a uid was NEVER assigned in this folder (UIDNEXT is the next one the
     *  server will ever hand out), so the row is a cross-wired phantom ("UID is
     *  not identity" corruption) — a real message stamped with the wrong
     *  folder_id. They break prefetch (the server returns 0 FETCH responses for
     *  uids it doesn't have → no body caches) and the normal set-diff reconcile
     *  refuses to remove them when they're >50% of the folder. Deleting by the
     *  UIDNEXT invariant is unconditional-safe: the uid is impossible, can't be a
     *  move target, and the message's real copy re-syncs from its true folder
     *  (the local store is a cache). Caller must have a TRUSTWORTHY uidNext
     *  (STATUS result) AND confirmed UIDVALIDITY is unchanged for this folder
     *  (full-sync and QRESYNC-success paths both satisfy that). Returns the count. */
    private sweepPhantomRows;
    private unlinkBodyFile;
    /** Fetch a single message body on demand, caching in the store.
     *
     *  Cache lookup is folder-agnostic: when a UID exists in multiple folders
     *  (Gmail labels, copy-instead-of-move) the prefetcher may have populated
     *  body_path on only one row. Looking up by (account, uid) without the
     *  folder filter finds the cached `.eml` regardless of which folder
     *  context the UI passed.
     *
     *  Server fetch goes through the unified ops queue on the fast lane —
     *  the user clicked, they're waiting, this jumps ahead of any background
     *  prefetch sitting in the slow lane. */
    fetchMessageBody(accountId: string, folderId: number, uid: number, force?: boolean): Promise<Buffer | null>;
    /** Fetch message body via Gmail/Outlook API.
     *  Throws `MessageNotFoundError` when the server says the message is gone
     *  (deleted from another device, for example). The caller uses that to
     *  delete the stale row locally instead of showing a generic error. */
    private fetchMessageBodyViaApi;
    /** Background body prefetch — download bodies for messages that don't have them.
     *  Server-side deletions (isNotFound) aren't errors here: we delete the
     *  stale row locally and keep going. Only unrelated errors (network,
     *  auth, rate limits) count against the error budget, and the budget is
     *  generous so a few transient failures don't kill the whole run. */
    /** Guard against concurrent prefetchBodies for the same account — mirror of
     *  `sendingAccounts`. Without this, every periodic-sync tick spawns a new
     *  prefetch session alongside any still in flight, blowing through Gmail's
     *  per-minute quota and racing on disk writes. One prefetch per account. */
    private prefetchingAccounts;
    /** Per-folder error cooldowns — `accountId:folderPath` → [timestamps].
     *  Used to skip folders that repeatedly time out (Dovecot on slow shared
     *  hosting SELECTs big `_Spam` / archive folders at 300s+ latency; the
     *  prefetch error budget was burning out on a handful of bad folders
     *  before the INBOX could finish). A folder with 2+ errors in the last
     *  15 minutes is skipped until the cooldown passes. User-reported via
     *  log analysis 2026-04-23: bobma prefetch timing out on Added2.organizations,
     *  Added2.technews, _Spam, "Prefirst.Jerry's Retreat", Added2.zines. */
    private folderErrorCooldown;
    private readonly FOLDER_ERROR_WINDOW_MS;
    private readonly FOLDER_ERROR_THRESHOLD;
    private shouldSkipFolder;
    private recordFolderError;
    private clearFolderErrors;
    /** UIDs that returned 0 bodies on a successful batch — either stale DB rows
     *  whose UIDVALIDITY has rotated, or a transient iflow parser miss that
     *  dropped the body literal mid-stream. The "NOT pruning" safety guard in
     *  `_prefetchBodies` refuses to delete them so a transient miss can't
     *  trigger data loss; the trade-off is they sit in `getMessagesWithoutBody`
     *  and would otherwise dominate the size-asc query, starving live folders.
     *
     *  Backoff schedule on repeated failure: 5 min → 30 min → 2 h → 12 h.
     *  Without a TTL the very first transient miss permanently blocklists the
     *  UID for the session and the row never shows the cached-body dot
     *  (the symptom Bob reported 2026-05-19: document/large messages with
     *  no teal mark). With TTL, transient misses self-heal in 5 minutes;
     *  genuine zombies back off to once-per-12-hour retries — bounded load.
     *  Key shape `${accountId}:${folderId}:${uid}`. */
    private markPrefetchEmpty;
    private isPrefetchEmpty;
    /** Clear a UID's prefetch-failure record after a successful fetch, so a
     *  message that healed (e.g. a transient iflow protocol desync that
     *  resolved on retry) doesn't carry a stale backoff count into the
     *  future. Without this, a once-failed-then-succeeded UID keeps its
     *  count and would back off longer than warranted if it ever re-enters
     *  the candidate set. */
    private clearPrefetchEmpty;
    /** Background body-cache backfill. Public so the Reconciler can schedule
     *  the periodic tick under its priority/back-pressure rules; existing
     *  in-method post-sync nudges (sync, fetchSince, fetchOne) call this
     *  too and the per-account `prefetchingAccounts` set deduplicates. */
    prefetchBodies(accountId: string): Promise<void>;
    private _prefetchBodies;
    /** Get the body store for direct access */
    getBodyStore(): FileMessageStore;
    /** Re-parse existing .eml files from disk to backfill has_attachments
     *  + preview for messages whose body landed before the prefetch-reparse
     *  fix shipped. No IMAP traffic — pure local-disk work. Throttled by the
     *  reconciler the same way as prefetch (back off when interactive lane
     *  has clicks pending) so it doesn't fight with the user. */
    private backfillingAccounts;
    backfillBodyMeta(accountId: string, limit?: number): Promise<void>;
    /** Move message across accounts using iflow's moveMessageToServer */
    moveMessageCrossAccount(fromAccountId: string, uid: number, fromFolderId: number, toAccountId: string, toFolderId: number): Promise<void>;
    /** Process pending sync actions for an account */
    processSyncActions(accountId: string): Promise<void>;
    /** Find a folder by specialUse, case-insensitive */
    private findFolder;
    /** Copy sent message to the Sent folder via IMAP APPEND */
    copyToSent(accountId: string, rawMessage: string | Buffer): Promise<void>;
    /** Save a draft to the Drafts folder via IMAP APPEND.
     *  Returns the UID of the saved draft (for replacing on next save). */
    saveDraft(accountId: string, rawMessage: string | Buffer, previousDraftUid?: number, draftId?: string): Promise<number | null>;
    /** Server check for a NEWER copy of a draft than the one just opened —
     *  same X-Mailx-Draft-ID, higher UID in the Drafts folder. That's the
     *  signature of an edit made on another machine that local sync hasn't
     *  pulled yet (each save deletes-and-reappends, so newer edits always
     *  have higher UIDs; this machine's own last save IS the opened row, so
     *  uid > sinceUid can only be foreign). Read-only — never mutates. */
    findNewerDraft(accountId: string, draftId: string, sinceUid: number): Promise<{
        uid: number;
        folderId: number;
    } | null>;
    /** Delete a draft (or all drafts with a stable X-Mailx-Draft-ID) after successful send.
     *  Tries the specific UID first, then falls back to searchByHeader so orphaned copies
     *  from earlier failed autosaves are cleaned up at the same time. */
    deleteDraft(accountId: string, draftUid: number, draftId?: string): Promise<void>;
    /** Queue outgoing message locally — never fails, worker handles IMAP+SMTP.
     *  Single path: write `~/.mailx/outbox/<acct>/*.ltr` synchronously, then
     *  kick processLocalQueue. The file IS the queue — durable across crashes,
     *  visible in the filesystem, consumed by the existing outbox worker that
     *  handles both IMAP-APPEND (non-Gmail) and direct SMTP (Gmail). The old
     *  sync_actions "send" branch was removed because it duplicated the same
     *  work and risked double-send when both paths fired on the same message. */
    queueOutgoingLocal(accountId: string, rawMessage: string): void;
    /** Scan the local outbox + sending/queued dirs and return counts + age.
     *  Cheap — a handful of readdir + head-read per file. Called by both the
     *  polling UI (status bar) and emitted as an event after queue mutations. */
    getOutboxStatus(): OutboxStatus;
    /** Emit outboxStatus now. Call after any queue mutation. */
    private emitOutboxStatus;
    /** Guard against concurrent processSendActions for the same account */
    private sendingAccounts;
    /** Process local send actions — APPEND to Outbox, which the outbox worker then sends */
    private processSendActions;
    private _processSendActions;
    private outboxInterval;
    private readonly hostname;
    /** Ensure Outbox folder exists, create if needed */
    private ensureOutbox;
    /** Save a copy of outgoing mail — label is a subdirectory (editing/queued/sent) */
    private saveSendingCopy;
    /** Drop a requeue.cmd / requeue.sh into a sending/<acct>/<label>/ directory
     *  so the user can re-queue a saved .eml/.ltr without remembering the
     *  rmfmail -send syntax. Idempotent: only writes when missing.
     *  The word is "requeue" — it puts the file back into the outbox queue;
     *  it does NOT send a fresh copy bypassing the queue. */
    private ensureRequeueScripts;
    /** Walk the sending tree at startup and drop a requeue.cmd/.sh into every
     *  existing sending/<acct>/<label>/ directory. Without this the scripts
     *  only appear on the next outgoing message, which doesn't help a user
     *  who needs to re-queue a stuck letter from before the upgrade. */
    private seedRequeueScripts;
    /** Queue a message for sending. Tries IMAP Outbox, falls back to local file. */
    queueOutgoing(accountId: string, rawMessage: string | Buffer): Promise<void>;
    /** Process local file queue — scan both outbox/ (IMAP-unreachable fallback)
     *  and sending/<acct>/queued/ (manual drop-in, crash recovery). The earlier
     *  double-send bug was caused by queueOutgoing() WRITING a debug copy to
     *  sending/queued/ on every send — that write is gone now, so scanning the
     *  directory is safe again. Any legitimate files that land there (crash
     *  recovery, manual drop) will get sent. */
    /** Recover stale `.sending-<host>-<pid>` claims for an account back to
     *  plain `.ltr` so the next tick can retry. A claim is stale if:
     *   (a) the PID is dead — original owner crashed/was replaced mid-send;
     *   (b) the PID is alive but it's not us and the file mtime is older than
     *       STALE_CLAIM_MS — `process.kill(pid,0)` only proves *some* process
     *       owns that PID, not our long-dead daemon (OS recycles PIDs); the
     *       age guard stops an unrelated Node (statusline/msger/npm) inheriting
     *       a recycled PID from pinning the claim forever (Bob's 7-hour stuck
     *       `.sending-rmf39-63196`);
     *   (c) it's our PID — never sweep our own live claim.
     *  Foreign hosts are left alone (we can't probe their PIDs); cross-host
     *  recovery is the IMAP-folder sweeper's job.
     *
     *  CRITICAL: this is a local filesystem op with NO network I/O, so it MUST
     *  run independently of send-backoff. It used to live inside
     *  processLocalQueue, which the outbox worker SKIPS when an account is in
     *  backoff — so a persistently-erroring account (e.g. Dovecot "Not
     *  connected" storms) never recovered its claims, leaving a message stuck
     *  "sending…" forever while healthy accounts drained. Bob 2026-05-31:
     *  "stuck while others are getting sent." Now called every tick for every
     *  account before the backoff gate. */
    private recoverStaleClaims;
    private processLocalQueue;
    /** Send a raw RFC 2822 message for an account, choosing the transport by
     *  provider. Outlook accounts have a Graph-scoped OAuth token (no SMTP
     *  audience), so they send via Graph POST /sendMail; Gmail and IMAP
     *  accounts send via SMTP (Gmail's mail.google.com token covers XOAUTH2
     *  SMTP). Graph /sendMail auto-files a copy in Sent, so no append needed. */
    private sendRawForAccount;
    /** Send a raw RFC 2822 message via SMTP for a given account.
     *  Uses @bobfrankston/smtp-direct with the same TransportFactory as IMAP —
     *  same TCP byte-stream interface, no nodemailer dependency. */
    private sendRawViaSMTP;
    /** Process Outbox — send pending messages with flag-based interlock.
     *  Each per-UID step is its own withConnection({slow}) call so the queue
     *  yields between messages: a click-to-view body in the middle of a
     *  10-message outbox drain doesn't wait for all 10 to finish. */
    /** Per-account re-entrancy guard for processOutbox. The worker tick and
     *  the chained post-APPEND call (processLocalQueue) can overlap; on
     *  servers without custom-keyword support ($Sending interlock) two
     *  concurrent drains could both fetch and SMTP the same Outbox UID —
     *  a double-send. One drain per account at a time; latecomers no-op
     *  (the 10s tick retries anything they would have handled). */
    private outboxDrainInFlight;
    processOutbox(accountId: string): Promise<void>;
    private _processOutbox;
    /** Start background Outbox worker — runs immediately then every 10 seconds */
    private outboxBackoff;
    private outboxBackoffDelay;
    /** Route a file dropped into the general `~/.rmfmail/outbox/` (no acct
     *  subdir) to one of the configured accounts. Match order:
     *   1. `From:` address matches an `account.email` exactly (case-insensitive).
     *   2. `From:` domain matches a known-provider domain → first account whose
     *      domain matches (gmail.com / googlemail.com → first Gmail account;
     *      outlook.com / hotmail.com / live.com → first Outlook account).
     *   3. Default = first account with an explicit IMAP override host (i.e.,
     *      not one of the known providers). Bob's bobma is the prototype.
     *  Returns the routed accountId or null if no candidate exists. */
    private routeGeneralOutboxFile;
    /** Scan the general outbox (`~/.rmfmail/outbox/*.ltr|*.eml` — no acct
     *  subdir) and route each file into the appropriate per-account dir. Runs
     *  once per outboxLoop tick before the per-account sweep, so a file
     *  manually dropped at the root gets handed off the same tick. */
    private routeGeneralOutbox;
    startOutboxWorker(): void;
    /** Stop Outbox worker */
    stopOutboxWorker(): void;
    /** Periodic Sent sweep — reconciles optimistic local-inserts against
     *  the server's actual Sent folder for every IMAP-path account.
     *
     *  Why: when a message is sent on an IMAP account, the local DB row is
     *  inserted with a PREDICTED UID (the Sent folder's UIDNEXT at submit
     *  time). The submission server's drain-Outbox sieve appends the real
     *  Sent copy at whatever UID it chooses, which may differ if anything
     *  else raced — another client, a sieve append from a different rule,
     *  or just the sieve picking its own UID. Without reconciliation:
     *    - On the next set-diff sync the local row's UID doesn't match
     *      any server UID → set-diff deletes it → "Sent copy missing".
     *    - Or the server's actual Sent copy gets reconciled as a NEW row
     *      → duplicate in the user's Sent view.
     *
     *  What this sweep does:
     *    For each IMAP-path account, walk local Sent rows newer than
     *    SENT_SWEEP_WINDOW_MS, query the server's Sent folder by
     *    Message-ID, and:
     *      - if found at the local-known UID → no-op
     *      - if found at a different UID → rebind the local row to the
     *        real UID
     *      - if not found and the .eml is on disk → IMAP APPEND it
     *        from disk (no SMTP)
     *      - if not found and no .eml → leave it (set-diff will GC; the
     *        user has bigger problems than the Sent reconcile)
     *
     *  Gmail-path accounts: skipped. Gmail's Sent is API-driven, the
     *  optimistic-insert path doesn't apply (gmail-api writes the row
     *  directly post-send with the real Gmail message ID, no UID race). */
    private sentSweepInterval;
    private readonly SENT_SWEEP_WINDOW_MS;
    private readonly SENT_SWEEP_INTERVAL_MS;
    startSentSweep(): void;
    stopSentSweep(): void;
    /** Message-IDs sweepSentOnce has already re-APPENDed this daemon run.
     *  HARD GUARD against perpetual motion: a re-appended copy syncs back in,
     *  refreshes the row's cached_at, and keeps it inside the sweep window —
     *  so if the header search ever reports "missing" again (it failed
     *  silently for ~13 days on a flaky lane connection, 2026-07-02, 3,704
     *  copies of one message), the sweep would append forever. One re-append
     *  per Message-ID per daemon lifetime; a genuinely-lost message still
     *  gets its one repair, and a false "missing" can no longer loop. */
    private sentSweepAppended;
    private sweepSentOnce;
    private configWatchers;
    private cloudPollTimers;
    /** Watch the local config files for external changes. On change, emit
     *  configChanged so the UI can show a "restart to apply" banner. Uses
     *  a debounce to coalesce rapid writes from save tools. */
    watchConfigFiles(): void;
    /** Stop all config file watchers */
    stopWatchingConfig(): void;
    /** Per-account in-flight guard so concurrent calls (startup + periodic
     *  timer + manual button) share one round-trip instead of stacking. */
    private contactsSyncing;
    /** Get an OAuth token for Google APIs (contacts, calendar, etc.)
     *  Uses the SAME token as IMAP — scopes are combined in one grant */
    private getContactsToken;
    /** Sync contacts from Google People API. Incremental: persists
     *  `nextSyncToken` per account in the kv table (`scope='contacts'`,
     *  `key=accountId`) so subsequent calls only fetch changed/deleted rows.
     *  First-ever call passes `requestSyncToken=true` so Google returns a
     *  token to use next time; without that, the first response has no
     *  `nextSyncToken` and incremental never kicks in. Returns the number of
     *  contacts added or removed in this run. */
    syncGoogleContacts(accountId: string): Promise<number>;
    private syncGoogleContactsImpl;
    /** Sync contacts for all GOOGLE OAuth accounts. The People API only
     *  accepts Google tokens — feeding it an Outlook/AOL OAuth token just
     *  logged `API error for outlook: 401` on every periodic tick. Outlook
     *  contacts belong to the Graph provider (blocked on Azure app reg). */
    syncAllContacts(): Promise<void>;
    /** Shut down all watchers and timers */
    shutdown(): Promise<void>;
}
//# sourceMappingURL=index.d.ts.map