/**
 * IMAP Service for reading emails via Proton Bridge
 */
import type { AddressObject } from 'mailparser';
import { EmailMessage, EmailFolder, SearchEmailOptions, SaveDraftOptions } from '../types/index.js';
/**
 * Thrown when an IMAP read path cannot reach the server. IMAP-012: read
 * methods used to catch the connection failure and return `[]`, which a caller
 * (or a model summarising "you have 0 emails") could not distinguish from a
 * genuinely empty folder. Surfacing a typed error lets the MCP dispatcher
 * serialise it as a structured error response instead of silent misinformation.
 */
export declare class IMAPNotConnectedError extends Error {
    constructor(message?: string);
}
/**
 * Truncate email body to a reasonable length for list views
 * @param body The full email body
 * @param maxLength Maximum length (default: 300 characters)
 * @returns Truncated body with ellipsis if needed
 */
export declare function stripHtml(html: string): string;
/**
 * Normalise a mailparser address field into a flat array of display strings.
 *
 * IMAP-007: `ParsedMail.to` / `.cc` are typed `AddressObject | AddressObject[]
 * | undefined`. When a message carries multiple separate `To:` header lines
 * (legal per RFC 5322 §3.6.3, and emitted by Proton on bridged forwards) the
 * field becomes an array; the old `parsed.to?.text ? [parsed.to.text] : []`
 * shape then collapsed to `[]` and the recipient list silently disappeared.
 */
export declare function normalizeAddressList(field: AddressObject | AddressObject[] | undefined): string[];
/**
 * Split a list of UID strings into wire-bounded chunks suitable for a single
 * IMAP command. Proton Bridge (and other servers) cap command lines around
 * 8 KB; ~800 nine-digit UIDs already exceed that, and IMAP-002 from the
 * 2026-05-28 audit observed the bulk paths silently degrading to per-UID
 * fallback (and minutes of held mailbox lock). This helper caps each chunk
 * at `maxLen` bytes of `,`-joined UIDs, leaving headroom for the IMAP tag,
 * command verb, and surrounding syntax.
 *
 * No sequence-set compression (`1:5,10`) — the runs aren't usually present
 * in production UID lists and the simpler flat representation keeps the
 * fallback behaviour identical.
 */
