/**
 * @bobfrankston/mailx-service
 * Pure business logic — no HTTP, no Express.
 * Both the Express API (mailx-api) and the Android bridge call these functions.
 */
import { MailxDB } from "@bobfrankston/mailx-store";
import { ImapManager } from "@bobfrankston/mailx-imap";
import type { Folder, AutocompleteRequest, AutocompleteResponse, AutocompleteSettings, AiTransformRequest, AiTransformResponse } from "@bobfrankston/mailx-types";
interface ReputationResult {
    flagged: boolean;
    listedCount: number;
    checkedCount: number;
    sources: Array<{
        service: string;
        flagged: boolean;
        verdict: string;
    }>;
    verdict: string;
    service: string;
}
export declare class MailxService {
    private db;
    private imapManager;
    private _accountsCache;
    constructor(db: MailxDB, imapManager: ImapManager);
    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;
    /** 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>;
    /** 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>;
    /** 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;
    getAccounts(): any[];
    getFolders(accountId: string): Folder[];
    getUnifiedInbox(page?: number, pageSize?: number): any;
    getMessages(accountId: string, folderId: number, page?: number, pageSize?: number, sort?: string, sortDir?: string, search?: string, flaggedOnly?: boolean): any;
    getMessage(accountId: string, uid: number, allowRemote?: boolean, folderId?: number): Promise<any>;
    /** 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;
    }>;
    /** 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>;
    updateFlags(accountId: string, uid: number, flags: string[]): Promise<void>;
    allowRemoteContent(type: "sender" | "domain" | "recipient", value: string): Promise<void>;
    /** 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;
    }>;
    search(q: string, page?: number, pageSize?: number, scope?: string, accountId?: string, folderId?: number): 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;
    /** 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[]>;
    /** 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>;
    /** Force re-authentication for an account (deletes token, opens browser consent) */
    reauthenticate(accountId: string): Promise<boolean>;
    send(msg: any): Promise<void>;
    deleteMessage(accountId: string, uid: number): Promise<void>;
    deleteMessages(accountId: string, uids: number[]): Promise<void>;
    moveMessage(accountId: string, uid: number, targetFolderId: number, targetAccountId?: string): Promise<void>;
    moveMessages(accountId: string, uids: number[], targetFolderId: 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[]): 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;
    }>;
    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>;
    renameFolder(accountId: string, folderId: number, newName: string): Promise<void>;
    deleteFolder(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;
    }>;
    saveDraft(accountId: string, subject: string, bodyHtml: string, bodyText: string, to?: string, cc?: string, previousDraftUid?: number, draftId?: 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(): 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. */
    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 section for a named config file, extracted from docs/config-help.md.
     *  Matches a level-2 heading whose text equals the filename. Returns markdown. */
    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): void;
    getStorageInfo(): {
        provider: string;
        mode: 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>;
}
export {};
//# sourceMappingURL=index.d.ts.map