/**
 * @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 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>;
    /** Persistent slow-lane operational connection per account. Used by sync,
     *  prefetch, outbox-append, large backfills, and any other "this might
     *  take a while" operation. */
    private opsClients;
    /** Lazy-allocated fast-lane client per account (C123). Lives alongside
     *  `opsClients` so click-time body fetches and flag toggles don't have
     *  to wait behind a multi-minute prefetch / backfill on the slow client.
     *  Created on the first fast-lane `withConnection` call; reused while
     *  alive; recreated on stale-detect or error-discard like opsClients.
     *  Costs +1 IMAP socket per active account, well under any reasonable
     *  per-user-IP cap (Dovecot's default is 20). */
    private fastClients;
    /** 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) a persistent connection for an account on the named
     *  lane. Two lanes today: `slow` (the original `opsClients` map — sync,
     *  prefetch, backfill, outbox) and `fast` (the C123 `fastClients` map —
     *  click-time body fetch, flag toggles, anything user-driven). Same
     *  stale-detect + reconnect semantics on both. logout() is wrapped as a
     *  no-op so legacy callers don't close the persistent client. */
    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 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 on each lane. Fast and slow lanes drain
     *  CONCURRENTLY — each on its own connection (C123). FIFO within each
     *  lane. The running flag per lane 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. */
    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;
    /** 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;
    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 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;
    /** 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;
    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;
    /** 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 prefetchFailures;
    private markPrefetchEmpty;
    private isPrefetchEmpty;
    /** 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>;
    /** 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;
    /** 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;
    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 OAuth accounts */
    syncAllContacts(): Promise<void>;
    /** Shut down all watchers and timers */
    shutdown(): Promise<void>;
}
//# sourceMappingURL=index.d.ts.map