/** * IMAP service — pure business logic for email read operations. * * No MCP dependency — fully unit-testable. */ import type { IConnectionManager } from '../connections/types.js'; import type { BulkResult, Contact, Email, EmailAddress, EmailMeta, EmailStats, LabelInfo, Mailbox, PaginatedResult, QuotaInfo } from '../types/index.js'; /** Exported so the cached read path produces addresses identically. */ export declare function parseAddresses(addrs: { name?: string; address?: string; }[] | undefined): EmailAddress[]; /** * Map a raw ImapFlow fetch result to `EmailMeta`. * * Exported so the IDLE watcher shares one definition — a second, slightly * different implementation there previously produced inconsistent records for * the same message. */ export declare function messageToEmailMeta(msg: Record): EmailMeta; /** The slice of the local mirror search needs — kept structural for testing. */ interface SearchCache { searchMessages(account: string, mailbox: string, uidValidity: string, query: string, limit?: number): { uid: number; }[]; } export default class ImapService { private connections; private labelStrategies; /** Optional local mirror used to supplement live text search. */ private cacheStore?; private labelStrategyPending; private readonly onReconnect; constructor(connections: IConnectionManager); /** * Attach the local mirror so text searches can also match what only the * cache knows — attachment filenames and porter-stemmed full text. */ setCacheStore(store: SearchCache): void; /** * Detach from the event bus. * * The server holds a single long-lived instance, so this matters mainly for * tests and for any future code that builds a service graph per request — * without it each instance leaks a listener on the shared bus. */ dispose(): void; /** * Lock a caller-supplied mailbox path. * * Trims and rejects the IMAP wildcards `*` and `%` first. Prefer this over * `client.getMailboxLock()` for any path that originated outside this * process — it keeps validation from drifting method by method, and * normalizes the path so " INBOX " and "INBOX" address one mailbox. * * Paths returned by `client.list()` are server-supplied and already valid; * those may be locked directly. */ private static lockMailbox; private getLabelStrategy; listMailboxes(accountName: string): Promise; /** * One cheap STATUS round trip, used by the cached read path as its * freshness probe (compare against the mirror's stored watermarks — the * IMAP equivalent of a conditional request). */ getMailboxStatus(accountName: string, mailbox: string): Promise<{ uidValidity?: string; uidNext?: number; highestModseq?: string; messages?: number; }>; listEmails(accountName: string, options?: { mailbox?: string; page?: number; pageSize?: number; since?: string; before?: string; from?: string; subject?: string; seen?: boolean; flagged?: boolean; hasAttachment?: boolean; answered?: boolean; }): Promise>; getEmail(accountName: string, emailId: string, mailbox?: string): Promise; getEmailFlags(accountName: string, emailId: string, mailbox?: string): Promise<{ seen: boolean; flagged: boolean; answered: boolean; labels: string[]; subject: string; from: string; date: string; }>; searchEmails(accountName: string, query: string, options?: { mailbox?: string; page?: number; pageSize?: number; to?: string; hasAttachment?: boolean; largerThan?: number; smallerThan?: number; answered?: boolean; }): Promise>; listLabels(accountName: string): Promise; addLabel(accountName: string, emailId: string, mailbox: string, label: string): Promise; removeLabel(accountName: string, emailId: string, mailbox: string, label: string): Promise; createLabel(accountName: string, name: string): Promise; deleteLabel(accountName: string, name: string): Promise; private static readonly VIRTUAL_SPECIAL_USE; private static assertRealMailbox; findEmailFolder(accountName: string, emailId: string, sourceMailbox: string): Promise<{ folders: string[]; messageId?: string; }>; moveEmail(accountName: string, emailId: string, sourceMailbox: string, destinationMailbox: string): Promise; deleteEmail(accountName: string, emailId: string, mailbox?: string, permanent?: boolean): Promise; setFlags(accountName: string, emailId: string, mailbox: string, action: 'read' | 'unread' | 'flag' | 'unflag'): Promise; /** * Tell the local mirror a mailbox changed under this process's own hands. * * Emitted only after the server acknowledged the write, so the mirror never * resyncs toward a state that was rejected. Fires after the lock is * released: the sync engine's reaction needs that same mailbox lock. */ private static emitChanged; bulkSetFlags(accountName: string, ids: number[], mailbox: string, action: 'mark_read' | 'mark_unread' | 'flag' | 'unflag'): Promise; bulkMove(accountName: string, ids: number[], mailbox: string, destination: string): Promise; bulkDelete(accountName: string, ids: number[], mailbox: string, permanent?: boolean): Promise; saveDraft(accountName: string, options: { to: string[]; subject: string; body: string; cc?: string[]; bcc?: string[]; html?: boolean; inReplyTo?: string; references?: string[]; }): Promise<{ id: number; mailbox: string; }>; /** * Fetch a draft message for sending. * Returns the parsed draft with recipients and content. */ fetchDraft(accountName: string, emailId: number, mailbox?: string): Promise<{ email: Email; mailbox: string; }>; /** * APPEND an already-sent message to the account's Sent folder. * * SMTP delivers; it does not file a copy. Providers that capture sent mail * server-side (Gmail, Outlook) make this unnecessary — and doing it anyway * would show the user two copies — so the caller decides, behind an opt-in * setting, and this method only carries it out. * * @returns the folder the copy landed in. */ appendToSent(accountName: string, raw: Buffer): Promise; /** Delete a draft after it has been sent. */ deleteDraft(accountName: string, emailId: number, mailbox: string): Promise; createMailbox(accountName: string, folderPath: string): Promise; renameMailbox(accountName: string, folderPath: string, newPath: string): Promise; deleteMailbox(accountName: string, folderPath: string): Promise; downloadAttachment(accountName: string, emailId: string, mailbox: string, selector: string | { filename?: string; partId?: string; }, maxSizeBytes?: number): Promise<{ filename: string; mimeType: string; size: number; contentBase64: string; }>; /** * Download and save all non-ICS attachments from an email to a local directory. * Returns metadata including the saved file paths and file:// URLs. * * Attachments larger than maxSizeBytes (default 25 MB) are skipped. */ saveEmailAttachments(accountName: string, emailId: string, mailbox: string, destDir: string, maxSizeBytes?: number): Promise<{ filename: string; localPath: string; fileUrl: string; mimeType: string; size: number; }[]>; /** * Reconstruct an email thread by following References / In-Reply-To chains. * Searches by Message-ID header for each reference and returns messages in * chronological order. Caps at MAX_THREAD_MESSAGES to prevent runaway chains. */ getThread(accountName: string, messageId: string, mailbox?: string): Promise<{ threadId: string; messages: Email[]; participants: EmailAddress[]; messageCount: number; }>; extractContacts(accountName: string, options?: { mailbox?: string; limit?: number; }): Promise; getEmailStats(accountName: string, mailbox: string, period: 'day' | 'week' | 'month'): Promise; getQuota(accountName: string): Promise; getCapabilities(accountName: string): Promise; getCalendarParts(accountName: string, mailbox: string, emailId: string): Promise; /** * Recursively find body parts with text/calendar content type. */ private findCalendarParts; } export {}; //# sourceMappingURL=imap.service.d.ts.map