import { ImapFlow } from "imapflow"; import type { EmailConfig } from "./credentials.js"; export interface EmailMessage { uid: number; messageId: string | null; inReplyTo: string | null; from: string; subject: string; date: string; bodyPreview: string; attachments: AttachmentMeta[]; } export interface PaginatedMessages { messages: EmailMessage[]; /** Pass as before_uid to get the next page of older messages. Omitted when results are exhausted. */ nextBeforeUid?: number; /** Kept-vs-hidden accounting, present only when a client-side recipient filter ran. */ recipientFilter?: RecipientFilterStats; /** Display-slice accounting, present only when matches exceeded the limit. */ truncation?: DisplayTruncationStats; } export declare const MAX_LIMIT = 50; /** * Resolve an IMAP folder name from a logical folder key. * - "inbox" → "INBOX" * - "sent" → the \Sent special-use folder (RFC 6154) * * Drafts resolution lives in resolveDraftsFolder — it carries a per-account * override and per-mailbox census observability that the inbox/sent paths do not. * * Throws if the requested folder cannot be found on this server. */ export declare function resolveFolderName(client: ImapFlow, folder: "inbox" | "sent"): Promise; /** Test-only: reset the per-mailbox drafts-census gate. */ export declare function resetDraftsCensusForTest(): void; /** * Resolve the mailbox's Drafts folder for a draft read/write/edit/delete. * * `overridePath` (config.draftsFolder), when set, is honoured before \Drafts * special-use discovery — the escape valve for a non-Gmail host whose * client-visible Drafts is not the \Drafts-flagged folder. It is validated * against the live folder list, so a wrong name is refused (naming the advertised * folders) rather than written to a nonexistent mailbox. When unset, the \Drafts * special-use folder is used, unchanged. * * `mailboxId` keys the once-per-session candidate-list census (see above) and * labels the resolution marker. Every resolution logs a `via=override|special-use` * marker so the resolution path is visible without reproduction. * * The no-\Drafts error preserves the substring "does not advertise" — appendDraft's * catch keys on it to rebrand the error with the mailbox name. */ export declare function resolveDraftsFolder(client: ImapFlow, mailboxId: string, overridePath?: string): Promise; /** * Strip HTML tags and decode common entities. * Used for extracting readable text from HTML-only email bodies. */ export declare function stripHtml(html: string): string; /** Which field of the parsed message yielded the body, for diagnostics. */ export type BodySource = "text" | "html" | "none"; /** * Decode the readable text body of a raw RFC822 message with mailparser. * * Handles every MIME shape — flat text, single-part HTML, multipart/alternative, * and nested multipart/related|mixed (Outlook's inline-image house style) — plus * all transfer encodings and non-UTF-8 charsets. Returns the plain-text part when * present, else the HTML part stripped to text, else the "(body unavailable)" * sentinel. Never truncates; callers cap previews with previewBody. */ export declare function extractBody(source: Buffer | string): Promise<{ body: string; kind: BodySource; }>; /** Cap a body to BODY_PREVIEW_LENGTH for list/triage output, appending an ellipsis. */ export declare function previewBody(body: string): string; /** Hard cap on a single attachment's bytes — over this size, metadata is surfaced * but bytes are not downloaded. Matches typical provider send limits. */ export declare const MAX_ATTACHMENT_BYTES: number; /** Attachment metadata as derived from IMAP bodyStructure — no bytes. */ export interface AttachmentMeta { /** IMAP body part key, e.g. "2" or "1.3". Used to fetch bytes later. */ partKey: string; filename: string; mimeType: string; sizeBytes: number; /** RFC 2045 transfer encoding — needed to decode bytes at fetch time. */ encoding: string | null; } /** * Body-structure node shape (subset of ImapFlow's BodyStructure). * Defined locally because ImapFlow does not export a public type for this. */ interface BodyStructureNode { part?: string; type?: string; encoding?: string; size?: number; disposition?: string; dispositionParameters?: { filename?: string; }; parameters?: { name?: string; }; childNodes?: BodyStructureNode[]; } /** * Walk an IMAP bodyStructure and return metadata for every attachment-shaped * part. No bytes are downloaded — cheap, runs at preview time. * * An attachment-shaped part is one of: * - disposition === "attachment" (RFC 2183), OR * - has a filename in parameters.name or dispositionParameters.filename AND * is not a multipart container. * * Inline parts without a filename (e.g. the message body) are excluded. */ export declare function walkAttachmentMetadata(structure: BodyStructureNode | undefined): AttachmentMeta[]; export declare function formatBytes(n: number): string; /** * Format an AttachmentMeta[] as a single-line operator-readable footer. * Returns empty string when the array is empty so callers can skip the * line entirely. */ export declare function formatAttachmentsFooter(attachments: AttachmentMeta[]): string; /** Outcome of resolving one attachment from a message's attachment list. */ export type SelectorResult = { ok: true; meta: AttachmentMeta; } | { ok: false; error: string; }; /** * Resolve exactly one attachment from a message's attachment list, by a 1-based * `index` (matching the numbered listing) or by `filename`. `index` takes * precedence over `filename` when both are given. With no selector and exactly * one attachment, that attachment is returned; with more than one and no * selector, or a filename that matches zero or more than one part, the call is * refused with a numbered listing so the caller picks one rather than guessing. */ export declare function resolveAttachmentSelector(attachments: AttachmentMeta[], sel: { index?: number; filename?: string; }): SelectorResult; /** * Fetch one INBOX message's attachment metadata by uid, without downloading any * attachment bytes. Reads only the bodyStructure and walks it. Returns * `found: false` when no message carries that uid. INBOX only — the byte-fetch * path (`fetchAttachmentBytesForMessages`) locks INBOX, so attachment reading * is scoped to the inbox. */ export declare function fetchMessageAttachments(config: EmailConfig, password: string, uid: number): Promise<{ found: boolean; attachments: AttachmentMeta[]; }>; /** * Result of attempting to fetch one attachment's bytes from IMAP. * `bytes === null` indicates a non-fatal failure (oversize, short buffer, * fetch error) — the caller surfaces metadata-only graph attach instead. */ export interface AttachmentBytesResult { uid: number; partKey: string; bytes: Buffer | null; failureReason: "oversize" | "short-buffer" | "fetch-error" | null; } /** * Download decoded bytes for a list of (uid, partKey) attachments from IMAP. * Opens one connection, reuses it across all requests. Per-attachment * failures are isolated — one bad part does not abort the batch. */ export declare function fetchAttachmentBytesForMessages(config: EmailConfig, password: string, requests: Array<{ uid: number; partKey: string; expectedSize: number; encoding: string | null; }>): Promise; /** Classified cause of an IMAP probe failure. */ export type ImapFailureKind = "auth" | "refused" | "not-found" | "tls" | "timeout" | "unknown"; /** * Classify an IMAP probe error by its structured fields, falling back to message * strings only where no structured field carries the signal. * * The auth case is why this exists: ImapFlow reports an authentication rejection * as a generic `Error("Command failed")` and carries the real signal in * `authenticationFailed` / `serverResponseCode` / `responseText` (set by * imapflow's login.js / authenticate.js), none of which appear in `err.message`. * The transport cases (refused/not-found/timeout) carry `err.code` from Node's * socket layer; TLS errors carry a cert/TLS/SSL marker in code or message. */ export declare function classifyImapError(err: unknown): ImapFailureKind; /** * Compose the attributable detail for a connection- or send-failure diag line: * the account it was for, the host:port it was reaching, and the classified * reason from classifyImapError, plus the raw message and elapsed time. A reader * of the email log can then pin a failure to one mailbox and one phase (timeout vs * auth vs refused vs tls vs dns) without a live reproduction — the exact gap that * let a VPN-induced connect timeout read as a lost-mailbox outage. */ export declare function connectionFailureDetail(account: string, host: string, port: number, err: unknown, elapsedMs: number): string; /** * Test IMAP connectivity and authentication. * Returns null on success, error message on failure. * * Emits one structured attempt line per probe (host/port/security/outcome/reason, * no secret) so a failing setup is visible server-side without opening the session * transcript. */ export declare function testConnection(config: EmailConfig, password: string, token?: string): Promise; /** * Check whether an IMAP envelope's TO or Cc field contains any of the target * addresses. Case-insensitive comparison. Strict: if the server returned * neither TO nor Cc addresses, the message is excluded — we cannot confirm it * was addressed to any target. Both the TO and Cc fields are inspected. * * Accepts a single address or a set of acceptable addresses; a message * matches when its TO or Cc contains at least one of them. */ export declare function matchesRecipient(envelope: { to?: Array<{ name?: string; address?: string; }>; cc?: Array<{ name?: string; address?: string; }>; }, target: string | string[]): boolean; /** * Resolve the implicit recipient-filter set for read/search when no explicit * `to` is supplied. The mailbox always owns its IMAP login (`config.email`), * and `config.agentAddress` is an additional alias to surface. When an alias * is configured (truthy and different from the login) both addresses are * returned, so replies that arrive at the login address are not hidden. * * When no distinct alias is configured, returns `undefined` — no recipient * filter is applied at all, preserving the pre-existing behaviour for plain * (non-alias) mailboxes. */ export declare function resolveDefaultRecipients(config: { email: string; agentAddress?: string; }): string[] | undefined; /** Kept-vs-hidden accounting for a client-side recipient filter pass. */ export interface RecipientFilterStats { kept: number; hidden: number; recipients: string[]; } /** * Render the operator-facing recipient-filter signal line. Returns null when * nothing was hidden, so a fully-matched window emits no misleading line. */ export declare function formatRecipientFilterLine(stats: RecipientFilterStats | undefined): string | null; /** Display-slice accounting: how many messages matched vs how many were shown. */ export interface DisplayTruncationStats { shown: number; matched: number; } /** * Render the operator-facing truncation signal. Returns null when the display * showed every match, so a complete result emits no misleading line. * * Reported separately from the recipient filter because the two do not coincide: * a filter that hides nothing (`hidden === 0`) emits no recipient line at all, * yet still over-fetches and can truncate. Truncation is only reachable when a * recipient filter is active — that is what widens the window past `limit` — * but an active filter is not enough to make the recipient line appear. * * `matched` counts matches within the fetched window, not the whole mailbox; * the wording says so, because a window-scoped count read as a mailbox total * is exactly the kind of false reassurance this signal exists to prevent. */ export declare function formatTruncationLine(stats: DisplayTruncationStats | undefined): string | null; /** * Build IMAP SEARCH criteria for fetchRecent. * Never includes TO — that filter is applied client-side to avoid * full-mailbox header scans that timeout on large mailboxes. */ export declare function buildFetchRecentCriteria(options: { sender?: string; since?: Date; before?: Date; to?: string | string[]; limit?: number; }): { criteria: Record; clientSideRecipients: string[] | undefined; fetchLimit: number; }; /** * Build IMAP SEARCH criteria for searchMessages. * Never includes TO — that filter is applied client-side. * When TO was the only filter (no other criteria provide bounding), * injects a since floor to prevent unbounded full-mailbox scans. */ export declare function buildSearchCriteria(options: { sender?: string; subject?: string; body?: string; since?: Date; before?: Date; to?: string | string[]; }): { criteria: Record; clientSideRecipients: string[] | undefined; }; /** * Fetch recent messages from a mailbox folder. * Returns the most recent N messages with cursor-based pagination. * * TO filtering is applied client-side (not via IMAP SEARCH) to avoid * full-mailbox header scans that exceed socketTimeout on large mailboxes. */ export declare function fetchRecent(config: EmailConfig, password: string, options?: { limit?: number; sender?: string; subjectPattern?: string; since?: Date; before?: Date; to?: string | string[]; folder?: "inbox" | "sent"; beforeUid?: number; }): Promise; /** * Search a mailbox folder by query criteria with cursor-based pagination. */ export declare function searchMessages(config: EmailConfig, password: string, query: { sender?: string; subject?: string; body?: string; since?: Date; before?: Date; limit?: number; to?: string | string[]; folder?: "inbox" | "sent"; beforeUid?: number; }): Promise; /** * Result from an incremental email fetch performed by the email-fetch tool. * Includes the raw envelope data needed for graph storage (not just display). */ export interface FetchedEmail { uid: number; messageId: string | null; inReplyTo: string | null; toAddresses: string[]; ccAddresses: string[]; fromAddress: string; fromName: string | null; subject: string; date: string; bodyPreview: string; /** Attachment metadata derived from bodyStructure; bytes downloaded later. */ attachments: AttachmentMeta[]; } /** * Fetch emails from INBOX since a given UID (exclusive) for incremental polling. * Returns the UIDVALIDITY and all matching messages with UID > sinceUid. * * When sinceUid is 0 (first poll), fetches emails from the last 7 days * to avoid pulling the entire mailbox on large accounts. * * Client-side To/Cc filtering is applied when a recipient set is supplied * (alias/catchall scenarios); when the set is undefined no filter runs and * every message in the UID window is returned. */ export declare function fetchSinceUid(config: EmailConfig, password: string, sinceUid: number, recipients: string[] | undefined): Promise<{ uidValidity: number; emails: FetchedEmail[]; maxUid: number; }>; /** * Resolve an envelope by Message-ID via IMAP. Searches INBOX first, then * the \Sent special-use folder. Returns null when no folder yields a hit. */ export declare function fetchEnvelopeByMessageId(config: EmailConfig, password: string, messageId: string): Promise<{ uid: number; mailbox: string; messageId: string; fromAddress: string; fromName: string | null; toAddresses: string[]; ccAddresses: string[]; subject: string; date: string; } | null>; /** Complete decoded body of a single message — no preview cap. */ export interface FullBody { uid: number; messageId: string | null; from: string; subject: string; date: string; body: string; bodySource: BodySource; bytes: number; } /** * Fetch the COMPLETE decoded body of one message, addressed by UID (in a * folder) or by Message-ID (resolved across INBOX and \Sent). Downloads the * full RFC822 source and decodes it with mailparser — no 500-char cap and no * MIME-shape blind spot. Returns null when the message cannot be located. */ export declare function fetchFullBody(config: EmailConfig, password: string, target: { uid: number; folder?: "inbox" | "sent"; } | { messageId: string; }): Promise; /** * Resolve the mailbox's Trash/Deleted folder. Prefers the folder the server marks * with the `\Trash` special-use flag; falls back to the first existing folder whose * path matches a known Trash name. Throws an error tagged `code: 'no-trash-folder'` * when neither exists, so the caller can refuse the delete and report why rather * than silently expunging. */ export declare function resolveTrashFolder(client: ImapFlow): Promise; /** * Delete messages by moving them to the mailbox's Trash folder (recoverable). The * Trash folder is resolved before the source folder is locked, so a mailbox with * no Trash errors and mutates nothing. A pre-search against the requested UIDs * finds which still exist; only those are moved, so a UID that is already gone is * counted not-moved without failing the batch. Returns { moved, requested, trash }. */ export declare function moveToTrash(config: EmailConfig, password: string, { folder, uids }: { folder?: "inbox" | "sent"; uids?: number[]; }): Promise<{ moved: number; requested: number; trash: string; }>; /** * Get the inbox message count. */ export declare function getInboxCount(config: EmailConfig, password: string, token?: string): Promise; /** * APPEND a composed RFC822 message to the mailbox's \Drafts special-use folder * with the \Draft flag set — the message is stored unsent for operator review. * * Resolution is strict: when the server advertises no \Drafts special-use * folder the call fails naming the mailbox; no fallback folder is ever picked. * `uid` is the APPENDUID when the server supports UIDPLUS, else null. */ export declare function appendDraft(config: EmailConfig, password: string, raw: Buffer): Promise<{ folder: string; uid: number | null; }>; /** * Fetch a stored draft's raw RFC822 bytes plus its parsed envelope recipients, * addressed by Drafts-folder UID. Returns null when the UID is not present in * the \Drafts folder. The envelope carries To/Cc/Bcc (the stored draft keeps * its Bcc header), which the send path uses to build the SMTP envelope. */ export declare function fetchDraftSource(config: EmailConfig, password: string, uid: number): Promise<{ folder: string; source: Buffer; toAddresses: string[]; ccAddresses: string[]; bccAddresses: string[]; messageId: string | null; } | null>; /** Which identity located the live draft during a replace. */ export type DraftAddressing = "uid-hit" | "message-id-relocate" | "created-fresh"; /** * Read-modify-replace cycle for an existing draft. The recorded Drafts UID is a * fast-path hint; the draft's own `Message-ID` is the stable identity, because * Gmail renumbers a draft's UID when it is touched or re-synced. * * Resolution order, inside the Drafts lock: the recorded UID if still live, else * a `SEARCH HEADER Message-ID` relocate (when a draftMessageId is supplied), * else nothing. In every path the replacement is APPENDed (with the \Draft flag) * BEFORE the resolved old copy is expunged, so a mid-cycle failure never leaves * zero copies. * * When neither identity resolves a live draft, the prior draft is genuinely * gone: the replacement is appended as a fresh draft and `created:true` is * returned — no throw. `newUid` is the APPENDUID when the server supports * UIDPLUS, else null. * * A single transient transport drop is absorbed by the bounded retry; permanent * errors (auth) surface immediately. */ export declare function replaceDraft(config: EmailConfig, password: string, oldUid: number, newRaw: Buffer, draftMessageId?: string | null): Promise<{ folder: string; oldUid: number; newUid: number | null; expunged: boolean; created: boolean; addressing: DraftAddressing; }>; /** * Delete a stored draft addressed by Drafts-folder UID (used by the send path * after SMTP accepts the message). An absent UID is treated as success with * nothing removed (`deleted:false`) — deletion's goal is already met, so a * stale UID is not an error. A single transient transport drop is absorbed by * the bounded retry. * * Message-ID relocation is deliberately not applied here: the send path * addresses by UID only and carries no draft Message-ID to relocate by. */ export declare function deleteDraft(config: EmailConfig, password: string, uid: number): Promise<{ folder: string; deleted: boolean; }>; export type FileToSentResult = { filed: true; sentFolder: string; sentUid: number; draftFolder: string; } | { filed: false; reason: "no-sent-folder" | "draft-absent" | "move-did-nothing" | "moved-but-not-in-sent"; draftFolder: string; }; /** * Relocate a sent draft from \Drafts into the mailbox's \Sent folder. * * Called only after SMTP has accepted the message, so the mail is already out: * every failure path leaves the draft where it is rather than destroying it. * This replaces the historical post-send expunge, which on Gmail destroyed the * only copy — the provider deduplicates its own Sent-copy against the draft * already held under that Message-ID, so expunging left no record of the send * anywhere in the account, not even in All Mail (Task 1821). * * Flags are set while the message is still in \Drafts, where the source UID is * addressable: after a MOVE the source UID is gone and the destination UID is * known only when the server supports UIDPLUS. * * `sentUid` is non-null only when the move reported a destination UID AND that * UID was read back from \Sent. A null means the copy was moved but could not * be located — callers must not report a UID they never confirmed. * * The retry wrapper makes this non-idempotent operation re-runnable: a drop * after a successful MOVE leaves attempt two looking at a \Drafts folder the * message has already left. `messageId` is what disambiguates that — when the * draft UID is gone, \Sent is searched for it, so an already-filed copy is * reported as filed rather than as a loss. * * `draft-absent` therefore means the UID is not in \Drafts AND the \Sent search * came back empty — or that no `messageId` was supplied to search with, which * is the one case where the two states cannot be told apart. */ export declare function fileDraftToSent(config: EmailConfig, password: string, uid: number, messageId: string | null): Promise; /** UID of a message in `folder` matching `messageId`, or null. Angle brackets are * restored because Gmail matches the raw header value, not the bare id. */ export declare function findUidByMessageId(client: ImapFlow, folder: string, messageId: string): Promise; export type SentCopyResult = { found: true; sentFolder: string; sentUid: number; } | { found: false; reason: "no-sent-folder"; } | { found: false; reason: "not-in-sent"; sentFolder: string; } | { found: false; reason: "check-failed"; error: string; }; /** * Whether a copy of `messageId` is actually present in the mailbox's \Sent * folder. * * Called after SMTP has accepted the message. The send already happened, so * this never throws — an unreachable server yields `check-failed`, which the * caller reports as "could not confirm", distinct from "confirmed absent". * Collapsing those two into one word is the failure this exists to remove: with * nothing in the tool result either affirming or denying the copy, the model * supplied the affirmation from expectation for four days (Task 1822). * * Providers do not file the copy synchronously with SMTP acceptance, so a * single immediate search would report a false absence on a healthy mailbox. * The search is retried on a fixed short delay before absence is concluded. */ export declare function confirmSentCopy(config: EmailConfig, password: string, messageId: string, opts?: { attempts?: number; delayMs?: number; }): Promise; /** * Which of `messageIds` are absent from the mailbox's \Sent folder. * * The right-hand side of the standing reconciliation (Task 1822). Presence is * asked per Message-ID rather than by counting the folder: \Sent holds * everything the owner sends from any client, so a bulk count is dominated by * messages this plugin never sent and a mailbox filing none of ours would * still look healthy. * * One connection serves the whole batch. Throws when the server advertises no * \Sent folder or the connection fails — the sweep reports that per mailbox as * check-failed rather than as a shortfall, so a mailbox that could not be read * is never mistaken for one that lost copies. */ export declare function findMissingInSent(config: EmailConfig, password: string, messageIds: string[]): Promise<{ folder: string; missing: string[]; }>; export {}; //# sourceMappingURL=imap.d.ts.map