/**
 * SQLite metadata index for mailx.
 * Stores message headers, folder structure, sync state.
 * Message bodies are NOT here -- they live in the MessageStore backend.
 */
import type { MessageEnvelope, Folder, EmailAddress, PagedResult, MessageQuery } from "@bobfrankston/mailx-types";
export declare class MailxDB {
    private db;
    constructor(dbDir: string);
    /** Fail loud + early if expected columns are missing. Cheap (PRAGMA only
     *  runs at startup). The user-facing message names the recovery command. */
    private verifySchema;
    /** Fetch a string from the kv table. Returns null when not set. */
    getKv(scope: string, key: string): string | null;
    /** Upsert a kv row. Pass `null` to delete. */
    setKv(scope: string, key: string, value: string | null): void;
    /** One-time: assign UUIDs to every `messages` row that's missing one.
     *  Runs on every startup but the WHERE clause makes it a no-op after the
     *  first pass. */
    private backfillUuids;
    /** Has this Message-ID already been sent? Used to prevent the outbox from
     *  re-sending the same raw file across crash/restart cycles. */
    hasSentMessage(messageId: string): boolean;
    /** Record a successfully sent message so future attempts are skipped. */
    recordSent(messageId: string, accountId: string, subject: string, recipients: string[]): void;
    /** Q49 heuristic: has the user ever sent a message to `recipientEmail`
     *  that had a non-empty Cc field? Used by compose to auto-expand the Cc
     *  input when replying to someone who customarily gets Cc'd with others.
     *  Query scans only Sent folders (special_use='sent') and matches the
     *  recipient's address inside `to_json` via LIKE. No special index — the
     *  Sent folder's row count is typically a few thousand at most; acceptable
     *  on the compose-open path. */
    hasCcHistoryTo(recipientEmail: string): boolean;
    /** Same shape as hasCcHistoryTo for the Bcc field. Bcc only appears in the
     *  user's own Sent copy, so it's still a reliable signal that this user
     *  habitually Bccs when writing to this recipient. */
    hasBccHistoryTo(recipientEmail: string): boolean;
    /** Mark a Message-ID as locally-deleted for an account. No-op if messageId
     *  is empty (e.g. provider stripped the header) — without a stable id we
     *  can't check against future sync results anyway. */
    addTombstone(accountId: string, messageId: string, subject?: string): void;
    /** Is this Message-ID tombstoned for this account? */
    hasTombstone(accountId: string, messageId: string): boolean;
    /** Remove a tombstone — used by "undelete" (Ctrl-Z) so a subsequent sync
     *  re-imports the message as normal. Also lets the user recover from a
     *  mistaken local delete. */
    removeTombstone(accountId: string, messageId: string): void;
    /** Age-out tombstones older than the given cutoff. Keeps the table from
     *  growing unboundedly. Default retention is 30 days; caller passes the
     *  actual cutoff in ms since epoch. */
    pruneTombstones(olderThanMs: number): number;
    upsertCalendarEvent(ev: {
        uuid?: string;
        accountId: string;
        providerId?: string;
        calendarId?: string;
        title: string;
        startMs: number;
        endMs: number;
        allDay?: boolean;
        location?: string;
        notes?: string;
        etag?: string;
        dirty?: boolean;
        recurringEventId?: string;
        htmlLink?: string;
    }): string;
    getCalendarEvents(accountId: string, fromMs: number, toMs: number): any[];
    /** Lookup by uuid only — used by patch/delete paths that don't have an
     *  accountId context. Returns the row even when it's soft-deleted. */
    getCalendarEventByUuid(uuid: string): any | null;
    getTaskByUuid(uuid: string): any | null;
    getDirtyCalendarEvents(accountId: string): any[];
    private calendarRowToObject;
    /** Find a calendar event by its Google Calendar event id (provider_id).
     *  Global lookup — not window-scoped — so repeat pulls dedup cleanly. */
    getCalendarEventByProviderId(accountId: string, providerId: string): any | null;
    markCalendarEventClean(uuid: string, providerId: string, etag: string): void;
    deleteCalendarEventLocal(uuid: string): void;
    purgeCalendarEvent(uuid: string): void;
    upsertTask(t: {
        uuid?: string;
        accountId: string;
        providerId?: string;
        listId?: string;
        title: string;
        notes?: string;
        dueMs?: number;
        completedMs?: number;
        etag?: string;
        dirty?: boolean;
    }): string;
    getTasks(accountId: string, includeCompleted?: boolean): any[];
    getDirtyTasks(accountId: string): any[];
    private taskRowToObject;
    markTaskClean(uuid: string, providerId: string, etag: string): void;
    deleteTaskLocal(uuid: string): void;
    purgeTask(uuid: string): void;
    /** Local delete for the two-way cache drainer (symmetric with calendar/tasks). */
    deleteContactLocal(email: string): void;
    enqueueStoreSync(kind: string, op: string, accountId: string, targetUuid: string, payload: any): void;
    getStoreSyncQueue(kind?: string, accountId?: string): any[];
    completeStoreSync(id: number): void;
    failStoreSync(id: number, error: string): void;
    /** Idempotently add a column to a table if it's missing. */
    private addColumnIfMissing;
    /** Compute a thread id for an incoming message. Strategy:
     *  1. If any ancestor (in_reply_to or references) is already present in
     *     messages with a thread_id, reuse it — this handles the case where
     *     replies arrive before / after the root.
     *  2. Otherwise use the oldest ref (first entry in References), or
     *     in_reply_to, or the message's own messageId as the thread root. */
    private computeThreadId;
    /** Get all messages in a thread (across folders) for a given account. */
    getThreadMessages(accountId: string, threadId: string): MessageEnvelope[];
    close(): void;
    upsertAccount(id: string, name: string, email: string, configJson: string): void;
    getAccounts(): {
        id: string;
        name: string;
        email: string;
        lastSync: number;
    }[];
    getAccountConfigs(): {
        id: string;
        name: string;
        email: string;
        configJson: string;
    }[];
    updateLastSync(accountId: string, timestamp: number): void;
    upsertFolder(accountId: string, folderPath: string, name: string, specialUse: string, delimiter: string): number;
    getFolders(accountId: string): Folder[];
    deleteFolder(folderId: number): void;
    markFolderRead(folderId: number): void;
    deleteAllMessages(accountId: string, folderId: number): void;
    updateFolderCounts(folderId: number, total: number, unread: number): void;
    updateFolderSync(folderId: number, uidvalidity: number, highestModseq: string): void;
    getFolderSync(folderId: number): {
        uidvalidity: number;
        highestModseq: string;
    };
    upsertMessage(msg: {
        accountId: string;
        folderId: number;
        uid: number;
        messageId: string;
        inReplyTo: string;
        references: string[];
        date: number;
        subject: string;
        from: EmailAddress;
        to: EmailAddress[];
        cc: EmailAddress[];
        flags: string[];
        size: number;
        hasAttachments: boolean;
        preview: string;
        bodyPath: string;
        providerId?: string;
    }): number;
    getMessages(query: MessageQuery): PagedResult<MessageEnvelope>;
    /** Unified inbox: all inbox folders across accounts, sorted by date, paginated in SQL */
    getUnifiedInbox(page?: number, pageSize?: number): PagedResult<MessageEnvelope>;
    /** Map a `messages` row to a MessageEnvelope. Exposes `uuid` (stable local
     *  identity) and `bodyPath` (authoritative on-disk location) in addition
     *  to the server-binding metadata. */
    private rowToEnvelope;
    getMessageByUid(accountId: string, uid: number, folderId?: number): MessageEnvelope;
    /** Look up a message by its stable local UUID. Returned envelope includes
     *  the current (folder_id, uid) — these may have changed since the UUID
     *  was minted (folder move or server UID renumber) but the UUID itself
     *  is stable. Use this as the identity in any long-lived reference
     *  (compose in-reply-to, dally, undo stacks). */
    getMessageByUuid(uuid: string): MessageEnvelope;
    getMessageBodyPath(accountId: string, uid: number): string;
    updateMessageFlags(accountId: string, uid: number, flags: string[]): void;
    updateMessageFolder(accountId: string, uid: number, targetFolderId: number): void;
    updateBodyPath(accountId: string, uid: number, bodyPath: string): void;
    /** Get messages without cached bodies (for background prefetch) */
    getMessagesWithoutBody(accountId: string, limit?: number): {
        uid: number;
        folderId: number;
    }[];
    getHighestUid(accountId: string, folderId: number): number;
    getOldestDate(accountId: string, folderId: number): number;
    getMessageCount(accountId: string, folderId: number): number;
    /** Get all UIDs for a folder */
    getUidsForFolder(accountId: string, folderId: number): number[];
    /** Delete a message by account + UID */
    deleteMessage(accountId: string, uid: number): void;
    /** Recalculate folder total/unread counts from actual messages */
    recalcFolderCounts(folderId: number): void;
    /** Bulk insert within a transaction for sync performance */
    beginTransaction(): void;
    commitTransaction(): void;
    rollbackTransaction(): void;
    /** Record an address used in sent mail */
    recordSentAddress(name: string, email: string): void;
    /** True if `email` (lowercased) appears in the active denylist. Cached
     *  in-memory; refreshed on contacts.jsonc reload via setContactsDenylist. */
    private _denylist;
    isAddressDenylisted(emailLower: string): boolean;
    setContactsDenylist(emails: string[]): void;
    /** Callback fired when local-DB contacts mutations happen (sends adding
     *  to discovered, corpus seeder finding new addresses). The service
     *  registers a debounced cloud flush here so the GDrive copy stays in
     *  sync. NOT fired from applyContactsConfig — that's the inbound path
     *  and would create a write loop. */
    private _onContactsChanged?;
    setOnContactsChanged(cb: () => void): void;
    private notifyContactsChanged;
    /** Seed `discovered`-tier contacts from every address that appears in
     *  any cached message — From / To / Cc / Bcc across all folders. One row
     *  per email; first non-empty name observed wins. Sent-folder rows skip
     *  the From (it's us). Junk addresses (noreply, mailer-daemon, *-bounces)
     *  and denylisted addresses are dropped at seed time so they never enter
     *  autocomplete.
     *
     *  Discovered is a single tier; sub-distinctions like sent-vs-received
     *  collapse here because the user-facing UI shows them as one "discovered"
     *  source. Recency-weighted use_count differentiates within the tier. */
    seedContactsFromMessages(): number;
    /** Apply the contents of contacts.jsonc — replaces all preferred-tier rows
     *  with the entries in `preferred[]`, merges `discovered[]` into the local
     *  cache, sets the in-memory denylist, and purges any discovered rows
     *  whose email is now denylisted. Preferred rows are *not* auto-purged on
     *  denylist hit — if the user explicitly added them they win that
     *  conflict; we just log a warning.
     *
     *  Discovered rows from the file are MERGED with whatever the local
     *  message-corpus seeder has produced. Each device contributes its
     *  observed addresses; over time GDrive accumulates the union. */
    applyContactsConfig(cfg: {
        preferred?: {
            name?: string;
            email: string;
            source?: string;
            organization?: string;
            org?: string;
        }[];
        denylist?: string[];
        discovered?: {
            name?: string;
            email: string;
            useCount?: number;
            lastUsed?: number;
        }[];
    }): {
        preferred: number;
        discovered: number;
        purged: number;
        conflicts: string[];
    };
    /** Build the contacts.jsonc shape from current DB state — for round-trip
     *  to GDrive. Preferred-tier rows come from anything not in the reserved
     *  system sources; discovered comes from `source='discovered'` rows;
     *  denylist comes from the in-memory set (set by applyContactsConfig).
     *  Caller is responsible for actually writing the cloud copy. */
    exportContactsConfig(): {
        preferred: {
            name: string;
            email: string;
            source: string;
            organization?: string;
        }[];
        denylist: string[];
        discovered: {
            name: string;
            email: string;
            useCount: number;
            lastUsed: number;
        }[];
    };
    /** Search contacts by name or email prefix.
     *
     *  Source-tier bonus is what makes the curated address book win against
     *  passive corpus harvest. Anything in `contacts.jsonc#preferred[]` (any
     *  source value other than the two reserved system sources) gets the
     *  highest tier — that's the user's explicit address book and overrides
     *  Google. Google sits in the middle (the auto-synced address book).
     *  `discovered` is the corpus-harvested floor.
     *
     *  Multi-name-per-email is supported: the same email can carry distinct
     *  (source, name) rows — Bob's wife and Bob Smith both at bob@example.com
     *  surface as two rows, each typing-completable by their own name. */
    searchContacts(query: string, limit?: number): {
        name: string;
        email: string;
        source: string;
        useCount: number;
    }[];
    /** List all contacts (address-book view) with pagination + optional filter. */
    listContacts(query: string, page?: number, pageSize?: number): {
        items: {
            name: string;
            email: string;
            source: string;
            googleId: string | null;
            useCount: number;
            lastUsed: number;
        }[];
        total: number;
        page: number;
        pageSize: number;
    };
    /** Update or insert a contact manually (from the address book UI). */
    upsertContact(name: string, email: string): void;
    /** Delete a contact by email (address book UI). */
    deleteContact(email: string): void;
    /** Delete contact rows by Google People resourceName. Used by the
     *  incremental People sync when a person comes back with `metadata.deleted = true`
     *  — the email may have already changed/disappeared, but the resourceName
     *  is stable. Removes all rows tied to that Google identity (a single
     *  contact can have multiple email addresses, each is its own row). */
    deleteContactByGoogleId(googleId: string): number;
    /** Full-text search across all messages. Supports qualifiers: from:, to:, subject: */
    searchMessages(query: string, page?: number, pageSize?: number, accountId?: string, folderId?: number): PagedResult<MessageEnvelope>;
    /** Rebuild FTS index from existing messages */
    rebuildSearchIndex(): number;
    /** Queue a local action for later sync to IMAP */
    queueSyncAction(accountId: string, action: string, uid: number, folderId: number, extra?: {
        targetFolderId?: number;
        flags?: string[];
        rawMessage?: string;
    }): void;
    /** Get all pending sync actions for an account */
    getPendingSyncActions(accountId: string): {
        id: number;
        action: string;
        uid: number;
        folderId: number;
        targetFolderId: number;
        flags: string[];
        rawMessage: string;
        attempts: number;
    }[];
    /** Remove a completed sync action */
    completeSyncAction(id: number): void;
    /** Mark a sync action as failed */
    failSyncAction(id: number, error: string): void;
    /** Count pending sync actions for an account */
    getPendingSyncCount(accountId: string): number;
    /** Count total pending sync actions across all accounts */
    getTotalPendingSyncCount(): number;
}
//# sourceMappingURL=db.d.ts.map