import type { Email, Mailbox, StateChange, AccountStates, Thread, Identity, EmailAddress, ContactCard, AddressBook, AddressBookRights, VacationResponse, Calendar, CalendarRights, CalendarEvent, CalendarEventFilter, CalendarTask, FileNode, FileNodeRights, Principal, PushSubscription, ScheduledEmail, SendEmailResult, SharedAccount } from "./types"; import type { SieveScript, SieveCapabilities } from "./sieve-types"; /** * Interface defining the public JMAP client contract. * * Both the real `JMAPClient` (network-backed) and `DemoJMAPClient` * (in-memory/browser-only) implement this interface so that stores * and UI code never need to know which one is active. */ export interface IJMAPClient { // ── Connection lifecycle ────────────────────────────────────── connect(): Promise; disconnect(): void; reconnect(): Promise; ping(): Promise; // ── Session / auth accessors ────────────────────────────────── getServerUrl(): string; getAuthHeader(): string; updateAccessToken(token: string): void; upgradeToBearer(accessToken: string, onRefresh?: () => Promise): void; enableTotpReauth(basePassword: string, callback: () => Promise): void; updateBasicAuth(newPassword: string): void; getAccountId(): string; getUsername(): string; // ── Capabilities ────────────────────────────────────────────── getCapabilities(): Record; hasAccountCapability(capability: string, accountId?: string): boolean; getMaxSizeUpload(): number; getMaxCallsInRequest(): number; getMaxObjectsInGet(): number; getMaxDelayedSend(accountId?: string): number; hasDelayedSend(accountId?: string): boolean; getEventSourceUrl(): string | null; supportsEmailSubmission(): boolean; supportsQuota(): boolean; supportsVacationResponse(): boolean; supportsContacts(): boolean; supportsCalendars(): boolean; supportsSieve(): boolean; supportsFiles(accountId?: string): boolean; // ── Push / state ────────────────────────────────────────────── setupPushNotifications(): boolean; closePushNotifications(): void; onConnectionChange(callback: (connected: boolean) => void): void; onRateLimit(callback: (rateLimited: boolean, retryAfterMs: number) => void): void; isRateLimited(): boolean; getRateLimitRemainingMs(): number; onStateChange(callback: (change: StateChange) => void): void; getLastStates(): AccountStates; setLastStates(states: AccountStates): void; // ── PushSubscription (RFC 8620 §7.2) ─────────────────────────── // Browser-driven Web Push setup: register a relay URL the JMAP server can // forward StateChange events to. Mobile uses the same primitives. listPushSubscriptions(): Promise; createPushSubscription(params: { deviceClientId: string; url: string; types: string[]; expires?: string; }): Promise; verifyPushSubscription(id: string, verificationCode: string): Promise; updatePushSubscription(id: string, patch: { expires?: string; types?: string[] }): Promise; destroyPushSubscription(id: string): Promise; // ── Quota ───────────────────────────────────────────────────── getQuota(): Promise<{ used: number; total: number } | null>; // ── Mailboxes ───────────────────────────────────────────────── getMailboxes(accountId?: string): Promise; getAllMailboxes(): Promise; createMailbox(name: string, parentId?: string, accountId?: string): Promise; updateMailbox(mailboxId: string, changes: { name?: string; parentId?: string | null; role?: string | null; sortOrder?: number }): Promise; deleteMailbox(mailboxId: string): Promise; // ── Emails ──────────────────────────────────────────────────── // `pinnedFirst` sorts emails carrying the $pinned keyword to the top // (server-side hasKeyword sort comparator, RFC 8621), then receivedAt desc. // `extraFilter` is an arbitrary JMAP FilterCondition/FilterOperator ANDed // into the view - used by the message-list category tabs (search-based). getEmails(mailboxId?: string, accountId?: string, limit?: number, position?: number, hasKeyword?: string, pinnedFirst?: boolean, extraFilter?: Record): Promise<{ emails: Email[]; hasMore: boolean; total: number }>; getEmailsInMailbox(mailboxId: string): Promise; getEmail(emailId: string, accountId?: string): Promise; getSomeEmails(emailsId: string[], accountId?: string): Promise getTagCounts(tagIds: string[]): Promise>; /** Per-tab unread counts for message-list category tabs (filter = resolved tab fragment, null = unfiltered). */ getCategoryUnreadCounts(mailboxId: string, tabs: Array<{ id: string; filter: Record | null }>, accountId?: string): Promise>; searchEmails(query: string, mailboxId?: string, accountId?: string, limit?: number, position?: number): Promise<{ emails: Email[]; hasMore: boolean; total: number }>; advancedSearchEmails( filter: Record, accountId?: string, limit?: number, position?: number, ): Promise<{ emails: Email[]; hasMore: boolean; total: number }>; /** * Lean recipient search for compose autocomplete ("search the server" action): * finds messages in `sentMailboxId` whose to/cc matches `query` and returns * only the matching addresses (fetches just the `to`/`cc` properties - no * bodies or attachments), deduped. */ searchSentRecipients(query: string, sentMailboxId: string, accountId?: string, limit?: number): Promise>; // ── Email mutations ─────────────────────────────────────────── markAsRead(emailId: string, read?: boolean, accountId?: string): Promise; batchMarkAsRead(emailIds: string[], read?: boolean, accountId?: string): Promise; toggleStar(emailId: string, starred: boolean, accountId?: string): Promise; updateEmailKeywords(emailId: string, keywords: Record, accountId?: string): Promise; setKeyword(emailId: string, keyword: string, accountId?: string): Promise; removeKeyword(emailId: string, keyword: string, accountId?: string): Promise; /** Apply one `keywords/` patch fragment (true=add, null=remove) to many messages in a single Email/set. */ batchUpdateKeywords(emailIds: string[], patch: Record, accountId?: string): Promise; migrateKeyword(oldKeyword: string, newKeyword: string): Promise; deleteEmail(emailId: string, accountId?: string): Promise; moveToTrash(emailId: string, trashMailboxId: string, accountId?: string, markAsRead?: boolean): Promise; batchDeleteEmails(emailIds: string[], accountId?: string): Promise; batchMoveEmails(emailIds: string[], toMailboxId: string, accountId?: string, markAsRead?: boolean): Promise; batchArchiveEmails( emails: Array<{ id: string; receivedAt: string }>, archiveMailboxId: string, mode: 'single' | 'year' | 'month', existingMailboxes: Mailbox[], accountId?: string, ): Promise; moveEmail(emailId: string, toMailboxId: string, accountId?: string): Promise; emptyMailbox(mailboxId: string, accountId?: string): Promise; markMailboxAsRead(mailboxId: string, accountId?: string): Promise; markAllAsRead(excludeMailboxIds?: string[], accountId?: string): Promise; markAsSpam(emailId: string, accountId?: string, markAsRead?: boolean): Promise; undoSpam(emailId: string, originalMailboxId: string, accountId?: string): Promise; // ── Threads ─────────────────────────────────────────────────── getThread(threadId: string, accountId?: string): Promise; getThreads(threadIds: string[], accountId?: string): Promise; getThreadEmails(threadId: string, accountId?: string): Promise; // ── Compose / Send ──────────────────────────────────────────── createDraft( to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string, attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>, fromName?: string, htmlBody?: string, ): Promise; sendEmail( to: string[], subject: string, body: string, cc?: string[], bcc?: string[], identityId?: string, fromEmail?: string, draftId?: string, fromName?: string, htmlBody?: string, attachments?: Array<{ blobId: string; name: string; type: string; size: number; disposition?: 'attachment' | 'inline'; cid?: string }>, inReplyTo?: string[], references?: string[], delayedUntil?: string, envelopeMailFrom?: string, options?: { requestReadReceipt?: boolean }, ): Promise; importEmail( blobId: string, mailboxIds: Record, keywords?: Record, accountId?: string, ): Promise; sendReadReceipt(params: { to: string; fromEmail: string; fromName?: string; identityId: string; originalMessageId?: string | string[]; originalSubject?: string; originalRecipient?: string; automatic?: boolean; accountId?: string; subject?: string; humanText?: string; }): Promise; sendRawEmail(blob: Blob, identityId: string, sentMailboxId: string, draftMailboxId?: string, delayedUntil?: string, envelopeRecipients?: string[]): Promise; submitRawEmail(blob: Blob, identityId: string, delayedUntil?: string, envelopeRecipients?: string[]): Promise; getScheduledEmails(limit?: number, position?: number): Promise<{ emails: ScheduledEmail[]; hasMore: boolean; total: number; nextPosition: number }>; cancelEmailSubmission(submissionId: string): Promise; rescheduleEmailSubmission(submissionId: string, emailId: string, identityId: string, delayedUntil: string): Promise; /** `sentMailboxId` is accepted for backwards compatibility but ignored: the message is placed in Drafts only. */ restoreEmailToDraft(emailId: string, draftMailboxId: string, sentMailboxId?: string): Promise; sendImipReply(opts: { organizerEmail: string; organizerName?: string; attendeeEmail: string; attendeeName?: string; uid: string; summary?: string; dtStart?: string; dtEnd?: string; timeZone?: string; isAllDay?: boolean; sequence?: number; status: 'ACCEPTED' | 'TENTATIVE' | 'DECLINED'; identityId?: string; }): Promise; sendImipInvitation(event: CalendarEvent): Promise; sendImipCancellation(event: CalendarEvent): Promise; // ── Blobs ───────────────────────────────────────────────────── uploadBlob( file: File, optsOrAccountId?: | string | { accountId?: string; onProgress?: (loaded: number, total: number) => void; signal?: AbortSignal; }, ): Promise<{ blobId: string; size: number; type: string }>; getBlobDownloadUrl(blobId: string, name?: string, type?: string, accountId?: string): string; fetchBlob(blobId: string, name?: string, type?: string, accountId?: string): Promise; fetchBlobAsObjectUrl(blobId: string, name?: string, type?: string, accountId?: string): Promise; fetchBlobArrayBuffer(blobId: string, name?: string, type?: string, accountId?: string): Promise; downloadBlob(blobId: string, name?: string, type?: string, accountId?: string): Promise; // ── Identities ──────────────────────────────────────────────── getIdentities(): Promise; createIdentity( name: string, email: string, replyTo?: EmailAddress[] | null, bcc?: EmailAddress[] | null, textSignature?: string | null, htmlSignature?: string | null, ): Promise; updateIdentity( identityId: string, updates: { name?: string | null; replyTo?: EmailAddress[] | null; bcc?: EmailAddress[] | null; textSignature?: string | null; htmlSignature?: string | null; }, ): Promise; deleteIdentity(identityId: string): Promise; // ── Vacation ────────────────────────────────────────────────── getVacationResponse(accountId?: string): Promise; setVacationResponse(updates: Partial, accountId?: string): Promise; // ── Contacts ────────────────────────────────────────────────── getContactsAccountId(): string; getAddressBooks(): Promise; getAllAddressBooks(): Promise; createAddressBook(name: string): Promise; updateAddressBook(addressBookId: string, updates: Partial, targetAccountId?: string): Promise; deleteAddressBook(addressBookId: string, targetAccountId?: string): Promise; getContacts(addressBookId?: string): Promise; getAllContacts(): Promise; getContact(contactId: string, accountId?: string): Promise; createContact(contact: Partial, targetAccountId?: string): Promise; updateContact(contactId: string, updates: Partial, targetAccountId?: string): Promise; deleteContact(contactId: string, targetAccountId?: string): Promise; searchContacts(query: string): Promise; // ── Calendars ───────────────────────────────────────────────── getCalendarsAccountId(): string; getCalendars(): Promise; getAllCalendars(): Promise; createCalendar(calendar: Partial, targetAccountId?: string): Promise; updateCalendar(calendarId: string, updates: Partial, targetAccountId?: string): Promise; setDefaultCalendar(calendarId: string, targetAccountId?: string): Promise; deleteCalendar(calendarId: string, targetAccountId?: string): Promise; getCalendarEvents(calendarIds?: string[], targetAccountId?: string): Promise; getCalendarEvent(id: string, targetAccountId?: string): Promise; createCalendarEvent(event: Partial, sendSchedulingMessages?: boolean, targetAccountId?: string): Promise; batchCreateCalendarEvents(events: Partial[], targetAccountId?: string): Promise<{ created: CalendarEvent[]; failed: string[] }>; updateCalendarEvent( eventId: string, updates: Partial, sendSchedulingMessages?: boolean, targetAccountId?: string, ): Promise; deleteCalendarEvent(eventId: string, sendSchedulingMessages?: boolean, targetAccountId?: string): Promise; batchDeleteCalendarEvents(eventIds: string[], targetAccountId?: string): Promise<{ destroyed: string[]; notDestroyed: string[] }>; queryCalendarEvents(filter: CalendarEventFilter, sort?: Array<{ property: string; isAscending: boolean }>, limit?: number, targetAccountId?: string): Promise; queryAllCalendarEvents(filter: CalendarEventFilter, sort?: Array<{ property: string; isAscending: boolean }>, limit?: number): Promise; parseCalendarEvents(accountId: string, blobId: string): Promise[]>; // ── Calendar Tasks ──────────────────────────────────────────── getCalendarTasks(calendarIds?: string[], targetAccountId?: string): Promise; createCalendarTask(task: Partial, targetAccountId?: string): Promise; updateCalendarTask(taskId: string, updates: Partial, targetAccountId?: string): Promise; deleteCalendarTask(taskId: string, targetAccountId?: string): Promise; // ── Sharing (RFC 9670 Principals) ───────────────────────────── supportsPrincipals(): boolean; getPrincipals(targetAccountId?: string): Promise; setCalendarShare(calendarId: string, principalId: string, rights: CalendarRights | null, targetAccountId?: string): Promise; setAddressBookShare(addressBookId: string, principalId: string, rights: AddressBookRights | null, targetAccountId?: string): Promise; setFileNodeShare(fileNodeId: string, principalId: string, rights: FileNodeRights | null, targetAccountId?: string): Promise; // ── Accounts (primary + shared/group) ──────────────────────── getSharedAccounts(): SharedAccount[]; // ── Sieve / Filters ────────────────────────────────────────── getSieveAccountId(): string; getSieveAccounts(): { id: string; name: string; isPrimary: boolean }[]; getSieveCapabilities(accountId?: string): SieveCapabilities | null; getSieveScripts(accountId?: string): Promise; getSieveScriptContent(blobId: string, accountId?: string): Promise; createSieveScript(name: string, content: string, activate?: boolean, accountId?: string): Promise; updateSieveScript(scriptId: string, content: string, activate?: boolean, accountId?: string): Promise; deleteSieveScript(scriptId: string, accountId?: string): Promise; validateSieveScript(content: string, accountId?: string): Promise<{ isValid: boolean; errors?: string[] }>; // ── Files (WebDAV / FileNode) ───────────────────────────────── getFilesAccountId(): string; probeFileNodeSupport(): Promise; listFileNodes(parentId: string | null): Promise; listAllFileNodes(): Promise; listAllFileNodesAcrossAccounts(): Promise; getFileNodes(ids: string[] | null, properties?: string[]): Promise; createFileDirectory(name: string, parentId: string | null): Promise; createFileNode(name: string, blobId: string, type: string, size: number, parentId: string | null): Promise; updateFileNode(id: string, updates: Partial>): Promise; updateFileNodes(updates: Record>>): Promise<{ updated: string[]; notUpdated: Record }>; destroyFileNodes(ids: string[]): Promise<{ destroyed: string[]; notDestroyed: string[] }>; copyFileNode(id: string, newName: string, parentId: string | null): Promise; // ── S/MIME raw-email helpers ────────────────────────────────── importRawEmail(blob: Blob, mailboxIds: Record, keywords?: Record, accountId?: string): Promise; submitEmail(emailId: string, identityId: string): Promise; }