/** * A session-visible account (the user's own primary account plus any * shared/group accounts delegated to them), tagged with the capabilities it * advertises. Produced by client.getSharedAccounts(); drives the settings * "Shared with me" list and the scoped-settings tab gating. */ export interface SharedAccount { id: string; name: string; isPrimary: boolean; capabilities: { mail: boolean; sieve: boolean; calendars: boolean; contacts: boolean; filenode: boolean; }; } /** * One account as the JMAP session describes it (RFC 8620 §2), unflattened: * the raw per-account capability objects rather than SharedAccount's * booleans, for callers that need a capability's fields. */ export interface AccountInfo { /** Stalwart names an account after its principal's address, a group's too. */ name: string; isPersonal: boolean; isReadOnly: boolean; accountCapabilities: Record; } export interface Email { id: string; threadId: string; mailboxIds: Record; keywords: Record; size: number; receivedAt: string; from?: EmailAddress[]; to?: EmailAddress[]; cc?: EmailAddress[]; bcc?: EmailAddress[]; replyTo?: EmailAddress[]; subject?: string; sentAt?: string; preview?: string; // Server-side search highlighting (SearchSnippet/get, RFC 8621 §5): the // subject / body excerpt with the matched terms in , set on search // hits only. See lib/search-snippet.ts. searchSnippet?: { subject: string | null; preview: string | null }; textBody?: EmailBodyPart[]; htmlBody?: EmailBodyPart[]; bodyValues?: Record; attachments?: Attachment[]; hasAttachment: boolean; // Extended header information messageId?: string; inReplyTo?: string[]; references?: string[]; headers?: Record; // Security headers parsed authenticationResults?: AuthenticationResults; spamScore?: number; spamStatus?: string; spamLLM?: { verdict: string; explanation: string; }; // S/MIME support blobId?: string; bodyStructure?: EmailBodyPart; // Unified mailbox support - set when displaying emails from multiple accounts. // `accountId` is a DISPLAY-only reference (avatar color / label / badge) and may // hold either an AccountEntry.id (personal) or the JMAP owner id (shared). For // resolving the client + JMAP routing use the two dedicated fields below, which // are always set on aggregated emails and unambiguous. accountId?: string; accountLabel?: string; // AccountEntry.id of the logged-in client through which this email is reachable. // Always a real login key → `useAuthStore.getClientForAccount(...)` resolves it. // For personal sources this equals the account itself; for shared/group sources // it is the delegating login (the shared account has no own login). sourceClientAccountId?: string; // JMAP account id of the email's owning account (personal: the client's primary; // shared/group: the owner account). Always safe to pass as the JMAP `accountId` // argument — equal to the client's primary for personal sources, so it is a no-op // there, and triggers owner-scoped routing + mailbox-id namespacing for shared. sourceAccountId?: string; // Name of the email's originating folder, stamped for the aggregate "All …" // views (All Mail, unified, cross-account) so the list can show where each // message lives. Transient/client-only, not part of the JMAP object. sourceFolder?: string; // Client-only scheduled-send metadata, populated from EmailSubmission/query. scheduledSendAt?: string; emailSubmissionId?: string; scheduledIdentityId?: string; scheduledUndoStatus?: 'pending' | 'final' | 'canceled'; scheduledDeliveryStatus?: Record; /** * JMAP account that owns the EmailSubmission. Set when the scheduled send * lives in a shared/group account rather than the primary submission * account, so cancel/reschedule can be routed back to it. */ scheduledAccountId?: string; isScheduled?: boolean; isSmimeScheduled?: boolean; } export interface SendEmailResult { scheduled: boolean; emailId?: string; emailSubmissionId?: string; sendAt?: string; isSmime?: boolean; /** * JMAP account the EmailSubmission was created in. Differs from the primary * submission account when sending from a shared/group identity, and lets a * later undo / send-now / cancel address the right account without having to * search for it. */ submissionAccountId?: string; /** * Set when the submission succeeded but a post-send step was rejected * (the implicit onSuccessUpdateEmail filing patch, or destroying the * old draft). The mail left the server - callers should warn, not fail. */ filingError?: string; /** * Recipients the server refused while accepting the submission (its * deliveryStatus says delivered "no"). The message went out to the others * only - callers should name these instead of reporting a plain success. * When it refused every one, the send throws RecipientsRejectedError * instead. */ rejectedRecipients?: RejectedRecipient[]; } /** A recipient refused at submission, with the SMTP reply the server gave. */ export interface RejectedRecipient { email: string; /** The server's SMTP reply, e.g. "550 5.1.2 Relay not allowed."; empty when it gave none. */ smtpReply: string; } export interface ScheduledEmail extends Email { scheduledSendAt: string; emailSubmissionId: string; scheduledIdentityId: string; scheduledUndoStatus: 'pending' | 'final' | 'canceled'; scheduledDeliveryStatus?: Record; scheduledAccountId?: string; isScheduled: true; isSmimeScheduled: boolean; } export interface AuthenticationResults { spf?: { result: 'pass' | 'fail' | 'softfail' | 'neutral' | 'none' | 'temperror' | 'permerror'; domain?: string; ip?: string; /** * All SPF results when the server evaluated multiple identities (HELO and * MAIL FROM). Present only when more than one result was found; `result` * above is the most severe of these. */ all?: Array<{ result: 'pass' | 'fail' | 'softfail' | 'neutral' | 'none' | 'temperror' | 'permerror'; domain?: string; identity?: 'helo' | 'mailfrom'; }>; }; dkim?: { result: 'pass' | 'fail' | 'policy' | 'neutral' | 'temperror' | 'permerror'; domain?: string; selector?: string; /** * Every DKIM result when the message carried more than one signature; * `result` above is the first. */ all?: Array<{ result: 'pass' | 'fail' | 'policy' | 'neutral' | 'temperror' | 'permerror'; domain?: string; selector?: string; }>; }; dmarc?: { result: 'pass' | 'fail' | 'none'; policy?: 'reject' | 'quarantine' | 'none'; domain?: string; }; iprev?: { result: 'pass' | 'fail'; ip?: string; }; } export interface EmailBodyValue { value: string; isEncodingProblem?: boolean; isTruncated?: boolean; } export interface EmailAddress { name?: string; email: string; } export interface EmailBodyPart { partId: string; blobId: string; size: number; name?: string; type: string; charset?: string; disposition?: string; cid?: string; language?: string[]; location?: string; subParts?: EmailBodyPart[]; } export interface Attachment { partId: string; blobId: string; size: number; name?: string; type: string; charset?: string; cid?: string; disposition?: string; } export interface Mailbox { id: string; originalId?: string; // Original JMAP ID (for shared mailboxes) name: string; parentId?: string; role?: string; sortOrder: number; totalEmails: number; unreadEmails: number; totalThreads: number; unreadThreads: number; myRights: MailboxRights; isSubscribed: boolean; // Sharing (urn:ietf:params:jmap:mail:share): principalId -> rights. Only // present when explicitly requested (see getMailboxShareWith). shareWith?: Record | null; // Shared folder support accountId?: string; accountName?: string; isShared?: boolean; } // Mailbox rights (RFC 8621 §2 myRights; `mayShare` from the mail:share // extension). Also the value type of Mailbox.shareWith. export interface MailboxRights { mayReadItems: boolean; mayAddItems: boolean; mayRemoveItems: boolean; maySetSeen: boolean; maySetKeywords: boolean; mayCreateChild: boolean; mayRename: boolean; mayDelete: boolean; maySubmit: boolean; mayShare?: boolean; /** Stalwart 1.0: may change the mailbox's shared metadata (urn:ietf:params:jmap:metadata). */ maySetMetadata?: boolean; } export interface Thread { id: string; emailIds: string[]; } // Thread grouping for UI display export interface ThreadGroup { threadId: string; // JMAP thread id - unique only within its account; use for Thread/get threadKey: string; // Client-side identity, unique across accounts (see threadKeyFor) emails: Email[]; // Emails in this thread (sorted by receivedAt desc) latestEmail: Email; // Most recent email participantNames: string[];// Unique participant names hasUnread: boolean; // Any unread emails in thread hasStarred: boolean; // Any starred emails in thread hasPinned: boolean; // Any pinned emails in thread ($pinned keyword) hasAttachment: boolean; // Any email has attachment hasAnswered: boolean; // Any email has been replied to hasForwarded: boolean; // Any email has been forwarded emailCount: number; // Total emails in thread } export interface Identity { id: string; name: string; email: string; replyTo?: EmailAddress[]; bcc?: EmailAddress[]; textSignature?: string; htmlSignature?: string; mayDelete: boolean; // See `Calendar.localAccountId` - set when the Pro shell aggregates // identities from multiple connected accounts so we can route sends // back through the owning JMAP client. `accountName` is the // user-facing label for the dropdown's optgroup. localAccountId?: string; accountName?: string; } // RFC 9553 JSContact / RFC 9610 JMAP for Contacts export interface ContactCard { id: string; originalId?: string; uid?: string; addressBookIds: Record; kind?: 'individual' | 'group' | 'org' | 'location' | 'device' | 'application'; accountId?: string; accountName?: string; isShared?: boolean; // Local account-store ID - set when the Pro shell aggregates contacts // from multiple connected accounts. See `Calendar.localAccountId`. localAccountId?: string; language?: string; name?: ContactName; nicknames?: Record; emails?: Record; phones?: Record; onlineServices?: Record; preferredLanguages?: Record; organizations?: Record; titles?: Record; addresses?: Record; anniversaries?: Record; personalInfo?: Record; notes?: Record; media?: Record; cryptoKeys?: Record; directories?: Record; links?: Record; relatedTo?: Record; keywords?: Record; members?: Record; speakToAs?: { grammaticalGender?: string; pronouns?: Record }>; }; calendars?: Record }>; schedulingAddresses?: Record; pref?: number }>; // Client-side flat views of `calendars`, `schedulingAddresses` and the // `directories` entry (vCard CALURI / FBURL / CALADRURI / SOURCE). They are // not RFC 9553 properties; lib/jmap/contact-wire.ts maps them. calendarUri?: string; schedulingUri?: string; freeBusyUri?: string; source?: string; prodId?: string; created?: string; updated?: string; } export interface ContactName { components?: NameComponent[]; isOrdered?: boolean; full?: string; defaultSeparator?: string; } export interface NameComponent { kind: 'given' | 'surname' | 'prefix' | 'suffix' | 'additional' | 'separator' | 'credential' | 'title' | 'middle' | 'given2' | 'surname2' | 'generation'; value: string; } export interface ContactEmail { address: string; contexts?: Record; label?: string; pref?: number; } export interface ContactPhone { number: string; contexts?: Record; features?: Record; label?: string; pref?: number; } export interface ContactOnlineService { service?: string; uri: string; user?: string; contexts?: Record; label?: string; pref?: number; } export interface ContactLanguagePref { language: string; contexts?: Record; pref?: number; } export interface ContactOrganization { name?: string; units?: Array<{ name: string }>; sortAs?: string; } export interface ContactTitle { name: string; kind?: 'title' | 'role'; organizationId?: string; } // RFC 9553 AddressComponent export interface AddressComponent { kind: 'room' | 'apartment' | 'floor' | 'building' | 'number' | 'name' | 'block' | 'subDistrict' | 'district' | 'locality' | 'region' | 'postcode' | 'country' | 'direction' | 'landmark' | 'postOfficeBox' | 'separator' | string; value: string; phonetic?: string; } export interface ContactAddress { // RFC 9553 format components?: AddressComponent[]; full?: string; isOrdered?: boolean; defaultSeparator?: string; // Legacy flat fields (from vCard import) street?: string; locality?: string; region?: string; postcode?: string; country?: string; countryCode?: string; fullAddress?: string; coordinates?: string; timeZone?: string; contexts?: Record; label?: string; pref?: number; } export interface ContactNickname { name: string; contexts?: Record; } export interface ContactNote { note: string; created?: string; author?: { name?: string; uri?: string }; } // RFC 9553 Media. The bytes are either at `uri` (an https or data: URI) or in // a blob: Stalwart 1.0 returns every embedded PHOTO, LOGO and SOUND as a // `blobId` without `uri`, while 0.16 returns them as data: URIs. export interface ContactMedia { kind: 'photo' | 'sound' | 'logo'; uri?: string; blobId?: string; mediaType?: string; label?: string; pref?: number; contexts?: Record; } // RFC 9553 PartialDate export interface PartialDate { '@type'?: 'PartialDate'; year?: number; month?: number; day?: number; calendarScale?: string; } // RFC 9553 Timestamp export interface Timestamp { '@type': 'Timestamp'; utc: string; } export type AnniversaryDate = string | PartialDate | Timestamp; export interface ContactAnniversary { '@type'?: 'Anniversary'; kind: 'birth' | 'death' | 'wedding' | 'other'; date: AnniversaryDate; place?: ContactAddress; } export interface ContactPersonalInfo { kind: 'expertise' | 'hobby' | 'interest' | 'other'; value: string; level?: 'high' | 'medium' | 'low'; } export interface ContactCryptoKey { uri: string; mediaType?: string; contexts?: Record; } export interface ContactDirectory { uri: string; kind?: 'directory' | 'entry'; mediaType?: string; } export interface ContactLink { uri: string; kind?: 'contact' | 'generic'; mediaType?: string; contexts?: Record; label?: string; pref?: number; } export interface ContactRelation { relation?: Record; } export interface AddressBook { id: string; originalId?: string; name: string; description?: string | null; sortOrder?: number; isDefault?: boolean; isSubscribed?: boolean; myRights?: AddressBookRights; shareWith?: Record | null; accountId?: string; accountName?: string; isShared?: boolean; // See `Calendar.localAccountId` - same purpose for address books. localAccountId?: string; } export interface AddressBookRights { mayRead: boolean; mayWrite: boolean; mayShare?: boolean; mayDelete: boolean; } // JMAP Principals (RFC 9670) export interface Principal { id: string; type: 'individual' | 'group' | 'resource' | 'location' | 'other'; name: string; description?: string | null; email?: string | null; timeZone?: string | null; capabilities?: Record; accountId?: string; } /** * How complete a principal listing is: * - `ok`: the whole directory; * - `restricted`: the server refused to list its directory (Stalwart 1.0 with * directory queries off, or a role without the permission). The principals * are what Principal/get without ids still returns, on 1.0 the user's own * account, their groups and the accounts that shared something with them; * - `error`: the listing failed part-way; the principals are what arrived. */ export type PrincipalDirectoryStatus = 'ok' | 'restricted' | 'error'; export interface PrincipalListing { principals: Principal[]; status: PrincipalDirectoryStatus; /** * With `restricted`: the administrator turned directory queries off, as * Stalwart 1.0 says in its refusal, rather than the user's role lacking * the permission. 0.16 has no such refusal over JMAP. */ directoryQueriesOff?: boolean; } /** * One busy interval from Principal/getAvailability (RFC 9670 §2.5 / * draft-ietf-jmap-calendars). `busyStatus` null means the server did not * classify the period (treated as busy). */ export interface BusyPeriod { utcStart: string; utcEnd: string; busyStatus: 'confirmed' | 'tentative' | 'unavailable' | null; } /** * One principal's answer to Principal/getAvailability: its busy periods, or * the JMAP error type the call was refused with (`forbidden`, `rateLimit`, * `notFound`, ...). */ export type PrincipalAvailability = { periods: BusyPeriod[] } | { error: string }; export interface VacationResponse { id: string; isEnabled: boolean; fromDate: string | null; toDate: string | null; subject: string; textBody: string; htmlBody: string | null; } export interface EmailSubmission { id: string; identityId: string; emailId: string; threadId?: string; envelope: { mailFrom: EmailAddress; rcptTo: EmailAddress[]; }; sendAt?: string; undoStatus: "pending" | "final" | "canceled"; deliveryStatus?: Record; dsnBlobIds?: string[]; mdnBlobIds?: string[]; } export interface DeliveryStatus { smtpReply: string; delivered: "queued" | "yes" | "no" | "unknown"; displayed: "unknown" | "yes"; } // JMAP Calendar Types (RFC 8984 JSCalendar + RFC 9553 JMAP Calendars) export interface Calendar { id: string; originalId?: string; name: string; description: string | null; color: string | null; sortOrder: number; isSubscribed: boolean; isVisible: boolean; isDefault: boolean; includeInAvailability: 'all' | 'attending' | 'none'; defaultAlertsWithTime: Record | null; defaultAlertsWithoutTime: Record | null; timeZone: string | null; shareWith: Record | null; myRights: CalendarRights; accountId?: string; accountName?: string; isShared?: boolean; // Local account-store ID (per JMAP server connection). Populated when the // Pro shell aggregates calendars from multiple connected accounts so we // can route mutations to the right client. Distinct from `accountId` // which is the JMAP server's own account UUID. localAccountId?: string; // Set when `color` has been replaced by the viewer's local override for a // shared calendar (see lib/shared-calendar-colors). When true, the override // wins over per-event colors so the whole shared calendar paints uniformly. colorIsLocalOverride?: boolean; // True when the calendar holds only tasks (VTODO) and no events, so it is // hidden from the event calendar UI while remaining available to the tasks // view (#761). Undefined means "not determined" (treated as not tasks-only). isTasksOnly?: boolean; } /** * iCalendar component types a calendar advertises through the CalDAV * supported-calendar-component-set. Sync clients such as DAVx5 use it to * decide whether a collection is offered to calendar apps, todo apps, or both * (#760). Only settable while the collection is created. */ export type CalendarComponentType = 'VEVENT' | 'VTODO'; export interface CreateCalendarOptions { /** Component set for the new calendar; defaults to events only (VEVENT). */ components?: CalendarComponentType[]; } export interface CalendarRights { mayReadFreeBusy: boolean; mayReadItems: boolean; mayWriteAll: boolean; mayWriteOwn: boolean; mayUpdatePrivate: boolean; mayRSVP: boolean; mayShare: boolean; mayDelete: boolean; } export interface CalendarEvent { id: string; originalId?: string; // Set by the server on every CalendarEvent/get result. For an occurrence // handed out by CalendarEvent/query?expandRecurrences=true (a "synthetic" // id) this is the id of the stored base event; for a base event it equals // `id`. See lib/recurrence-instances.ts. baseEventId?: string | null; calendarIds: Record; originalCalendarIds?: Record; accountId?: string; accountName?: string; isShared?: boolean; // See `Calendar.localAccountId` - same purpose for events. localAccountId?: string; isDraft: boolean; isOrigin: boolean; utcStart: string | null; utcEnd: string | null; '@type': 'Event'; uid: string; title: string; description: string; descriptionContentType: string; created: string | null; updated: string; sequence: number; start: string; duration: string; timeZone: string | null; showWithoutTime: boolean; status: 'tentative' | 'confirmed' | 'cancelled'; freeBusyStatus: 'free' | 'busy'; privacy: 'public' | 'private' | 'secret'; color: string | null; keywords: Record | null; categories: Record | null; locale: string | null; replyTo: Record | null; organizerCalendarAddress: string | null; participants: Record | null; mayInviteSelf: boolean; mayInviteOthers: boolean; hideAttendees: boolean; recurrenceId: string | null; recurrenceIdTimeZone: string | null; recurrenceRules: CalendarRecurrenceRule[] | null; recurrenceOverrides: Record> | null; excludedRecurrenceRules: CalendarRecurrenceRule[] | null; useDefaultAlerts: boolean; alerts: Record | null; locations: Record | null; virtualLocations: Record | null; links: Record | null; relatedTo: Record | null; } export interface CalendarParticipant { '@type': 'Participant'; name: string; email: string; calendarAddress: string | null; description: string | null; sendTo: Record | null; kind: 'individual' | 'group' | 'location' | 'resource'; roles: Record; participationStatus: 'accepted' | 'declined' | 'tentative' | 'delegated' | 'needs-action'; participationComment: string | null; expectReply: boolean; scheduleAgent: 'server' | 'client' | 'none'; scheduleForceSend: boolean; scheduleId: string | null; scheduleSequence: number; scheduleStatus: string[] | null; scheduleUpdated: string | null; invitedBy: string | null; delegatedTo: Record | null; delegatedFrom: Record | null; memberOf: Record | null; locationId: string | null; language: string | null; links: Record | null; } export interface CalendarRecurrenceRule { '@type': 'RecurrenceRule'; frequency: 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'minutely' | 'secondly'; interval: number; // Optional and normally omitted: these RFC 7529 (RSCALE/SKIP) fields only // apply to non-Gregorian scales / invalid-date handling. Emitting a default // SKIP=OMIT breaks some CalDAV clients (DAVx5) - see lib/recurrence-rule.ts (#805). rscale?: string; skip?: 'omit' | 'backward' | 'forward'; firstDayOfWeek: 'mo' | 'tu' | 'we' | 'th' | 'fr' | 'sa' | 'su'; byDay: CalendarNDay[] | null; byMonthDay: number[] | null; byMonth: string[] | null; byYearDay: number[] | null; byWeekNo: number[] | null; byHour: number[] | null; byMinute: number[] | null; bySecond: number[] | null; bySetPosition: number[] | null; count: number | null; until: string | null; } export interface CalendarNDay { day: string; nthOfPeriod?: number; } export interface CalendarEventAlert { '@type': 'Alert'; trigger: CalendarOffsetTrigger | CalendarAbsoluteTrigger; action: 'display' | 'email'; acknowledged: string | null; relatedTo: Record | null; } export interface CalendarOffsetTrigger { '@type': 'OffsetTrigger'; offset: string; relativeTo: 'start' | 'end'; } export interface CalendarAbsoluteTrigger { '@type': 'AbsoluteTrigger'; when: string; } export interface CalendarLocation { '@type': 'Location'; name: string; description: string | null; locationTypes: Record | null; coordinates: string | null; timeZone: string | null; links: Record | null; relativeTo: 'start' | 'end' | null; } export interface CalendarVirtualLocation { '@type': 'VirtualLocation'; name: string | null; description: string | null; uri: string; features: Record | null; } export interface CalendarLink { '@type': 'Link'; href: string; cid: string | null; contentType: string | null; size: number | null; rel: string | null; display: string | null; title: string | null; } export interface CalendarRelation { '@type': 'Relation'; relation: Record | null; } export interface CalendarTask { id: string; calendarIds: Record; '@type': 'Task'; uid: string; title: string; description: string; due: string | null; start: string | null; duration: string | null; timeZone: string | null; showWithoutTime: boolean; progress: 'needs-action' | 'in-process' | 'completed' | 'cancelled'; progressUpdated?: string | null; priority: number; privacy: 'public' | 'private' | 'secret'; keywords: Record | null; categories: Record | null; color: string | null; created: string | null; updated: string; recurrenceRules: CalendarRecurrenceRule[] | null; // While on, Stalwart 1.0 fills the calendar's default alerts into `alerts`. useDefaultAlerts?: boolean; alerts: Record | null; relatedTo: Record | null; // Client-only, set on tasks from a shared calendar account. Their `id` and // `calendarIds` are namespaced `accountId:id` like shared events. originalId?: string; accountId?: string; isShared?: boolean; } /** * A ParticipantIdentity (draft-ietf-jmap-calendars §6): one of the calendar * addresses the user may act as when organising or replying to events. The * server derives them from the account's addresses; the default one is what * new invitations are sent from. */ export interface CalendarParticipantIdentity { id: string; name: string; /** `mailto:` URI of the scheduling address. */ calendarAddress: string; isDefault: boolean; } export interface CalendarEventNotification { id: string; created: string; changedBy: { name: string; email: string; principalId: string | null; scheduleId: string | null; }; comment: string | null; type: 'created' | 'updated' | 'destroyed'; calendarEventId: string; isDraft: boolean; event?: CalendarEvent; eventPatch?: Record; } export interface CalendarEventFilter { /** * A FilterOperator (RFC 8620 §5.5) sets these and no property keys: * Stalwart rejects a filter that mixes the two. */ operator?: 'AND' | 'OR' | 'NOT'; conditions?: CalendarEventFilter[]; inCalendars?: string[]; /** One calendar: what Stalwart (0.16 and 1.0) takes; 0.16 refuses `inCalendars`. */ inCalendar?: string; after?: string; before?: string; text?: string; title?: string; description?: string; location?: string; owner?: string; attendee?: string; participationStatus?: string; uid?: string; types?: string[]; } // JMAP Push Notification Types (RFC 8620 Section 7) export interface StateChange { '@type': 'StateChange'; changed: { [accountId: string]: { Email?: string; Mailbox?: string; Thread?: string; EmailDelivery?: string; EmailSubmission?: string; Identity?: string; ContactCard?: string; AddressBook?: string; Calendar?: string; CalendarEvent?: string; SieveScript?: string; ShareNotification?: string; CalendarEventNotification?: string; }; }; } /** * A ShareNotification (RFC 9670 §3): someone changed this user's rights on * one of their collections. `oldRights` null = newly shared, `newRights` * null = access revoked. */ export interface ShareNotification { id: string; created: string; changedBy: { name: string; email: string | null; principalId: string | null }; objectType: 'Mailbox' | 'Calendar' | 'AddressBook' | 'FileNode' | string; objectAccountId: string; objectId: string; name: string; oldRights: Record | null; newRights: Record | null; } // draft-ietf-jmap-emailpush EmailPushConfig: the server evaluates `filter` // against every newly delivered message and only pushes when it matches, so a // client can keep spam (Junk) out of its push channel server-side. Keyed by // account id on PushSubscription.emailPush. export interface EmailPushConfig { filter: Record | null; properties: string[]; urgency?: 'very-low' | 'low' | 'normal' | 'high'; } export interface PushSubscription { id: string; deviceClientId: string; url: string; keys: { p256dh: string; auth: string; } | null; expires: string | null; types: string[] | null; // Only present when the server advertises urn:ietf:params:jmap:emailpush // and the property was requested; null when the subscription has no // per-account delivery filter. emailPush?: Record | null; } // For tracking last known states /** * A `Foo/changes` response (RFC 8620 §5.2) as consumed by the stores' delta * sync. `updatedProperties` is set when the server can tell that only those * properties changed on the updated records (Stalwart reports the four * Mailbox counters this way). */ export interface CollectionChanges { oldState: string; newState: string; hasMoreChanges: boolean; created: string[]; updated: string[]; destroyed: string[]; updatedProperties: string[] | null; } export interface AccountStates { [accountId: string]: { Email?: string; Mailbox?: string; Thread?: string; }; } // JMAP FileNode types (draft-ietf-jmap-filenode / Stalwart implementation) export type FileNodeType = 'file' | 'directory' | 'symlink'; export interface FileNode { id: string; parentId: string | null; name: string; type: string; // "d" for directory, MIME type for files // Returned by Stalwart 0.16.6+; 1.0 adds symlinks. Read it through // fileNodeKind(), and never send it in FileNode/set. nodeType?: FileNodeType; blobId: string | null; size: number; created: string; // Last content/metadata change, server-maintained. The property is named // `modified` in draft-ietf-jmap-filenode and in Stalwart - there is no // `updated` on a FileNode. Asking for the wrong name silently yields // undefined, which made the UI show the creation date forever (#700). modified: string; // JMAP Sharing (RFC 9670). Populated only when the server advertises the // filenode capability and the properties are explicitly requested. A node is // shared-out when `shareWith` has entries; `myRights` describes what the // viewer may do (always full rights on owned nodes). myRights?: FileNodeRights; shareWith?: Record | null; // True when this node was fetched from another principal's account that was // shared with the logged-in user (mirrors Calendar.isShared / AddressBook.isShared). isShared?: boolean; // A folder of another account that is listed only because something below // it was shared with the user (Stalwart 1.0): name and place in the tree, // no dates, no rights. It leads to the shared node but is not shown itself. discoverableOnly?: boolean; // Owning account's JMAP id and display name, set when aggregating nodes // across connected/shared accounts so mutations route to the right account. accountId?: string; accountName?: string; // Local account-store id (per JMAP connection) in multi-account contexts. // See Calendar.localAccountId. localAccountId?: string; } // FileNode rights as defined by Stalwart's JmapSharedObject implementation. export interface FileNodeRights { mayRead: boolean; mayAddChildren: boolean; mayRename: boolean; mayDelete: boolean; mayModifyContent: boolean; mayShare: boolean; } export interface FileNodeFilter { parentId?: string | null; name?: string; type?: string; } // Unified mailbox virtual IDs and types export const UNIFIED_INBOX = '__unified_inbox__'; export const UNIFIED_SENT = '__unified_sent__'; export const UNIFIED_DRAFTS = '__unified_drafts__'; export const UNIFIED_TRASH = '__unified_trash__'; export const UNIFIED_ARCHIVE = '__unified_archive__'; export const UNIFIED_JUNK = '__unified_junk__'; export type UnifiedMailboxRole = 'inbox' | 'sent' | 'drafts' | 'trash' | 'archive' | 'junk'; export const UNIFIED_MAILBOX_IDS: Record = { inbox: UNIFIED_INBOX, sent: UNIFIED_SENT, drafts: UNIFIED_DRAFTS, trash: UNIFIED_TRASH, archive: UNIFIED_ARCHIVE, junk: UNIFIED_JUNK, }; export const UNIFIED_ROLE_BY_ID: Record = Object.fromEntries( Object.entries(UNIFIED_MAILBOX_IDS).map(([role, id]) => [id, role as UnifiedMailboxRole]) ) as Record; export function isUnifiedMailboxId(id: string): boolean { return id in UNIFIED_ROLE_BY_ID; } /** * Cross views shown in the unified ("Unified Mailbox") section: All mail / * Unread / Starred. Each merges messages across the account boundary (the active * account + its shared folders by default, or every logged-in account when the * cross-account sub-option is on), narrowed by the user's folder selection (see * `allMailFolderIds`). Distinct from the per-role unified ids (one role across * accounts). */ export const CROSS_UNREAD = '__cross_unread__'; export const CROSS_STARRED = '__cross_starred__'; export const CROSS_ALL = '__cross_all__'; export type CrossView = 'unread' | 'starred' | 'all'; export const CROSS_VIEW_IDS: Record = { unread: CROSS_UNREAD, starred: CROSS_STARRED, all: CROSS_ALL, }; export const CROSS_VIEW_BY_ID: Record = Object.fromEntries( Object.entries(CROSS_VIEW_IDS).map(([view, id]) => [id, view as CrossView]) ) as Record; export function isCrossViewId(id: string): boolean { return id in CROSS_VIEW_BY_ID; } /** * Mailbox roles excluded from the cross-account views. Everything else (inbox * and custom/no-role folders) is included. `all` (RFC 6154 \All) holds every * message, so including it would bring back sent/archive mail and count inbox * unread twice. */ export const CROSS_EXCLUDED_ROLES: ReadonlySet = new Set([ 'junk', 'sent', 'archive', 'trash', 'drafts', 'all', ]);