Files
jmap-webmail/lib/jmap/types.ts
T
Matthieu MALVACHE 17abe7ff56 feat(contacts): add address book with JMAP sync and local fallback
Implement Phase 1 of contacts support — the biggest missing feature
on the roadmap. Uses RFC 9553 (JSContact) data model and RFC 9610
(JMAP for Contacts) when the server advertises the capability,
falling back to localStorage for servers without contact support.

- JMAP types for ContactCard, AddressBook, NameComponent, etc.
- JMAP client methods: CRUD, search, address book listing
- Refactored request() to accept dynamic `using` capabilities
- Zustand contact store with dual-mode persistence
- Full contacts page with two-column layout (list + detail/edit)
- Contact form with multi-email/phone, context labels, validation
- Composer autocomplete on To/Cc/Bcc with keyboard nav and ARIA
- Sidebar navigation link to contacts
- Auth integration: fetch on login, clear on logout
- i18n: contacts.* namespace across all 8 languages
2026-02-16 17:07:45 +01:00

295 lines
6.3 KiB
TypeScript

export interface EmailHeader {
name: string;
value: string;
}
export interface Email {
id: string;
threadId: string;
mailboxIds: Record<string, boolean>;
keywords: Record<string, boolean>;
size: number;
receivedAt: string;
from?: EmailAddress[];
to?: EmailAddress[];
cc?: EmailAddress[];
bcc?: EmailAddress[];
replyTo?: EmailAddress[];
subject?: string;
sentAt?: string;
preview?: string;
textBody?: EmailBodyPart[];
htmlBody?: EmailBodyPart[];
bodyValues?: Record<string, EmailBodyValue>;
attachments?: Attachment[];
hasAttachment: boolean;
// Extended header information
messageId?: string;
inReplyTo?: string[];
references?: string[];
headers?: Record<string, string | string[]>;
// Security headers parsed
authenticationResults?: AuthenticationResults;
spamScore?: number;
spamStatus?: string;
spamLLM?: {
verdict: string;
explanation: string;
};
}
export interface AuthenticationResults {
spf?: {
result: 'pass' | 'fail' | 'softfail' | 'neutral' | 'none' | 'temperror' | 'permerror';
domain?: string;
ip?: string;
};
dkim?: {
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: {
mayReadItems: boolean;
mayAddItems: boolean;
mayRemoveItems: boolean;
maySetSeen: boolean;
maySetKeywords: boolean;
mayCreateChild: boolean;
mayRename: boolean;
mayDelete: boolean;
maySubmit: boolean;
};
isSubscribed: boolean;
// Shared folder support
accountId?: string;
accountName?: string;
isShared?: boolean;
}
export interface Thread {
id: string;
emailIds: string[];
}
// Thread grouping for UI display
export interface ThreadGroup {
threadId: string;
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
hasAttachment: boolean; // Any email has attachment
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;
}
// RFC 9553 JSContact / RFC 9610 JMAP for Contacts
export interface ContactCard {
id: string;
uid?: string;
addressBookIds: Record<string, boolean>;
kind?: 'individual' | 'group' | 'org';
name?: ContactName;
emails?: Record<string, ContactEmail>;
phones?: Record<string, ContactPhone>;
organizations?: Record<string, ContactOrganization>;
addresses?: Record<string, ContactAddress>;
nicknames?: Record<string, ContactNickname>;
notes?: Record<string, ContactNote>;
created?: string;
updated?: string;
}
export interface ContactName {
components: NameComponent[];
isOrdered?: boolean;
}
export interface NameComponent {
kind: 'given' | 'surname' | 'prefix' | 'suffix' | 'additional';
value: string;
}
export interface ContactEmail {
address: string;
contexts?: Record<string, boolean>;
label?: string;
}
export interface ContactPhone {
number: string;
contexts?: Record<string, boolean>;
label?: string;
}
export interface ContactOrganization {
name?: string;
units?: Array<{ name: string }>;
}
export interface ContactAddress {
street?: string;
locality?: string;
region?: string;
postcode?: string;
country?: string;
contexts?: Record<string, boolean>;
label?: string;
}
export interface ContactNickname {
name: string;
}
export interface ContactNote {
note: string;
}
export interface AddressBook {
id: string;
name: string;
description?: string | null;
sortOrder?: number;
isDefault?: boolean;
isSubscribed?: boolean;
myRights?: AddressBookRights;
}
export interface AddressBookRights {
mayRead: boolean;
mayWrite: boolean;
mayShare: boolean;
mayDelete: boolean;
}
export interface EmailSubmission {
id: string;
identityId: string;
emailId: string;
threadId?: string;
envelope: {
mailFrom: EmailAddress;
rcptTo: EmailAddress[];
};
sendAt?: string;
undoStatus: "pending" | "final" | "canceled";
deliveryStatus?: Record<string, DeliveryStatus>;
dsnBlobIds?: string[];
mdnBlobIds?: string[];
}
export interface DeliveryStatus {
smtpReply: string;
delivered: "queued" | "yes" | "no" | "unknown";
displayed: "unknown" | "yes";
}
// 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;
};
};
}
export interface PushSubscription {
id: string;
deviceClientId: string;
url: string;
keys: {
p256dh: string;
auth: string;
} | null;
expires: string | null;
types: string[] | null;
}
// For tracking last known states
export interface AccountStates {
[accountId: string]: {
Email?: string;
Mailbox?: string;
Thread?: string;
};
}