/**
 * @bobfrankston/mailx-service
 * Pure business logic — no HTTP, no Express.
 * Both the Express API (mailx-api) and the Android bridge call these functions.
 */
import { ImapManager } from "@bobfrankston/mailx-imap";
import { Store } from "@bobfrankston/mailx-store";
export { spawnSyncWorker, type SpawnedSyncWorker } from "./sync-worker-client.js";
import type { Folder, AutocompleteRequest, AutocompleteResponse, AutocompleteSettings, AiTransformRequest, AiTransformResponse, MailxApi } from "@bobfrankston/mailx-types";
interface ReputationResult {
    flagged: boolean;
    listedCount: number;
    checkedCount: number;
    sources: Array<{
        service: string;
        flagged: boolean;
        verdict: string;
    }>;
    verdict: string;
    service: string;
}
/** Injected popup function — wraps mailx-host's showMessageBox so the
 *  service can pop OS-level always-on-top reminder windows without
 *  itself depending on mailx-host (keeps the host abstraction clean).
 *  Set via setPopupFn in bin/mailx.ts after construction. Returns the
 *  label of the button the user clicked, or "closed" / "dismissed" /
 *  "" when the window was closed without picking one. */
export type PopupFn = (opts: any) => Promise<{
    button?: string;
    closed?: boolean;
    dismissed?: boolean;
}>;
/** Injected popout-window function — bin/mailx.ts wraps mailx-host's
 *  showMessageBoxEx pointed at the loopback popout server, so "open this
 *  message in its own OS window" spawns a native msger WebView window
 *  instead of falling back to the OS browser. Injection (not import)
 *  keeps mailx-service host-agnostic, same as PopupFn. */
export type PopoutWindowFn = (opts: {
    accountId: string;
    uid: number;
    folderId?: number;
    subject?: string;
}) => Promise<{
    ok: boolean;
    reason?: string;
}>;
/** Injected compose-popout functions — bin/mailx.ts wraps mailx-host's
 *  showMessageBoxEx pointed at the popout server's static compose page, so
 *  "take this compose into its own OS window" spawns a native msger window.
 *  `open` receives the stash id the page will consume; `close` tears the
 *  window down after send/discard. Injection keeps mailx-service host-agnostic. */
