From c4673acb65faca04d3ed74ca3e0e950cfebe1bba Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:51:21 +0200 Subject: [PATCH] fix: replace unguarded crypto.randomUUID() with safe generateUUID() utility --- app/[locale]/contacts/page.tsx | 4 ++-- components/calendar/event-modal.tsx | 5 ++--- components/email/email-viewer.tsx | 4 ++-- components/filters/filter-rule-modal.tsx | 4 ++-- lib/calendar-participants.ts | 5 ++--- lib/smime/mime-builder.ts | 6 ++++-- lib/smime/pkcs12-import.ts | 3 ++- lib/template-utils.ts | 3 ++- lib/utils.ts | 7 +++++++ lib/vcard.ts | 3 ++- stores/calendar-store.ts | 5 ++--- stores/contact-store.ts | 5 +++-- stores/smime-store.ts | 3 ++- stores/template-store.ts | 6 ++++-- 14 files changed, 38 insertions(+), 25 deletions(-) diff --git a/app/[locale]/contacts/page.tsx b/app/[locale]/contacts/page.tsx index 00aa29a6..3fdafc5e 100644 --- a/app/[locale]/contacts/page.tsx +++ b/app/[locale]/contacts/page.tsx @@ -18,7 +18,7 @@ import { useContactStore, getContactDisplayName } from "@/stores/contact-store"; import { useAuthStore, redirectToLogin } from "@/stores/auth-store"; import { useEmailStore } from "@/stores/email-store"; import { toast } from "@/stores/toast-store"; -import { cn } from "@/lib/utils"; +import { cn, generateUUID } from "@/lib/utils"; import { NavigationRail } from "@/components/layout/navigation-rail"; import { SidebarAppsModal } from "@/components/layout/sidebar-apps-modal"; import { InlineAppView } from "@/components/layout/inline-app-view"; @@ -274,7 +274,7 @@ export default function ContactsPage() { toast.success(t("toast.created")); } else { const localContact: ContactCard = { - id: `local-${crypto.randomUUID()}`, + id: `local-${generateUUID()}`, addressBookIds: {}, ...data, }; diff --git a/components/calendar/event-modal.tsx b/components/calendar/event-modal.tsx index 2c250edd..51bcb835 100644 --- a/components/calendar/event-modal.tsx +++ b/components/calendar/event-modal.tsx @@ -20,6 +20,7 @@ import { } from "@/lib/calendar-participants"; import { PluginSlot } from "@/components/plugins/plugin-slot"; import { useSettingsStore } from "@/stores/settings-store"; +import { generateUUID } from "@/lib/utils"; export interface PendingEventPreview { start: Date; @@ -402,9 +403,7 @@ export function EventModal({ if (!event || !onDuplicate) return; const start = getEventStartDate(event); const newStart = addDays(start, 1); - const newUid = typeof crypto !== 'undefined' && crypto.randomUUID - ? crypto.randomUUID() - : `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; + const newUid = generateUUID(); const data: Partial = { uid: newUid, title: event.title, diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index d0e8ce5b..5af5cca9 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -8,7 +8,7 @@ import { EMAIL_SANITIZE_CONFIG, collapseBlockedImageContainers } from "@/lib/ema import { hasMeaningfulHtmlBody } from "@/lib/signature-utils"; import { Button } from "@/components/ui/button"; import { Avatar } from "@/components/ui/avatar"; -import { formatFileSize, cn, buildMailboxTree, MailboxNode, formatDateTime } from "@/lib/utils"; +import { formatFileSize, cn, buildMailboxTree, MailboxNode, formatDateTime, generateUUID } from "@/lib/utils"; import { getSecurityStatus, extractListHeaders } from "@/lib/email-headers"; import { Reply, @@ -4874,7 +4874,7 @@ export function EmailViewer({ if (client && supportsSync) { createContact(client, contactData).then(() => toast.success('Contact added')); } else { - addLocalContact({ id: `local-${crypto.randomUUID()}`, addressBookIds: {}, ...contactData } as ContactCard); + addLocalContact({ id: `local-${generateUUID()}`, addressBookIds: {}, ...contactData } as ContactCard); toast.success('Contact added'); } }} diff --git a/components/filters/filter-rule-modal.tsx b/components/filters/filter-rule-modal.tsx index 9bd5ed36..3dd5f4da 100644 --- a/components/filters/filter-rule-modal.tsx +++ b/components/filters/filter-rule-modal.tsx @@ -16,7 +16,7 @@ import type { FilterActionType, } from "@/lib/jmap/sieve-types"; import type { Mailbox } from "@/lib/jmap/types"; -import { buildMailboxTree, flattenMailboxTree, type MailboxNode } from "@/lib/utils"; +import { buildMailboxTree, flattenMailboxTree, type MailboxNode, generateUUID } from "@/lib/utils"; interface FilterRuleModalProps { rule?: FilterRule; @@ -109,7 +109,7 @@ export function FilterRuleModal({ } onSave({ - id: rule?.id || crypto.randomUUID(), + id: rule?.id || generateUUID(), name: trimmedName, enabled: rule?.enabled ?? true, matchType, diff --git a/lib/calendar-participants.ts b/lib/calendar-participants.ts index b5cc94eb..8c0b4406 100644 --- a/lib/calendar-participants.ts +++ b/lib/calendar-participants.ts @@ -1,4 +1,5 @@ import type { CalendarEvent, CalendarParticipant } from '@/lib/jmap/types'; +import { generateUUID } from '@/lib/utils'; export interface ParticipantInfo { id: string; @@ -104,9 +105,7 @@ export function buildParticipantMap( ): Record> { const participants: Record> = {}; - const generateId = () => typeof crypto !== 'undefined' && crypto.randomUUID - ? crypto.randomUUID() - : `p-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; + const generateId = () => generateUUID(); participants[generateId()] = { '@type': 'Participant', diff --git a/lib/smime/mime-builder.ts b/lib/smime/mime-builder.ts index 1f9b3c1a..18e8cfba 100644 --- a/lib/smime/mime-builder.ts +++ b/lib/smime/mime-builder.ts @@ -5,6 +5,8 @@ * All line endings are CRLF per RFC 5322. */ +import { generateUUID } from '@/lib/utils'; + const CRLF = '\r\n'; export interface MimeAttachment { @@ -43,7 +45,7 @@ export function buildMimeMessage(input: MimeMessageInput): Uint8Array { // BCC is intentionally omitted from the MIME headers per RFC 5322 lines.push(formatHeader('Subject', encodeHeaderValue(input.subject))); lines.push(formatHeader('Date', formatDate(input.date ?? new Date()))); - lines.push(formatHeader('Message-ID', input.messageId ?? `<${crypto.randomUUID()}@smime.local>`)); + lines.push(formatHeader('Message-ID', input.messageId ?? `<${generateUUID()}@smime.local>`)); if (input.inReplyTo) { lines.push(formatHeader('In-Reply-To', input.inReplyTo)); } @@ -245,7 +247,7 @@ export function wrapCmsAsSmimeMessage(cmsBlob: Blob | ArrayBuffer | Uint8Array, } lines.push(formatHeader('Subject', encodeHeaderValue(input.subject))); lines.push(formatHeader('Date', formatDate(input.date ?? new Date()))); - lines.push(formatHeader('Message-ID', input.messageId ?? `<${crypto.randomUUID()}@smime.local>`)); + lines.push(formatHeader('Message-ID', input.messageId ?? `<${generateUUID()}@smime.local>`)); if (input.inReplyTo) { lines.push(formatHeader('In-Reply-To', input.inReplyTo)); } diff --git a/lib/smime/pkcs12-import.ts b/lib/smime/pkcs12-import.ts index a503e85d..e4a16435 100644 --- a/lib/smime/pkcs12-import.ts +++ b/lib/smime/pkcs12-import.ts @@ -1,5 +1,6 @@ import * as asn1js from 'asn1js'; import * as pkijs from 'pkijs'; +import { generateUUID } from '@/lib/utils'; import { extractCertificateInfo, classifyCapabilities, @@ -154,7 +155,7 @@ export async function importPkcs12( const email = certInfo.emailAddresses[0] ?? ''; const keyRecord: SmimeKeyRecord = { - id: crypto.randomUUID(), + id: generateUUID(), email: email.toLowerCase(), certificate: leafCertDer, certificateChain: chainCertsDer, diff --git a/lib/template-utils.ts b/lib/template-utils.ts index 36be47a3..a693ef89 100644 --- a/lib/template-utils.ts +++ b/lib/template-utils.ts @@ -1,6 +1,7 @@ import DOMPurify from 'dompurify'; import type { EmailTemplate } from './template-types'; import { BUILT_IN_PLACEHOLDERS } from './template-types'; +import { generateUUID } from './utils'; const PLACEHOLDER_REGEX = /\{\{(\w+)\}\}/g; const MAX_TEMPLATE_NAME_LENGTH = 200; @@ -149,7 +150,7 @@ export function importTemplates(json: string): ImportResult { const recipients = t.defaultRecipients as Record | undefined; templates.push({ - id: crypto.randomUUID(), + id: generateUUID(), name: sanitizeText(t.name), subject: sanitizeText(t.subject), body: sanitizeText(t.body), diff --git a/lib/utils.ts b/lib/utils.ts index f6130fd4..a9b03047 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -7,6 +7,13 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +export function generateUUID(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; +} + export function formatDate(date: Date | string): string { const d = typeof date === "string" ? new Date(date) : date; const now = new Date(); diff --git a/lib/vcard.ts b/lib/vcard.ts index e4896a5e..3b6fa97c 100644 --- a/lib/vcard.ts +++ b/lib/vcard.ts @@ -1,4 +1,5 @@ import type { ContactCard, NameComponent, ContactOnlineService, AnniversaryDate, PartialDate } from "@/lib/jmap/types"; +import { generateUUID } from "@/lib/utils"; // Convert RFC 9553 AnniversaryDate (PartialDate|Timestamp|string) to vCard date string function anniversaryDateToVcardString(date: AnniversaryDate): string { @@ -153,7 +154,7 @@ export function parseVCard(vcfString: string): ContactCard[] { } function buildContact(raw: Record): ContactCard | null { - const id = `import-${crypto.randomUUID()}`; + const id = `import-${generateUUID()}`; const card: ContactCard = { id, addressBookIds: {} }; for (const [fullKey, values] of Object.entries(raw)) { diff --git a/stores/calendar-store.ts b/stores/calendar-store.ts index 58f592f1..2cf8c08b 100644 --- a/stores/calendar-store.ts +++ b/stores/calendar-store.ts @@ -6,6 +6,7 @@ import { debug } from '@/lib/debug'; import { normalizeAllDayDuration } from '@/lib/calendar-utils'; import { sanitizeOutgoingCalendarEventData } from '@/lib/calendar-event-normalization'; import { expandRecurringEvents } from '@/lib/recurrence-expansion'; +import { generateUUID } from '@/lib/utils'; export type CalendarViewMode = 'month' | 'week' | 'day' | 'agenda' | 'tasks'; @@ -722,9 +723,7 @@ export const useCalendarStore = create()( if (!calendar) throw new Error('Failed to create calendar'); const subscription: ICalSubscription = { - id: typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' - ? crypto.randomUUID() - : `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`, + id: generateUUID(), url, calendarId: calendar.id, name, diff --git a/stores/contact-store.ts b/stores/contact-store.ts index 9677dd80..9ea3ac78 100644 --- a/stores/contact-store.ts +++ b/stores/contact-store.ts @@ -2,6 +2,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { ContactCard, AddressBook, ContactName } from '@/lib/jmap/types'; import type { IJMAPClient } from '@/lib/jmap/client-interface'; +import { generateUUID } from '@/lib/utils'; export function getContactDisplayName(contact: ContactCard): string { if (contact.name?.components) { @@ -378,7 +379,7 @@ export const useContactStore = create()( set((state) => ({ contacts: [...state.contacts, created] })); } else { const localGroup: ContactCard = { - id: `local-${crypto.randomUUID()}`, + id: `local-${generateUUID()}`, addressBookIds: {}, ...groupData, } as ContactCard; @@ -617,7 +618,7 @@ export const useContactStore = create()( } else { const localContact: ContactCard = { ...contact, - id: `local-${crypto.randomUUID()}`, + id: `local-${generateUUID()}`, }; set((state) => ({ contacts: [...state.contacts, localContact] })); } diff --git a/stores/smime-store.ts b/stores/smime-store.ts index 25b7594c..078a3adf 100644 --- a/stores/smime-store.ts +++ b/stores/smime-store.ts @@ -1,6 +1,7 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import type { SmimeKeyRecord, SmimePublicCert } from '@/lib/smime/types'; +import { generateUUID } from '@/lib/utils'; import { saveKeyRecord, listKeyRecords, @@ -287,7 +288,7 @@ export const useSmimeStore = create()( const email = info.emailAddresses[0] ?? ''; const publicCert: SmimePublicCert = { - id: crypto.randomUUID(), + id: generateUUID(), accountId: get().currentAccountId ?? undefined, email: email.toLowerCase(), certificate: der, diff --git a/stores/template-store.ts b/stores/template-store.ts index 829694d1..b6e516b3 100644 --- a/stores/template-store.ts +++ b/stores/template-store.ts @@ -6,6 +6,7 @@ import { importTemplates as importUtil, filterTemplates, } from '@/lib/template-utils'; +import { generateUUID } from '@/lib/utils'; const MAX_RECENT = 5; @@ -39,7 +40,7 @@ export const useTemplateStore = create()( const now = new Date().toISOString(); const template: EmailTemplate = { ...data, - id: crypto.randomUUID(), + id: generateUUID(), createdAt: now, updatedAt: now, }; @@ -73,8 +74,9 @@ export const useTemplateStore = create()( const now = new Date().toISOString(); const duplicate: EmailTemplate = { ...original, - id: crypto.randomUUID(), + id: generateUUID(), name: `${original.name} ${nameSuffix || '(copy)'}`, + isFavorite: false, createdAt: now, updatedAt: now,