fix: resolve calendar crash on undefined duration and sieve filter save error

parseDuration() now handles undefined event.duration gracefully instead
of crashing on .match(). CalendarEvent.duration type updated to reflect
that JMAP servers may omit this field.

Sieve filter activation replaced isActive (server-set per RFC 9661) with
onSuccessActivateScript. Create/update calls merged with activation into
single JMAP roundtrips.

Closes #31, closes #21
This commit is contained in:
Matthieu MALVACHE
2026-03-16 12:41:10 +01:00
parent 2790b877c9
commit 4aaef1ebf1
4 changed files with 44 additions and 43 deletions
+2 -1
View File
@@ -28,7 +28,8 @@ function getEventColor(event: CalendarEvent, calendar?: Calendar): string {
return sanitizeColor(event.color, sanitizeColor(calendar?.color));
}
function parseDuration(duration: string): number {
function parseDuration(duration: string | undefined): number {
if (!duration) return 0;
let totalMinutes = 0;
const weekMatch = duration.match(/(\d+)W/);
const hourMatch = duration.match(/(\d+)H/);
+39 -37
View File
@@ -1433,17 +1433,22 @@ export class JMAPClient {
throw new Error('Invalid upload response: blobId not found');
}
async createSieveScript(name: string, content: string): Promise<SieveScript> {
async createSieveScript(name: string, content: string, activate?: boolean): Promise<SieveScript> {
const blobId = await this.uploadSieveBlob(content);
const accountId = this.getSieveAccountId();
const setArgs: Record<string, unknown> = {
accountId,
create: {
"new-script": { name, blobId }
},
};
if (activate) {
setArgs.onSuccessActivateScript = "#new-script";
}
const response = await this.request([
["SieveScript/set", {
accountId,
create: {
"new-script": { name, blobId }
}
}, "0"]
["SieveScript/set", setArgs, "0"]
], this.sieveUsing());
if (response.methodResponses?.[0]?.[0] === "SieveScript/set") {
@@ -1462,17 +1467,22 @@ export class JMAPClient {
throw new Error("Failed to create sieve script");
}
async updateSieveScript(scriptId: string, content: string): Promise<void> {
async updateSieveScript(scriptId: string, content: string, activate?: boolean): Promise<void> {
const blobId = await this.uploadSieveBlob(content);
const accountId = this.getSieveAccountId();
const setArgs: Record<string, unknown> = {
accountId,
update: {
[scriptId]: { blobId }
},
};
if (activate) {
setArgs.onSuccessActivateScript = scriptId;
}
const response = await this.request([
["SieveScript/set", {
accountId,
update: {
[scriptId]: { blobId }
}
}, "0"]
["SieveScript/set", setArgs, "0"]
], this.sieveUsing());
if (response.methodResponses?.[0]?.[0] === "SieveScript/set") {
@@ -1513,44 +1523,36 @@ export class JMAPClient {
const response = await this.request([
["SieveScript/set", {
accountId,
update: {
[scriptId]: { isActive: true }
}
onSuccessActivateScript: scriptId,
}, "0"]
], this.sieveUsing());
if (response.methodResponses?.[0]?.[0] === "SieveScript/set") {
const result = response.methodResponses[0][1];
if (result.notUpdated?.[scriptId]) {
const error = result.notUpdated[scriptId];
throw new Error(error.description || "Failed to activate sieve script");
}
return;
const [methodName, result] = response.methodResponses?.[0] || [];
if (methodName === "error") {
throw new Error(result?.description || "Failed to activate sieve script");
}
if (methodName !== "SieveScript/set") {
throw new Error("Failed to activate sieve script");
}
throw new Error("Failed to activate sieve script");
}
async deactivateSieveScript(scriptId: string): Promise<void> {
async deactivateSieveScript(): Promise<void> {
const accountId = this.getSieveAccountId();
const response = await this.request([
["SieveScript/set", {
accountId,
update: {
[scriptId]: { isActive: false }
}
onSuccessActivateScript: null,
}, "0"]
], this.sieveUsing());
if (response.methodResponses?.[0]?.[0] === "SieveScript/set") {
const result = response.methodResponses[0][1];
if (result.notUpdated?.[scriptId]) {
const error = result.notUpdated[scriptId];
throw new Error(error.description || "Failed to deactivate sieve script");
}
return;
const [methodName, result] = response.methodResponses?.[0] || [];
if (methodName === "error") {
throw new Error(result?.description || "Failed to deactivate sieve script");
}
if (methodName !== "SieveScript/set") {
throw new Error("Failed to deactivate sieve script");
}
throw new Error("Failed to deactivate sieve script");
}
async validateSieveScript(content: string): Promise<{ isValid: boolean; errors?: string[] }> {
+1 -1
View File
@@ -312,7 +312,7 @@ export interface CalendarEvent {
updated: string;
sequence: number;
start: string;
duration: string;
duration?: string;
timeZone: string | null;
showWithoutTime: boolean;
status: 'tentative' | 'confirmed' | 'cancelled';
+2 -4
View File
@@ -94,11 +94,9 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
}
if (activeScriptId) {
await client.updateSieveScript(activeScriptId, content);
await client.activateSieveScript(activeScriptId);
await client.updateSieveScript(activeScriptId, content, true);
} else {
const script = await client.createSieveScript('filters', content);
await client.activateSieveScript(script.id);
const script = await client.createSieveScript('filters', content, true);
set({ activeScriptId: script.id });
}