From 0fb8b81acd8d445743ab317be7ce4189b1a449d7 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:06:28 +0200 Subject: [PATCH] fix: deduplicate UIDs during iCal import to prevent mass failures #113 --- components/calendar/ical-import-modal.tsx | 2 +- locales/de/common.json | 2 +- locales/en/common.json | 2 +- locales/es/common.json | 2 +- locales/fr/common.json | 2 +- locales/it/common.json | 2 +- locales/ja/common.json | 2 +- locales/nl/common.json | 2 +- locales/pt/common.json | 2 +- locales/ru/common.json | 2 +- stores/calendar-store.ts | 21 ++++++++++++++++++++- 11 files changed, 30 insertions(+), 11 deletions(-) diff --git a/components/calendar/ical-import-modal.tsx b/components/calendar/ical-import-modal.tsx index cf2a640b..834ad009 100644 --- a/components/calendar/ical-import-modal.tsx +++ b/components/calendar/ical-import-modal.tsx @@ -18,7 +18,7 @@ interface ICalImportModalProps { onClose: () => void; } -const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB +const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const ACCEPTED_EXTENSIONS = [".ics", ".ical"]; type ImportStep = "select" | "preview" | "importing"; diff --git a/locales/de/common.json b/locales/de/common.json index ab129075..b684234d 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -2057,7 +2057,7 @@ "importing": "Termine werden importiert...", "success": "{count} Termine erfolgreich importiert", "error": "Kalender konnte nicht importiert werden", - "file_too_large": "Datei überschreitet das 5-MB-Limit", + "file_too_large": "Datei überschreitet das 10-MB-Limit", "invalid_format": "Ungültiges Kalenderdateiformat" }, "management": { diff --git a/locales/en/common.json b/locales/en/common.json index d4cf1140..09678a7c 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -2057,7 +2057,7 @@ "importing": "Importing events...", "success": "{count} events imported successfully", "error": "Failed to import calendar", - "file_too_large": "File exceeds 5MB limit", + "file_too_large": "File exceeds 10MB limit", "invalid_format": "Invalid calendar file format" }, "management": { diff --git a/locales/es/common.json b/locales/es/common.json index 1781c586..e0bad98e 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -2057,7 +2057,7 @@ "importing": "Importando eventos...", "success": "{count} eventos importados correctamente", "error": "Error al importar el calendario", - "file_too_large": "El archivo supera el límite de 5 MB", + "file_too_large": "El archivo supera el límite de 10 MB", "invalid_format": "Formato de archivo de calendario no válido" }, "management": { diff --git a/locales/fr/common.json b/locales/fr/common.json index 1695a805..c9d22d53 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -2057,7 +2057,7 @@ "importing": "Importation en cours...", "success": "{count} événements importés avec succès", "error": "Échec de l'importation du calendrier", - "file_too_large": "Le fichier dépasse la limite de 5 Mo", + "file_too_large": "Le fichier dépasse la limite de 10 Mo", "invalid_format": "Format de fichier calendrier invalide" }, "management": { diff --git a/locales/it/common.json b/locales/it/common.json index 12b69385..465b9aa1 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -2057,7 +2057,7 @@ "importing": "Importazione eventi...", "success": "{count} eventi importati con successo", "error": "Importazione del calendario fallita", - "file_too_large": "Il file supera il limite di 5 MB", + "file_too_large": "Il file supera il limite di 10 MB", "invalid_format": "Formato del file calendario non valido" }, "management": { diff --git a/locales/ja/common.json b/locales/ja/common.json index 3127bed1..fbc07b24 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -2057,7 +2057,7 @@ "importing": "イベントをインポート中...", "success": "{count}件のイベントをインポートしました", "error": "カレンダーのインポートに失敗しました", - "file_too_large": "ファイルサイズが5MBを超えています", + "file_too_large": "ファイルサイズが10MBを超えています", "invalid_format": "無効なカレンダーファイル形式" }, "management": { diff --git a/locales/nl/common.json b/locales/nl/common.json index 958756d0..5cd53c79 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -2057,7 +2057,7 @@ "importing": "Evenementen importeren...", "success": "{count} evenementen succesvol geïmporteerd", "error": "Agenda importeren mislukt", - "file_too_large": "Bestand overschrijdt de limiet van 5 MB", + "file_too_large": "Bestand overschrijdt de limiet van 10 MB", "invalid_format": "Ongeldig agendabestandsformaat" }, "management": { diff --git a/locales/pt/common.json b/locales/pt/common.json index 2c1edbeb..c6273eb4 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -2057,7 +2057,7 @@ "importing": "Importando eventos...", "success": "{count} eventos importados com sucesso", "error": "Falha ao importar calendário", - "file_too_large": "Arquivo excede o limite de 5 MB", + "file_too_large": "Arquivo excede o limite de 10 MB", "invalid_format": "Formato de arquivo de calendário inválido" }, "management": { diff --git a/locales/ru/common.json b/locales/ru/common.json index 290fa646..aecd83fb 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -2057,7 +2057,7 @@ "importing": "Импортирование событий...", "success": "{count} событий успешно импортировано", "error": "Не удалось импортировать календарь", - "file_too_large": "Файл превышает лимит 5 МБ", + "file_too_large": "Файл превышает лимит 10 МБ", "invalid_format": "Неверный формат файла календаря" }, "management": { diff --git a/stores/calendar-store.ts b/stores/calendar-store.ts index cf4617cd..940f2f25 100644 --- a/stores/calendar-store.ts +++ b/stores/calendar-store.ts @@ -383,9 +383,28 @@ export const useCalendarStore = create()( const realCalendarId = cal?.originalId || calendarId; const targetAccountId = cal?.accountId; + // Deduplicate: skip events whose UIDs already exist on the server + // (Stalwart enforces UID uniqueness across all calendars) + let eventsToProcess = events; + try { + const allServerEvents = await client.getCalendarEvents(undefined, targetAccountId); + const existingUids = new Set(); + for (const e of allServerEvents) { + if (e.uid) existingUids.add(e.uid); + } + const before = eventsToProcess.length; + eventsToProcess = eventsToProcess.filter(e => !e.uid || !existingUids.has(e.uid)); + const skipped = before - eventsToProcess.length; + if (skipped > 0) { + debug.log(`Import: skipped ${skipped} events with duplicate UIDs (already on server)`); + } + } catch (error) { + debug.warn('Could not fetch existing events for deduplication, proceeding without:', error); + } + // Prepare all events for batch creation const prepared: Partial[] = []; - for (const event of events) { + for (const event of eventsToProcess) { const src = sanitizeOutgoingCalendarEventData(event as Partial); let cleanParticipants: Record | null = null; if (src.participants) {