mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
Stalwart 0.16 (0.16.6 to 0.16.23) sends a change of one occurrence of a meeting with that occurrence's SEQUENCE raised, but does not keep the raise itself: in its organizer update code the list of components to raise is shadowed for changes of single occurrences. A later change of every occurrence that does not raise the number again - a new title, another description - then went out with the old number for that occurrence, and the attendee's server refused the whole message as out of sequence. The attendee got the "Updated invitation" mail, but their calendar stayed as it was (live on 0.16.23; 1.0 raises and keeps the number itself). A change of every occurrence that the user as organizer sends to the attendees, of something they see, now raises the SEQUENCE of the series and of each changed occurrence by one, on a series that has changed occurrences. One up from what the organizer's copy holds is at least what the attendees got, and it keeps the series and its occurrences on equal numbers, which 0.16 needs to list an occurrence once. 1.0 raises the number of every occurrence the same way for a change it sends, and keeps a number it would have given: the result there is the same (live).
575 lines
32 KiB
TypeScript
575 lines
32 KiB
TypeScript
import type { Attachment, Email, Mailbox, MailboxRights, StateChange, AccountStates, CollectionChanges, ShareNotification, BusyPeriod, CalendarParticipantIdentity, CalendarEventNotification, Thread, Identity, EmailAddress, ContactCard, AddressBook, AddressBookRights, VacationResponse, Calendar, CreateCalendarOptions, CalendarRights, CalendarEvent, CalendarEventFilter, CalendarTask, FileNode, FileNodeRights, Principal, PrincipalAvailability, PrincipalListing, PushSubscription, EmailPushConfig, ScheduledEmail, SendEmailResult, SharedAccount } from "./types";
|
|
import type { SieveScript, SieveCapabilities } from "./sieve-types";
|
|
import type { FileNameRules } from "@/lib/file-name-rules";
|
|
import type { SortLevel } from "@/lib/message-list-order";
|
|
import type { AccountInfo } from "./types";
|
|
|
|
/** What `migrateKeyword` managed to do. */
|
|
export interface KeywordMigration {
|
|
/** Messages now carrying the new keyword. */
|
|
migrated: number;
|
|
/**
|
|
* Messages the server refused to update for a reason other than being gone.
|
|
* Those still carry the old keyword.
|
|
*/
|
|
refused: number;
|
|
}
|
|
|
|
export interface KeywordInfo {
|
|
id: string;
|
|
name: string;
|
|
color: string | null;
|
|
total: number;
|
|
unread: number;
|
|
isProviderLabel: boolean;
|
|
source: 'provider' | 'message';
|
|
}
|
|
|
|
export interface KeywordDiscoveryResult {
|
|
keywords: Record<string, number>;
|
|
labels: KeywordInfo[];
|
|
scanned: number;
|
|
total: number;
|
|
complete: boolean;
|
|
}
|
|
|
|
/** What `getBlobsAsBase64` brought back. */
|
|
export interface BlobBatch {
|
|
/** Contents as base64 by blobId, for every blob the server returned. */
|
|
data: Map<string, string>;
|
|
/** Blobs left unfetched because a request failed; they can still be downloaded one by one. */
|
|
failed: string[];
|
|
}
|
|
|
|
/**
|
|
* 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 CalendarEventUpdateOptions {
|
|
/**
|
|
* Send `sequence` instead of stripping it with the other server-managed
|
|
* fields. Only for a patch that creates a recurrence override and must carry
|
|
* the series' sequence (see buildOccurrenceRsvpPatch), or a change of a
|
|
* whole series that raises it (see withRaisedSequences).
|
|
*/
|
|
keepSequence?: boolean;
|
|
}
|
|
|
|
export interface CalendarQueryOptions {
|
|
/** Stops a query of several requests (a long range, several accounts) between them. */
|
|
signal?: AbortSignal;
|
|
}
|
|
|
|
/**
|
|
* FileNode trees by account, shared by the copies of one paste so that each
|
|
* account is read once rather than once per pasted item (see copyFileNode).
|
|
* Start each paste with an empty map.
|
|
*/
|
|
export type FileNodeListings = Map<string, Promise<FileNode[]>>;
|
|
|
|
export interface IJMAPClient {
|
|
// ── Connection lifecycle ──────────────────────────────────────
|
|
connect(): Promise<void>;
|
|
disconnect(): void;
|
|
reconnect(): Promise<void>;
|
|
ping(): Promise<void>;
|
|
|
|
// ── Session / auth accessors ──────────────────────────────────
|
|
getServerUrl(): string;
|
|
getAuthHeader(): string;
|
|
updateAccessToken(token: string): void;
|
|
upgradeToBearer(accessToken: string, onRefresh?: () => Promise<string | null>): void;
|
|
enableTotpReauth(basePassword: string, callback: () => Promise<string | null>): void;
|
|
updateBasicAuth(newPassword: string): void;
|
|
getAccountId(): string;
|
|
getUsername(): string;
|
|
|
|
// ── Capabilities ──────────────────────────────────────────────
|
|
getCapabilities(): Record<string, unknown>;
|
|
hasAccountCapability(capability: string, accountId?: string): boolean;
|
|
/** How the session describes an account; null when it doesn't list the id. */
|
|
getAccountInfo(accountId: string): AccountInfo | null;
|
|
/** Whether sender and subject sorted lists of the account keep the server's order (Stalwart 1.0). */
|
|
keepsServerTextOrder?(accountId?: string): boolean;
|
|
/** FileNode naming rules the server publishes (Stalwart 0.16.6+), if any. */
|
|
getFileNameRules?(accountId?: string): FileNameRules | null;
|
|
/** How deep a FileNode may sit (Stalwart 1.0: 64), or null for no limit. */
|
|
getMaxFileNodeDepth?(accountId?: string): number | null;
|
|
getMaxSizeUpload(): number;
|
|
/** RFC 8621 maxSizeAttachmentsPerEmail of the mail account (0 = unknown). */
|
|
getMaxSizeAttachmentsPerEmail?(): number;
|
|
/** Calendars capability maxParticipantsPerEvent of the account (null = not advertised). */
|
|
getMaxParticipantsPerEvent?(accountId?: string): number | null;
|
|
getMaxCallsInRequest(): number;
|
|
getMaxObjectsInGet(): number;
|
|
getMaxObjectsInSet(): number;
|
|
/** RFC 8620 maxConcurrentRequests: API calls and downloads the server runs at once for the user. */
|
|
getMaxConcurrentRequests?(): 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.
|
|
// A refused object throws a jmapSetError carrying the SetError type
|
|
// (`forbidden`, `overQuota`, `notFound`...); a refused method call throws a
|
|
// plain jmapError, so callers can tell the two apart (isJmapSetError).
|
|
/** Throws when the server can't be asked - never an empty list instead. */
|
|
listPushSubscriptions(): Promise<PushSubscription[]>;
|
|
createPushSubscription(params: {
|
|
deviceClientId: string;
|
|
url: string;
|
|
types: string[];
|
|
expires?: string;
|
|
// Per-account delivery filter (draft-ietf-jmap-emailpush). Only sent when
|
|
// the server advertises urn:ietf:params:jmap:emailpush.
|
|
emailPush?: Record<string, EmailPushConfig>;
|
|
}): Promise<string>;
|
|
verifyPushSubscription(id: string, verificationCode: string): Promise<void>;
|
|
updatePushSubscription(
|
|
id: string,
|
|
patch: { expires?: string; types?: string[]; emailPush?: Record<string, EmailPushConfig> | null },
|
|
): Promise<void>;
|
|
destroyPushSubscription(id: string): Promise<void>;
|
|
|
|
// ── Quota ─────────────────────────────────────────────────────
|
|
getQuota(): Promise<{ used: number; total: number } | null>;
|
|
|
|
// ── Mailboxes ─────────────────────────────────────────────────
|
|
getMailboxes(accountId?: string): Promise<Mailbox[]>;
|
|
getAllMailboxes(): Promise<Mailbox[]>;
|
|
/**
|
|
* getAllMailboxes plus the Mailbox collection state per account, so the
|
|
* store can resolve a later push with Mailbox/changes (RFC 8620 §5.2)
|
|
* instead of re-fetching every folder tree. Optional: clients without it
|
|
* (demo) always take the full-refresh path.
|
|
*/
|
|
getAllMailboxesWithState?(): Promise<{ mailboxes: Mailbox[]; states: Record<string, string> }>;
|
|
/** Mailbox/get restricted to `ids`, mapped like getAllMailboxes (delta patching). */
|
|
getMailboxesByIds?(ids: string[], accountId?: string): Promise<Mailbox[]>;
|
|
/** Mailbox/changes since `sinceState`; null when the server cannot compute the delta. */
|
|
getMailboxChanges?(sinceState: string, accountId?: string, maxChanges?: number): Promise<CollectionChanges | null>;
|
|
/** Email/changes since `sinceState`; null when the server cannot compute the delta. */
|
|
getEmailChanges?(sinceState: string, accountId?: string, maxChanges?: number): Promise<CollectionChanges | null>;
|
|
createMailbox(name: string, parentId?: string, accountId?: string): Promise<Mailbox>;
|
|
updateMailbox(mailboxId: string, changes: { name?: string; parentId?: string | null; role?: string | null; sortOrder?: number }, accountId?: string): Promise<void>;
|
|
// `removeEmails` destroys the folder's messages too (onDestroyRemoveEmails,
|
|
// RFC 8621 §2.5) instead of failing with mailboxHasEmail.
|
|
deleteMailbox(mailboxId: string, accountId?: string, options?: { removeEmails?: boolean }): Promise<void>;
|
|
// CalendarEventNotification (draft-ietf-jmap-calendars §7): invitations,
|
|
// updates and cancellations made by other participants. Optional.
|
|
getCalendarEventNotifications?(): Promise<CalendarEventNotification[]>;
|
|
destroyCalendarEventNotifications?(ids: string[]): Promise<void>;
|
|
// ParticipantIdentity (draft-ietf-jmap-calendars §6): the addresses the
|
|
// user organises events as. Optional.
|
|
getParticipantIdentities?(): Promise<CalendarParticipantIdentity[]>;
|
|
setDefaultParticipantIdentity?(id: string): Promise<void>;
|
|
// Free/busy (Principal/getAvailability, principals:availability). Optional.
|
|
supportsAvailability?(): boolean;
|
|
mayGetAvailability?(): Promise<boolean>;
|
|
getPrincipalAvailability?(principalId: string, utcStart: Date, utcEnd: Date): Promise<BusyPeriod[]>;
|
|
getPrincipalsAvailability?(principalIds: string[], utcStart: Date, utcEnd: Date): Promise<Record<string, PrincipalAvailability>>;
|
|
// ShareNotification (RFC 9670 §3): who shared what with this user. Optional
|
|
// (demo client).
|
|
supportsShareNotifications?(): boolean;
|
|
getShareNotifications?(): Promise<ShareNotification[]>;
|
|
destroyShareNotifications?(ids: string[]): Promise<void>;
|
|
// Mailbox sharing (urn:ietf:params:jmap:mail:share). Optional: the demo
|
|
// client has no principals to share with.
|
|
supportsMailboxSharing?(accountId?: string): boolean;
|
|
getMailboxShareWith?(mailboxId: string, accountId?: string): Promise<Record<string, MailboxRights> | null>;
|
|
setMailboxShare?(mailboxId: string, principalId: string, rights: MailboxRights | null, accountId?: string): Promise<void>;
|
|
|
|
// ── 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).
|
|
// `order` is the user's configured message-list order (#718), applied
|
|
// server-side after pinned-first; see lib/message-list-order.ts.
|
|
// `state` is the Email collection state the page was read at (RFC 8620
|
|
// §5.1), when the client reports it; the store uses it for Email/changes.
|
|
getEmails(mailboxId?: string, accountId?: string, limit?: number, position?: number, hasKeyword?: string, pinnedFirst?: boolean, extraFilter?: Record<string, unknown>, order?: SortLevel[]): Promise<{ emails: Email[]; hasMore: boolean; total: number; state?: string }>;
|
|
/**
|
|
* Sort properties the account's server advertises for Email/query
|
|
* (`emailQuerySortOptions` in the mail capability, RFC 8621 §1.3), or null
|
|
* when the server does not say. Used to grey out keyword-based ordering
|
|
* criteria the server cannot honour.
|
|
*/
|
|
getEmailQuerySortOptions?(accountId?: string): string[] | null;
|
|
getEmailsInMailbox(mailboxId: string): Promise<Email[]>;
|
|
getEmail(emailId: string, accountId?: string): Promise<Email | null>;
|
|
getSomeEmails(emailsId: string[], accountId?: string): Promise<Email[]>
|
|
/**
|
|
* Attachment parts per email id for list-row chips, fetched lazily because
|
|
* the server reads each full raw message to answer it (#1089). Optional:
|
|
* clients whose list emails already carry `attachments` need not implement it.
|
|
*/
|
|
getEmailAttachments?(emailIds: string[], accountId?: string): Promise<Map<string, Attachment[]>>;
|
|
/**
|
|
* Total / unread message counts per tag id. `accountId` scopes the count to
|
|
* a group/shared account reached through this client (defaults to the
|
|
* client's own account); the store sums it over every account a tag view
|
|
* spans (#1038).
|
|
*/
|
|
getTagCounts(tagIds: string[], accountId?: string): Promise<Record<string, { total: number; unread: number }>>;
|
|
/**
|
|
* Enumerate account keywords for extensions. Servers supporting Keyword/get
|
|
* can return exact counts and provider-label metadata; other servers use the
|
|
* bounded scan.
|
|
*/
|
|
getKeywords(options?: {
|
|
limit?: number;
|
|
onProgress?: (scanned: number, total: number) => void;
|
|
signal?: AbortSignal;
|
|
}): Promise<KeywordDiscoveryResult>;
|
|
/**
|
|
* Every keyword currently set on the account's messages and how many of the
|
|
* walked messages carry it, found by walking the message list - JMAP offers no
|
|
* way to ask for the keywords in use. `complete` is false when `limit` (or an
|
|
* abort) ended the walk early, which also makes every count a floor.
|
|
*/
|
|
discoverKeywords(options?: {
|
|
limit?: number;
|
|
onProgress?: (scanned: number, total: number) => void;
|
|
signal?: AbortSignal;
|
|
}): Promise<{ keywords: Record<string, number>; scanned: number; total: number; complete: boolean }>;
|
|
/** Per-tab unread counts for message-list category tabs (filter = resolved tab fragment, null = unfiltered). */
|
|
getCategoryUnreadCounts(mailboxId: string, tabs: Array<{ id: string; filter: Record<string, unknown> | null }>, accountId?: string): Promise<Record<string, number>>;
|
|
searchEmails(query: string, mailboxId?: string, accountId?: string, limit?: number, position?: number): Promise<{ emails: Email[]; hasMore: boolean; total: number }>;
|
|
advancedSearchEmails(
|
|
filter: Record<string, unknown>,
|
|
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<Array<{ name: string; email: string }>>;
|
|
|
|
// ── Email mutations ───────────────────────────────────────────
|
|
markAsRead(emailId: string, read?: boolean, accountId?: string): Promise<void>;
|
|
batchMarkAsRead(emailIds: string[], read?: boolean, accountId?: string): Promise<void>;
|
|
toggleStar(emailId: string, starred: boolean, accountId?: string): Promise<void>;
|
|
updateEmailKeywords(emailId: string, keywords: Record<string, boolean>, accountId?: string): Promise<void>;
|
|
setKeyword(emailId: string, keyword: string, accountId?: string): Promise<void>;
|
|
removeKeyword(emailId: string, keyword: string, accountId?: string): Promise<void>;
|
|
/** Apply one `keywords/<name>` patch fragment (true=add, null=remove) to many messages in a single Email/set. */
|
|
batchUpdateKeywords(emailIds: string[], patch: Record<string, boolean | null>, accountId?: string): Promise<void>;
|
|
/**
|
|
* Rewrite one keyword to another on every message that carries it, in
|
|
* batches. Fails outright only when the server refuses the whole call;
|
|
* per-message refusals are reported in the result, since the messages
|
|
* migrated alongside them stay migrated.
|
|
*/
|
|
migrateKeyword(oldKeyword: string, newKeyword: string): Promise<KeywordMigration>;
|
|
deleteEmail(emailId: string, accountId?: string): Promise<void>;
|
|
moveToTrash(emailId: string, trashMailboxId: string, accountId?: string, markAsRead?: boolean): Promise<void>;
|
|
batchDeleteEmails(emailIds: string[], accountId?: string): Promise<void>;
|
|
batchMoveEmails(emailIds: string[], toMailboxId: string, accountId?: string, markAsRead?: boolean): Promise<void>;
|
|
batchArchiveEmails(
|
|
emails: Array<{ id: string; receivedAt: string }>,
|
|
archiveMailboxId: string,
|
|
mode: 'single' | 'year' | 'month',
|
|
existingMailboxes: Mailbox[],
|
|
accountId?: string,
|
|
): Promise<void>;
|
|
moveEmail(emailId: string, toMailboxId: string, accountId?: string): Promise<void>;
|
|
emptyMailbox(mailboxId: string, accountId?: string): Promise<number>;
|
|
markMailboxAsRead(mailboxId: string, accountId?: string): Promise<number>;
|
|
markAllAsRead(excludeMailboxIds?: string[], accountId?: string): Promise<number>;
|
|
markAsSpam(emailId: string, accountId?: string, markAsRead?: boolean): Promise<void>;
|
|
undoSpam(emailId: string, originalMailboxId: string, accountId?: string): Promise<void>;
|
|
|
|
// ── Threads ───────────────────────────────────────────────────
|
|
getThread(threadId: string, accountId?: string): Promise<Thread | null>;
|
|
getThreads(threadIds: string[], accountId?: string): Promise<Thread[]>;
|
|
getThreadEmails(threadId: string, accountId?: string): Promise<Email[]>;
|
|
|
|
// ── 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,
|
|
// The account the previous version (`draftId`) lives in, when it is not
|
|
// the client's own; the new version is always saved in the own account.
|
|
draftAccountId?: string,
|
|
): Promise<string>;
|
|
|
|
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,
|
|
// The SMTP MAIL FROM wanted instead of the identity's address (a From
|
|
// override). Sent through the identity that owns it when there is one;
|
|
// a server that refuses it gets the identity's address instead.
|
|
envelopeMailFrom?: string,
|
|
// requestDsn / requireTls map to RFC 3461 / RFC 8689 envelope parameters
|
|
// and need the matching `submissionExtensions` entry (see
|
|
// supportsSubmissionExtension). `draftAccountId` is the account `draftId`
|
|
// lives in, when it is not the client's own (see createDraft).
|
|
options?: { requestReadReceipt?: boolean; requestDsn?: boolean; requireTls?: boolean; draftAccountId?: string },
|
|
): Promise<SendEmailResult>;
|
|
/** Whether the submission account advertises an SMTP extension ("DSN", "REQUIRETLS", …). */
|
|
supportsSubmissionExtension?(extension: string, accountId?: string): boolean;
|
|
|
|
importEmail(
|
|
blobId: string,
|
|
mailboxIds: Record<string, boolean>,
|
|
keywords?: Record<string, boolean>,
|
|
accountId?: string,
|
|
): Promise<string | null>;
|
|
|
|
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<void>;
|
|
|
|
sendRawEmail(blob: Blob, identityId: string, sentMailboxId: string, draftMailboxId?: string, delayedUntil?: string, envelopeRecipients?: string[]): Promise<SendEmailResult>;
|
|
submitRawEmail(blob: Blob, identityId: string, delayedUntil?: string, envelopeRecipients?: string[]): Promise<SendEmailResult>;
|
|
getScheduledEmails(limit?: number, position?: number): Promise<{ emails: ScheduledEmail[]; hasMore: boolean; total: number; totalByAccount?: Record<string, number>; nextPosition: number }>;
|
|
/** Cancels a pending submission, then destroys its record. */
|
|
cancelEmailSubmission(submissionId: string, accountId?: string): Promise<void>;
|
|
rescheduleEmailSubmission(submissionId: string, emailId: string, identityId: string, delayedUntil: string, accountId?: string): Promise<SendEmailResult>;
|
|
/**
|
|
* Destroys EmailSubmission records, which never stops their delivery - but
|
|
* a pending one could no longer be cancelled, so only pass records that
|
|
* can't be. Resolves with the ids destroyed; never rejects.
|
|
*/
|
|
destroyEmailSubmissions(ids: string[], accountId?: string): Promise<string[]>;
|
|
/**
|
|
* Destroys the canceled and delivered submission records of every
|
|
* submission account, at most once per account every few minutes unless
|
|
* `force` is set. Resolves with the number destroyed; never rejects.
|
|
*/
|
|
pruneEmailSubmissions(options?: { force?: boolean }): Promise<number>;
|
|
/**
|
|
* `sentMailboxId` is accepted for backwards compatibility but ignored: the
|
|
* message is placed in Drafts only. `accountId` is the account the message
|
|
* lives in (a group or shared one for mail sent from its identity), and
|
|
* `draftMailboxId` must be that account's Drafts.
|
|
*/
|
|
restoreEmailToDraft(emailId: string, draftMailboxId: string, sentMailboxId?: string, accountId?: string): Promise<void>;
|
|
|
|
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<void>;
|
|
|
|
sendImipInvitation(event: CalendarEvent): Promise<void>;
|
|
sendImipCancellation(event: CalendarEvent): Promise<void>;
|
|
|
|
// ── 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<Blob>;
|
|
fetchBlobAsObjectUrl(blobId: string, name?: string, type?: string, accountId?: string): Promise<string>;
|
|
fetchBlobArrayBuffer(blobId: string, name?: string, type?: string, accountId?: string, rangeHeader?: number): Promise<ArrayBuffer>;
|
|
/**
|
|
* Many blobs at once through Blob/get (RFC 9404), for exports; null when
|
|
* the session doesn't advertise the blob capability.
|
|
*/
|
|
getBlobsAsBase64?(blobIds: string[], accountId?: string): Promise<BlobBatch | null>;
|
|
downloadBlob(blobId: string, name?: string, type?: string, accountId?: string): Promise<void>;
|
|
|
|
// ── Identities ────────────────────────────────────────────────
|
|
getIdentities(): Promise<Identity[]>;
|
|
createIdentity(
|
|
name: string,
|
|
email: string,
|
|
replyTo?: EmailAddress[] | null,
|
|
bcc?: EmailAddress[] | null,
|
|
textSignature?: string | null,
|
|
htmlSignature?: string | null,
|
|
): Promise<Identity>;
|
|
updateIdentity(
|
|
identityId: string,
|
|
updates: {
|
|
name?: string | null;
|
|
replyTo?: EmailAddress[] | null;
|
|
bcc?: EmailAddress[] | null;
|
|
textSignature?: string | null;
|
|
htmlSignature?: string | null;
|
|
},
|
|
): Promise<void>;
|
|
deleteIdentity(identityId: string): Promise<void>;
|
|
|
|
// ── Vacation ──────────────────────────────────────────────────
|
|
getVacationResponse(accountId?: string): Promise<VacationResponse>;
|
|
setVacationResponse(updates: Partial<VacationResponse>, accountId?: string): Promise<void>;
|
|
|
|
// ── Contacts ──────────────────────────────────────────────────
|
|
getContactsAccountId(): string;
|
|
getAddressBooks(options?: { throwOnError?: boolean }): Promise<AddressBook[]>;
|
|
getAllAddressBooks(): Promise<AddressBook[]>;
|
|
createAddressBook(name: string): Promise<AddressBook>;
|
|
updateAddressBook(addressBookId: string, updates: Partial<AddressBook>, targetAccountId?: string): Promise<void>;
|
|
setDefaultAddressBook(addressBookId: string, targetAccountId?: string): Promise<void>;
|
|
deleteAddressBook(addressBookId: string, targetAccountId?: string, options?: { removeContents?: boolean }): Promise<void>;
|
|
getContacts(addressBookId?: string, options?: { throwOnError?: boolean }): Promise<ContactCard[]>;
|
|
getAllContacts(): Promise<ContactCard[]>;
|
|
getContact(contactId: string, accountId?: string): Promise<ContactCard | null>;
|
|
createContact(contact: Partial<ContactCard>, targetAccountId?: string): Promise<ContactCard>;
|
|
updateContact(contactId: string, updates: Partial<ContactCard>, targetAccountId?: string): Promise<void>;
|
|
deleteContact(contactId: string, targetAccountId?: string): Promise<void>;
|
|
/** Whether ContactCard/set in the account takes media entries that carry a blobId (Stalwart 1.0). */
|
|
supportsContactBlobMedia?(accountId?: string): boolean;
|
|
/** The account refused a blobId in media: supportsContactBlobMedia answers false from now on. */
|
|
rejectContactBlobMedia?(accountId?: string): void;
|
|
searchContacts(query: string): Promise<ContactCard[]>;
|
|
|
|
// ── Calendars ─────────────────────────────────────────────────
|
|
getCalendarsAccountId(): string;
|
|
getCalendars(): Promise<Calendar[]>;
|
|
getAllCalendars(): Promise<Calendar[]>;
|
|
createCalendar(calendar: Partial<Calendar>, targetAccountId?: string, options?: CreateCalendarOptions): Promise<Calendar>;
|
|
updateCalendar(calendarId: string, updates: Partial<Calendar>, targetAccountId?: string): Promise<void>;
|
|
setDefaultCalendar(calendarId: string, targetAccountId?: string): Promise<void>;
|
|
deleteCalendar(calendarId: string, targetAccountId?: string): Promise<void>;
|
|
getCalendarEvents(calendarIds?: string[], targetAccountId?: string): Promise<CalendarEvent[]>;
|
|
getCalendarEvent(id: string, targetAccountId?: string): Promise<CalendarEvent | null>;
|
|
createCalendarEvent(event: Partial<CalendarEvent>, sendSchedulingMessages?: boolean, targetAccountId?: string): Promise<CalendarEvent>;
|
|
batchCreateCalendarEvents(events: Partial<CalendarEvent>[], targetAccountId?: string): Promise<{ created: CalendarEvent[]; failed: string[]; notCreated: Record<string, { type?: string; description?: string }> }>;
|
|
updateCalendarEvent(
|
|
eventId: string,
|
|
updates: Partial<CalendarEvent>,
|
|
sendSchedulingMessages?: boolean,
|
|
targetAccountId?: string,
|
|
options?: CalendarEventUpdateOptions,
|
|
): Promise<void>;
|
|
deleteCalendarEvent(eventId: string, sendSchedulingMessages?: boolean, targetAccountId?: string): Promise<void>;
|
|
batchDeleteCalendarEvents(eventIds: string[], targetAccountId?: string): Promise<{ destroyed: string[]; notDestroyed: Record<string, { type?: string; description?: string }> }>;
|
|
queryCalendarEvents(filter: CalendarEventFilter, sort?: Array<{ property: string; isAscending: boolean }>, limit?: number, targetAccountId?: string, options?: CalendarQueryOptions): Promise<CalendarEvent[]>;
|
|
queryAllCalendarEvents(filter: CalendarEventFilter, sort?: Array<{ property: string; isAscending: boolean }>, limit?: number, options?: CalendarQueryOptions): Promise<CalendarEvent[]>;
|
|
parseCalendarEvents(accountId: string, blobId: string): Promise<Partial<CalendarEvent>[]>;
|
|
|
|
// ── Calendar Tasks ────────────────────────────────────────────
|
|
getCalendarTasks(calendarIds?: string[], targetAccountId?: string): Promise<CalendarTask[]>;
|
|
createCalendarTask(task: Partial<CalendarTask>, targetAccountId?: string): Promise<CalendarTask>;
|
|
updateCalendarTask(taskId: string, updates: Partial<CalendarTask>, targetAccountId?: string): Promise<void>;
|
|
deleteCalendarTask(taskId: string, targetAccountId?: string): Promise<void>;
|
|
|
|
// ── Sharing (RFC 9670 Principals) ─────────────────────────────
|
|
supportsPrincipals(): boolean;
|
|
getPrincipals(targetAccountId?: string): Promise<Principal[]>;
|
|
// The same list with how complete it is. Optional (demo client).
|
|
listPrincipals?(targetAccountId?: string): Promise<PrincipalListing>;
|
|
setCalendarShare(calendarId: string, principalId: string, rights: CalendarRights | null, targetAccountId?: string): Promise<void>;
|
|
setAddressBookShare(addressBookId: string, principalId: string, rights: AddressBookRights | null, targetAccountId?: string): Promise<void>;
|
|
setFileNodeShare(fileNodeId: string, principalId: string, rights: FileNodeRights | null, targetAccountId?: string): Promise<void>;
|
|
|
|
// ── 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<SieveScript[]>;
|
|
getSieveScriptContent(blobId: string, accountId?: string): Promise<string>;
|
|
createSieveScript(name: string, content: string, activate?: boolean, accountId?: string): Promise<SieveScript>;
|
|
updateSieveScript(scriptId: string, content: string, activate?: boolean, accountId?: string): Promise<void>;
|
|
deleteSieveScript(scriptId: string, accountId?: string): Promise<void>;
|
|
validateSieveScript(content: string, accountId?: string): Promise<{ isValid: boolean; errors?: string[] }>;
|
|
|
|
// ── Files (WebDAV / FileNode) ─────────────────────────────────
|
|
getFilesAccountId(): string;
|
|
probeFileNodeSupport(): Promise<boolean>;
|
|
listFileNodes(parentId: string | null): Promise<FileNode[]>;
|
|
listAllFileNodes(): Promise<FileNode[]>;
|
|
listAllFileNodesAcrossAccounts(): Promise<FileNode[]>;
|
|
getFileNodes(ids: string[] | null, properties?: string[]): Promise<FileNode[]>;
|
|
createFileDirectory(name: string, parentId: string | null): Promise<FileNode>;
|
|
createFileNode(name: string, blobId: string, type: string, size: number, parentId: string | null): Promise<FileNode>;
|
|
updateFileNode(id: string, updates: Partial<Pick<FileNode, 'name' | 'parentId'>>): Promise<void>;
|
|
updateFileNodes(updates: Record<string, Partial<Pick<FileNode, 'name' | 'parentId'>>>): Promise<{ updated: string[]; notUpdated: Record<string, string> }>;
|
|
destroyFileNodes(ids: string[]): Promise<{ destroyed: string[]; notDestroyed: string[] }>;
|
|
copyFileNode(id: string, newName: string, parentId: string | null, listings?: FileNodeListings): Promise<FileNode>;
|
|
|
|
// ── S/MIME raw-email helpers ──────────────────────────────────
|
|
importRawEmail(blob: Blob, mailboxIds: Record<string, boolean>, keywords?: Record<string, boolean>, accountId?: string): Promise<string>;
|
|
submitEmail(emailId: string, identityId: string): Promise<void>;
|
|
/**
|
|
* Server-side move of one email across accounts reachable through THIS client
|
|
* (JMAP `Email/copy` + destroy-original). Used for delegated/shared folders,
|
|
* where the two accounts share a client but a client can't stage a blob in a
|
|
* delegated account (so the blob copy+import path doesn't work). Returns the
|
|
* new email id in the destination account.
|
|
*/
|
|
copyEmailAcrossAccounts(emailId: string, fromAccountId: string, toAccountId: string, destMailboxId: string): Promise<string>;
|
|
}
|