/**
 * @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 { MailxDB, FileMessageStore } from "@bobfrankston/mailx-store";
import type { AccountConfig, MessageEnvelope, EmailAddress, 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 fetchClients;
    private db;
    private bodyStore;
    private syncIntervals;
    /** Track which accounts have already shown an error banner — only emit once per session */
    private accountErrorShown;
    private syncing;
    private inboxSyncing;
    /** 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(db: MailxDB, 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>;
    /** Persistent operational connections — one per account, reused for all operations.
     *  Body fetch, sync, prefetch, outbox-append, flag/move all serialize through
     *  this single client per account via withConnection(). The priority lane
     *  in the queue lets interactive clicks jump ahead of background prefetch. */
    private opsClients;
    /** Two-lane operation queue per account — interactive ops (body fetch on
     *  click, flag toggle) drain before background ops (sync, prefetch). FIFO
     *  within each lane. The single ops connection means there's never a race
     *  on which folder is SELECTed; commands run strictly sequentially. */
    private opsQueues;
    /** Per-host semaphore — caps simultaneous IMAP socket opens to one server.
     *  Defensive guardrail: with the single-ops-per-account model an individual
     *  user's mailx never hits more than (#accounts × 2) sockets per host, well
     *  under any reasonable server cap. Exists for the multi-account-on-one-host
     *  case (e.g. bobma + bobma2 both on imap.iecc.com). */
    private hostSemaphores;
    private static readonly HOST_PERMITS;
    /** Get (or create) the persistent operational connection for an account.
     *  logout() is wrapped as a no-op so legacy callers don't close it. */
    private getOpsClient;
    /** Run an operation on the account's connection — queued, sequential, no concurrency */
    /** Run an operation against the account's single ops connection. Tasks
     *  queue strictly sequentially per account — only one IMAP command in
     *  flight at a time. This eliminates the SELECT-races and "stale client
     *  recovery" paths the old multi-client design needed.
     *
     *  Default lane is `fast` — covers virtually everything (body fetch,
     *  flag toggle, move, incremental sync). Pass `slow: true` only for
     *  operations the caller knows will take a long time and shouldn't
     *  block the user (multi-folder prefetch batches, large backfills).
     *  When both lanes have tasks, fast drains first.
     *
     *  Within a lane, FIFO. The running task always finishes — IMAP can't
     *  preempt a command mid-flight. */
    withConnection<T>(accountId: string, fn: (client: any) => Promise<T>, opts?: {
        slow?: boolean;
        timeoutMs?: number;
    }): Promise<T>;
    /** Run the next queued task. Fast lane drains before slow.
     *  Idempotent — safe to call after each task completes; the running
     *  flag prevents reentrant draining. */
    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. */
    private newClient;
    /** Force-close every IMAP socket for an account — ops + 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 connection 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;
    /** 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;
    /** Sync messages for a specific folder */
    syncFolder(accountId: string, folderId: number, client?: any): Promise<number>;
    /** 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 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 ops connection */
    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>;
    private quickImapCheck;
    private quickGmailCheck;
    /** Check all accounts (used by legacy callers) */
    quickInboxCheck(): Promise<void>;
    /** Start periodic sync */
    startPeriodicSync(intervalMinutes: number): void;
    /** Stop periodic sync */
    stopPeriodicSync(): void;
    /** Check if an account is OAuth (Gmail/Outlook — generous connection limits) */
    isOAuthAccount(accountId: string): boolean;
    /** Start IMAP IDLE watchers for INBOX on each account */
    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. */
    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): 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;
    private prefetchBodies;
    private _prefetchBodies;
    /** Get the body store for direct access */
    getBodyStore(): FileMessageStore;
    /** Bulk trash messages — local-first, single IMAP connection for all */
    trashMessages(accountId: string, messages: {
        uid: number;
        folderId: number;
    }[]): Promise<void>;
    /** Bulk move messages — queues the IMAP action only. The service layer
     *  (MailxService.moveMessages) owns the local DB mutation via
     *  updateMessageFolder; this method used to ALSO deleteMessage here,
     *  which wiped the row the service just updated — the message vanished
     *  on the next reconcile and "spam folder empty" was the symptom. */
    moveMessages(accountId: string, messages: {
        uid: number;
        folderId: number;
    }[], targetFolderId: number): Promise<void>;
    /** Debounced sync actions — batches rapid local changes into one IMAP operation */
    private syncActionTimers;
    private debounceSyncActions;
    /** Move a message to Trash (delete) — local-first, queues IMAP sync */
    trashMessage(accountId: string, folderId: number, uid: number): Promise<void>;
    /** Move a message between folders — queues IMAP sync only. Service
     *  layer owns the local DB update (see MailxService.moveMessage). */
    moveMessage(accountId: string, uid: number, fromFolderId: number, toFolderId: number): Promise<void>;
    /** Move message across accounts using iflow's moveMessageToServer */
    moveMessageCrossAccount(fromAccountId: string, uid: number, fromFolderId: number, toAccountId: string, toFolderId: number): Promise<void>;
    /** Undelete — move from Trash back to original folder */
    undeleteMessage(accountId: string, uid: number, originalFolderId: number): Promise<void>;
    /** Update flags — local-first, queues IMAP sync */
    updateFlagsLocal(accountId: string, uid: number, folderId: number, flags: string[]): Promise<void>;
    /** Process pending sync actions for an account */
    processSyncActions(accountId: string): Promise<void>;
    /** Find a folder by specialUse, case-insensitive */
    private findFolder;
    /** Optimistic local-first Sent insert: write a row into the local DB's
     *  Sent folder the moment the user hits Send, so the list reflects it
     *  immediately instead of waiting for SMTP + IMAP-APPEND + syncFolder
     *  (five server round-trips against a Dovecot that caps at 20 conns).
     *
     *  Uses a synthetic negative UID so it can't collide with a real APPENDUID
     *  (which is always positive). When the real sync eventually picks the
     *  message up in Sent with the server's UID, `db.upsertMessage` spots
     *  the Message-ID match and rebinds the existing row's UID — no duplicate.
     *  Negative UID also makes the row render pink (getMessages flags uid<0
     *  as pending) so the user sees it's not-yet-reconciled.
     *
     *  Best-effort — any failure path (no Sent folder yet, parse error, store
     *  write error) is logged and swallowed; the send itself is unaffected. */
    insertOptimisticSentRow(accountId: string, envelope: {
        messageId: string;
        inReplyTo: string;
        references: string[];
        subject: string;
        from: EmailAddress;
        to: EmailAddress[];
        cc: EmailAddress[];
        bcc: EmailAddress[];
        date: number;
    }, rawMessage: string): Promise<void>;
    /** 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>;
    /** 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;
    /** 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. */
    private processLocalQueue;
    /** 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. */
    processOutbox(accountId: string): Promise<void>;
    /** Start background Outbox worker — runs immediately then every 10 seconds */
    private outboxBackoff;
    private outboxBackoffDelay;
    startOutboxWorker(): void;
    /** Stop Outbox worker */
    stopOutboxWorker(): void;
    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 OAuth accounts */
    syncAllContacts(): Promise<void>;
    /** Shut down all watchers and timers */
    shutdown(): Promise<void>;
}
//# sourceMappingURL=index.d.ts.map