diff --git a/components/calendar/calendar-app.tsx b/components/calendar/calendar-app.tsx index d8b3ae5fe..c4ab94ceb 100644 --- a/components/calendar/calendar-app.tsx +++ b/components/calendar/calendar-app.tsx @@ -63,6 +63,7 @@ import type { Calendar, CalendarEvent, CalendarParticipant, CalendarRights } fro import { ShareCollectionDialog } from "@/components/settings/share-collection-dialog"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { useConfirmDialog } from "@/hooks/use-confirm-dialog"; +import { SchedulingDeniedError } from "@/lib/jmap/scheduling-error"; import { CreateCalendarModal } from "@/components/calendar/create-calendar-modal"; import { getUserParticipantId, collectUserCalendarAddresses } from "@/lib/calendar-participants"; import { generateBirthdayEvents, createBirthdayCalendar, BIRTHDAY_CALENDAR_ID } from "@/lib/birthday-calendar"; @@ -842,45 +843,59 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = jumpTo(eventDate); }, [jumpTo]); - const handleSaveEvent = useCallback(async (data: Partial, sendSchedulingMessages?: boolean) => { + const handleSaveEvent = useCallback(async (data: Partial, requestedScheduling?: boolean) => { if (!client) { toast.error(t("notifications.event_error")); return; } - try { - if (editEvent) { - if (isRecurringEvent(editEvent)) { - setPendingScopeAction({ - type: "edit", - event: editEvent, - updates: data, - sendScheduling: sendSchedulingMessages, - }); - setShowEventModal(false); - setEditEvent(null); - return; - } - await updateEvent(client, editEvent.id, data, sendSchedulingMessages); - if (data.start) { - focusCalendarOnEvent({ start: data.start }); - } - toast.success(t("notifications.event_updated")); - } else { - const created = await createEvent(client, data, sendSchedulingMessages); - if (!created) { - toast.error(t("notifications.event_error")); - return; - } - focusCalendarOnEvent(created); - if (sendSchedulingMessages) { - toast.success(t("notifications.invitation_sent")); + const save = async (sendSchedulingMessages: boolean | undefined): Promise => { + try { + if (editEvent) { + if (isRecurringEvent(editEvent)) { + setPendingScopeAction({ + type: "edit", + event: editEvent, + updates: data, + sendScheduling: sendSchedulingMessages, + }); + setShowEventModal(false); + setEditEvent(null); + return; + } + await updateEvent(client, editEvent.id, data, sendSchedulingMessages); + if (data.start) { + focusCalendarOnEvent({ start: data.start }); + } + toast.success(t("notifications.event_updated")); } else { - toast.success(t("notifications.event_created")); + const created = await createEvent(client, data, sendSchedulingMessages); + if (!created) { + toast.error(t("notifications.event_error")); + return; + } + focusCalendarOnEvent(created); + if (sendSchedulingMessages) { + toast.success(t("notifications.invitation_sent")); + } else { + toast.success(t("notifications.event_created")); + } } + setShowEventModal(false); + setEditEvent(null); + } catch (error) { + // The server refuses to send the invitations (Stalwart 0.16.21+ fails + // the whole save then). Offer to keep the event without them. + if (error instanceof SchedulingDeniedError && sendSchedulingMessages) { + const ok = await confirmAction({ + title: t("notifications.invitations_denied_title"), + message: t("notifications.invitations_denied", { reason: error.reason }), + confirmText: t("notifications.save_without_invitations"), + }); + if (ok) await save(false); + return; + } + toast.error(t("notifications.event_error")); } - setShowEventModal(false); - setEditEvent(null); - } catch { - toast.error(t("notifications.event_error")); - } - }, [client, editEvent, createEvent, updateEvent, focusCalendarOnEvent, t]); + }; + await save(requestedScheduling); + }, [client, editEvent, createEvent, updateEvent, focusCalendarOnEvent, confirmAction, t]); const handleDuplicateEvent = useCallback(async (data: Partial) => { if (!client) { toast.error(t("notifications.event_error")); return; } diff --git a/lib/__tests__/jmap-calendar-scheduling.test.ts b/lib/__tests__/jmap-calendar-scheduling.test.ts new file mode 100644 index 000000000..514f3e9b7 --- /dev/null +++ b/lib/__tests__/jmap-calendar-scheduling.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi } from 'vitest'; +import { JMAPClient, SchedulingDeniedError } from '../jmap/client'; + +type Call = [string, Record, string]; + +// Stalwart 0.16.21+ fails the whole CalendarEvent/set with `forbidden` when +// the account may not send iTIP messages; the UI can then save without them. +function clientRefusing(type: string) { + const client = new JMAPClient('https://jmap.example.com', 'user@example.com', 'pass'); + Object.assign(client, { accountId: 'me' }); + vi.spyOn(client as unknown as { getCalendarsAccountId: () => string }, 'getCalendarsAccountId').mockReturnValue('me'); + vi.spyOn(client as unknown as { request: (c: Call[]) => Promise }, 'request') + .mockImplementation(async (calls: Call[]) => { + const [, args, id] = calls[0]; + const error = { type, description: 'Scheduling is not allowed for this account' }; + return { + methodResponses: [['CalendarEvent/set', args.create + ? { notCreated: { 'new-event': error } } + : { notUpdated: Object.fromEntries(Object.keys(args.update as object).map((k) => [k, error])) }, id]], + }; + }); + return client; +} + +describe('calendar scheduling refusals', () => { + it('reports a refused invitation as SchedulingDeniedError', async () => { + const client = clientRefusing('forbidden'); + await expect(client.createCalendarEvent({ title: 'Meet' }, true)).rejects.toBeInstanceOf(SchedulingDeniedError); + await expect(client.updateCalendarEvent('e1', { title: 'Meet' }, true)).rejects.toThrow(SchedulingDeniedError); + }); + + it('keeps other failures as plain errors', async () => { + await expect(clientRefusing('forbidden').createCalendarEvent({ title: 'Meet' }, false)) + .rejects.not.toBeInstanceOf(SchedulingDeniedError); + await expect(clientRefusing('invalidProperties').createCalendarEvent({ title: 'Meet' }, true)) + .rejects.not.toBeInstanceOf(SchedulingDeniedError); + }); +}); diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index 0cb8eabec..73af50acc 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -14,6 +14,9 @@ import { DEFAULT_CALENDAR_COMPONENTS, mkCalendarCollection, newCalendarCollectio import { sanitizeDisplayName, splitMailbox } from "@/lib/rfc5322-mailbox"; import { decodeFileNodeName } from "./filenode-name"; import { contactFromWire, contactToWire } from "./contact-wire"; +import { SchedulingDeniedError } from "./scheduling-error"; + +export { SchedulingDeniedError }; import { acceptedFileName, fileNameRulesFrom, type FileNameRules } from "@/lib/file-name-rules"; import { getEffectiveTimeZone, toLocalDateTime } from "@/lib/timezone"; import { buildEmailSort, compareEmails, hasKeywordLevels, type KeywordSortPolarity, type SortLevel } from "@/lib/message-list-order"; @@ -6719,6 +6722,9 @@ export class JMAPClient implements IJMAPClient { debug.warn('calendar', 'CalendarEvent/create invalid properties', error.properties); debug.warn('calendar', 'CalendarEvent/create sent keys', Object.keys(cleanEvent)); debug.groupEnd(); + if (sendSchedulingMessages && error.type === 'forbidden') { + throw new SchedulingDeniedError(error.description || 'forbidden'); + } throw new Error(error.description || "Failed to create calendar event"); } @@ -6909,6 +6915,9 @@ export class JMAPClient implements IJMAPClient { if (result.notUpdated?.[eventId]) { const error = result.notUpdated[eventId]; debug.error('CalendarEvent/set notUpdated', { eventId, error }); + if (sendSchedulingMessages && error.type === 'forbidden') { + throw new SchedulingDeniedError(error.description || 'forbidden'); + } throw new Error(error.description || "Failed to update calendar event"); } debug.log('calendar', 'CalendarEvent/set update full response', { methodName, result }); diff --git a/lib/jmap/scheduling-error.ts b/lib/jmap/scheduling-error.ts new file mode 100644 index 000000000..6ca9d090e --- /dev/null +++ b/lib/jmap/scheduling-error.ts @@ -0,0 +1,12 @@ +/** + * The server refused the change because of the iTIP messages it would send + * (Stalwart 0.16.21+ fails the whole CalendarEvent/set with `forbidden` + * when the account may not send scheduling messages). The same change + * without `sendSchedulingMessages` can still be saved. + */ +export class SchedulingDeniedError extends Error { + constructor(readonly reason: string) { + super(reason); + this.name = 'SchedulingDeniedError'; + } +} diff --git a/locales/ar/common.json b/locales/ar/common.json index 4d22061ed..40bf97f61 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -3035,6 +3035,9 @@ "alert_now": "يبدأ الآن", "alert_in_minutes": "خلال {count} دقيقة", "invitation_sent": "تم إرسال الدعوات", + "invitations_denied_title": "تعذر إرسال الدعوات", + "invitations_denied": "رفض الخادم إرسال الدعوات: {reason}. هل تريد حفظ الحدث دون إرسالها؟", + "save_without_invitations": "حفظ دون دعوات", "rsvp_updated": "تم تحديث الرد", "rsvp_error": "فشل تحديث الرد", "event_duplicated": "تم تكرار الحدث", diff --git a/locales/ca/common.json b/locales/ca/common.json index 42477daf4..0285102d3 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -3035,6 +3035,9 @@ "alert_now": "Comença ara", "alert_in_minutes": "D'aquí a {count} min", "invitation_sent": "Invitacions enviades", + "invitations_denied_title": "No es poden enviar les invitacions", + "invitations_denied": "El servidor ha refusat enviar les invitacions: {reason}. Voleu desar l'esdeveniment sense enviar-les?", + "save_without_invitations": "Desa sense invitacions", "rsvp_updated": "Resposta actualitzada", "rsvp_error": "No s'ha pogut actualitzar la resposta", "event_duplicated": "Esdeveniment duplicat", diff --git a/locales/cs/common.json b/locales/cs/common.json index 9aca0a3f7..ce1ad044a 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -3009,6 +3009,9 @@ "alert_now": "Začíná nyní", "alert_in_minutes": "Za {count} min", "invitation_sent": "Pozvánky byly odeslány", + "invitations_denied_title": "Pozvánky nelze odeslat", + "invitations_denied": "Server odmítl odeslat pozvánky: {reason}. Uložit událost bez jejich odeslání?", + "save_without_invitations": "Uložit bez pozvánek", "rsvp_updated": "Odpověď byla aktualizována", "rsvp_error": "Aktualizace odpovědi selhala", "event_duplicated": "Událost byla duplikována", diff --git a/locales/da/common.json b/locales/da/common.json index acb068c82..4e724b617 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -3035,6 +3035,9 @@ "alert_now": "Starter nu", "alert_in_minutes": "Om {count} min", "invitation_sent": "Invitationer sendt", + "invitations_denied_title": "Invitationerne kan ikke sendes", + "invitations_denied": "Serveren afviste at sende invitationerne: {reason}. Gem begivenheden uden at sende dem?", + "save_without_invitations": "Gem uden invitationer", "rsvp_updated": "Svar opdateret", "rsvp_error": "Kunne ikke opdatere svar", "event_duplicated": "Begivenhed duplikeret", diff --git a/locales/de/common.json b/locales/de/common.json index 9620e5bb2..3a91a2f6c 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -3009,6 +3009,9 @@ "alert_now": "Beginnt jetzt", "alert_in_minutes": "In {count} Min.", "invitation_sent": "Einladungen gesendet", + "invitations_denied_title": "Einladungen können nicht gesendet werden", + "invitations_denied": "Der Server hat das Senden der Einladungen abgelehnt: {reason}. Termin ohne Einladungen speichern?", + "save_without_invitations": "Ohne Einladungen speichern", "rsvp_updated": "Antwort aktualisiert", "rsvp_error": "Antwort konnte nicht aktualisiert werden", "event_duplicated": "Termin dupliziert", diff --git a/locales/en/common.json b/locales/en/common.json index 9846f1ad2..3b2a08047 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -3035,6 +3035,9 @@ "alert_now": "Starting now", "alert_in_minutes": "In {count} min", "invitation_sent": "Invitations sent", + "invitations_denied_title": "Invitations can't be sent", + "invitations_denied": "The server refused to send the invitations: {reason}. Save the event without sending them?", + "save_without_invitations": "Save without invitations", "rsvp_updated": "Response updated", "rsvp_error": "Failed to update response", "event_duplicated": "Event duplicated", diff --git a/locales/es/common.json b/locales/es/common.json index 0c9a4d471..b6a7980a1 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -3009,6 +3009,9 @@ "alert_now": "Comienza ahora", "alert_in_minutes": "En {count} min", "invitation_sent": "Invitaciones enviadas", + "invitations_denied_title": "No se pueden enviar las invitaciones", + "invitations_denied": "El servidor rechazó enviar las invitaciones: {reason}. ¿Guardar el evento sin enviarlas?", + "save_without_invitations": "Guardar sin invitaciones", "rsvp_updated": "Respuesta actualizada", "rsvp_error": "Error al actualizar la respuesta", "event_duplicated": "Evento duplicado", diff --git a/locales/fa/common.json b/locales/fa/common.json index edb8cd4b2..ae6368236 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -3035,6 +3035,9 @@ "alert_now": "همین الان شروع می‌شود", "alert_in_minutes": "{count} دقیقه دیگر", "invitation_sent": "دعوت‌نامه‌ها ارسال شد", + "invitations_denied_title": "ارسال دعوت‌نامه‌ها ممکن نیست", + "invitations_denied": "سرور از ارسال دعوت‌نامه‌ها خودداری کرد: {reason}. رویداد بدون ارسال آن‌ها ذخیره شود؟", + "save_without_invitations": "ذخیره بدون دعوت‌نامه", "rsvp_updated": "پاسخ به‌روزرسانی شد", "rsvp_error": "به‌روزرسانی پاسخ ناموفق بود", "event_duplicated": "رویداد کپی شد", diff --git a/locales/fr/common.json b/locales/fr/common.json index e68d10fe5..ac8116a00 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -3035,6 +3035,9 @@ "alert_now": "Commence maintenant", "alert_in_minutes": "Dans {count} min", "invitation_sent": "Invitations envoyées", + "invitations_denied_title": "Impossible d'envoyer les invitations", + "invitations_denied": "Le serveur a refusé d'envoyer les invitations : {reason}. Enregistrer l'événement sans les envoyer ?", + "save_without_invitations": "Enregistrer sans invitations", "rsvp_updated": "Réponse mise à jour", "rsvp_error": "Échec de la mise à jour de la réponse", "event_duplicated": "Événement dupliqué", diff --git a/locales/he/common.json b/locales/he/common.json index 495744bda..9b23d2bb0 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -2933,6 +2933,9 @@ "alert_now": "מתחיל עכשיו", "alert_in_minutes": "ב-{count}דקות", "invitation_sent": "נשלחו הזמנות", + "invitations_denied_title": "לא ניתן לשלוח את ההזמנות", + "invitations_denied": "השרת סירב לשלוח את ההזמנות: {reason}. לשמור את האירוע בלי לשלוח אותן?", + "save_without_invitations": "שמור בלי הזמנות", "rsvp_updated": "התגובה עודכנה", "rsvp_error": "עדכון התגובה נכשל", "event_duplicated": "האירוע שוכפל", diff --git a/locales/hu/common.json b/locales/hu/common.json index e28f3194b..359ca9311 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -3035,6 +3035,9 @@ "alert_now": "Most kezdődik", "alert_in_minutes": "{count} perc múlva", "invitation_sent": "Meghívók elküldve", + "invitations_denied_title": "A meghívók nem küldhetők el", + "invitations_denied": "A kiszolgáló nem küldte el a meghívókat: {reason}. Mentsük az eseményt a meghívók nélkül?", + "save_without_invitations": "Mentés meghívók nélkül", "rsvp_updated": "Válasz frissítve", "rsvp_error": "Nem sikerült frissíteni a választ", "event_duplicated": "Esemény duplikálva", diff --git a/locales/it/common.json b/locales/it/common.json index 481e8e534..fc516c013 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -3009,6 +3009,9 @@ "alert_now": "Inizia ora", "alert_in_minutes": "Tra {count} min", "invitation_sent": "Inviti inviati", + "invitations_denied_title": "Impossibile inviare gli inviti", + "invitations_denied": "Il server ha rifiutato di inviare gli inviti: {reason}. Salvare l'evento senza inviarli?", + "save_without_invitations": "Salva senza inviti", "rsvp_updated": "Risposta aggiornata", "rsvp_error": "Impossibile aggiornare la risposta", "event_duplicated": "Evento duplicato", diff --git a/locales/ja/common.json b/locales/ja/common.json index fe0c05886..9aacfeb5e 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -3009,6 +3009,9 @@ "alert_now": "まもなく開始", "alert_in_minutes": "{count}分後", "invitation_sent": "招待を送信しました", + "invitations_denied_title": "招待状を送信できません", + "invitations_denied": "サーバーが招待状の送信を拒否しました: {reason}。招待状を送らずに予定を保存しますか?", + "save_without_invitations": "招待状なしで保存", "rsvp_updated": "回答を更新しました", "rsvp_error": "回答の更新に失敗しました", "event_duplicated": "予定を複製しました", diff --git a/locales/ko/common.json b/locales/ko/common.json index 415c14435..0a8bf6fb3 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -3009,6 +3009,9 @@ "alert_now": "지금 시작해요", "alert_in_minutes": "{count}분 후 시작", "invitation_sent": "초대장이 전송되었어요", + "invitations_denied_title": "초대장을 보낼 수 없어요", + "invitations_denied": "서버가 초대장 발송을 거부했어요: {reason}. 초대장 없이 일정을 저장할까요?", + "save_without_invitations": "초대장 없이 저장", "rsvp_updated": "응답이 업데이트되었어요", "rsvp_error": "응답을 업데이트하지 못했어요", "event_duplicated": "일정이 복제되었어요", diff --git a/locales/lv/common.json b/locales/lv/common.json index 1bc90a5a0..83654ad8c 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -3008,6 +3008,9 @@ "alert_now": "Sākas tagad", "alert_in_minutes": "Pēc {count} min", "invitation_sent": "Uzaicinājumi nosūtīti", + "invitations_denied_title": "Ielūgumus nevar nosūtīt", + "invitations_denied": "Serveris atteicās nosūtīt ielūgumus: {reason}. Saglabāt notikumu, tos nenosūtot?", + "save_without_invitations": "Saglabāt bez ielūgumiem", "rsvp_updated": "Atbilde atjaunināta", "rsvp_error": "Neizdevās atjaunināt atbildi", "event_duplicated": "Pasākums dublēts", diff --git a/locales/mn/common.json b/locales/mn/common.json index c622a7018..0cf9fd04e 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -3035,6 +3035,9 @@ "alert_now": "Одооноос эхлэн", "alert_in_minutes": "{count} мин", "invitation_sent": "Урилга илгээсэн", + "invitations_denied_title": "Урилга илгээх боломжгүй", + "invitations_denied": "Сервер урилга илгээхээс татгалзлаа: {reason}. Урилга илгээхгүйгээр үйл явдлыг хадгалах уу?", + "save_without_invitations": "Урилгагүйгээр хадгалах", "rsvp_updated": "Хариулт шинэчлэгдсэн", "rsvp_error": "Хариултыг шинэчилж чадсангүй", "event_duplicated": "Үйл явдал давхардсан", diff --git a/locales/nb/common.json b/locales/nb/common.json index f9c4cb85f..9902a39a7 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -3035,6 +3035,9 @@ "alert_now": "Starter nå", "alert_in_minutes": "Om {count} min", "invitation_sent": "Invitasjonene er sendt", + "invitations_denied_title": "Invitasjonene kan ikke sendes", + "invitations_denied": "Serveren nektet å sende invitasjonene: {reason}. Lagre hendelsen uten å sende dem?", + "save_without_invitations": "Lagre uten invitasjoner", "rsvp_updated": "Svaret er oppdatert", "rsvp_error": "Kunne ikke oppdatere svaret", "event_duplicated": "Hendelsen er duplisert", diff --git a/locales/nl/common.json b/locales/nl/common.json index fca7b5f45..2dc2dc2ae 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -3009,6 +3009,9 @@ "alert_now": "Begint nu", "alert_in_minutes": "Over {count} min", "invitation_sent": "Uitnodigingen verzonden", + "invitations_denied_title": "Uitnodigingen kunnen niet worden verzonden", + "invitations_denied": "De server weigerde de uitnodigingen te verzenden: {reason}. Het evenement opslaan zonder ze te verzenden?", + "save_without_invitations": "Opslaan zonder uitnodigingen", "rsvp_updated": "Reactie bijgewerkt", "rsvp_error": "Reactie kon niet worden bijgewerkt", "event_duplicated": "Evenement gedupliceerd", diff --git a/locales/pl/common.json b/locales/pl/common.json index aa8296cdd..83a4bf41e 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -3009,6 +3009,9 @@ "alert_now": "Zaczyna się teraz", "alert_in_minutes": "Za {count} min", "invitation_sent": "Zaproszenia wysłano", + "invitations_denied_title": "Nie można wysłać zaproszeń", + "invitations_denied": "Serwer odmówił wysłania zaproszeń: {reason}. Zapisać wydarzenie bez ich wysyłania?", + "save_without_invitations": "Zapisz bez zaproszeń", "rsvp_updated": "Odpowiedź zaktualizowano", "rsvp_error": "Nie udało się zaktualizować odpowiedzi", "event_duplicated": "Wydarzenie zduplikowano", diff --git a/locales/pt/common.json b/locales/pt/common.json index 594fbd1b4..c7cf695b3 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -3035,6 +3035,9 @@ "alert_now": "Começa agora", "alert_in_minutes": "Em {count} min", "invitation_sent": "Convites enviados", + "invitations_denied_title": "Não é possível enviar os convites", + "invitations_denied": "O servidor recusou enviar os convites: {reason}. Guardar o evento sem os enviar?", + "save_without_invitations": "Guardar sem convites", "rsvp_updated": "Resposta atualizada", "rsvp_error": "Falha ao atualizar resposta", "event_duplicated": "Evento duplicado", diff --git a/locales/ro/common.json b/locales/ro/common.json index d1b6ac83f..75ac498e7 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -3035,6 +3035,9 @@ "alert_now": "Începând de acum", "alert_in_minutes": "În {count} min", "invitation_sent": "Invitații trimise", + "invitations_denied_title": "Invitațiile nu pot fi trimise", + "invitations_denied": "Serverul a refuzat trimiterea invitațiilor: {reason}. Salvezi evenimentul fără a le trimite?", + "save_without_invitations": "Salvează fără invitații", "rsvp_updated": "Răspuns actualizat", "rsvp_error": "Nu s-a putut actualiza răspunsul", "event_duplicated": "Eveniment duplicat", diff --git a/locales/ru/common.json b/locales/ru/common.json index 0f3d4c570..76d37b856 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -3009,6 +3009,9 @@ "alert_now": "Начинается сейчас", "alert_in_minutes": "Через {count} мин", "invitation_sent": "Приглашения отправлены", + "invitations_denied_title": "Не удаётся отправить приглашения", + "invitations_denied": "Сервер отказался отправить приглашения: {reason}. Сохранить событие без их отправки?", + "save_without_invitations": "Сохранить без приглашений", "rsvp_updated": "Ответ обновлён", "rsvp_error": "Не удалось обновить ответ", "event_duplicated": "Событие дублировано", diff --git a/locales/sk/common.json b/locales/sk/common.json index e971addd0..f800d7159 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -3035,6 +3035,9 @@ "alert_now": "Začína teraz", "alert_in_minutes": "Za {count} min", "invitation_sent": "Pozvánky boli odoslané", + "invitations_denied_title": "Pozvánky nie je možné odoslať", + "invitations_denied": "Server odmietol odoslať pozvánky: {reason}. Uložiť udalosť bez ich odoslania?", + "save_without_invitations": "Uložiť bez pozvánok", "rsvp_updated": "Odpoveď bola aktualizovaná", "rsvp_error": "Aktualizácia odpovede zlyhala", "event_duplicated": "Udalosť bola duplikovaná", diff --git a/locales/tr/common.json b/locales/tr/common.json index 203b050e4..101ad5ae9 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -3035,6 +3035,9 @@ "alert_now": "Şimdi başlıyor", "alert_in_minutes": "{count} dk içinde", "invitation_sent": "Davetler gönderildi", + "invitations_denied_title": "Davetler gönderilemiyor", + "invitations_denied": "Sunucu davetleri göndermeyi reddetti: {reason}. Etkinlik davet gönderilmeden kaydedilsin mi?", + "save_without_invitations": "Davetsiz kaydet", "rsvp_updated": "Yanıt güncellendi", "rsvp_error": "Yanıt güncellenemedi", "event_duplicated": "Etkinlik çoğaltıldı", diff --git a/locales/uk/common.json b/locales/uk/common.json index 973e53ad7..4aebaa127 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -3009,6 +3009,9 @@ "alert_now": "Починаючи зараз", "alert_in_minutes": "Через {count} хв", "invitation_sent": "Запрошення надіслано", + "invitations_denied_title": "Не вдається надіслати запрошення", + "invitations_denied": "Сервер відмовився надіслати запрошення: {reason}. Зберегти подію без їх надсилання?", + "save_without_invitations": "Зберегти без запрошень", "rsvp_updated": "Відповідь оновлено", "rsvp_error": "Не вдалося оновити відповідь", "event_duplicated": "Подія дублюється", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index ad8fc78b1..8a4167469 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -3035,6 +3035,9 @@ "alert_now": "現在開始", "alert_in_minutes": "{count} 分鐘後", "invitation_sent": "已傳送邀請", + "invitations_denied_title": "無法傳送邀請", + "invitations_denied": "伺服器拒絕傳送邀請:{reason}。是否不傳送邀請直接儲存活動?", + "save_without_invitations": "不傳送邀請並儲存", "rsvp_updated": "回覆已更新", "rsvp_error": "無法更新回覆", "event_duplicated": "已建立活動副本", diff --git a/locales/zh/common.json b/locales/zh/common.json index ca21bcfb2..4a1a739cb 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -3009,6 +3009,9 @@ "alert_now": "现在开始", "alert_in_minutes": "{count} 分钟后", "invitation_sent": "已发送邀请", + "invitations_denied_title": "无法发送邀请", + "invitations_denied": "服务器拒绝发送邀请:{reason}。是否不发送邀请直接保存活动?", + "save_without_invitations": "不发送邀请并保存", "rsvp_updated": "回复已更新", "rsvp_error": "无法更新回复", "event_duplicated": "活动已复制", diff --git a/stores/calendar-store.ts b/stores/calendar-store.ts index 1c84ddf5c..6530edf3e 100644 --- a/stores/calendar-store.ts +++ b/stores/calendar-store.ts @@ -8,6 +8,7 @@ import { displayNow } from '@/lib/timezone'; import { parseDuration } from '@/components/calendar/event-card'; import { sanitizeOutgoingCalendarEventData } from '@/lib/calendar-event-normalization'; import { expandRecurringEvents } from '@/lib/recurrence-expansion'; +import { SchedulingDeniedError } from '@/lib/jmap/scheduling-error'; import { baseEventStoreId, buildFallbackExcludePatch, @@ -711,6 +712,8 @@ export const useCalendarStore = create()( } catch (error) { debug.error('Failed to create event:', error); set({ error: 'Failed to create event' }); + // The caller can offer to save it without the invitations. + if (error instanceof SchedulingDeniedError) throw error; return null; } },