export type ComposePopoutFns = {
    open: (opts: {
        id: string;
        subject?: string;
    }) => Promise<{
        ok: boolean;
        reason?: string;
    }>;
    close: (id: string) => void;
};
export declare class MailxService implements MailxApi {
    /** The Store is the nexus — owns DB + .eml files + bus + operations.
     *  MailxService is the IPC adapter on top: it routes UI requests to
     *  Store reads/writes and to the SyncQueue/Reconciler for server-
     *  mirror work. The raw `db` getter below is a transitional shim so
     *  the ~50 `this.db.X(...)` callsites in this file don't all need
     *  touching at once; all writes should migrate to `this.localStore.X(...)`. */
    private store;
    private imapManager;
    private _accountsCache;
    private _contactsCorpusSeeded;
    private _contactsRetries;
    private _settingsCache;
    private popupFn;
    private popoutWindowFn;
    /** Inject the popup implementation. Called once at startup from
     *  bin/mailx.ts with mailx-host's showMessageBox. Without this,
     *  showReminderPopup returns a "no host" reason. */
    setPopupFn(fn: PopupFn): void;
    /** Inject the popout-window implementation (native msger window per
     *  message). Without this, popoutWindow returns a "no host" reason and
     *  the client falls back to its in-window overlay. */
    setPopoutWindowFn(fn: PopoutWindowFn): void;
    private mailtoRegisterFn;
    /** Inject the OS mailto:-handler registration (Settings → "Make rmfmail
     *  the default email app"). The registry/xdg work lives in bin/mailx.ts —
     *  platform-specific code stays out of this package. */
    setMailtoRegisterFn(fn: () => Promise<{
        ok: boolean;
        message: string;
    }> | {
        ok: boolean;
        message: string;
    }): void;
    registerMailto(): Promise<{
        ok: boolean;
        message: string;
    }>;
    /** Open a message in its own native OS window (msger WebView). The
     *  client's 🗗 button lands here via jsonrpc. */
    popoutWindow(accountId: string, uid: number, folderId?: number, subject?: string): Promise<{
        ok: boolean;
        reason?: string;
    }>;
    private composePopoutFns;
    /** Compose-popout init stash, keyed by the id embedded in the popout
     *  window's URL. One-shot: consumePopoutComposeInit deletes on read. */
    private composePopoutInits;
    setComposePopoutFns(fns: ComposePopoutFns): void;
    /** Take a live compose into its own native OS window. The overlay hands
     *  us its full state (fields, body, attachments, draft identity); we
     *  stash it under a one-shot id and spawn the window pointed at the
     *  popout server's compose page with that id in the URL. */
    popoutCompose(init: any): Promise<{
        ok: boolean;
        reason?: string;
    }>;
    /** One-shot read of a stashed compose-popout init (popout page boot). */
    consumePopoutComposeInit(id: string): {
        init: any | null;
    };
    /** Fetch a remote image server-side and return it as a data: URI. Used
     *  by compose paste: clipboard HTML often references remote images that
     *  the editor cannot load (hotlink protection, blocked remote content,
     *  auth cookies) — Bob 2026-07-12 pasted a GIF and got the broken-image
     *  alt text. Inlining also makes the outgoing mail self-contained. */
    fetchImageAsDataUri(url: string): Promise<{
        dataUri?: string;
        error?: string;
    }>;
    /** Close a compose-popout window (after send / discard in the popout). */
    closeComposePopout(id: string): {
        ok: boolean;
    };
    private mainPopoutFn;
    setMainPopoutFn(fn: (target?: {
        accountId?: string;
        folderId?: number;
        name?: string;
    }) => Promise<{
        ok: boolean;
        reason?: string;
    }>): void;
    /** Open ANOTHER full mailx window (View → New window) — e.g. Drafts in
     *  one OS window, Sent in another. Served by the popout server with the
     *  HTTP bridge, so all request/response IPC works; push events do not
     *  reach secondary windows (no live new-mail update — known limit). */
    popoutMainWindow(target?: {
        accountId?: string;
        folderId?: number;
        name?: string;
    }): Promise<{
        ok: boolean;
        reason?: string;
    }>;
    /** Local-first read/write facade. Every UI IPC handler that touches the
     *  local DB or body store goes through this — no awaiting IMAP, no
     *  awaiting Gmail API, no awaiting SMTP. See docs/local-first-plan.md. */
    private localStore;
    /** Phase 0 read isolation (docs/clean-architecture.md §5). The DB read
     *  surface runs on its OWN thread with its own read-only SQLite handle, so
     *  UI list/search/open reads can NEVER be head-of-line-blocked behind a
     *  synchronous sync write or a hung IMAP op on the main event loop. Null
     *  until the worker has finished its init handshake (a few ms at startup)
     *  and null forever if spawning failed — in both cases reads fall back to
     *  the in-process `localStore`, same code, same answers. `readReady`
     *  resolves once the spawn attempt settles (either outcome). */
    private dbWorker;
    private readReady;
    /** Persistent (and in-memory body-fetch) queue. UI handlers commit
     *  locally, then enqueue a server-mirror task here. */
    private syncQueue;
    /** Background loop: drains body-fetch tasks, retries failed message
     *  actions, emits sync-state events for the status pill. */
    private reconciler;
    constructor(
    /** The Store is the nexus — owns DB + .eml files + bus + operations.
     *  MailxService is the IPC adapter on top: it routes UI requests to
     *  Store reads/writes and to the SyncQueue/Reconciler for server-
     *  mirror work. The raw `db` getter below is a transitional shim so
     *  the ~50 `this.db.X(...)` callsites in this file don't all need
     *  touching at once; all writes should migrate to `this.localStore.X(...)`. */
    store: Store, imapManager: ImapManager);
    private _walCheckpointTimer;
    /** Bring up the read-worker. Resolves whether it succeeded or not — on
     *  failure `dbWorker` stays null and every read uses `localStore`. */
    private spawnReadWorker;
    /** In-flight read coalescing. A slow daemon makes the UI re-fire the same
     *  read (scroll loadMore retries, poll loops, re-render storms): the log
     *  showed the IDENTICAL getUnifiedInbox queued dozens of times and all
     *  draining at once after a 78s stall. Keyed by method+args, a duplicate
     *  request that arrives while one is already running shares that single
     *  promise instead of enqueuing another worker job. Same args ⇒ same
     *  result, so this is always safe; different args (other pages) key
     *  separately. This caps the worker queue at one job per distinct read. */
    private inflightReads;
    /** Route a read through the worker bus if it's up, else run it in-process.
     *  A worker-side error (or a dead worker) transparently falls back so a
     *  read never fails just because the isolation layer hiccuped. Identical
     *  concurrent reads are coalesced (see inflightReads). */
    private read;
    /** Transitional getter — direct DB access from MailxService for the
     *  ~50 callsites that haven't yet migrated to Store methods. Future:
     *  every mutation routes through `this.localStore.X(...)`. */
    private get db();
    private _contactsFlushTimer;
    private _contactsFlushInFlight;
    private readonly CONTACTS_FLUSH_DEBOUNCE_MS;
    /** Schedule a debounced flush of the local contacts state to GDrive.
     *  Multiple changes within the debounce window collapse to one write. */
    markContactsDirty(): void;
    /** Serializes read-modify-write cycles on the cloud contacts.jsonc within
     *  this process. Without it, the debounced flush can read the file, an
     *  addToDenylist/setPriority write can land, and the flush write-back then
     *  clobbers it — the "never suggest didn't stick" bug. Cross-device
     *  overlap is handled separately by flushContactsConfig merging instead
     *  of overwriting. */
    private _contactsCloudChain;
    private contactsCloudSerial;
    /** Write current DB contacts state to GDrive contacts.jsonc. Called via
     *  the debounced timer; also exposed for force-flush on shutdown or
     *  after a manual seed. Idempotent — safe to call multiple times. */
    flushContactsConfig(): Promise<void>;
    private flushContactsConfigNow;
    /** Read contacts.jsonc from cloud + apply (preferred + denylist + discovered)
     *  into the DB. On first run with no file, seed from message corpus and
     *  write a fresh contacts.jsonc to GDrive — that auto-bootstrap is what
     *  makes a new device useful immediately on a shared GDrive setup. */
    loadContactsConfig(): Promise<{
        preferred: number;
        discovered: number;
        purged: number;
        conflicts: string[];
    } | null>;
    /** Append an entry to contacts.jsonc#preferred[] and write back to cloud,
     *  then re-apply. Mutates the file in place — preserves existing entries
     *  and the user's hand-formatting where the parser permits. */
    addPreferredContact(entry: {
        name: string;
        email: string;
        source?: string;
        organization?: string;
    }): Promise<void>;
    /** Return the priority sender / domain index — derived from the most
     *  recently loaded contacts.jsonc. Used by the client to flag list
     *  rows visually. */
    getPriorityLists(): {
        senders: string[];
        domains: string[];
    };
    /** Mark / unmark a sender as priority. Finds the matching preferred[]
     *  entry (by lowercased email), sets `priority: true|false`, and writes
     *  contacts.jsonc back to cloud. If no preferred entry exists, one is
     *  inserted with the given name. Reloads the in-memory index. */
    setPrioritySender(email: string, value: boolean, name?: string): Promise<void>;
    /** Mark / unmark a domain as priority. Maintained in
     *  contacts.jsonc → priorityDomains[] (separate from preferred[]
     *  because per-address contacts don't have a domain shape). */
    setPriorityDomain(domain: string, value: boolean): Promise<void>;
    /** Append an email to contacts.jsonc#denylist[] and write back to cloud,
     *  then re-apply (which purges any matching discovered rows). */
    addToDenylist(email: string): Promise<void>;
    /** Return accounts from cache — load once, reuse until configChanged. */
    private getCachedAccounts;
    /** Return full settings from cache — load once, reuse until
     *  configChanged invalidates. Hot-path callers (getMessage on every
     *  preview click, refreshCalendarEvents on every poll) MUST go
     *  through here; calling raw `loadSettings()` blocks the event loop
     *  with a sync GDrive-mounted readFileSync. See `_settingsCache`
     *  comment for the wedge-28-seconds story. */
    private getCachedSettings;
    /** Resolve the reminder sound for the client when an alarm fires.
     *  Returns `{mute:true}` for "none", the raw sound bytes (base64 + mime)
     *  for a custom file, or `{}` to mean "play the built-in chime" (the
     *  default, AND the fallback when a configured file can't be read). The
     *  custom path is resolved relative to the config: the LOCAL config dir
     *  (~/.rmfmail/<name>, binary-safe, subpaths OK) first, then the shared
     *  cloud config folder on Drive (flat, basename only) — so the sound can
     *  live next to the JSONC config and follow the user across machines. */
    getReminderSound(): Promise<{
        mute?: boolean;
        dataBase64?: string;
        mime?: string;
    }>;
    getAccounts(): any[];
    getFolders(accountId: string): Folder[] | Promise<Folder[]>;
    getUnifiedInbox(page?: number, pageSize?: number, flaggedOnly?: boolean, dateBasis?: "sent" | "received"): any;
    getMessages(accountId: string, folderId: number, page?: number, pageSize?: number, sort?: string, sortDir?: string, search?: string, flaggedOnly?: boolean, dateBasis?: "sent" | "received"): any;
    /** UI body read — local-first. Returns immediately from the local cache:
     *  `cached: true` with the full parsed body when on disk, or `cached: false`
     *  with envelope-only when not. In the cache-miss case we kick off a
     *  fire-and-forget IMAP fetch in the background and emit `bodyAvailable`
     *  when the body lands; the UI listens for that event and re-requests.
     *
     *  No `await imap*` in the click → render path. The 60s body-fetch race
     *  and structured `bodyError` shape that lived here previously are gone —
     *  step 1 of the local-first refactor (docs/local-first-plan.md). */
    getMessage(accountId: string, uid: number, allowRemote?: boolean, folderId?: number): Promise<any>;
    /** Diagnostic accessor — exposed for the sync-status pill / debug UI.
     *  Returns the current queue counts so the UI can render
     *  "Sync OK / Syncing N items" without polling per-account state. */
    getSyncStatus(): {
        messageActions: number;
        bodyFetches: number;
    };
    /** RFC 8058 one-click unsubscribe: POST `List-Unsubscribe=One-Click` to the
     *  HTTPS URL the message's List-Unsubscribe header advertised. Done server-
     *  side because the unsubscribe endpoint usually doesn't set CORS headers,
     *  so a browser-side fetch would be blocked. */
    unsubscribeOneClick(url: string): Promise<{
        ok: boolean;
        status: number;
        statusText: string;
    }>;
    /** Per-session map: editId → temp file path + watcher cleanup.
     *  Lives in memory only — cleared when the user closes compose or sends. */
    private wordEdits;
    /** Hand the current compose body off to Microsoft Word for editing. Writes
     *  the HTML to `~/.mailx/external-edit/<editId>.html`, opens it via the
     *  default OS handler (Word on Windows when .html is associated; otherwise
     *  the user's chosen editor for HTML), and starts an fs.watch that emits
     *  `wordEditUpdated` when Word saves. The compose UI listens for that
     *  event and reloads the editor.
     *
     *  Windows-only by current default — on Mac/Linux there's no equivalent
     *  reliable round-trip. The compose toolbar should hide the button on
     *  non-win32 platforms. */
    openInWord(editId: string, html: string): Promise<{
        ok: boolean;
        path: string;
        opener: string;
    }>;
    /** Fallback path used when html-to-docx fails. Reproduces the old
     *  .html-based flow: writes <editId>.html, watches it, emits the body
     *  content on save. User has to Save-As-Web-Page in Word for the file
     *  to update — but at least the edit path works. */
    private openExternalHtmlFallback;
    /** Show an OS-level always-on-top reminder popup. Spawns a separate
     *  msger window (via the injected popupFn) with the supplied HTML
     *  and button list; the chosen button label is returned to the
     *  client which then snoozes/dismisses/opens the underlying event.
     *  No-op (returns reason) when no popupFn was injected — keeps the
     *  service usable in test harnesses without a UI host. */
    showReminderPopup(opts: {
        title: string;
        html: string;
        buttons: string[];
        size?: {
            width: number;
            height: number;
        };
        pos?: {
            x: number;
            y: number;
        };
    }): Promise<{
        button: string;
        form?: any;
        reason?: string;
    }>;
    /** Read + delete the pending mailto: file (P115). Client calls this
     *  on startup so a `mailx --mailto <url>` invocation that spawned us
     *  doesn't lose its compose payload to the daemon-fires-before-app-
     *  registers race window. Returns null when no pending file is present.
     *  The file lives at `~/.rmfmail/pending-mailto.json` and is one-shot —
     *  reading it deletes it. */
    consumePendingMailto(): {
        to: string[];
        cc: string[];
        bcc: string[];
        subject: string;
        body: string;
        inReplyTo: string;
    } | null;
    /** End external editing. Stops the watcher, removes the temp file.
     *  Caller is the compose UI when the user closes the window or sends. */
    closeWordEdit(editId: string): Promise<void>;
    /** Toggle flags. Local DB write + Store event + server-mirror enqueue.
     *  No IMAP code path runs in the click→ack window — the queued drain
     *  fires 1s later, coalescing rapid toggles. */
    updateFlags(accountId: string, uid: number, flags: string[], folderId?: number): Promise<void>;
    allowRemoteContent(type: "sender" | "domain" | "recipient", value: string): Promise<void>;
    getUserDict(): Promise<string[]>;
    addUserDictWord(word: string): Promise<string[]>;
    /** Bulk add — used to reconcile a client's localStorage cache up to the
     *  cloud file (e.g. words added before the cloud round-trip existed). */
    addUserDictWords(words: string[]): Promise<string[]>;
    removeUserDictWord(word: string): Promise<string[]>;
    /** Domain-reputation cache. Lookups are fast (~50ms each, three in
     *  parallel) but we still don't want to redo them on every render of
     *  the same sender's mail. Five-minute TTL — long enough that scrolling
     *  a folder fans out one query set, short enough that a newly-listed
     *  domain surfaces within minutes. */
    private reputationCache;
    private static readonly REPUTATION_TTL_MS;
    private static readonly REPUTATION_TIMEOUT_MS;
    /** Check a domain against three free no-key DNS blocklists in parallel:
     *
     *    Spamhaus DBL    — `<d>.dbl.spamhaus.org`     spam/phish/malware
     *    SURBL multi     — `<d>.multi.surbl.org`      mixed (ph/mw/abuse)
     *    URIBL multi     — `<d>.multi.uribl.com`      black/grey/red lists
     *
     *  Each lookup is bounded at 500 ms; missing/slow services are treated
     *  as "unknown" (don't poison the cache). Returns the aggregate plus
     *  the per-service detail so the UI can show "N of 3 services flag
     *  this domain" with the contributing source list.
     *
     *  Privacy: each query leaks the bare domain to that DNSBL's
     *  infrastructure plus the user's local resolver. Opt-in via Settings.
     *
     *  No API keys, free for personal use across all three services. */
    checkDomainReputation(domain: string): Promise<ReputationResult | null>;
    /** Mark a sender or domain as suspect. Surfaced in the remote-content
     *  banner as a red warning on subsequent messages. Toggle: calling with
     *  the same value removes it. Returns the new state for UI feedback. */
    flagSenderOrDomain(type: "sender" | "domain", value: string): Promise<{
        flagged: boolean;
    }>;
    /** Monotonic generation for in-flight server searches. A new server
     *  search or a cancelServerSearch() call bumps it; the server-search
     *  loop checks it between folder batches and bails when superseded. */
    private serverSearchGen;
    /** Abort any in-flight server search. The client calls this when the
     *  search box is edited or cleared so a 90-folder IMAP sweep doesn't
     *  keep churning for a query the user has already moved on from. */
    cancelServerSearch(): void;
    search(q: string, page?: number, pageSize?: number, scope?: string, accountId?: string, folderId?: number, includeTrashSpam?: boolean): Promise<any>;
    rebuildSearchIndex(): number;
    getSyncPending(): {
        pending: number;
    };
    /** Outbox queue depth + retry status for the UI status bar. Cheap to call. */
    getOutboxStatus(): any;
    /** Per-account health snapshot: inactivity-timeout count, conn-cap hits,
     *  last failed IMAP command. Drives the diagnostics ⚠ badge in the UI. */
    getDiagnostics(): any;
    /** Return the account that supplies `feature` data (calendar / tasks /
     *  contacts). Resolution order:
     *    1. Any account with `primary<Feature>: true`   (per-feature override)
     *    2. Any account with `primary: true`            (catch-all default)
     *    3. First account                               (fallback)
     *  Called without `feature` it returns the catch-all primary — same
     *  semantics as the original single-flag version for back-compat. */
    getPrimaryAccount(feature?: string): any;
    /** Feature names that have already emitted authScopeError this session.
     *  Stops the "banner flashing on and off continually" loop where every
     *  5-min poll / sidebar nav re-fired the event and the client re-rendered
     *  the red banner. Cleared when the user hits Re-authenticate. */
    private scopeErrorEmitted;
    /** Quota cooldown — feature → epoch-ms when the next API call is allowed.
     *  Set when Google returns 429 (rate limit / daily-quota exceeded). While
     *  cooldown is in effect, getCalendarEvents/getTasks return local DB rows
     *  without firing a refresh. Heuristic cooldown is one hour; the daily
     *  Google Tasks quota actually resets at Pacific midnight, but a one-hour
     *  short-circuit keeps the log clean and avoids hammering after a burst. */
    private quotaCooldown;
    /** Sticky "quota exceeded" emit guard — same shape as scopeErrorEmitted. */
    private quotaErrorEmitted;
    /** In-flight refresh promises keyed by feature, so concurrent UI calls
     *  share one Google round-trip instead of stacking N parallel fetches.
     *  The fire-and-forget loop where `tasksUpdated` re-triggers `getTasks`
     *  used to spawn a new refresh on every event RTT — this dedupes them. */
    private refreshingCalendar;
    private refreshingTasks;
    /** Wall-clock of the last Google refresh per account, for calendar and
     *  tasks. The alarm poll calls getCalendarEvents/getTasks every 30 s;
     *  without a throttle that hammered Google's APIs and exhausted the
     *  Tasks "Queries per day" project quota (Bob 2026-05-16). These gate
     *  the actual network refresh to GOOGLE_REFRESH_MIN_INTERVAL_MS — the
     *  call still returns local rows instantly, it just doesn't re-pull. */
    private lastCalendarRefresh;
    private lastTasksRefresh;
    /** Delete the cached Google OAuth token (the one used for Calendar / Tasks
     *  / Contacts scopes — NOT the IMAP token which `reauthenticate()` handles)
     *  and clear the sticky auth-error state so a subsequent refresh can
     *  re-trigger browser consent with the current scope set. Equivalent of
     *  `mailx -reauth` but callable from the UI. Returns `{ cleared: N }` so
     *  the caller can tell the user what happened. */
    reauthGoogleScopes(): {
        cleared: number;
    };
    private primaryTokenProvider;
    /** Return cal events visible in [fromMs..toMs), refreshing from Google
     *  in the background. Caller displays local results immediately; after
     *  the refresh completes the service emits `calendarUpdated` so the UI
     *  re-renders with pulled-in rows. Fire-and-forget-with-event, not
     *  fire-and-forget-and-pray. */
    getCalendarEvents(fromMs: number, toMs: number): Promise<any[]>;
    /** List the user's *selected* Google calendars (id, display name, color,
     *  primary flag) so the sidebar can render one checkbox + icon per
     *  calendar. The user curates the set by selecting calendars in Google;
     *  mailx only reflects it. Returns [] on quota cooldown / auth failure
     *  — the sidebar then just shows no per-calendar controls. */
    getCalendars(): Promise<Array<{
        id: string;
        name: string;
        color: string;
        primary: boolean;
    }>>;
    /** Returns true if the feature is currently in a quota-exceeded cooldown. */
    private inQuotaCooldown;
    /** Single error-handling path for Google refresh failures.
     *  Distinguishes 429 (quota) from 401/403 (scope) so each gets the right
     *  cooldown + sticky-emit treatment without duplicating the regex blocks. */
    private handleGoogleRefreshError;
    /** Pull events in [fromMs..toMs) from Google, upsert locally, reconcile
     *  server-side deletions. Returns true if anything changed so callers
     *  can decide whether to emit a refresh event. `changed` is only true
     *  when at least one row's data actually differs — without this guard
     *  the UI's `calendarUpdated` listener re-triggers `getCalendarEvents`,
     *  which fires another `refreshCalendarEvents`, which emits again, etc.
     *  Tight loop = 429 quota burn. */
    private refreshCalendarEvents;
    createCalendarEventLocal(ev: {
        title: string;
        startMs: number;
        endMs: number;
        allDay?: boolean;
        location?: string;
        notes?: string;
    }): Promise<string>;
    updateCalendarEventLocal(uuid: string, patch: {
        title?: string;
        startMs?: number;
        endMs?: number;
        allDay?: boolean;
        location?: string;
        notes?: string;
    }): Promise<void>;
    deleteCalendarEventLocal(uuid: string): Promise<void>;
    getTasks(includeCompleted?: boolean): Promise<any[]>;
    private refreshTasks;
    createTaskLocal(t: {
        title: string;
        notes?: string;
        dueMs?: number;
    }): Promise<string>;
    updateTaskLocal(uuid: string, patch: {
        title?: string;
        notes?: string;
        dueMs?: number;
        completedMs?: number;
    }): Promise<void>;
    deleteTaskLocal(uuid: string): Promise<void>;
    /** Drain the store_sync queue — calendar / tasks / contacts push-to-server.
     *  Called on every local edit, and on a periodic tick from the outbox worker. */
    drainStoreSync(): Promise<void>;
    /** List queued outgoing messages with parsed envelope headers so the UI
     *  can render a pink-row "pending" view before IMAP APPEND succeeds. */
    listQueuedOutgoing(): any[];
    /** Manually drop a queued message (not yet sent). Removes the .ltr file. */
    cancelQueuedOutgoing(filePath: string): {
        ok: true;
    };
    syncAll(): Promise<void>;
    syncAccount(accountId: string): Promise<void>;
    /** Sync ONE folder now. Backs the lazy-folder-sync model (Bob 2026-05-28):
     *  the client fires this when the user opens a folder, so an on-demand
     *  folder is fresh without the app full-sweeping all 79 folders every
     *  5 minutes. Gmail folders are API-synced; IMAP folders go through
     *  syncFolder. Fire-and-forget from the caller's view — the folderSynced
     *  event refreshes the list when it lands. */
    syncFolderNow(accountId: string, folderId: number): Promise<void>;
    /** Force re-authentication for an account (deletes token, opens browser consent) */
    reauthenticate(accountId: string): Promise<boolean>;
    send(msg: any): Promise<void>;
    /** Trash a single message. Local-first: local row moves to Trash (or
     *  expunges if already there); the server mirror is queued. */
    deleteMessage(accountId: string, uid: number, folderId?: number): Promise<void>;
    /** Bulk trash. Same shape as deleteMessage but loops; each row gets its
     *  own pending-delete flag, local move/expunge, and queue entry. */
    deleteMessages(accountId: string, uids: number[], folderIds?: number[]): Promise<void>;
    /** Move a message to another folder. Same-account moves go through the
     *  Store + queue. Cross-account moves still synchronously bridge two
     *  IMAP connections (rare; local-first violation noted as future work). */
    moveMessage(accountId: string, uid: number, targetFolderId: number, targetAccountId?: string): Promise<void>;
    /** Bulk COPY: the messages stay where they are; a copy is queued to the
     *  target folder (server APPEND via the sync queue). No local row is
     *  moved/removed — the target copy is imported when its folder syncs
     *  (the copy action triggers that). folderIds[] disambiguates each uid to
     *  its source folder, index-aligned with uids (same contract as
     *  deleteMessages). */
    copyMessages(accountId: string, uids: number[], folderIds: number[] | undefined, targetFolderId: number): Promise<void>;
    /** Bulk move. Loops the single-move pattern under a withBatch so the
     *  one folder-counts re-render fires once per affected folder, not per
     *  message. */
    moveMessages(accountId: string, uids: number[], targetFolderId: number, folderIds?: number[]): Promise<void>;
    /** Move messages to the account's configured spam folder (accounts.jsonc "spam" path).
     *  Throws if the account has no spam folder configured or the folder doesn't exist locally. */
    markAsSpamMessages(accountId: string, uids: number[], folderIds?: number[]): Promise<{
        targetFolderId: number;
        moved: number;
    }>;
    /** Append a spam report row to `~/.mailx/spam.csv` — placeholder mechanism
     *  per user 2026-04-23 ("let's make it smart later; no auto-delete until
     *  safety issues are addressed"). One row per click. Columns: timestamp
     *  (ms since epoch), ISO date, ISO time, accountId, Delivered-To, From
     *  address, Subject, eml file path. CSV fields RFC 4180-quoted so commas
     *  and quotes in subjects survive. No move, no flag change, no server
     *  hit — just the log. Useful as training data for a future classifier.
     */
    recordSpamReport(accountId: string, uid: number, folderId: number): Promise<{
        ok: true;
        row: string;
    }>;
    /** Restore from Trash. Local-first: move the row back, clear tombstone
     *  and pending-delete flag, then either retract the still-queued MOVE
     *  to trash or queue a counter-MOVE (trash → original). */
    undeleteMessage(accountId: string, uid: number, folderId: number): Promise<void>;
    deleteOnServer(accountId: string, folderPath: string, uid: number): Promise<void>;
    createFolder(accountId: string, parentPath: string, name: string): Promise<void>;
    /** Rename a folder in place, or — when `newParentId` is given — reparent it.
     *  Both are a single server-side mailbox rename (no copy-and-delete). Path
     *  computation, guard rails (special-use refusal, name validation, collision
     *  / self-descendant checks), the API-vs-IMAP branch, and the local
     *  folder-row + descendant-prefix rewrite all live in ImapManager (on the
     *  sync worker) so this stays a thin delegate. */
    renameFolder(accountId: string, folderId: number, newName: string, newParentId?: number): Promise<void>;
    deleteFolder(accountId: string, folderId: number): Promise<void>;
    /** Move a folder into the account's Trash. Default delete action; user
     *  has to opt in to permanent removal via Shift+Delete (which routes
     *  to `deleteFolder` above).
     *
     *  Strategy:
     *  - IMAP RENAME `<path>` → `<trashPath><delim><name>`. Most servers
     *    bring the message contents and any subfolders along automatically.
     *  - Name collision in Trash → append " (YYYY-MM-DD)", then a counter
     *    " (YYYY-MM-DD 2)", ... up to a sane cap.
     *  - RENAME rejected (server forbids subfolders under Trash, e.g. some
     *    Dovecot configs flag Trash \Noinferiors) → fall back to moving the
     *    folder's messages into Trash root via the existing trash path,
     *    then `mailboxDelete` on the now-empty folder. Children lost in
     *    that fallback get the same treatment recursively.
     *  - Folder is itself Trash, or already inside Trash → throw (caller
     *    should detect and route to `deleteFolder` instead).
     */
    moveFolderToTrash(accountId: string, folderId: number): Promise<void>;
    markFolderRead(folderId: number): void;
    emptyFolder(accountId: string, folderId: number): Promise<void>;
    getAttachment(accountId: string, uid: number, attachmentId: number, folderId?: number): Promise<{
        content: Buffer;
        contentType: string;
        filename: string;
    }>;
    /** Raw RFC 822 source of a message — for "Save message as .eml". Reads the
     *  on-disk body file (same resolution as getAttachment), falling back to a
     *  server fetch. Returned base64 so it survives the IPC JSON channel. */
    getMessageSource(accountId: string, uid: number, folderId?: number): Promise<{
        dataBase64: string;
        filename: string;
    }>;
    /** Save an attachment to a local temp dir and open it with the OS default
     *  application. The desktop UI uses this instead of a browser download —
     *  a programmatic `<a download>` click is silently dropped inside msger's
     *  WebView2, so the open must happen in the Node process (same pattern as
     *  openInWord / openLocalPath). Cross-platform: start / open / xdg-open. */
    openAttachment(accountId: string, uid: number, attachmentId: number, folderId?: number, filename?: string): Promise<{
        ok: boolean;
        path: string;
    }>;
    /** Open an http/https/mailto URL in the OS default browser/handler from the
     *  Node process. The UI used to rely on `window.open(url, "_blank")`, but
     *  inside msger's WebView2 that opens a local in-app window (or no-ops)
     *  rather than the system browser — so clicking a link in a message, or an
     *  unsubscribe "click here", "stayed local" (Bob 2026-06-13, "old bug
     *  back"). `mailxapi.openExternal` now routes here.
     *
     *  Security: the URL comes from untrusted email. We (1) parse it and allow
     *  ONLY http/https/mailto — no file:, javascript:, data:, etc. — and (2)
     *  pass it as a single spawn ARG with no shell, and on Windows use rundll32
     *  (not `cmd /c start`, which re-parses `&`/`^` in query strings and is
     *  injection-prone). Same cross-platform spawn pattern as openAttachment. */
    openExternal(url: string): Promise<{
        ok: boolean;
        reason?: string;
    }>;
    saveDraft(accountId: string, subject: string, bodyHtml: string, bodyText: string, to?: string, cc?: string, previousDraftUid?: number, draftId?: string, bcc?: string): Promise<{
        draftUid: number | null;
        draftId: string;
    }>;
    deleteDraft(accountId: string, draftUid: number, draftId?: string): Promise<void>;
    searchContacts(query: string): any[];
    /** Q49: boolean hint for compose to auto-expand Cc when replying to this
     *  address. True when at least one past sent message to the same recipient
     *  had a non-empty Cc field. */
    hasCcHistoryTo(email: string): boolean;
    /** Q49: same shape, for Bcc. Sent folder is the only place Bcc appears,
     *  so the signal is local-only but still reflects the user's habit. */
    hasBccHistoryTo(email: string): boolean;
    syncGoogleContacts(): Promise<void>;
    seedContacts(): Promise<number>;
    /** Explicit add to address book — used by the right-click "Add to contacts"
     *  action on From/To/Cc addresses in the message viewer. Just calls the same
     *  validated upsert path as recordSentAddress. */
    addContact(name: string, email: string): boolean;
    /** Address-book listing — paginated, filterable. */
    listContacts(query: string, page?: number, pageSize?: number): any;
    /** Upsert a contact from the address book UI (edit name). Two-way cache:
     *  commits locally, queues a Google People push. */
    upsertContact(name: string, email: string): {
        ok: true;
    };
    /** Delete a contact from the address book. Also pushes the deletion to
     *  Google People if the contact had a resourceName (i.e. was synced). */
    deleteContact(email: string): {
        ok: true;
    };
    /** Open a configured local path in the OS file explorer. Whitelisted to
     *  avoid the UI poking at arbitrary paths. */
    /** Open an absolute file path in the OS default *text* editor.
     *  Distinct from "open with the file's associated app" — for a .eml
     *  that would usually be Outlook / a mail client; the user wants a
     *  plain-text viewer like Notepad / TextEdit / xdg's text handler.
     *
     *  Cross-platform strategy:
     *  - Windows: `notepad.exe <path>`. Always present since Windows 95.
     *    Notepad reads any file regardless of extension.
     *  - macOS: `open -t <path>` — the `-t` flag explicitly routes to the
     *    user's default text editor (TextEdit or whatever they've set).
     *  - Linux: try `$VISUAL` / `$EDITOR` first (user's stated preference),
     *    then fall back to `xdg-open` with a `.txt` symlink so xdg picks
     *    the text MIME handler rather than the .eml handler.
     *
     *  Path whitelist: must live under the configured store path or
     *  ~/.rmfmail, so the IPC can't be coerced into opening arbitrary
     *  files (e.g., system passwords).
     */
    openInTextEditor(filePath: string): Promise<{
        ok: boolean;
        opener: string;
        reason?: string;
    }>;
    openLocalPath(which: "config" | "log"): Promise<{
        ok: true;
        path: string;
    }>;
    /** Get all messages in a thread (across folders) for an account. */
    getThreadMessages(accountId: string, threadId: string): any;
    /** Read a JSONC config file from the shared cloud dir or local ~/.mailx.
     *  Names are whitelisted so the UI can't read arbitrary files.
     *  `config.jsonc` is the local per-machine config (not cloud-synced). */
    readJsoncFile(name: string): Promise<string | null>;
    formatJsonc(content: string): Promise<string>;
    /** Return the help markdown for a named config file. Prefers a per-file
     *  doc (`<name>.md` minus the `.jsonc` suffix — e.g. `contacts.md` for
     *  `contacts.jsonc`) shipped in the package's `docs/` dir; falls back to
     *  the legacy `config-help.md`-with-sections format if the per-file
     *  isn't available. */
    readConfigHelp(name: string): Promise<string>;
    /** Write a JSONC config file. Validates that the content parses as JSONC
     *  (loosely — strips comments/trailing commas) before writing.
     *  Saves the prior content to a dated backup file first — manual edits
     *  occasionally have typos that survive validation (semantically wrong
     *  but syntactically OK), and a one-key undo isn't enough; the user
     *  asked to be able to recover yesterday's accounts.jsonc. Automatic
     *  saveAccounts/saveAllowlist paths skip backups (they're driven by
     *  trusted code, not the JSONC editor). */
    writeJsoncFile(name: string, content: string): Promise<void>;
    /** Read the current content of a config file (cloud or local) so it can
     *  be saved as a backup before being overwritten. Returns null if the
     *  file doesn't exist yet (first save — nothing to back up). */
    private readJsoncForBackup;
    /** Write the prior content to `<configDir>/backup/<name>.<ts>.bak` and
     *  prune so at most 10 backups per file remain AND none are older than 7
     *  days. Skipped when previous content is null (first write) or
     *  identical to the new content (no-op save). */
    private backupJsoncIfChanged;
    getSettings(): any;
    saveSettings(settings: any): Promise<void>;
    getStorageInfo(): {
        provider: string;
        mode: string;
        cloudPath?: string;
        folderName?: string;
        folderId?: string;
        configDir?: string;
        cloudError?: string;
    };
    setupAccount(name: string, email: string, password?: string): Promise<{
        ok: boolean;
        error?: string;
        message?: string;
    }>;
    repairAccounts(): Promise<{
        ok: boolean;
        error?: string;
        message?: string;
    }>;
    getAutocompleteSettings(): AutocompleteSettings;
    saveAutocompleteSettings(settings: AutocompleteSettings): void;
    autocomplete(req: AutocompleteRequest): Promise<AutocompleteResponse>;
    /** Generic AI text transform — translate / proofread / summarize.
     *  Shares the autocomplete provider config (provider, key, model). Each
     *  feature has its own opt-in toggle (translateEnabled / proofreadEnabled),
     *  default false. Returns empty text + reason when disabled or on error. */
    aiTransform(req: AiTransformRequest): Promise<AiTransformResponse>;
    private harperInit;
    private static readonly EMPTY_LINTS;
    grammarLint(blocks: string[]): Promise<{
        blocks: {
            start: number;
            end: number;
            kind: string;
            message: string;
            suggestions: string[];
        }[][];
    }>;
    /** Event extraction via the interface `gcal add -clip` uses — keep the
     *  prompt and model IN SYNC with Y:/dev/utils/cardx/gcal/glib/aihelper.ts
     *  (EVENT_EXTRACTION_PROMPT / extractEventsFromText). Key sources, in
     *  order: accounts.jsonc `keys.anthropic`, ANTHROPIC_API_KEY env, the
     *  gcards shared key file (%APPDATA%/gcards/keys.env — same file gcal
     *  and gcards read). Returns null ONLY when no key is available, which
     *  lets aiTransform fall through to the configured provider. */
    private extractEventGcalStyle;
    sendMessage(msg: any): Promise<void>;
    searchMessages(q: string, page?: number, pageSize?: number, scope?: string, accountId?: string, folderId?: number, includeTrashSpam?: boolean): Promise<any>;
    createCalendarEvent(ev: any): Promise<{
        uuid: string;
    }>;
    updateCalendarEvent(uuid: string, patch: any): Promise<{
        ok: true;
    }>;
    deleteCalendarEvent(uuid: string): Promise<{
        ok: true;
    }>;
    createTask(t: {
        title: string;
        notes?: string;
        dueMs?: number;
    }): Promise<{
        uuid: string;
    }>;
    updateTask(uuid: string, patch: any): Promise<{
        ok: true;
    }>;
    deleteTask(uuid: string): Promise<{
        ok: true;
    }>;
    saveSettingsData(settings: any): Promise<void>;
}
//# sourceMappingURL=index.d.ts.map