export declare function chunkUidsForWire(uids: string[], maxLen?: number): string[];
export declare function expandImapSequence(range: string): number[];
export declare class SimpleIMAPService {
    private client;
    private isConnected;
    private emailCache;
    /** Running byte estimate for emailCache — updated by evictCacheEntry/clearCacheAll/setCacheEntry. */
    private cacheByteEstimate;
    private folderCache;
    /** Timestamp (ms) of the last successful folderCache refresh. 0 = never. */
    private folderCachedAt;
    /** TTL for folderCache entries. After expiry, getFolders() fetches fresh data from IMAP. */
    private static readonly FOLDER_CACHE_TTL_MS;
    private connectionConfig;
    /** Tracks UIDVALIDITY per folder path to detect server-side mailbox rebuilds. */
    private uidValidityMap;
    /** True when TLS certificate validation is disabled (no Bridge cert configured). */
    insecureTls: boolean;
    /**
     * Rough byte estimate for a single cached EmailMessage.
     * Counts only the variable-length string fields; constant overhead is negligible.
     */
    private static estimateCacheBytes;
    /**
     * Remove one entry from emailCache and decrement the byte estimate.
     * Use in place of direct `this.emailCache.delete(id)` everywhere.
     */
    private evictCacheEntry;
    /**
     * Evict every cached INBOX entry. Called from the IDLE EXISTS/EXPUNGE
     * handlers. IMAP-005: snapshot the keys first (`Array.from`) so a concurrent
     * `setCacheEntry` from a parallel main-client fetch cannot corrupt the
     * iteration, and match the folder case-insensitively so entries cached under
     * an aliased INBOX path (`Inbox`, `inbox`) are still invalidated.
     */
    private evictInboxCacheEntries;
    /**
     * Clear the entire emailCache and reset the byte estimate to zero.
     * Use in place of direct `this.emailCache.clear()` everywhere.
     */
    private clearCacheAll;
    /**
     * Write an entry to emailCache, evicting oldest entries (FIFO) when either
     * the count cap (500) or the byte cap (50 MB) is reached.
     * Attachment binary content is stripped before caching to avoid multi-MB
     * buffers accumulating in memory (GAP 7.5).
     */
    private setCacheEntry;
    /**
     * Check if the UIDVALIDITY for a folder has changed since we last opened it.
     * If it has, the cached UIDs for that folder are stale — clear the email cache
     * and update the stored value (GAP 7.4).
     */
    private checkAndUpdateUidValidity;
    /**
     * Walk an imapflow bodyStructure tree and count attachment parts.
     * A part is considered an attachment if its disposition is 'attachment'
     * or if its type is neither 'text' nor 'multipart' (GAP 2.4).
     */
    private countAttachments;
    private static readonly CACHE_TTL_MS;
    /** PARSE-014: upper bound on a single attachment download. Raw + base64 +
     *  parser overhead are held in memory simultaneously, so the effective peak
     *  is ~2.5x this. 25 MB matches Proton's own attachment limit. */
    private static readonly MAX_ATTACHMENT_DOWNLOAD_BYTES;
    private getCacheEntry;
    /** Folder-agnostic cache lookup — scans all entries for a matching UID. */
    private findCacheEntryByUid;
    /** Validate that an email ID is a numeric UID string (prevents IMAP injection) */
    private validateEmailId;
    /**
     * Issue a UID FETCH within the currently locked folder and return the set of
     * UIDs that actually exist there. Caller must already hold a mailbox lock on
     * the folder. UIDs not returned by the server are absent from the folder —
     * mutations against them would be silent no-ops, so callers should treat
     * them as failed rather than reporting false success.
     *
     * Throws on transport errors (network reset, BAD response, command-too-long).
     * Bulk callers must distinguish "UID not in folder" from "I couldn't even
     * check" — collapsing both into `failed` lies to the caller and recreates
     * the v3.0.41 false-success pattern (IMAP-006 from the 2026-05-28 audit).
     */
    /**
     * Chunked batch IMAP op + per-UID fallback. Centralises the IMAP-002
     * wire-line-cap handling so every bulk path (move/delete/copy/flag) shares
     * one chunking implementation. The per-chunk `perChunk` callback receives
     * a comma-joined UID set bounded by `chunkUidsForWire`; if a chunk fails,
     * its UIDs fall back to the per-UID `perUid` callback. `onSuccess` /
     * `onFailure` let callers update their own counters and cache.
     *
     * IMAP-016: `finalize` (optional) runs exactly once after all chunks, and
     * only if the per-UID fallback was actually exercised. Delete callers pass a
     * single trailing `expunge()` here and make `perUid` a cheap `STORE +FLAGS
     * \Deleted` — so a failed bulk delete degrades to N STOREs + 1 EXPUNGE
     * instead of N serial EXPUNGE round-trips that hold the mailbox lock (and
     * block IDLE) for minutes on Bridge.
     */
    private chunkedBatchOp;
    private findExistingUidsInLockedFolder;
    /**
     * Fetch the Message-ID header for a set of UIDs in the CURRENTLY-LOCKED
     * mailbox. Returns uid → Message-ID. Used to verify, after a copy/move, that
     * a message actually landed in the target — UIDs are per-mailbox, so
     * Message-ID is the only stable cross-mailbox identity.
     * MUST be called while the source mailbox is held by getMailboxLock().
     */
    private fetchMessageIdsForUids;
    /**
     * Return the subset of `messageIds` that are actually present in
     * `targetFolder`, by searching `HEADER MESSAGE-ID` per id. Locks the target
     * mailbox, so the caller must NOT hold another mailbox lock when calling.
     *
     * This is the honest-counts backstop: a copy/move is only counted as a
     * success once the message is verifiably in the destination. Proton Bridge
     * can answer COPY/MOVE from the All Mail union with OK while doing nothing
     * (RFC-strict servers like Greenmail reject it, which is why this class of
     * silent false-success never surfaced in CI) — verification turns that into
     * an honest failure instead of a lie.
     */
    private findMessageIdsInFolder;
    /**
     * Record, from an imapflow COPY/MOVE response, which SOURCE uids the server
     * confirmed it relocated. UIDPLUS (advertised by Proton Bridge and Greenmail)
     * returns a COPYUID `uidMap` (source uid → destination uid); its presence is
     * a per-message, identity-independent proof the message landed — cheaper than
     * a Message-ID search and it works even for messages with no Message-ID.
     *
     * Returns true if the response carried a `uidMap` (i.e. the server supports
     * UIDPLUS, so its verdict is authoritative and a Message-ID fallback is not
     * needed). An empty uidMap means the server reported relocating nothing — a
     * no-op — so those uids stay unconfirmed and will be counted as failures.
     */
    private recordRelocatedUids;
    /** Validate a folder name — reject empty, whitespace-only, overly long, or
     *  names with control characters.
     *
     *  IMAP folder names are encoded as UTF-7 modified strings in IMAP commands.
     *  There is no RFC-mandated maximum, but imapflow serialises the name into an
     *  IMAP command literal; an unbounded name causes excessive command-buffer
     *  allocation in the client and bloats log output.  A 1 000-character limit
     *  is well above any real-world folder name and caps the DoS surface.
     */
    private validateFolderName;
    /**
     * Walk an imapflow bodyStructure tree and extract attachment metadata
     * (filename, contentType, size, contentId) without downloading binary content.
     * Used by getEmails() list view (GAP 2.4).
     */
    private extractAttachmentMeta;
    /**
     * Establish an authenticated IMAP connection to the Proton Bridge.
     * @param host Bridge hostname (default: localhost)
     * @param port Bridge IMAP port (default: 1143)
     * @param username Bridge login username
     * @param password Bridge login password
     * @param bridgeCertPath Optional path to a Bridge TLS certificate for localhost trust
     * @param secure Whether to use implicit TLS (true) or STARTTLS (false, default for Bridge)
     * @param allowInsecureBridge Explicit opt-in to run localhost without a pinned cert (default false)
     */
    connect(host?: string, port?: number, username?: string, password?: string, bridgeCertPath?: string, secure?: boolean, allowInsecureBridge?: boolean): Promise<void>;
    /** Version detected from the IMAP ID response, populated after connect. */
    bridgeVersion: string | null;
    /**
     * Issue an IMAP ID (RFC 2971) command and compare the reported Bridge
     * version against BRIDGE_MIN_VERSION. Logs a warning if the running
     * Bridge is older than the floor. Never throws — version detection is
     * best-effort and must not break the connection path.
     *
     * Intentionally warn-only, not a hard block: Bridge occasionally
     * misreports its version via ID (macOS vendor strings sometimes elide
     * the patch component, and some distributions carry security patches
     * into older-looking version strings). Refusing to connect on a false
     * "too old" signal would be worse than a noisy log — Proton's own
     * release notes are the authoritative source. A user who wants a hard
     * gate can wrap the server launch in their own pre-flight check.
     */
    private checkBridgeVersion;
    /** Log out and close the IMAP connection gracefully. */
    disconnect(): Promise<void>;
    /**
     * Attempt to reconnect to IMAP server if connection was lost
     */
    private reconnect;
    /**
     * Ensure connection is active, reconnect if needed
     */
    private ensureConnection;
    /** Returns true if the IMAP connection is currently active. */
    isActive(): boolean;
    /**
     * Probe the IMAP connection by sending a NOOP command.
     *
     * Unlike `isActive()`, which only inspects the in-memory `isConnected` flag,
     * this method performs a real round-trip to the server so it can detect
     * silent TCP drops where the socket is dead but the flag still reads true.
     *
     * @returns `true` if the server acknowledged the NOOP, `false` otherwise.
     *   Never throws — failures are caught and returned as `false`.
     */
    healthCheck(): Promise<boolean>;
    /**
     * Clear folderCache and reset the TTL timestamp so the next getFolders() call
     * fetches fresh data from IMAP.  Use in place of direct `this.folderCache.clear()`.
     */
    private clearFolderCache;
    /** Fetch all IMAP folders with message and unseen counts. Results are cached for {@link FOLDER_CACHE_TTL_MS}. */
    getFolders(): Promise<EmailFolder[]>;
    /**
     * Fetch a paginated list of emails from an IMAP folder.
     * @param folder Folder path (default: INBOX)
     * @param limit Max emails to return, clamped to 1–200 (default: 50)
     * @param offset Zero-based start index within the folder (default: 0)
     * @returns Array of EmailMessage objects, newest first
     */
    getEmails(folder?: string, limit?: number, offset?: number): Promise<EmailMessage[]>;
    /**
     * Fetch a single email by its IMAP UID. Searches all folders; results are cached.
     * @param emailId Numeric UID string (e.g. "12345")
     * @returns The EmailMessage, or null if not found
     */
    getEmailById(emailId: string, folderHint?: string): Promise<EmailMessage | null>;
    /**
     * Search a single folder — extracted so multi-folder search can call it per folder.
     * Caller is responsible for ensuring the IMAP client is connected.
     */
    private searchSingleFolder;
    /**
     * Search emails across one or more folders using IMAP SEARCH criteria.
     * @param options Search filters (from, to, subject, dateFrom, dateTo, isRead, isStarred, folders)
     * @returns Array of matching EmailMessage objects, up to the configured per-folder limit
     */
    searchEmails(options: SearchEmailOptions): Promise<EmailMessage[]>;
    /**
     * Download the binary content of an attachment.
     * The content is sourced from the in-process email cache (populated by
     * getEmailById / getEmails). If the email is not yet cached, a fetch is
     * triggered first.
     *
     * Returns null if the email or attachment index is not found.
     */
    downloadAttachment(emailId: string, attachmentIndex: number): Promise<{
        filename: string;
        contentType: string;
        size: number;
        content: string;
        encoding: "base64";
    } | null>;
    /**
     * Fetch a single email's full RFC 2822 source WITHOUT caching the result.
     * Used by downloadAttachment() to retrieve attachment binary content on demand
     * when the cache entry has had its attachment content stripped (GAP 7.5).
     */
    private fetchEmailFullSource;
    /**
     * Resolve the server-side Drafts folder path.
     * Prefers the folder with specialUse === '\\Drafts'; falls back to a
     * case-insensitive name match against common names.
     *
     * SMTP-011: returns `null` when no Drafts folder can be resolved instead of a
     * literal "Drafts" string. The old fallback caused `append("Drafts", ...)` to
     * fail late with an opaque server error on accounts where Drafts was renamed
     * (e.g. "Brouillons"), localised, or deleted. `saveDraft` turns the null into
     * an actionable error.
     */
    private findDraftsFolder;
    private pickDraftsFolder;
    /**
     * Save an email as a draft in the Drafts folder via IMAP APPEND.
     * Builds the raw MIME message using nodemailer's stream transport, then
     * appends it with the \Draft flag set.
     *
     * Returns the UID assigned by the server, or undefined if the server does
     * not report one.
     */
    saveDraft(options: SaveDraftOptions): Promise<{
        success: boolean;
        uid?: number;
        error?: string;
    }>;
    /**
     * Set the \Seen flag on an email.
     * @param emailId Numeric UID string of the email to update
     * @param isRead true to mark as read, false to mark as unread (default: true)
     * @param sourceFolder When provided, operate directly on this folder. Skips
     *   the cache lookup that can collide on cross-folder UIDs. Strongly
     *   recommended whenever the UID came from a folder other than INBOX.
     * @returns true on success, false if not connected. Throws if the UID does
     *   not exist in the resolved folder.
     */
    markEmailRead(emailId: string, isRead?: boolean, sourceFolder?: string): Promise<boolean>;
    /**
     * Set the \Flagged (starred) flag on an email.
     * @param emailId Numeric UID string of the email to update
     * @param isStarred true to star, false to unstar (default: true)
     * @param sourceFolder Folder containing the UID. See markEmailRead for why
     *   passing this avoids cross-folder UID collisions.
     * @returns true on success, false if not connected. Throws if the UID does
     *   not exist in the resolved folder.
     */
    starEmail(emailId: string, isStarred?: boolean, sourceFolder?: string): Promise<boolean>;
    /**
     * Move an email to a different IMAP folder.
     * @param emailId Numeric UID string of the email to move
     * @param targetFolder Destination folder path (e.g. "Trash", "Folders/Work")
     * @param sourceFolder Folder currently holding the UID. Strongly recommended
     *   whenever the UID came from a folder other than INBOX — IMAP UIDs are
     *   folder-scoped, so without this the wrong folder may be selected.
     * @returns true on success, false if not connected. Throws if the UID does
     *   not exist in the resolved source folder.
     */
    moveEmail(emailId: string, targetFolder: string, sourceFolder?: string): Promise<boolean>;
    /**
     * Copy an email to a target folder using IMAP COPY (message stays in original folder).
     * Use this for label operations in Proton Bridge's label model.
     * @param emailId Numeric UID string of the email to copy
     * @param targetFolder Destination folder path (e.g. "Labels/Work")
     * @param sourceFolder Folder currently holding the UID. Strongly recommended
     *   whenever the UID came from a folder other than INBOX.
     * @returns true on success, false if not connected. Throws if the UID does
     *   not exist in the resolved source folder.
     */
    copyEmailToFolder(emailId: string, targetFolder: string, sourceFolder?: string): Promise<boolean>;
    /**
     * Delete an email from a specific folder (used for label removal).
     * Opens a lock on the given folder and deletes the message there.
     * @param emailId Numeric UID string
     * @param folder The folder from which to delete (e.g. "Labels/Work")
     * @returns true on success, false if not connected
     */
    deleteFromFolder(emailId: string, folder: string): Promise<boolean>;
    /**
     * Set or clear an IMAP flag on an email.
     * @param emailId Numeric UID string of the email
     * @param flag The IMAP flag to set/clear (e.g. '\\Answered', '$Forwarded')
     * @param set true to add the flag, false to remove it (default: true)
     * @param sourceFolder Folder containing the UID. Strongly recommended
     *   whenever the UID came from a folder other than INBOX — IMAP UIDs are
     *   folder-scoped, and without this the all-folders scan can pick a
     *   colliding UID in the wrong mailbox (IMAP-009 from the 2026-05-28 audit).
     * @returns true on success. Throws if the UID does not exist in the
     *   resolved source folder (IMAP-008 from the 2026-05-28 audit).
     */
    setFlag(emailId: string, flag: string, set?: boolean, sourceFolder?: string): Promise<boolean>;
    /**
     * Move many emails to `targetFolder` in one IMAP UID MOVE per source folder.
     *
     * @param sourceFolder When provided, all `emailIds` are assumed to live in
     *   this folder; cache lookup is skipped. Strongly recommended whenever the
     *   UIDs came from anything other than INBOX — IMAP UIDs are folder-scoped
     *   and silent no-ops are otherwise possible.
     *
     * Before each batch IMAP MOVE the method does a UID FETCH inside the lock to
     * determine which UIDs actually exist in the folder. UIDs that don't exist
     * are reported in `results.failed` rather than silently counted as success.
     */
    bulkMoveEmails(emailIds: string[], targetFolder: string, sourceFolder?: string): Promise<{
        success: number;
        failed: number;
        errors: string[];
    }>;
    deleteEmail(emailId: string, sourceFolder?: string): Promise<boolean>;
    /**
     * Permanently delete many emails in one IMAP UID STORE+EXPUNGE per source folder.
     *
     * @param sourceFolder When provided, all UIDs are assumed to live in this
     *   folder; cache lookup is skipped. Strongly recommended whenever the UIDs
     *   came from anything other than INBOX. Pre-flight UID existence check
     *   prevents silent no-ops from being counted as success.
     */
    bulkDeleteEmails(emailIds: string[], sourceFolder?: string): Promise<{
        success: number;
        failed: number;
        errors: string[];
    }>;
    /**
     * Bulk-toggle the \Seen flag on many emails using a single IMAP UID STORE
     * per folder. Mirrors the bulkDeleteEmails / bulkMoveEmails pattern:
     * group by cached folder, lock once, batch flag-set, fall back to per-UID
     * on a batch error.
     */
    bulkMarkRead(emailIds: string[], isRead?: boolean, sourceFolder?: string): Promise<{
        success: number;
        failed: number;
        errors: string[];
    }>;
    /** Bulk-toggle the \Flagged (starred) flag on many emails. Same shape as bulkMarkRead. */
    bulkStar(emailIds: string[], isStarred?: boolean, sourceFolder?: string): Promise<{
        success: number;
        failed: number;
        errors: string[];
    }>;
    /** Copy many emails into `targetFolder` in a single IMAP UID COPY per source folder.
     *  Used by bulk_move_to_label (the target is the Labels/<name> pseudo-folder). */
    bulkCopyToFolder(emailIds: string[], targetFolder: string, sourceFolder?: string): Promise<{
        success: number;
        failed: number;
        errors: string[];
    }>;
    /** Delete many emails from a SPECIFIC folder (label removal). Unlike
     *  bulkDeleteEmails this targets a single folder rather than the
     *  cached/INBOX-fallback folder per UID. UIDs are folder-scoped, so the
     *  caller must pass UIDs known to live in `folder`. */
    bulkDeleteFromFolder(emailIds: string[], folder: string): Promise<{
        success: number;
        failed: number;
        errors: string[];
    }>;
    /**
     * Create a mailbox if it does not already exist; no-op when it is present.
     * Used by the label tools so "move/copy to label X" actually creates label X
     * when missing (the COPY would otherwise target a nonexistent Labels/<name>,
     * which Bridge may silently no-op).
     */
    ensureFolderExists(folderName: string): Promise<void>;
    /** Create a new folder (throws if it already exists). */
    createFolder(folderName: string): Promise<boolean>;
    /** Special-use attributes that mark a mailbox as undeletable / unrenamable. */
    private static readonly PROTECTED_SPECIAL_USE;
    private static readonly PROTECTED_NAMES;
    /**
     * IMAP-014: decide whether a folder is a protected system mailbox before a
     * destructive op. Trims + casefolds the input (so `'INBOX  '` can't slip
     * through a literal compare) and, when folder discovery is available, also
     * matches on the server-reported `specialUse` attribute so localised paths
     * (e.g. `Papelera` for `\Trash`) are still protected.
     */
    private isProtectedFolder;
    /**
     * Delete a folder (must be empty)
     */
    deleteFolder(folderName: string): Promise<boolean>;
    /**
     * Rename a folder
     */
    renameFolder(oldName: string, newName: string): Promise<boolean>;
    private idleClient;
    private idleActive;
    /** Start a background IMAP IDLE connection on INBOX to receive push invalidations. */
    startIdle(): Promise<void>;
    private runIdleLoop;
    /** Stop the background IDLE connection. */
    stopIdle(): void;
    /** Clear all in-memory email and folder caches, forcing fresh IMAP fetches on next access. */
    clearCache(): void;
    /** Securely wipe all cached data and stored credentials from memory. */
    wipeCache(): void;
}
//# sourceMappingURL=simple-imap-service.d.ts.map