feat: calendar invitations RSVP, trust assessment, file preview
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { useSettingsStore } from '../settings-store';
|
||||
|
||||
describe('settings-store attachment action', () => {
|
||||
beforeEach(() => {
|
||||
useSettingsStore.getState().resetToDefaults();
|
||||
});
|
||||
|
||||
it('defaults to preview when settings are reset', () => {
|
||||
expect(useSettingsStore.getState().mailAttachmentAction).toBe('preview');
|
||||
});
|
||||
|
||||
it('includes the attachment action in exported settings', () => {
|
||||
useSettingsStore.getState().updateSetting('mailAttachmentAction', 'download');
|
||||
|
||||
const exported = JSON.parse(useSettingsStore.getState().exportSettings()) as {
|
||||
mailAttachmentAction?: string;
|
||||
};
|
||||
|
||||
expect(exported.mailAttachmentAction).toBe('download');
|
||||
});
|
||||
|
||||
it('includes calendar invitation parsing in exported settings', () => {
|
||||
useSettingsStore.getState().updateSetting('calendarInvitationParsingEnabled', false);
|
||||
|
||||
const exported = JSON.parse(useSettingsStore.getState().exportSettings()) as {
|
||||
calendarInvitationParsingEnabled?: boolean;
|
||||
};
|
||||
|
||||
expect(exported.calendarInvitationParsingEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -36,7 +36,7 @@ interface CalendarStore {
|
||||
createEvent: (client: JMAPClient, event: Partial<CalendarEvent>, sendSchedulingMessages?: boolean) => Promise<CalendarEvent | null>;
|
||||
updateEvent: (client: JMAPClient, id: string, updates: Partial<CalendarEvent>, sendSchedulingMessages?: boolean) => Promise<void>;
|
||||
deleteEvent: (client: JMAPClient, id: string, sendSchedulingMessages?: boolean) => Promise<void>;
|
||||
rsvpEvent: (client: JMAPClient, eventId: string, participantId: string, status: string) => Promise<void>;
|
||||
rsvpEvent: (client: JMAPClient, eventId: string, participantId: string, status: string, replyTo?: Record<string, string> | null) => Promise<void>;
|
||||
importEvents: (client: JMAPClient, events: Partial<CalendarEvent>[], calendarId: string) => Promise<number>;
|
||||
updateCalendar: (client: JMAPClient, calendarId: string, updates: Partial<Calendar>) => Promise<void>;
|
||||
createCalendar: (client: JMAPClient, calendar: Partial<Calendar>) => Promise<Calendar | null>;
|
||||
@@ -138,17 +138,27 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
rsvpEvent: async (client, eventId, participantId, status) => {
|
||||
rsvpEvent: async (client, eventId, participantId, status, replyTo) => {
|
||||
set({ error: null });
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(participantId)) {
|
||||
// JMAP participant IDs are opaque strings — they can contain @, ., :, / etc.
|
||||
// Only reject empty or obviously malicious values (path traversal).
|
||||
if (!participantId || participantId.includes('..')) {
|
||||
set({ error: 'Invalid participant ID' });
|
||||
throw new Error('Invalid participant ID');
|
||||
}
|
||||
try {
|
||||
const patchKey = `participants/${participantId}/participationStatus`;
|
||||
// Escape per RFC 6901 (JSON Pointer): ~ → ~0, / → ~1
|
||||
const escapedId = participantId.replace(/~/g, '~0').replace(/\//g, '~1');
|
||||
const patchKey = `participants/${escapedId}/participationStatus`;
|
||||
const patch: Record<string, unknown> = { [patchKey]: status };
|
||||
// Include replyTo so the server knows where to deliver the iTIP reply
|
||||
// (may be missing if the event was imported or auto-created without it).
|
||||
if (replyTo) {
|
||||
patch.replyTo = replyTo;
|
||||
}
|
||||
await client.updateCalendarEvent(
|
||||
eventId,
|
||||
{ [patchKey]: status } as unknown as Partial<CalendarEvent>,
|
||||
patch as unknown as Partial<CalendarEvent>,
|
||||
true
|
||||
);
|
||||
set((state) => ({
|
||||
@@ -183,6 +193,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
'@type': 'Participant',
|
||||
name: p.name,
|
||||
email: p.email,
|
||||
calendarAddress: p.calendarAddress,
|
||||
description: p.description,
|
||||
sendTo: p.sendTo,
|
||||
kind: p.kind,
|
||||
@@ -224,6 +235,7 @@ export const useCalendarStore = create<CalendarStore>()(
|
||||
keywords: src.keywords,
|
||||
categories: src.categories,
|
||||
locale: src.locale,
|
||||
replyTo: src.replyTo || (src.organizerCalendarAddress ? { imip: src.organizerCalendarAddress } : undefined),
|
||||
locations: src.locations,
|
||||
virtualLocations: src.virtualLocations,
|
||||
links: src.links,
|
||||
|
||||
@@ -28,6 +28,7 @@ export type DateFormat = 'regional' | 'iso' | 'custom';
|
||||
export type TimeFormat = '12h' | '24h';
|
||||
export type FirstDayOfWeek = 0 | 1; // 0 = Sunday, 1 = Monday
|
||||
export type ExternalContentPolicy = 'ask' | 'block' | 'allow';
|
||||
export type MailAttachmentAction = 'preview' | 'download';
|
||||
export type ToolbarPosition = 'top' | 'below-subject';
|
||||
|
||||
export interface KeywordDefinition {
|
||||
@@ -81,6 +82,7 @@ interface SettingsState {
|
||||
showPreview: boolean;
|
||||
emailsPerPage: number;
|
||||
externalContentPolicy: ExternalContentPolicy;
|
||||
mailAttachmentAction: MailAttachmentAction;
|
||||
|
||||
// Composer
|
||||
autoSaveDraftInterval: number; // milliseconds
|
||||
@@ -94,6 +96,7 @@ interface SettingsState {
|
||||
// Calendar Notifications
|
||||
calendarNotificationsEnabled: boolean;
|
||||
calendarNotificationSound: boolean;
|
||||
calendarInvitationParsingEnabled: boolean;
|
||||
|
||||
// Layout
|
||||
toolbarPosition: ToolbarPosition;
|
||||
@@ -160,6 +163,7 @@ const DEFAULT_SETTINGS = {
|
||||
showPreview: true,
|
||||
emailsPerPage: 50,
|
||||
externalContentPolicy: 'ask' as ExternalContentPolicy,
|
||||
mailAttachmentAction: 'preview' as MailAttachmentAction,
|
||||
|
||||
// Composer
|
||||
autoSaveDraftInterval: 60000, // 1 minute
|
||||
@@ -173,6 +177,7 @@ const DEFAULT_SETTINGS = {
|
||||
// Calendar Notifications
|
||||
calendarNotificationsEnabled: true,
|
||||
calendarNotificationSound: true,
|
||||
calendarInvitationParsingEnabled: true,
|
||||
|
||||
// Layout
|
||||
toolbarPosition: 'top' as ToolbarPosition,
|
||||
@@ -237,6 +242,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
showPreview: state.showPreview,
|
||||
emailsPerPage: state.emailsPerPage,
|
||||
externalContentPolicy: state.externalContentPolicy,
|
||||
mailAttachmentAction: state.mailAttachmentAction,
|
||||
trustedSenders: state.trustedSenders,
|
||||
autoSaveDraftInterval: state.autoSaveDraftInterval,
|
||||
sendConfirmation: state.sendConfirmation,
|
||||
@@ -244,6 +250,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
sessionTimeout: state.sessionTimeout,
|
||||
calendarNotificationsEnabled: state.calendarNotificationsEnabled,
|
||||
calendarNotificationSound: state.calendarNotificationSound,
|
||||
calendarInvitationParsingEnabled: state.calendarInvitationParsingEnabled,
|
||||
toolbarPosition: state.toolbarPosition,
|
||||
senderFavicons: state.senderFavicons,
|
||||
folderIcons: state.folderIcons,
|
||||
|
||||
Reference in New Issue
Block a user