mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
chore: remove unused exports and the dead plugin error boundary
This commit is contained in:
@@ -179,54 +179,3 @@ export function MobileHeader({
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<header
|
||||
className={cn(
|
||||
"flex items-center justify-between px-2 h-14 border-b border-border bg-background shrink-0",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onBack}
|
||||
className="h-11 w-11"
|
||||
aria-label={t('mobile.go_back')}
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<h1 className="flex-1 font-medium text-sm truncate px-2 text-center">
|
||||
{subject || "(No Subject)"}
|
||||
</h1>
|
||||
|
||||
<div className="flex items-center">
|
||||
{/* Placeholder for additional actions - kept minimal */}
|
||||
<div className="w-10" />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<PluginErrorBoundaryProps, PluginErrorBoundaryState> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -108,12 +108,6 @@ async function fetchServerConfig(): Promise<ConfigData> {
|
||||
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<ConfigData> {
|
||||
// Return cached config if available
|
||||
if (configCache) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) ────────────────────────────
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-32
@@ -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<boolean> {
|
||||
}
|
||||
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 };
|
||||
}
|
||||
|
||||
+1
-22
@@ -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<boolean> {
|
||||
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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
assertWritable('delete plugin config');
|
||||
try { await unlink(configPath(pluginId)); } catch { /* ok if missing */ }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -110,11 +110,6 @@ async function isOwnEndpoint(endpoint: string, serverUrl: string): Promise<boole
|
||||
const DISCOVERY_CACHE_MAX = 32;
|
||||
const discoveryCache = new Map<string, LiteOAuthDiscovery>();
|
||||
|
||||
/** 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
|
||||
|
||||
@@ -20,13 +20,6 @@ export interface EmailFilenameOptions extends FilenameTransformOptions {
|
||||
template?: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_TRANSFORM: Required<FilenameTransformOptions> = {
|
||||
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}";
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+1
-4
@@ -1,7 +1,4 @@
|
||||
export interface EmailHeader {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A session-visible account (the user's own primary account plus any
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+1
-20
@@ -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'));
|
||||
}
|
||||
|
||||
+1
-18
@@ -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<void> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,11 +63,6 @@ export function readStalwartAuthContextFromStore(
|
||||
return accountName ? { serverUrl, username, authHeader, accountName } : { serverUrl, username, authHeader };
|
||||
}
|
||||
|
||||
export async function readStalwartAuthContext(slot: number): Promise<StalwartAuthContext | null> {
|
||||
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<void> {
|
||||
const cookieStore = await cookies();
|
||||
clearStalwartAuthContextInStore(cookieStore, slot);
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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 */
|
||||
|
||||
+1
-40
@@ -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[] = [];
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user