mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 08:06:13 +00:00
Someone who writes to a calendar shared with them (read & write) changes the owner's meeting there on the owner's behalf: Stalwart sends the change from the addresses of the account the event is in. The app decided who organizes by the user's own addresses only, so it took the sharee for an attendee (0.16 and 1.0 alike): - the editor wrote no participants, hid "Send invitations" and sent nothing, so the attendees kept the old time and their answers - "All events" asked nobody again, and "This and following events" made the rest a new series that kept the old answers, which Stalwart sends to nobody who had answered The app now reads `isOrigin`, which both versions compute from the addresses of the account the event is in: true for the owner's meeting and every expanded occurrence of it, false for an invitation the owner got. `userOrganizes` counts such a copy as the organizer's; an invitation is still changed without messages. "This and following" asks by the old series, since the new one carries no `isOrigin` yet. The editor no longer leaves the user's own main address off the attendees it opens with, only the organizer's (or the user's, for an event that names none yet): a user who invited their main address to a meeting organized under an alias lost it on the next save.
704 lines
21 KiB
TypeScript
704 lines
21 KiB
TypeScript
// ─── JMAP Session & Core ─────────────────────────────────
|
|
|
|
export interface JMAPSession {
|
|
apiUrl: string;
|
|
downloadUrl: string;
|
|
uploadUrl: string;
|
|
eventSourceUrl: string;
|
|
primaryAccounts: Record<string, string>;
|
|
accounts: Record<string, JMAPAccountInfo>;
|
|
capabilities: Record<string, unknown>;
|
|
state: string;
|
|
// RFC 8620 §2 — authenticated user identifier. Optional because not all
|
|
// servers populate it; OAuth login uses it as the account label when
|
|
// there's no password username to fall back on.
|
|
username?: string;
|
|
}
|
|
|
|
export interface JMAPAccountInfo {
|
|
name: string;
|
|
isPersonal: boolean;
|
|
isReadOnly: boolean;
|
|
// Per-account capability objects (RFC 8620 §2). Optional because not every
|
|
// server populates it; the Sieve panel reads its limits/extensions from here.
|
|
accountCapabilities?: Record<string, unknown>;
|
|
}
|
|
|
|
export type JMAPMethodCall = [string, Record<string, unknown>, string];
|
|
|
|
export interface JMAPRequestBody {
|
|
using: string[];
|
|
methodCalls: JMAPMethodCall[];
|
|
}
|
|
|
|
export interface JMAPResponseBody {
|
|
methodResponses: Array<[string, Record<string, any>, string]>;
|
|
sessionState?: string;
|
|
}
|
|
|
|
// ─── Email (RFC 8621) ────────────────────────────────────
|
|
|
|
export interface EmailAddress {
|
|
name?: string;
|
|
email: string;
|
|
}
|
|
|
|
export interface BodyPart {
|
|
partId?: string;
|
|
blobId?: string;
|
|
type?: string;
|
|
name?: string;
|
|
disposition?: string;
|
|
size?: number;
|
|
cid?: string;
|
|
}
|
|
|
|
export interface Attachment {
|
|
blobId: string;
|
|
type: string;
|
|
name?: string;
|
|
size?: number;
|
|
disposition?: string;
|
|
cid?: string;
|
|
}
|
|
|
|
export interface Email {
|
|
id: string;
|
|
threadId: string;
|
|
mailboxIds: Record<string, boolean>;
|
|
keywords: Record<string, boolean>;
|
|
size: number;
|
|
receivedAt: string;
|
|
sentAt?: string;
|
|
from?: EmailAddress[];
|
|
to?: EmailAddress[];
|
|
cc?: EmailAddress[];
|
|
bcc?: EmailAddress[];
|
|
replyTo?: EmailAddress[];
|
|
subject?: string;
|
|
preview?: string;
|
|
hasAttachment: boolean;
|
|
// RFC 8621 §4.1.2.3 - bare msg-ids without angle brackets. Needed for
|
|
// reply threading (In-Reply-To / References) and read-receipt dedupe.
|
|
messageId?: string[] | null;
|
|
inReplyTo?: string[] | null;
|
|
references?: string[] | null;
|
|
// Raw headers (RFC 8621 §4.1.3) when fetched with `headers`; the viewer
|
|
// derives SPF/DKIM/DMARC chips, List-Unsubscribe, MDN requests from them.
|
|
headers?: Array<{ name: string; value: string }>;
|
|
textBody?: BodyPart[];
|
|
htmlBody?: BodyPart[];
|
|
bodyValues?: Record<string, { value: string; isEncodingProblem?: boolean; isTruncated?: boolean }>;
|
|
attachments?: Attachment[];
|
|
blobId?: string;
|
|
bodyStructure?: BodyPart;
|
|
/**
|
|
* Client-side stamp, never sent by the server: the JMAP account a row of a
|
|
* list that spans accounts (an "All folders" search, a tag view) was
|
|
* fetched from, the user's own account included. Rows of a single folder carry none; they
|
|
* belong to the folder's account.
|
|
*/
|
|
jmapAccountId?: string;
|
|
}
|
|
|
|
export interface MailboxRights {
|
|
mayReadItems: boolean;
|
|
mayAddItems: boolean;
|
|
mayRemoveItems: boolean;
|
|
maySetSeen: boolean;
|
|
maySetKeywords: boolean;
|
|
mayCreateChild: boolean;
|
|
mayRename: boolean;
|
|
mayDelete: boolean;
|
|
maySubmit: boolean;
|
|
}
|
|
|
|
export interface Mailbox {
|
|
/**
|
|
* Unique within the app. For the user's own folders this is the raw JMAP id;
|
|
* for a shared (group) account's folders it's `<accountId>:<rawId>` so ids
|
|
* from different accounts can't collide. Use `originalId` when talking to
|
|
* the server. Mirrors the webmail's scheme in [lib/jmap/client.ts].
|
|
*/
|
|
id: string;
|
|
/** Raw JMAP id — set only on shared folders, where `id` carries a prefix. */
|
|
originalId?: string;
|
|
name: string;
|
|
parentId?: string | null;
|
|
role?: string | null;
|
|
sortOrder?: number;
|
|
totalEmails: number;
|
|
unreadEmails: number;
|
|
totalThreads: number;
|
|
unreadThreads: number;
|
|
myRights: MailboxRights;
|
|
isSubscribed?: boolean;
|
|
/** JMAP account the folder lives under (primary or a shared/group account). */
|
|
accountId?: string;
|
|
/** Display name of the owning account, used as the sidebar group header. */
|
|
accountName?: string;
|
|
/** True when the folder belongs to a shared/group account, not the user. */
|
|
isShared?: boolean;
|
|
}
|
|
|
|
export interface Thread {
|
|
id: string;
|
|
emailIds: string[];
|
|
}
|
|
|
|
// ─── Identity & Submission ───────────────────────────────
|
|
|
|
export interface Identity {
|
|
id: string;
|
|
name: string;
|
|
email: string;
|
|
replyTo?: EmailAddress[];
|
|
bcc?: EmailAddress[];
|
|
textSignature?: string;
|
|
htmlSignature?: string;
|
|
mayDelete: boolean;
|
|
}
|
|
|
|
export interface EmailSubmission {
|
|
id: string;
|
|
emailId: string;
|
|
identityId: string;
|
|
sendAt?: string;
|
|
undoStatus?: string;
|
|
}
|
|
|
|
// ─── Contacts (RFC 9553 JSContact) ──────────────────────
|
|
|
|
export type ContactKind = 'individual' | 'group' | 'org' | 'location' | 'device' | 'application';
|
|
|
|
export type NameComponentKind =
|
|
| 'given' | 'surname' | 'middle' | 'prefix' | 'suffix'
|
|
| 'additional' | 'separator' | 'credential' | 'title'
|
|
| 'given2' | 'surname2' | 'generation';
|
|
|
|
export interface NameComponent {
|
|
kind: NameComponentKind | string;
|
|
value: string;
|
|
}
|
|
|
|
export interface ContactName {
|
|
components?: NameComponent[];
|
|
isOrdered?: boolean;
|
|
full?: string;
|
|
defaultSeparator?: string;
|
|
}
|
|
|
|
export interface ContactNickname {
|
|
name: string;
|
|
contexts?: Record<string, boolean>;
|
|
}
|
|
|
|
export interface ContactEmail {
|
|
address: string;
|
|
contexts?: Record<string, boolean>;
|
|
label?: string;
|
|
pref?: number;
|
|
}
|
|
|
|
export interface ContactPhone {
|
|
number: string;
|
|
contexts?: Record<string, boolean>;
|
|
features?: Record<string, boolean>;
|
|
label?: string;
|
|
pref?: number;
|
|
}
|
|
|
|
export interface AddressComponent {
|
|
kind: string;
|
|
value: string;
|
|
phonetic?: string;
|
|
}
|
|
|
|
export interface ContactAddress {
|
|
components?: AddressComponent[];
|
|
full?: string;
|
|
isOrdered?: boolean;
|
|
defaultSeparator?: string;
|
|
// Legacy flat fields
|
|
street?: string;
|
|
locality?: string;
|
|
region?: string;
|
|
postcode?: string;
|
|
country?: string;
|
|
countryCode?: string;
|
|
fullAddress?: string;
|
|
coordinates?: string;
|
|
timeZone?: string;
|
|
contexts?: Record<string, boolean>;
|
|
label?: string;
|
|
pref?: number;
|
|
}
|
|
|
|
export interface ContactOrganization {
|
|
name?: string;
|
|
units?: Array<{ name: string }>;
|
|
sortAs?: string;
|
|
}
|
|
|
|
export interface ContactTitle {
|
|
name: string;
|
|
kind?: 'title' | 'role';
|
|
organizationId?: string;
|
|
}
|
|
|
|
export interface ContactOnlineService {
|
|
service?: string;
|
|
uri: string;
|
|
user?: string;
|
|
contexts?: Record<string, boolean>;
|
|
label?: string;
|
|
pref?: number;
|
|
}
|
|
|
|
export interface ContactLanguagePref {
|
|
language: string;
|
|
contexts?: Record<string, boolean>;
|
|
pref?: number;
|
|
}
|
|
|
|
export interface PartialDate {
|
|
'@type'?: 'PartialDate';
|
|
year?: number;
|
|
month?: number;
|
|
day?: number;
|
|
calendarScale?: string;
|
|
}
|
|
|
|
export interface Timestamp {
|
|
'@type': 'Timestamp';
|
|
utc: string;
|
|
}
|
|
|
|
export type AnniversaryDate = string | PartialDate | Timestamp;
|
|
|
|
export interface ContactAnniversary {
|
|
'@type'?: 'Anniversary';
|
|
kind: 'birth' | 'death' | 'wedding' | 'other' | string;
|
|
date: AnniversaryDate;
|
|
place?: ContactAddress;
|
|
}
|
|
|
|
export interface ContactPersonalInfo {
|
|
kind: 'expertise' | 'hobby' | 'interest' | 'other' | string;
|
|
value: string;
|
|
level?: 'high' | 'medium' | 'low';
|
|
}
|
|
|
|
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`, photos saved as data: URIs included, while 0.16
|
|
// returns them as data: URIs.
|
|
export interface ContactMedia {
|
|
kind: 'photo' | 'sound' | 'logo' | string;
|
|
uri?: string;
|
|
blobId?: string;
|
|
mediaType?: string;
|
|
label?: string;
|
|
pref?: number;
|
|
contexts?: Record<string, boolean>;
|
|
}
|
|
|
|
export interface ContactRelation {
|
|
relation?: Record<string, boolean>;
|
|
}
|
|
|
|
export interface ContactLink {
|
|
uri: string;
|
|
kind?: 'contact' | 'generic' | string;
|
|
mediaType?: string;
|
|
contexts?: Record<string, boolean>;
|
|
label?: string;
|
|
pref?: number;
|
|
}
|
|
|
|
export interface ContactCryptoKey {
|
|
uri: string;
|
|
mediaType?: string;
|
|
contexts?: Record<string, boolean>;
|
|
}
|
|
|
|
export interface ContactDirectory {
|
|
uri: string;
|
|
kind?: 'directory' | 'entry' | string;
|
|
mediaType?: string;
|
|
pref?: number;
|
|
}
|
|
|
|
export interface ContactPronouns {
|
|
pronouns: string;
|
|
pref?: number;
|
|
contexts?: Record<string, boolean>;
|
|
}
|
|
|
|
export interface ContactSpeakToAs {
|
|
grammaticalGender?: string;
|
|
pronouns?: Record<string, ContactPronouns>;
|
|
}
|
|
|
|
export interface ContactCard {
|
|
id: string;
|
|
originalId?: string;
|
|
uid?: string;
|
|
addressBookIds: Record<string, boolean>;
|
|
kind?: ContactKind | string;
|
|
language?: string;
|
|
name?: ContactName;
|
|
nicknames?: Record<string, ContactNickname>;
|
|
emails?: Record<string, ContactEmail>;
|
|
phones?: Record<string, ContactPhone>;
|
|
onlineServices?: Record<string, ContactOnlineService>;
|
|
preferredLanguages?: Record<string, ContactLanguagePref>;
|
|
organizations?: Record<string, ContactOrganization>;
|
|
titles?: Record<string, ContactTitle>;
|
|
addresses?: Record<string, ContactAddress>;
|
|
anniversaries?: Record<string, ContactAnniversary>;
|
|
personalInfo?: Record<string, ContactPersonalInfo>;
|
|
notes?: Record<string, ContactNote>;
|
|
media?: Record<string, ContactMedia>;
|
|
relatedTo?: Record<string, ContactRelation>;
|
|
links?: Record<string, ContactLink>;
|
|
cryptoKeys?: Record<string, ContactCryptoKey>;
|
|
directories?: Record<string, ContactDirectory>;
|
|
keywords?: Record<string, boolean>;
|
|
members?: Record<string, boolean>;
|
|
speakToAs?: ContactSpeakToAs;
|
|
calendarUri?: string;
|
|
schedulingUri?: string;
|
|
freeBusyUri?: string;
|
|
source?: string;
|
|
prodId?: string;
|
|
created?: string;
|
|
updated?: string;
|
|
// Client-only metadata for cards that live in a shared / group account.
|
|
// Never sent to the server (api/contacts.ts strips them). `id` is
|
|
// namespaced as `<accountId>:<originalId>` for shared cards.
|
|
accountId?: string;
|
|
accountName?: string;
|
|
isShared?: boolean;
|
|
}
|
|
|
|
export interface AddressBookRights {
|
|
mayRead?: boolean;
|
|
mayWrite?: boolean;
|
|
mayShare?: boolean;
|
|
mayDelete?: boolean;
|
|
}
|
|
|
|
export interface AddressBook {
|
|
id: string;
|
|
name: string;
|
|
description?: string | null;
|
|
isDefault?: boolean;
|
|
isSubscribed?: boolean;
|
|
sortOrder?: number;
|
|
myRights?: AddressBookRights;
|
|
// RFC 9610 share map: principal id → rights.
|
|
shareWith?: Record<string, AddressBookRights> | null;
|
|
// Client-only metadata for books that live in a shared / group account.
|
|
// `id` is namespaced as `<accountId>:<originalId>` for shared books so it
|
|
// cannot collide with the user's own "default" book.
|
|
originalId?: string;
|
|
accountId?: string;
|
|
accountName?: string;
|
|
isShared?: boolean;
|
|
}
|
|
|
|
export interface AddressBookWithCount extends AddressBook {
|
|
count: number;
|
|
}
|
|
|
|
// ─── Calendar (RFC 8984 JSCalendar) ─────────────────────
|
|
|
|
export interface Participant {
|
|
'@type'?: 'Participant';
|
|
name?: string;
|
|
email?: string;
|
|
sendTo?: Record<string, string>;
|
|
// Stalwart uses calendarAddress (a `mailto:` URI) instead of email/sendTo.
|
|
calendarAddress?: string;
|
|
kind?: string;
|
|
roles?: Record<string, boolean>;
|
|
participationStatus?: 'needs-action' | 'accepted' | 'declined' | 'tentative' | 'delegated' | string;
|
|
participationComment?: string;
|
|
// 'server' asks Stalwart to deliver the iTIP messages for this participant.
|
|
scheduleAgent?: 'server' | 'client' | 'none' | string;
|
|
scheduleStatus?: string[];
|
|
expectReply?: boolean;
|
|
description?: string;
|
|
}
|
|
|
|
// JSCalendar Location (RFC 8984 §4.2.5) — a physical place.
|
|
export interface EventLocation {
|
|
'@type'?: 'Location';
|
|
name?: string;
|
|
description?: string;
|
|
locationTypes?: Record<string, boolean>;
|
|
relativeTo?: string;
|
|
timeZone?: string;
|
|
coordinates?: string;
|
|
}
|
|
|
|
// JSCalendar VirtualLocation (RFC 8984 §4.2.6) — a video/online place.
|
|
export interface VirtualLocation {
|
|
'@type'?: 'VirtualLocation';
|
|
name?: string;
|
|
description?: string;
|
|
uri: string;
|
|
features?: Record<string, boolean>;
|
|
}
|
|
|
|
export interface RecurrenceRule {
|
|
'@type'?: 'RecurrenceRule';
|
|
frequency: string;
|
|
interval?: number;
|
|
until?: string;
|
|
count?: number;
|
|
byDay?: Array<{ day: string; nthOfPeriod?: number }>;
|
|
byMonth?: string[];
|
|
byMonthDay?: number[];
|
|
byYearDay?: number[];
|
|
byWeekNo?: number[];
|
|
byHour?: number[];
|
|
byMinute?: number[];
|
|
bySecond?: number[];
|
|
bySetPosition?: number[];
|
|
firstDayOfWeek?: string;
|
|
rscale?: string;
|
|
skip?: string;
|
|
}
|
|
|
|
export interface Alert {
|
|
'@type'?: 'Alert';
|
|
trigger: { '@type': string; offset?: string; when?: string; relativeTo?: 'start' | 'end' | string };
|
|
action?: string;
|
|
// UTCDateTime the user acknowledged the alert; null/absent when pending.
|
|
acknowledged?: string | null;
|
|
}
|
|
|
|
export interface CalendarEvent {
|
|
id: string;
|
|
'@type'?: 'Event' | 'Task';
|
|
uid: string;
|
|
calendarIds: Record<string, boolean>;
|
|
title: string;
|
|
description?: string;
|
|
start: string;
|
|
duration?: string;
|
|
// `null` on write clears a field the server holds (JSON drops `undefined`,
|
|
// so an omitted key keeps the old value). All-day events carry a null zone.
|
|
timeZone?: string | null;
|
|
showWithoutTime?: boolean;
|
|
utcStart?: string;
|
|
utcEnd?: string;
|
|
status?: string;
|
|
freeBusyStatus?: string;
|
|
participants?: Record<string, Participant> | null;
|
|
// Scheduling (iTIP/iMIP). Stalwart derives the iCalendar ORGANIZER solely
|
|
// from `organizerCalendarAddress` (a `mailto:` URI); RFC 8984's `replyTo` is
|
|
// retired in jscalendarbis and only kept here so old values round-trip.
|
|
replyTo?: Record<string, string> | null;
|
|
organizerCalendarAddress?: string | null;
|
|
// Server-set, never written: the event's organizer is an address of the
|
|
// account the event is in - the organizer's own copy, and every expanded
|
|
// occurrence of it - as opposed to an invitation that account got (0.16
|
|
// and 1.0 alike).
|
|
isOrigin?: boolean;
|
|
sequence?: number;
|
|
recurrenceRules?: RecurrenceRule[] | null;
|
|
recurrenceOverrides?: Record<string, Partial<CalendarEvent>> | null;
|
|
excludedRecurrenceRules?: RecurrenceRule[] | null;
|
|
recurrenceId?: string;
|
|
// The zone of a LocalDateTime `recurrenceId` (server-expanded occurrences).
|
|
recurrenceIdTimeZone?: string | null;
|
|
// Server-expanded occurrences (CalendarEvent/query expandRecurrences) carry
|
|
// a synthetic `id`; this is the raw id of the stored event they come from.
|
|
// A stored event fetched directly carries its own id here.
|
|
baseEventId?: string | null;
|
|
originalId?: string;
|
|
// Client-only: the event's server-side calendarIds (raw, unprefixed), kept
|
|
// when `calendarIds` is remapped to namespaced store ids for shared events.
|
|
originalCalendarIds?: Record<string, boolean>;
|
|
useDefaultAlerts?: boolean;
|
|
alerts?: Record<string, Alert> | null;
|
|
locations?: Record<string, EventLocation> | null;
|
|
virtualLocations?: Record<string, VirtualLocation> | null;
|
|
links?: Record<string, { href: string; rel?: string }>;
|
|
created?: string;
|
|
updated?: string;
|
|
// Task-only (RFC 8984 JSTask) fields.
|
|
progress?: 'needs-action' | 'in-process' | 'completed' | 'failed' | 'cancelled' | string;
|
|
progressUpdated?: string;
|
|
due?: string | null;
|
|
priority?: number;
|
|
percentComplete?: number;
|
|
color?: string;
|
|
// Client-only: account the event/occurrence belongs to (for shared/virtual).
|
|
localAccountId?: string;
|
|
// Client-only: JMAP account the event was fetched from. Absent for the
|
|
// primary account; set for events on calendars shared with the user so
|
|
// mutations can be routed to the owning account.
|
|
accountId?: string;
|
|
// Client-only: true when the event lives on a calendar shared with the user.
|
|
isShared?: boolean;
|
|
}
|
|
|
|
export interface CalendarRights {
|
|
mayReadFreeBusy?: boolean;
|
|
mayReadItems?: boolean;
|
|
mayWriteAll?: boolean;
|
|
mayWriteOwn?: boolean;
|
|
mayUpdatePrivate?: boolean;
|
|
mayRSVP?: boolean;
|
|
mayShare?: boolean;
|
|
mayDelete?: boolean;
|
|
}
|
|
|
|
export interface Calendar {
|
|
id: string;
|
|
name: string;
|
|
description?: string | null;
|
|
color?: string;
|
|
isVisible?: boolean;
|
|
isSubscribed?: boolean;
|
|
isDefault?: boolean;
|
|
sortOrder?: number;
|
|
myRights?: CalendarRights;
|
|
// Principals this (owned) calendar is shared with and their rights.
|
|
shareWith?: Record<string, CalendarRights> | null;
|
|
// Alerts applied to events that set `useDefaultAlerts` (RFC 8620 calendars).
|
|
defaultAlertsWithTime?: Record<string, Alert> | null;
|
|
defaultAlertsWithoutTime?: Record<string, Alert> | null;
|
|
// Client-only: JMAP account the calendar was fetched from. Absent for the
|
|
// primary account; set for calendars shared with the user.
|
|
accountId?: string;
|
|
// Client-only: the real JMAP id of a shared calendar, before its `id` was
|
|
// namespaced as `${accountId}:${id}` to stay unique in the store. Mutations
|
|
// and event queries must use this raw id against the owning account.
|
|
originalId?: string;
|
|
// Client-only: true when the calendar lives in another account (shared
|
|
// with the user by its owner).
|
|
isShared?: boolean;
|
|
// Client-only: set when `color` has been replaced by the viewer's local
|
|
// override for a shared calendar (see lib/calendar-utils). When true, the
|
|
// override wins over per-event colors so the whole shared calendar paints
|
|
// uniformly.
|
|
colorIsLocalOverride?: boolean;
|
|
}
|
|
|
|
// ─── Files (FileNode) ───────────────────────────────────
|
|
|
|
export type FileNodeType = 'file' | 'directory' | 'symlink';
|
|
|
|
export interface FileNode {
|
|
id: string;
|
|
name: string;
|
|
parentId?: string | null;
|
|
type: string;
|
|
// 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 modification time (UTCDate). Older builds asked for a non-existent
|
|
// `updated` property, which the server silently left undefined (#700).
|
|
modified?: string;
|
|
// JMAP Sharing (RFC 9670). Populated only when the server advertises the
|
|
// principals capability and the properties are requested explicitly. 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<string, FileNodeRights> | null;
|
|
// True when this node was fetched from another principal's account that was
|
|
// shared with the logged-in user ("Shared with me").
|
|
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 accounts so blob downloads route to the right account.
|
|
accountId?: string;
|
|
accountName?: 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;
|
|
}
|
|
|
|
// RFC 9670 principal — an entity (user, group, resource) that files and
|
|
// calendars can be shared with. On Stalwart the principal id doubles as the
|
|
// account id of that user/group.
|
|
export interface Principal {
|
|
id: string;
|
|
type: 'individual' | 'group' | 'resource' | 'location' | 'other';
|
|
name: string;
|
|
description?: string | null;
|
|
email?: string | null;
|
|
}
|
|
|
|
// ─── Push / SSE ─────────────────────────────────────────
|
|
|
|
export interface StateChange {
|
|
'@type': 'StateChange';
|
|
changed: Record<string, Record<string, string>>;
|
|
}
|
|
|
|
// RFC 8620 §7.2. A PushSubscription is session-scoped (not per-account) and
|
|
// tells the server where to deliver StateChange events for the listed types.
|
|
export interface PushSubscription {
|
|
id: string;
|
|
deviceClientId: string;
|
|
url: string;
|
|
keys?: { p256dh: string; auth: string } | null;
|
|
verificationCode?: string | null;
|
|
expires?: string | null;
|
|
types?: string[] | null;
|
|
emailPush?: Record<string, EmailPushConfig> | 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<string, unknown> | null;
|
|
properties: string[];
|
|
urgency?: 'very-low' | 'low' | 'normal' | 'high';
|
|
}
|
|
|
|
// ─── Capability URNs ────────────────────────────────────
|
|
|
|
export const CAPABILITIES = {
|
|
CORE: 'urn:ietf:params:jmap:core',
|
|
MAIL: 'urn:ietf:params:jmap:mail',
|
|
SUBMISSION: 'urn:ietf:params:jmap:submission',
|
|
VACATION: 'urn:ietf:params:jmap:vacationresponse',
|
|
CONTACTS: 'urn:ietf:params:jmap:contacts',
|
|
CALENDARS: 'urn:ietf:params:jmap:calendars',
|
|
SIEVE: 'urn:ietf:params:jmap:sieve',
|
|
QUOTA: 'urn:ietf:params:jmap:quota',
|
|
FILES: 'urn:ietf:params:jmap:filenode',
|
|
PRINCIPALS: 'urn:ietf:params:jmap:principals',
|
|
PRINCIPALS_OWNER: 'urn:ietf:params:jmap:principals:owner',
|
|
EMAIL_PUSH: 'urn:ietf:params:jmap:emailpush',
|
|
BLOB: 'urn:ietf:params:jmap:blob',
|
|
} as const;
|