From 4d733aa598784d337dd088f55900181342249de7 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:36:51 +0200 Subject: [PATCH] chore: remove unused exports and the dead plugin error boundary --- components/layout/mobile-header.tsx | 51 ------------------ components/plugins/plugin-error-boundary.tsx | 36 ------------- hooks/use-config.ts | 6 --- hooks/use-media-query.ts | 16 ------ hooks/use-pro-multi-account-identities.ts | 11 ---- lib/__tests__/plugin-slot.test.tsx | 43 --------------- lib/account-utils.ts | 5 -- lib/admin/csp-frame-origins.ts | 1 - lib/admin/jmap-servers.ts | 18 ------- lib/admin/migrate.ts | 33 +----------- lib/admin/paths.ts | 23 +------- lib/admin/plugin-approvals.ts | 6 --- lib/admin/plugin-config.ts | 8 --- lib/admin/types.ts | 6 --- lib/auth/lite-oauth.ts | 5 -- lib/download-filename.ts | 7 --- lib/global-search/run-global-search.ts | 4 -- lib/global-search/types.ts | 4 -- lib/jalali-utils.ts | 11 ---- lib/jmap/types.ts | 5 +- lib/lite-config.ts | 42 --------------- lib/plugin-sandbox/protocol.ts | 55 -------------------- lib/plugin-sandbox/shortcuts.ts | 11 ---- lib/server-auth-status.ts | 4 -- lib/setup/state.ts | 21 +------- lib/setup/token.ts | 19 +------ lib/stalwart/auth-context.ts | 10 ---- lib/template-types.ts | 7 --- lib/timezone.ts | 5 -- lib/utils.ts | 41 +-------------- stores/settings-store.ts | 2 - 31 files changed, 6 insertions(+), 510 deletions(-) delete mode 100644 components/plugins/plugin-error-boundary.tsx diff --git a/components/layout/mobile-header.tsx b/components/layout/mobile-header.tsx index b9e71a9ac..7e929ddae 100644 --- a/components/layout/mobile-header.tsx +++ b/components/layout/mobile-header.tsx @@ -179,54 +179,3 @@ export function MobileHeader({ ); } - -/** - * Viewer header for mobile - shows when viewing an email - */ -interface MobileViewerHeaderProps { - subject?: string; - onBack: () => void; - onDelete?: () => void; - onArchive?: () => void; - className?: string; -} - -export function MobileViewerHeader({ - subject, - onBack, - onDelete: _onDelete, - onArchive: _onArchive, - className, -}: MobileViewerHeaderProps) { - const t = useTranslations('sidebar'); - const isPaneDesktop = useIsDesktop(); - if (isPaneDesktop) return null; - - return ( - - - - - - - {subject || "(No Subject)"} - - - - {/* Placeholder for additional actions - kept minimal */} - - - - ); -} diff --git a/components/plugins/plugin-error-boundary.tsx b/components/plugins/plugin-error-boundary.tsx deleted file mode 100644 index dd1fae745..000000000 --- a/components/plugins/plugin-error-boundary.tsx +++ /dev/null @@ -1,36 +0,0 @@ -'use client'; - -import React from 'react'; - -export interface PluginErrorBoundaryProps { - pluginId: string; - children?: React.ReactNode; - fallback?: React.ReactNode; -} - -interface PluginErrorBoundaryState { - hasError: boolean; - error?: Error; -} - -export class PluginErrorBoundary extends React.Component { - constructor(props: PluginErrorBoundaryProps) { - super(props); - this.state = { hasError: false }; - } - - static getDerivedStateFromError(error: Error): PluginErrorBoundaryState { - return { hasError: true, error }; - } - - componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { - console.error(`[plugin:${this.props.pluginId}] Render error:`, error, errorInfo); - } - - render(): React.ReactNode { - if (this.state.hasError) { - return this.props.fallback ?? null; - } - return this.props.children; - } -} diff --git a/hooks/use-config.ts b/hooks/use-config.ts index 5813858c4..e69c6f650 100644 --- a/hooks/use-config.ts +++ b/hooks/use-config.ts @@ -108,12 +108,6 @@ async function fetchServerConfig(): Promise { throw lastError; } -/** Test hook: forget the cached config so the next fetch hits the network again. */ -export function resetConfigCache(): void { - configCache = null; - configPromise = null; -} - export async function fetchConfig(): Promise { // Return cached config if available if (configCache) { diff --git a/hooks/use-media-query.ts b/hooks/use-media-query.ts index 16465b74d..b61f356f1 100644 --- a/hooks/use-media-query.ts +++ b/hooks/use-media-query.ts @@ -96,24 +96,8 @@ export function useIsMobile() { return paneWidth !== null ? paneWidth < BREAKPOINTS.md : viewport; } -export function useIsTablet() { - const paneWidth = usePaneSize(); - const viewport = useMediaQuery( - `(min-width: ${BREAKPOINTS.md}px) and (max-width: ${BREAKPOINTS.lg - 1}px)` - ); - return paneWidth !== null - ? paneWidth >= BREAKPOINTS.md && paneWidth < BREAKPOINTS.lg - : viewport; -} - export function useIsDesktop() { const paneWidth = usePaneSize(); const viewport = useMediaQuery(`(min-width: ${BREAKPOINTS.lg}px)`); return paneWidth !== null ? paneWidth >= BREAKPOINTS.lg : viewport; } - -export function useBreakpoint(breakpoint: keyof typeof BREAKPOINTS) { - const paneWidth = usePaneSize(); - const viewport = useMediaQuery(`(min-width: ${BREAKPOINTS[breakpoint]}px)`); - return paneWidth !== null ? paneWidth >= BREAKPOINTS[breakpoint] : viewport; -} diff --git a/hooks/use-pro-multi-account-identities.ts b/hooks/use-pro-multi-account-identities.ts index 00d0e5163..a714af72f 100644 --- a/hooks/use-pro-multi-account-identities.ts +++ b/hooks/use-pro-multi-account-identities.ts @@ -26,17 +26,6 @@ export function cachedRemoteIdentities(): Identity[] { return [...remoteIdentityCache.values()].flat(); } -/** Cross-account identity IDs are namespaced to avoid collisions between JMAP - * servers that happen to issue the same opaque ID. EVERY aggregated account is - * namespaced (including the active one) so an id doesn't change form when the - * active account changes; consumers resolve the raw id via - * stripCrossAccountIdentityPrefix. Single-account mode (hook disabled) uses the - * identity store's raw ids directly. - */ -export function isCrossAccountIdentityId(id: string): boolean { - return id.includes(CROSS_ACCOUNT_IDENTITY_DELIMITER); -} - export function stripCrossAccountIdentityPrefix(id: string): { localAccountId: string | null; rawId: string } { const idx = id.indexOf(CROSS_ACCOUNT_IDENTITY_DELIMITER); if (idx < 0) return { localAccountId: null, rawId: id }; diff --git a/lib/__tests__/plugin-slot.test.tsx b/lib/__tests__/plugin-slot.test.tsx index b820c96ce..9e2c39a06 100644 --- a/lib/__tests__/plugin-slot.test.tsx +++ b/lib/__tests__/plugin-slot.test.tsx @@ -21,7 +21,6 @@ vi.mock('@/components/plugins/plugin-iframe-slot', () => ({ // Import after mocks import { PluginSlot } from '@/components/plugins/plugin-slot'; -import { PluginErrorBoundary } from '@/components/plugins/plugin-error-boundary'; beforeEach(() => { Object.keys(mockOffers).forEach(k => delete mockOffers[k]); @@ -60,45 +59,3 @@ describe('PluginSlot', () => { expect(container.querySelector('[data-plugin-slot="sidebar-widget"]')).toBeTruthy(); }); }); - -describe('PluginErrorBoundary', () => { - it('renders children when no error', () => { - const { getByText } = render( - React.createElement( - PluginErrorBoundary, - { pluginId: 'test' }, - React.createElement('span', null, 'Child') - ) - ); - expect(getByText('Child')).toBeTruthy(); - }); - - it('renders fallback on error', () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const ThrowingComponent = () => { throw new Error('boom'); }; - const { getByText } = render( - React.createElement( - PluginErrorBoundary, - { pluginId: 'err', fallback: React.createElement('span', null, 'Error caught') }, - React.createElement(ThrowingComponent) - ) - ); - expect(getByText('Error caught')).toBeTruthy(); - consoleSpy.mockRestore(); - }); - - it('renders null on error when no fallback provided', () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const ThrowingComponent = () => { throw new Error('boom'); }; - const { container } = render( - React.createElement( - PluginErrorBoundary, - { pluginId: 'err2' }, - React.createElement(ThrowingComponent) - ) - ); - // ErrorBoundary renders null fallback - expect(container.innerHTML).toBe(''); - consoleSpy.mockRestore(); - }); -}); diff --git a/lib/account-utils.ts b/lib/account-utils.ts index f1c35592b..a426e9fcc 100644 --- a/lib/account-utils.ts +++ b/lib/account-utils.ts @@ -92,11 +92,6 @@ export function getInitials(name: string, email?: string): string { return '?'; } -/** Build an account-scoped localStorage key */ -export function getAccountScopedKey(baseKey: string, accountId: string): string { - return `${baseKey}::${accountId}`; -} - /** * Hard upper bound on cookie slots. Each slot can hold up to ~3 cookies * (session, refresh token, server id, auth context), so 50 slots ≈ 125 diff --git a/lib/admin/csp-frame-origins.ts b/lib/admin/csp-frame-origins.ts index ebbfe9ad6..0eb5e2d95 100644 --- a/lib/admin/csp-frame-origins.ts +++ b/lib/admin/csp-frame-origins.ts @@ -58,7 +58,6 @@ export function sanitizeFrameOrigins(input: unknown): string[] { * intent is explicit at every call site (frame embedding vs. HTTP fetch). */ export const sanitizeHttpOrigins = sanitizeFrameOrigins; -export const isValidHttpOrigin = isValidFrameOrigin; // ─── apiPostPaths (manifest field) ──────────────────────────── diff --git a/lib/admin/jmap-servers.ts b/lib/admin/jmap-servers.ts index c1d54e777..aa79b727a 100644 --- a/lib/admin/jmap-servers.ts +++ b/lib/admin/jmap-servers.ts @@ -2,8 +2,6 @@ * Multi-server JMAP support: schema, parsing, lookup, and redaction helpers. */ -import { toAsciiDomain } from '@/lib/idn'; - export interface JmapServerOAuthConfig { clientId?: string; issuerUrl?: string; @@ -152,22 +150,6 @@ export function findServerByUrl(servers: JmapServerEntry[], url: string | null | return servers.find((s) => normalizeUrl(s.url) === target); } -function domainKey(domain: string): string { - return toAsciiDomain(domain) ?? domain.trim().toLowerCase(); -} - -/** - * Find the server whose `domains` array matches the given email's domain - * (case-insensitive, IDN domains compared in their ASCII form). - */ -export function findServerByEmailDomain(servers: JmapServerEntry[], email: string | null | undefined): JmapServerEntry | undefined { - if (!email || !email.includes('@')) return undefined; - const domain = email.split('@')[1]?.trim(); - if (!domain) return undefined; - const key = domainKey(domain); - return servers.find((s) => (s.domains ?? []).some((d) => domainKey(d) === key)); -} - /** * Resolve a client-supplied JMAP URL to a trusted upstream URL by checking it * against the configured server list and the global `jmapServerUrl`. Returns diff --git a/lib/admin/migrate.ts b/lib/admin/migrate.ts index 53375e775..195d17a53 100644 --- a/lib/admin/migrate.ts +++ b/lib/admin/migrate.ts @@ -1,4 +1,4 @@ -import { readFile, writeFile, rename, stat, unlink } from 'node:fs/promises'; +import { readFile, writeFile, rename, unlink } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { logger } from '@/lib/logger'; import { @@ -223,34 +223,3 @@ async function migrateAuditLog(): Promise { } return moved; } - -/** - * Returns approximate size of legacy data still mixed in the config dir - * (for diagnostics / admin UI). Always returns 0 once migration has run. - */ -export async function getLegacyDataInfo(): Promise<{ adminJsonHasTimestamps: boolean; auditLogInConfigDir: boolean }> { - let adminJsonHasTimestamps = false; - const adminJsonPath = getConfigPath('admin.json'); - if (existsSync(adminJsonPath)) { - try { - const raw = await readFile(adminJsonPath, 'utf-8'); - const parsed = JSON.parse(raw); - adminJsonHasTimestamps = - 'createdAt' in parsed || - 'lastLogin' in parsed || - 'passwordChangedAt' in parsed; - } catch { - /* ignore */ - } - } - - let auditLogInConfigDir = false; - try { - await stat(getConfigPath('audit.log')); - auditLogInConfigDir = true; - } catch { - /* not present - good */ - } - - return { adminJsonHasTimestamps, auditLogInConfigDir }; -} diff --git a/lib/admin/paths.ts b/lib/admin/paths.ts index b791ecaac..f4c99495b 100644 --- a/lib/admin/paths.ts +++ b/lib/admin/paths.ts @@ -1,7 +1,6 @@ import { existsSync } from 'node:fs'; -import { mkdir, writeFile, unlink } from 'node:fs/promises'; +import { mkdir } from 'node:fs/promises'; import path from 'node:path'; -import { logger } from '@/lib/logger'; /** * Admin data directories. @@ -91,26 +90,6 @@ export function isConfigReadOnly(): boolean { return cachedReadOnly; } -/** - * Probe the config dir by writing a temp file. Used to auto-detect RO mounts - * when ADMIN_CONFIG_READONLY is not set explicitly. Run once at startup; - * cheap on local FS, can be slow on networked FS, hence opt-in. - */ -export async function probeConfigReadOnly(): Promise { - if (process.env.ADMIN_CONFIG_READONLY) return isConfigReadOnly(); - try { - const probe = path.join(getConfigDir(), '.rw-probe'); - await writeFile(probe, ''); - await unlink(probe); - cachedReadOnly = false; - return false; - } catch { - cachedReadOnly = true; - logger.info('Config dir is read-only (auto-detected)'); - return true; - } -} - export class ConfigReadOnlyError extends Error { constructor(operation: string) { super( diff --git a/lib/admin/plugin-approvals.ts b/lib/admin/plugin-approvals.ts index bed833151..12fec4879 100644 --- a/lib/admin/plugin-approvals.ts +++ b/lib/admin/plugin-approvals.ts @@ -167,9 +167,3 @@ export async function revokeApproval(pluginId: string, bundleHash: string): Prom file.entries.splice(idx, 1); await flushToDisk(); } - -/** Force a re-read on next access. Used in tests / after a manual file edit. */ -export function invalidateApprovalsCache(): void { - cached = null; - loadPromise = null; -} diff --git a/lib/admin/plugin-config.ts b/lib/admin/plugin-config.ts index 68c7e68da..ca78e1d9d 100644 --- a/lib/admin/plugin-config.ts +++ b/lib/admin/plugin-config.ts @@ -71,11 +71,3 @@ export async function deletePluginConfigKey(pluginId: string, key: string): Prom await writeFile(tmpPath, JSON.stringify(config, null, 2), 'utf-8'); await rename(tmpPath, filePath); } - -/** - * Delete all config for a plugin (used when uninstalling). - */ -export async function deleteAllPluginConfig(pluginId: string): Promise { - assertWritable('delete plugin config'); - try { await unlink(configPath(pluginId)); } catch { /* ok if missing */ } -} diff --git a/lib/admin/types.ts b/lib/admin/types.ts index 9ff06655d..75e9b5055 100644 --- a/lib/admin/types.ts +++ b/lib/admin/types.ts @@ -20,12 +20,6 @@ export interface AdminStateData { passwordChangedAt: string; } -/** - * Combined view used by getAdminMeta() and tests. Constructed by merging - * admin.json + admin-state.json at read time. - */ -export interface AdminData extends AdminConfigData, AdminStateData {} - export interface AdminSessionPayload { role: 'admin'; iat: number; diff --git a/lib/auth/lite-oauth.ts b/lib/auth/lite-oauth.ts index de06ac829..ece9f5af8 100644 --- a/lib/auth/lite-oauth.ts +++ b/lib/auth/lite-oauth.ts @@ -110,11 +110,6 @@ async function isOwnEndpoint(endpoint: string, serverUrl: string): Promise(); -/** Test hook. */ -export function resetLiteOAuthDiscoveryCache(): void { - discoveryCache.clear(); -} - /** * Asks Stalwart which provider signs `username` in. `null` when the server * has no such endpoint (not Stalwart, or older than 0.16), the answer is diff --git a/lib/download-filename.ts b/lib/download-filename.ts index 8e048e3db..9b60f0888 100644 --- a/lib/download-filename.ts +++ b/lib/download-filename.ts @@ -20,13 +20,6 @@ export interface EmailFilenameOptions extends FilenameTransformOptions { template?: string; } -export const DEFAULT_TRANSFORM: Required = { - spaceReplacement: "keep", - lowercase: false, - stripDiacritics: false, - collapseSeparators: true, -}; - export const DEFAULT_EMAIL_TEMPLATE = "{date} ({from}-{to}) {subject}"; export const DEFAULT_ATTACHMENT_TEMPLATE = "{filename}"; export const DEFAULT_BUNDLE_TEMPLATE = "emails-{count}"; diff --git a/lib/global-search/run-global-search.ts b/lib/global-search/run-global-search.ts index affc92132..2374f03b2 100644 --- a/lib/global-search/run-global-search.ts +++ b/lib/global-search/run-global-search.ts @@ -40,10 +40,6 @@ export function hasRemoteQuery(parsed: ParsedQuery, kind: SearchKind): boolean { || mail.dateAfter || mail.dateBefore || parsed.mailboxRole); } -export function isAbortError(error: unknown): boolean { - return error instanceof Error && error.name === 'AbortError'; -} - function snapshot(outcome: SearchOutcome): SearchOutcome { return { hits: { ...outcome.hits }, diff --git a/lib/global-search/types.ts b/lib/global-search/types.ts index 0f2367a35..5d1430623 100644 --- a/lib/global-search/types.ts +++ b/lib/global-search/types.ts @@ -15,10 +15,6 @@ export type SearchKind = 'mail' | 'contacts' | 'calendar' | 'files'; export const SEARCH_KINDS: readonly SearchKind[] = ['mail', 'contacts', 'calendar', 'files']; -export function isSearchKind(value: unknown): value is SearchKind { - return typeof value === 'string' && (SEARCH_KINDS as readonly string[]).includes(value); -} - /** Where a hit came from. Remote hits win over local ones when both exist (they carry snippets). */ export type HitSource = 'local' | 'remote'; diff --git a/lib/jalali-utils.ts b/lib/jalali-utils.ts index 1670e37e9..4331d8107 100644 --- a/lib/jalali-utils.ts +++ b/lib/jalali-utils.ts @@ -63,11 +63,6 @@ export function jalaliMonthLength(jy: number, jm: number): number { return jalaali.jalaaliMonthLength(jy, jm); } -/** Is the given Jalali year a leap year? */ -export function isJalaliLeapYear(jy: number): boolean { - return jalaali.isLeapJalaaliYear(jy); -} - // --------------------------------------------------------------------------- // Calendar grid helpers (analogous to date-fns startOfWeek / eachDayOfInterval) // --------------------------------------------------------------------------- @@ -161,9 +156,3 @@ export function getDayHeaderKeys( export function shouldUseJalaliCalendar(locale: string): boolean { return locale === 'fa'; } - -/** Default `firstDayOfWeek` for a given locale. */ -export function defaultFirstDayOfWeek(locale: string): number { - if (locale === 'fa') return 6; // Saturday - return 1; // Monday (ISO convention) -} diff --git a/lib/jmap/types.ts b/lib/jmap/types.ts index 1b0620363..befd90f9f 100644 --- a/lib/jmap/types.ts +++ b/lib/jmap/types.ts @@ -1,7 +1,4 @@ -export interface EmailHeader { - name: string; - value: string; -} + /** * A session-visible account (the user's own primary account plus any diff --git a/lib/lite-config.ts b/lib/lite-config.ts index d97452745..ac1a4ea1b 100644 --- a/lib/lite-config.ts +++ b/lib/lite-config.ts @@ -3,48 +3,6 @@ import type { SettingsPolicy } from '@/lib/admin/types'; import { DEFAULT_POLICY } from '@/lib/admin/types'; import { httpUrlOrEmpty } from '@/lib/config-validation'; -/** - * Runtime configuration for the static Lite build. - * - * `/api/config` used to merge env vars, the admin dashboard and per-host - * branding on the server. Lite reads `config.json` instead: a deployer edits a - * handful of keys (server URL, app name, logos) and everything else falls - * back to the same defaults `useConfig` already assumes. Whatever the file - * says, the flags that only work with a Next.js server behind them are forced - * off so no surface ever calls an endpoint that does not exist. - */ - -/** Keys a deployer may set in config.json. Everything else is ignored. */ -export const LITE_CONFIG_KEYS = [ - 'appName', - 'jmapServerUrl', - 'allowCustomJmapEndpoint', - 'jmapServers', - 'jmapServerAutoPickByDomain', - 'demoMode', - 'rememberMeEnabled', - 'faviconUrl', - 'appLogoLightUrl', - 'appLogoDarkUrl', - 'loginLogoLightUrl', - 'loginLogoDarkUrl', - 'loginCompanyName', - 'loginImprintUrl', - 'loginPrivacyPolicyUrl', - 'loginWebsiteUrl', - 'loginLogoMaxHeight', - 'loginLogoMaxWidth', - 'loginShowHeading', - 'loginShowSubtitle', - 'loginShowTotp', - 'loginShowTokenLogin', - 'loginShowRememberMe', - 'loginShowVersion', - 'embeddedMode', - 'parentOrigin', - 'sourceCodeUrl', -] as const; - /** * Flags that need the Next.js server (cookies, filesystem, OAuth relay). They * are pinned regardless of what config.json contains. `oauthEnabled` is the diff --git a/lib/plugin-sandbox/protocol.ts b/lib/plugin-sandbox/protocol.ts index 06122ce65..f24f7d5ac 100644 --- a/lib/plugin-sandbox/protocol.ts +++ b/lib/plugin-sandbox/protocol.ts @@ -11,10 +11,6 @@ import type { SlotName, PluginTier } from '../plugin-types'; import type { ThemeSnapshot } from './host-theme'; -// ─── Sandbox mode ──────────────────────────────────────────── - -export type SandboxMode = 'background' | 'slot'; - /** Initialisation payload for a background-instance iframe (one per plugin). */ export interface BackgroundInit { mode: 'background'; @@ -200,29 +196,6 @@ export type HostToSandbox = | PropsUpdateMsg | SlotShouldShowMsg; -/** Marker used in extraProps for function values that the host owns. */ -export interface PluginCallbackMarker { - __pluginCallback: string; -} - -export function isCallbackMarker(value: unknown): value is PluginCallbackMarker { - return ( - typeof value === 'object' && - value !== null && - typeof (value as { __pluginCallback?: unknown }).__pluginCallback === 'string' - ); -} - -// ─── Type guards ───────────────────────────────────────────── - -export function isSandboxMessage(value: unknown): value is SandboxToHost { - return ( - typeof value === 'object' && - value !== null && - typeof (value as { type?: unknown }).type === 'string' - ); -} - // ─── Constants ─────────────────────────────────────────────── /** Path used for the untrusted (null-origin) sandbox iframe `src`. Matched in @@ -236,31 +209,3 @@ export const SANDBOX_PATH = '/plugin-sandbox'; * `proxy.ts`. Renders the identical `SandboxRuntime`. */ export const SANDBOX_PRIVILEGED_PATH = '/plugin-sandbox-privileged'; - -/** Methods callable by a plugin via api-request. Host enforces permissions. */ -export const API_METHODS = [ - 'storage.get', 'storage.set', 'storage.remove', 'storage.keys', - 'http.post', 'http.fetch', - 'crypto.createWebAuthn', 'crypto.getWebAuthn', 'crypto.getPublicKeys', 'crypto.createPublicKey', 'crypto.removePublicKey', 'crypto.getEncryptionAtRest', 'crypto.setEncryptionAtRest', 'crypto.getPublicKeyFromWKD', - 'jmap.fetchBlob', 'jmap.uploadBlob', 'jmap.sendRaw', - // Narrow keyword helpers. Unlike the raw-blob methods, these are available - // to untrusted plugins with email:read/email:write as appropriate. - 'jmap.getKeywords', 'jmap.setKeywords', 'jmap.setKeyword', 'jmap.removeKeyword', - 'upfiles.get', 'upfiles.save', - 'contact.get', 'contact.update', 'contact.create', 'contact.search', - 'admin.getConfig', 'admin.getAllConfig', 'admin.setConfig', 'admin.deleteConfig', - 'toast.success', 'toast.error', 'toast.info', 'toast.warning', - 'ui.confirm', 'ui.alert', 'ui.prompt', 'ui.rerenderEmail', 'ui.rerenderFetchedEmails', 'ui.openExternalUrl', 'ui.downloadFile', - // Email keyword mutations (JMAP Email/set keyword patches). - 'email.setKeyword', 'email.removeKeyword', - // Native tag definitions, discovery, and sidebar counts. - 'keywords.list', 'keywords.add', 'keywords.reorder', 'keywords.discover', 'keywords.getCounts', 'keywords.refreshCounts', - // Message-list category tabs (Gmail-style inbox tabs). - 'tabs.set', 'tabs.clear', 'tabs.getState', 'tabs.categorize', 'tabs.refreshCounts', - // Run a JMAP Email filter as the message-list search. - 'search.run', - // Sieve integration for delivery-time classification plugins. - 'sieve.isSupported', 'sieve.getActiveScript', 'sieve.validateScript', 'sieve.regenerate', -] as const; - -export type ApiMethod = (typeof API_METHODS)[number]; diff --git a/lib/plugin-sandbox/shortcuts.ts b/lib/plugin-sandbox/shortcuts.ts index 4d8b082ab..1b1408270 100644 --- a/lib/plugin-sandbox/shortcuts.ts +++ b/lib/plugin-sandbox/shortcuts.ts @@ -112,14 +112,3 @@ export function registerShortcuts( for (const k of keys) bindings.delete(k); }; } - -/** Snapshot of currently active shortcuts. Used by the help modal. */ -export function listShortcuts(): Array<{ pluginId: string; id: string; keys: string; label: string; category?: string }> { - return [...bindings.values()].map(b => ({ - pluginId: b.pluginId, - id: b.shortcutId, - keys: b.keys, - label: b.label, - category: b.category, - })); -} diff --git a/lib/server-auth-status.ts b/lib/server-auth-status.ts index a6f9e0dea..1ee7f553f 100644 --- a/lib/server-auth-status.ts +++ b/lib/server-auth-status.ts @@ -15,10 +15,6 @@ export function setServerAuthIssue(reason: string | null): void { issue = reason; } -export function getServerAuthIssue(): string | null { - return issue; -} - /** * Error text for a failed response from a cookie-gated route: the recorded * reason when the route answered 401, otherwise the route's own message. diff --git a/lib/setup/state.ts b/lib/setup/state.ts index ee23f9b23..2e6c3b824 100644 --- a/lib/setup/state.ts +++ b/lib/setup/state.ts @@ -1,6 +1,5 @@ -import { existsSync } from 'node:fs'; import { configManager } from '@/lib/admin/config-manager'; -import { getConfigPath, isConfigReadOnly } from '@/lib/admin/paths'; +import { isConfigReadOnly } from '@/lib/admin/paths'; /** * The three lifecycle states for the running container. @@ -33,21 +32,3 @@ export function detectSetupState(): SetupState { if (isConfigReadOnly()) return 'bootstrap'; return 'bootstrap'; } - -/** - * Whether the wizard's UI and APIs should be reachable. - */ -export function isSetupActive(): boolean { - return detectSetupState() === 'bootstrap'; -} - -/** - * The persisted `.config-locked` marker the wizard drops when the operator - * checks "lock configuration after setup" on the review screen. Purely - * advisory - the actual locking is the operator's `:ro` mount or the - * ADMIN_CONFIG_READONLY env var. This file is what the admin UI uses to - * remind the operator that they intended to lock. - */ -export function lockMarkerExists(): boolean { - return existsSync(getConfigPath('.config-locked')); -} diff --git a/lib/setup/token.ts b/lib/setup/token.ts index 58fd4de8f..dec3e568e 100644 --- a/lib/setup/token.ts +++ b/lib/setup/token.ts @@ -1,5 +1,5 @@ import { randomBytes, timingSafeEqual } from 'node:crypto'; -import { readFile, writeFile, unlink, stat } from 'node:fs/promises'; +import { readFile, writeFile, unlink } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { logger } from '@/lib/logger'; import { ensureStateDir, getStatePath } from '@/lib/admin/paths'; @@ -92,20 +92,3 @@ export async function clearSetupToken(): Promise { }); } } - -/** - * For diagnostics / startup logging. - */ -export async function getTokenInfo(): Promise<{ exists: boolean; expiresInSeconds: number | null }> { - const path = getStatePath(TOKEN_FILE); - if (!existsSync(path)) return { exists: false, expiresInSeconds: null }; - try { - await stat(path); - const payload = await readToken(); - if (!payload) return { exists: false, expiresInSeconds: null }; - const elapsed = Date.now() / 1000 - payload.issuedAt; - return { exists: true, expiresInSeconds: Math.max(0, Math.floor(payload.ttlSeconds - elapsed)) }; - } catch { - return { exists: false, expiresInSeconds: null }; - } -} diff --git a/lib/stalwart/auth-context.ts b/lib/stalwart/auth-context.ts index 38505b6b3..7d22b3f63 100644 --- a/lib/stalwart/auth-context.ts +++ b/lib/stalwart/auth-context.ts @@ -63,11 +63,6 @@ export function readStalwartAuthContextFromStore( return accountName ? { serverUrl, username, authHeader, accountName } : { serverUrl, username, authHeader }; } -export async function readStalwartAuthContext(slot: number): Promise { - const cookieStore = await cookies(); - return readStalwartAuthContextFromStore(cookieStore, slot); -} - export function setStalwartAuthContextInStore( cookieStore: CookieStore, slot: number, @@ -88,8 +83,3 @@ export async function setStalwartAuthContext(slot: number, context: StalwartAuth export function clearStalwartAuthContextInStore(cookieStore: CookieStore, slot: number): void { cookieStore.delete(stalwartAuthContextCookieName(slot)); } - -export async function clearStalwartAuthContext(slot: number): Promise { - const cookieStore = await cookies(); - clearStalwartAuthContextInStore(cookieStore, slot); -} \ No newline at end of file diff --git a/lib/template-types.ts b/lib/template-types.ts index b25eb0539..421842562 100644 --- a/lib/template-types.ts +++ b/lib/template-types.ts @@ -16,11 +16,6 @@ export interface EmailTemplate { updatedAt: string; } -export interface PlaceholderVariable { - name: string; - value: string; -} - export const BUILT_IN_PLACEHOLDERS = [ 'recipient_name', 'company', @@ -28,5 +23,3 @@ export const BUILT_IN_PLACEHOLDERS = [ 'day_of_week', 'sender_name', ] as const; - -export type BuiltInPlaceholder = (typeof BUILT_IN_PLACEHOLDERS)[number]; diff --git a/lib/timezone.ts b/lib/timezone.ts index 25fe7aebe..ef27591f7 100644 --- a/lib/timezone.ts +++ b/lib/timezone.ts @@ -79,11 +79,6 @@ export function getEffectiveTimeZone(): string { return resolveTimeZone(useSettingsStore.getState().timeZone); } -/** True when the user's override is active and differs from the browser zone. */ -export function isTimeZoneOverridden(): boolean { - return getEffectiveTimeZone() !== getBrowserTimeZone(); -} - export interface WallClock { year: number; /** 1-12 */ diff --git a/lib/utils.ts b/lib/utils.ts index 77f3a3b80..76f40cfbc 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,7 +1,6 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; -import { Mailbox, UNIFIED_MAILBOX_IDS } from "./jmap/types"; -import type { UnifiedMailboxRole } from "./jmap/types"; +import { Mailbox } from "./jmap/types"; import { debug } from "./debug"; import { getEffectiveLocale } from '@/i18n/detect-locale'; import { useSettingsStore } from "@/stores/settings-store"; @@ -326,11 +325,6 @@ export function cleanPreview(text: string | null | undefined): string { return stripInvisibleLeading(stripLeadingCss(stripInvisibleLeading(text ?? ''))); } -export function truncateText(text: string, maxLength: number): string { - if (text.length <= maxLength) return text; - return text.substring(0, maxLength).trim() + "..."; -} - /** * A size people can read at a glance: whole numbers from 10 up ("67 KB"), * one decimal below that ("2.1 MB"), never a trailing ".0". @@ -645,39 +639,6 @@ export function buildMailboxTree(mailboxes: Mailbox[]): MailboxNode[] { return rootMailboxes; } -/** - * Builds virtual MailboxNode entries for unified mailbox roles with aggregated counts. - */ -export function buildUnifiedMailboxNodes( - counts: Array<{ role: UnifiedMailboxRole; unreadEmails: number; totalEmails: number }>, -): MailboxNode[] { - return counts.map((count) => ({ - id: UNIFIED_MAILBOX_IDS[count.role], - name: count.role, // Display name is handled by i18n in the component - role: count.role, - parentId: undefined, - sortOrder: 0, - totalEmails: count.totalEmails, - unreadEmails: count.unreadEmails, - totalThreads: 0, - unreadThreads: 0, - myRights: { - mayReadItems: true, - mayAddItems: false, - mayRemoveItems: false, - maySetSeen: true, - maySetKeywords: true, - mayCreateChild: false, - mayRename: false, - mayDelete: false, - maySubmit: false, - }, - isSubscribed: true, - children: [], - depth: 0, - })); -} - // Flatten a mailbox tree for rendering with proper depth info export function flattenMailboxTree(nodes: MailboxNode[]): MailboxNode[] { const result: MailboxNode[] = []; diff --git a/stores/settings-store.ts b/stores/settings-store.ts index 2a7862171..bd863173b 100644 --- a/stores/settings-store.ts +++ b/stores/settings-store.ts @@ -165,8 +165,6 @@ export type Density = 'extra-compact' | 'compact' | 'regular' | 'comfortable'; * docked compose window) that keeps the theme's icons, type and palette. */ export type InterfaceLayout = 'default' | 'mountain-view'; -/** @deprecated Use Density instead */ -export type ListDensity = Density; export type DeleteAction = 'trash' | 'trash-and-read' | 'permanent'; export type ReplyMode = 'reply' | 'replyAll'; export type SignaturePosition = 'above_quote' | 'below_quote';