fix: account isolation, auto-import signer certs, and no-key error handling #35

This commit is contained in:
Linus Rath
2026-03-28 17:32:06 +01:00
parent 6e0c79ca2c
commit c7c22bd210
18 changed files with 240 additions and 45 deletions
+34 -6
View File
@@ -908,7 +908,7 @@ export function EmailViewer({
// Tablet list visibility
const { isTablet, isMobile } = useDeviceDetection();
const { tabletListVisible } = useUIStore();
const { identities, client, isDemoMode } = useAuthStore();
const { identities, client, isDemoMode, activeAccountId } = useAuthStore();
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
const { startTour } = useTour();
const [showFullHeaders, setShowFullHeaders] = useState(false);
@@ -962,9 +962,9 @@ export function EmailViewer({
// Ensure S/MIME key records are loaded from IndexedDB
useLayoutEffect(() => {
smimeStore.load();
smimeStore.load(activeAccountId ?? undefined);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [activeAccountId]);
// Build mailbox tree for move-to dropdown
const moveTargetIds = useMemo(() => new Set(
@@ -1544,6 +1544,18 @@ export function EmailViewer({
// Encrypted message
const { keyRecords, unlockedDecryptionKeys } = smimeStore;
smimeDebug('[S/MIME] decrypt attempt:', { keyRecordCount: keyRecords.length, unlockedKeyCount: unlockedDecryptionKeys.size, keyRecordIds: keyRecords.map(k => k.id) });
// Short-circuit: no keys imported at all
if (keyRecords.length === 0) {
smimeDebug('[S/MIME] no key records available, skipping decrypt');
setSmimeStatus({
isSigned: false,
isEncrypted: true,
decryptionError: 'no-key',
});
return;
}
try {
let result: Awaited<ReturnType<typeof smimeDecrypt>> | null = null;
let lastError: unknown = null;
@@ -1635,8 +1647,15 @@ export function EmailViewer({
if (!existing) {
try {
await smimeStore.importPublicCert(verifyResult.status.signerCert.certificate, 'signed-email');
} catch { /* ignore import errors */ }
smimeDebug('[S/MIME] auto-imported signer cert:', { email: verifyResult.status.signerCert.email, fingerprint: verifyResult.status.signerCert.fingerprint });
} catch (importErr) {
smimeError('[S/MIME] auto-import signer cert failed:', importErr);
}
} else {
smimeDebug('[S/MIME] signer cert already imported:', { email: existing.email, fingerprint: existing.fingerprint });
}
} else if (verifyResult.status.signatureValid && verifyResult.status.signerCert) {
smimeDebug('[S/MIME] auto-import disabled, skipping signer cert:', { email: verifyResult.status.signerCert.email });
}
} catch (error) {
smimeError('[S/MIME] nested signature verify failed:', {
@@ -1674,10 +1693,12 @@ export function EmailViewer({
decryptionError: 'locked',
});
} else {
const errMsg = err instanceof Error ? err.message : 'Decryption failed';
const isNoKeyError = errMsg.includes('No imported S/MIME key matches');
setSmimeStatus({
isSigned: false,
isEncrypted: true,
decryptionError: err instanceof Error ? err.message : 'Decryption failed',
decryptionError: isNoKeyError ? 'no-key' : errMsg,
});
}
}
@@ -1739,8 +1760,15 @@ export function EmailViewer({
if (!existing) {
try {
await smimeStore.importPublicCert(result.status.signerCert.certificate, 'signed-email');
} catch { /* ignore import errors */ }
smimeDebug('[S/MIME] auto-imported signer cert:', { email: result.status.signerCert.email, fingerprint: result.status.signerCert.fingerprint });
} catch (importErr) {
smimeError('[S/MIME] auto-import signer cert failed:', importErr);
}
} else {
smimeDebug('[S/MIME] signer cert already imported:', { email: existing.email, fingerprint: existing.fingerprint });
}
} else if (result.status.signatureValid && result.status.signerCert) {
smimeDebug('[S/MIME] auto-import disabled, skipping signer cert:', { email: result.status.signerCert.email });
}
} catch (err) {
if (cancelled) return;
+6
View File
@@ -30,6 +30,12 @@ export function SmimeStatusBanner({ status, onUnlockKey, className }: SmimeStatu
text: t('unlock_key_desc'),
variant: 'warning',
});
} else if (status.decryptionError === 'no-key') {
items.push({
icon: <Lock className="w-4 h-4" />,
text: t('status_encrypted_no_key'),
variant: 'warning',
});
} else {
items.push({
icon: <ShieldX className="w-4 h-4" />,
+4 -2
View File
@@ -19,6 +19,7 @@ import { SmimePassphraseDialog } from "@/components/settings/smime-passphrase-di
import { SmimeCertificateModal } from "@/components/settings/smime-certificate-modal";
import { useSmimeStore } from "@/stores/smime-store";
import { useIdentityStore } from "@/stores/identity-store";
import { useAuthStore } from "@/stores/auth-store";
import { exportPkcs12, downloadPkcs12 } from "@/lib/smime/pkcs12-export";
import type { SmimeKeyRecord, SmimePublicCert } from "@/lib/smime/types";
@@ -50,6 +51,7 @@ export function SmimeSettings() {
} = useSmimeStore();
const { identities } = useIdentityStore();
const activeAccountId = useAuthStore((s) => s.activeAccountId);
// Local UI state
const [importDialogOpen, setImportDialogOpen] = useState(false);
@@ -75,8 +77,8 @@ export function SmimeSettings() {
const [exportError, setExportError] = useState<string | null>(null);
useEffect(() => {
load();
}, [load]);
load(activeAccountId ?? undefined);
}, [load, activeAccountId]);
// ── PKCS#12 import flow ────────────────────────────────────────
+2
View File
@@ -11,6 +11,7 @@ import { useFilterStore } from '@/stores/filter-store';
import { DEFAULT_SEARCH_FILTERS } from '@/lib/jmap/search-utils';
import { useIdentityStore } from '@/stores/identity-store';
import { useVacationStore } from '@/stores/vacation-store';
import { useSmimeStore } from '@/stores/smime-store';
// Minimal snapshot shapes — we only capture what we need
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -117,6 +118,7 @@ export function clearAllStores(): void {
useVacationStore.getState().clearState();
useCalendarStore.getState().clearState();
useFilterStore.getState().clearState();
useSmimeStore.getState().clearState();
}
/** Evict cached state for one account */
+46
View File
@@ -148,4 +148,50 @@ describe('key-storage', () => {
expect(certs.find(c => c.id === id)).toBeUndefined();
});
});
describe('accountId filtering', () => {
it('listKeyRecords filters by accountId', async () => {
const id1 = uid();
const id2 = uid();
await saveKeyRecord(makeKeyRecord({ id: id1, email: `${id1}@a.com`, accountId: 'acct-1' }));
await saveKeyRecord(makeKeyRecord({ id: id2, email: `${id2}@b.com`, accountId: 'acct-2' }));
const acct1Records = await listKeyRecords('acct-1');
expect(acct1Records.find(r => r.id === id1)).toBeDefined();
expect(acct1Records.find(r => r.id === id2)).toBeUndefined();
});
it('listKeyRecords includes records without accountId when filtering', async () => {
const id1 = uid();
const id2 = uid();
await saveKeyRecord(makeKeyRecord({ id: id1, email: `${id1}@a.com` }));
await saveKeyRecord(makeKeyRecord({ id: id2, email: `${id2}@b.com`, accountId: 'acct-1' }));
const acct1Records = await listKeyRecords('acct-1');
expect(acct1Records.find(r => r.id === id1)).toBeDefined();
expect(acct1Records.find(r => r.id === id2)).toBeDefined();
});
it('listPublicCerts filters by accountId', async () => {
const id1 = uid();
const id2 = uid();
await savePublicCert(makePublicCert({ id: id1, email: `${id1}@a.com`, accountId: 'acct-1' }));
await savePublicCert(makePublicCert({ id: id2, email: `${id2}@b.com`, accountId: 'acct-2' }));
const acct1Certs = await listPublicCerts('acct-1');
expect(acct1Certs.find(c => c.id === id1)).toBeDefined();
expect(acct1Certs.find(c => c.id === id2)).toBeUndefined();
});
it('listPublicCerts includes certs without accountId when filtering', async () => {
const id1 = uid();
const id2 = uid();
await savePublicCert(makePublicCert({ id: id1, email: `${id1}@a.com` }));
await savePublicCert(makePublicCert({ id: id2, email: `${id2}@b.com`, accountId: 'acct-1' }));
const acct1Certs = await listPublicCerts('acct-1');
expect(acct1Certs.find(c => c.id === id1)).toBeDefined();
expect(acct1Certs.find(c => c.id === id2)).toBeDefined();
});
});
});
+3 -1
View File
@@ -57,7 +57,9 @@ beforeEach(() => {
defaultSignIdentity: {},
defaultEncrypt: false,
rememberUnlockedKeys: false,
autoImportSignerCerts: false,
autoImportSignerCerts: true,
accountPreferences: {},
currentAccountId: null,
isLoading: false,
error: null,
});
+26 -9
View File
@@ -1,22 +1,35 @@
import type { SmimeKeyRecord, SmimePublicCert } from './types';
const DB_NAME = 'smime-store';
const DB_VERSION = 1;
const DB_VERSION = 2;
const KEY_RECORDS_STORE = 'key-records';
const PUBLIC_CERTS_STORE = 'public-certs';
function openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = () => {
request.onupgradeneeded = (event) => {
const db = request.result;
if (!db.objectStoreNames.contains(KEY_RECORDS_STORE)) {
const oldVersion = event.oldVersion;
if (oldVersion < 1) {
const keyStore = db.createObjectStore(KEY_RECORDS_STORE, { keyPath: 'id' });
keyStore.createIndex('email', 'email', { unique: false });
}
if (!db.objectStoreNames.contains(PUBLIC_CERTS_STORE)) {
keyStore.createIndex('accountId', 'accountId', { unique: false });
const certStore = db.createObjectStore(PUBLIC_CERTS_STORE, { keyPath: 'id' });
certStore.createIndex('email', 'email', { unique: false });
certStore.createIndex('accountId', 'accountId', { unique: false });
}
if (oldVersion >= 1 && oldVersion < 2) {
// Add accountId index to existing stores
const tx = request.transaction!;
const keyStore = tx.objectStore(KEY_RECORDS_STORE);
if (!keyStore.indexNames.contains('accountId')) {
keyStore.createIndex('accountId', 'accountId', { unique: false });
}
const certStore = tx.objectStore(PUBLIC_CERTS_STORE);
if (!certStore.indexNames.contains('accountId')) {
certStore.createIndex('accountId', 'accountId', { unique: false });
}
}
};
request.onsuccess = () => resolve(request.result);
@@ -62,9 +75,11 @@ export async function getKeyRecordForEmail(email: string): Promise<SmimeKeyRecor
});
}
export async function listKeyRecords(): Promise<SmimeKeyRecord[]> {
export async function listKeyRecords(accountId?: string): Promise<SmimeKeyRecord[]> {
const db = await openDB();
return txPromise(db, KEY_RECORDS_STORE, 'readonly', (s) => s.getAll());
const all = await txPromise<SmimeKeyRecord[]>(db, KEY_RECORDS_STORE, 'readonly', (s) => s.getAll());
if (!accountId) return all;
return all.filter((r) => r.accountId === accountId || !r.accountId);
}
export async function deleteKeyRecord(id: string): Promise<void> {
@@ -90,9 +105,11 @@ export async function getPublicCertForEmail(email: string): Promise<SmimePublicC
});
}
export async function listPublicCerts(): Promise<SmimePublicCert[]> {
export async function listPublicCerts(accountId?: string): Promise<SmimePublicCert[]> {
const db = await openDB();
return txPromise(db, PUBLIC_CERTS_STORE, 'readonly', (s) => s.getAll());
const all = await txPromise<SmimePublicCert[]>(db, PUBLIC_CERTS_STORE, 'readonly', (s) => s.getAll());
if (!accountId) return all;
return all.filter((c) => c.accountId === accountId || !c.accountId);
}
export async function deletePublicCert(id: string): Promise<void> {
+2
View File
@@ -1,6 +1,7 @@
/** Stored record for an imported S/MIME private key + certificate. */
export interface SmimeKeyRecord {
id: string;
accountId?: string;
email: string;
certificate: ArrayBuffer; // DER-encoded X.509 leaf cert
certificateChain: ArrayBuffer[]; // DER-encoded intermediates
@@ -34,6 +35,7 @@ export interface SmimeUnlockedKey {
/** A recipient or contact public certificate. */
export interface SmimePublicCert {
id: string;
accountId?: string;
email: string;
certificate: ArrayBuffer; // DER-encoded X.509
issuer: string;
+1 -1
View File
@@ -477,7 +477,7 @@
"smime_sign_off": "S/MIME-Signatur deaktiviert",
"smime_encrypt_on": "S/MIME-Verschlüsselung aktiviert",
"smime_encrypt_off": "S/MIME-Verschlüsselung deaktiviert",
"smime_encrypt_unavailable": "S/MIME-Verschlüsselung nicht möglich: {reason}",
"smime_encrypt_unavailable": "S/MIME-Verschlüsselung nicht möglich fehlende Empfängerzertifikate",
"smime_unlock_title": "S/MIME-Schlüssel entsperren",
"smime_unlock_message": "Geben Sie die Passphrase ein, um Ihren S/MIME-Schlüssel zu entsperren.",
"smime_unlock_button": "Entsperren",
+1 -1
View File
@@ -477,7 +477,7 @@
"smime_sign_off": "Firma S/MIME desactivada",
"smime_encrypt_on": "Cifrado S/MIME activado",
"smime_encrypt_off": "Cifrado S/MIME desactivado",
"smime_encrypt_unavailable": "El cifrado S/MIME no está disponible: {reason}",
"smime_encrypt_unavailable": "Cifrado S/MIME no disponible faltan certificados de destinatario",
"smime_unlock_title": "Desbloquear clave S/MIME",
"smime_unlock_message": "Introduce la contraseña para desbloquear tu clave S/MIME.",
"smime_unlock_button": "Desbloquear",
+1 -1
View File
@@ -477,7 +477,7 @@
"smime_sign_off": "Signature S/MIME désactivée",
"smime_encrypt_on": "Chiffrement S/MIME activé",
"smime_encrypt_off": "Chiffrement S/MIME désactivé",
"smime_encrypt_unavailable": "Le chiffrement S/MIME n'est pas disponible : {reason}",
"smime_encrypt_unavailable": "Chiffrement S/MIME indisponible certificats des destinataires manquants",
"smime_unlock_title": "Déverrouiller la clé S/MIME",
"smime_unlock_message": "Entrez la phrase secrète pour déverrouiller votre clé S/MIME.",
"smime_unlock_button": "Déverrouiller",
+1 -1
View File
@@ -477,7 +477,7 @@
"smime_sign_off": "Firma S/MIME disattivata",
"smime_encrypt_on": "Cifratura S/MIME attivata",
"smime_encrypt_off": "Cifratura S/MIME disattivata",
"smime_encrypt_unavailable": "La cifratura S/MIME non è disponibile: {reason}",
"smime_encrypt_unavailable": "Cifratura S/MIME non disponibile certificati destinatario mancanti",
"smime_unlock_title": "Sblocca chiave S/MIME",
"smime_unlock_message": "Inserisci la passphrase per sbloccare la tua chiave S/MIME.",
"smime_unlock_button": "Sblocca",
+1 -1
View File
@@ -477,7 +477,7 @@
"smime_sign_off": "S/MIME 署名を無効化しました",
"smime_encrypt_on": "S/MIME 暗号化を有効化しました",
"smime_encrypt_off": "S/MIME 暗号化を無効化しました",
"smime_encrypt_unavailable": "S/MIME 暗号化は利用できません: {reason}",
"smime_encrypt_unavailable": "S/MIME 暗号化は利用できません 受信者の証明書がありません",
"smime_unlock_title": "S/MIME 鍵のロックを解除",
"smime_unlock_message": "S/MIME 鍵を解除するためのパスフレーズを入力してください。",
"smime_unlock_button": "ロック解除",
+1 -1
View File
@@ -477,7 +477,7 @@
"smime_sign_off": "S/MIME-ondertekening uitgeschakeld",
"smime_encrypt_on": "S/MIME-versleuteling ingeschakeld",
"smime_encrypt_off": "S/MIME-versleuteling uitgeschakeld",
"smime_encrypt_unavailable": "S/MIME-versleuteling is niet beschikbaar: {reason}",
"smime_encrypt_unavailable": "S/MIME-versleuteling niet beschikbaar ontbrekende ontvangercertificaten",
"smime_unlock_title": "S/MIME-sleutel ontgrendelen",
"smime_unlock_message": "Voer de wachtwoordzin in om uw S/MIME-sleutel te ontgrendelen.",
"smime_unlock_button": "Ontgrendelen",
+1 -1
View File
@@ -477,7 +477,7 @@
"smime_sign_off": "Assinatura S/MIME desativada",
"smime_encrypt_on": "Criptografia S/MIME ativada",
"smime_encrypt_off": "Criptografia S/MIME desativada",
"smime_encrypt_unavailable": "A criptografia S/MIME não está disponível: {reason}",
"smime_encrypt_unavailable": "Criptografia S/MIME indisponível certificados de destinatário ausentes",
"smime_unlock_title": "Desbloquear chave S/MIME",
"smime_unlock_message": "Insira a frase secreta para desbloquear sua chave S/MIME.",
"smime_unlock_button": "Desbloquear",
+110 -20
View File
@@ -123,14 +123,23 @@ async function restoreRememberedKeys(keyRecords: SmimeKeyRecord[]): Promise<{
}
interface SmimePersistedState {
identityKeyBindings: Record<string, string>; // identityId → keyRecordId
defaultSignIdentity: Record<string, boolean>; // identityId → sign by default
defaultEncrypt: boolean;
/** Account-scoped preferences: accountId → { identityKeyBindings, defaultSignIdentity, defaultEncrypt } */
accountPreferences: Record<string, {
identityKeyBindings: Record<string, string>;
defaultSignIdentity: Record<string, boolean>;
defaultEncrypt: boolean;
}>;
rememberUnlockedKeys: boolean;
autoImportSignerCerts: boolean;
}
interface SmimeStore extends SmimePersistedState {
// Current account scope
currentAccountId: string | null;
// Account-scoped convenience accessors (derived from accountPreferences + currentAccountId)
identityKeyBindings: Record<string, string>;
defaultSignIdentity: Record<string, boolean>;
defaultEncrypt: boolean;
// Loaded from IndexedDB
keyRecords: SmimeKeyRecord[];
publicCerts: SmimePublicCert[];
@@ -141,7 +150,8 @@ interface SmimeStore extends SmimePersistedState {
error: string | null;
// Actions
load: () => Promise<void>;
load: (accountId?: string) => Promise<void>;
clearState: () => void;
importPKCS12: (file: ArrayBuffer, p12Passphrase: string, storagePassphrase: string) => Promise<SmimeKeyRecord>;
importPublicCert: (data: ArrayBuffer | string, source: SmimePublicCert['source'], contactId?: string) => Promise<SmimePublicCert>;
bindIdentityToKey: (identityId: string, keyRecordId: string | null) => void;
@@ -166,13 +176,15 @@ export const useSmimeStore = create<SmimeStore>()(
persist(
(set, get) => ({
// Persisted preferences
accountPreferences: {},
rememberUnlockedKeys: false,
autoImportSignerCerts: true,
// Runtime state
currentAccountId: null,
identityKeyBindings: {},
defaultSignIdentity: {},
defaultEncrypt: false,
rememberUnlockedKeys: false,
autoImportSignerCerts: false,
// Runtime state
keyRecords: [],
publicCerts: [],
unlockedKeys: new Map(),
@@ -180,12 +192,30 @@ export const useSmimeStore = create<SmimeStore>()(
isLoading: false,
error: null,
load: async () => {
set({ isLoading: true, error: null });
load: async (accountId) => {
const acctId = accountId ?? get().currentAccountId;
set({ isLoading: true, error: null, currentAccountId: acctId });
// Restore account-scoped preferences
const prefs = acctId ? get().accountPreferences[acctId] : undefined;
if (prefs) {
set({
identityKeyBindings: prefs.identityKeyBindings,
defaultSignIdentity: prefs.defaultSignIdentity,
defaultEncrypt: prefs.defaultEncrypt,
});
} else {
set({
identityKeyBindings: {},
defaultSignIdentity: {},
defaultEncrypt: false,
});
}
try {
const [keyRecords, publicCerts] = await Promise.all([
listKeyRecords(),
listPublicCerts(),
listKeyRecords(acctId ?? undefined),
listPublicCerts(acctId ?? undefined),
]);
if (get().rememberUnlockedKeys) {
@@ -219,6 +249,8 @@ export const useSmimeStore = create<SmimeStore>()(
set({ isLoading: true, error: null });
try {
const { keyRecord } = await importPkcs12(file, p12Passphrase, storagePassphrase);
const acctId = get().currentAccountId;
if (acctId) keyRecord.accountId = acctId;
await saveKeyRecord(keyRecord);
set((state) => ({
keyRecords: [...state.keyRecords, keyRecord],
@@ -245,6 +277,7 @@ export const useSmimeStore = create<SmimeStore>()(
const publicCert: SmimePublicCert = {
id: crypto.randomUUID(),
accountId: get().currentAccountId ?? undefined,
email: email.toLowerCase(),
certificate: der,
issuer: info.issuer,
@@ -279,7 +312,15 @@ export const useSmimeStore = create<SmimeStore>()(
} else {
bindings[identityId] = keyRecordId;
}
return { identityKeyBindings: bindings };
const accountPreferences = { ...state.accountPreferences };
const acctId = state.currentAccountId;
if (acctId) {
accountPreferences[acctId] = {
...(accountPreferences[acctId] ?? { identityKeyBindings: {}, defaultSignIdentity: {}, defaultEncrypt: false }),
identityKeyBindings: bindings,
};
}
return { identityKeyBindings: bindings, accountPreferences };
});
},
@@ -296,11 +337,17 @@ export const useSmimeStore = create<SmimeStore>()(
for (const [identityId, keyId] of Object.entries(bindings)) {
if (keyId === id) delete bindings[identityId];
}
const accountPreferences = { ...state.accountPreferences };
const acctId = state.currentAccountId;
if (acctId && accountPreferences[acctId]) {
accountPreferences[acctId] = { ...accountPreferences[acctId], identityKeyBindings: bindings };
}
return {
keyRecords: state.keyRecords.filter((k) => k.id !== id),
unlockedKeys,
unlockedDecryptionKeys,
identityKeyBindings: bindings,
accountPreferences,
};
});
},
@@ -378,13 +425,32 @@ export const useSmimeStore = create<SmimeStore>()(
},
setSignDefault: (identityId, value) => {
set((state) => ({
defaultSignIdentity: { ...state.defaultSignIdentity, [identityId]: value },
}));
set((state) => {
const defaultSignIdentity = { ...state.defaultSignIdentity, [identityId]: value };
const accountPreferences = { ...state.accountPreferences };
const acctId = state.currentAccountId;
if (acctId) {
accountPreferences[acctId] = {
...(accountPreferences[acctId] ?? { identityKeyBindings: {}, defaultSignIdentity: {}, defaultEncrypt: false }),
defaultSignIdentity,
};
}
return { defaultSignIdentity, accountPreferences };
});
},
setEncryptDefault: (value) => {
set({ defaultEncrypt: value });
set((state) => {
const accountPreferences = { ...state.accountPreferences };
const acctId = state.currentAccountId;
if (acctId) {
accountPreferences[acctId] = {
...(accountPreferences[acctId] ?? { identityKeyBindings: {}, defaultSignIdentity: {}, defaultEncrypt: false }),
defaultEncrypt: value,
};
}
return { defaultEncrypt: value, accountPreferences };
});
},
setRememberUnlockedKeys: (value) => {
@@ -403,17 +469,41 @@ export const useSmimeStore = create<SmimeStore>()(
getUnlockedKey: (id) => get().unlockedKeys.get(id),
clearState: () => {
clearRememberedUnlocks();
set({
keyRecords: [],
publicCerts: [],
unlockedKeys: new Map(),
unlockedDecryptionKeys: new Map(),
identityKeyBindings: {},
defaultSignIdentity: {},
defaultEncrypt: false,
currentAccountId: null,
isLoading: false,
error: null,
});
},
setError: (error) => set({ error }),
}),
{
name: 'smime-preferences',
partialize: (state): SmimePersistedState => ({
identityKeyBindings: state.identityKeyBindings,
defaultSignIdentity: state.defaultSignIdentity,
defaultEncrypt: state.defaultEncrypt,
accountPreferences: state.accountPreferences,
rememberUnlockedKeys: state.rememberUnlockedKeys,
autoImportSignerCerts: state.autoImportSignerCerts,
}),
merge: (persisted, current) => {
const p = persisted as Partial<SmimePersistedState & { identityKeyBindings?: Record<string, string>; defaultSignIdentity?: Record<string, boolean>; defaultEncrypt?: boolean }>;
return {
...current,
// Migrate legacy flat preferences into accountPreferences
accountPreferences: p?.accountPreferences ?? {},
rememberUnlockedKeys: p?.rememberUnlockedKeys ?? false,
autoImportSignerCerts: p?.autoImportSignerCerts ?? true,
};
},
},
),
);
Binary file not shown.
Binary file not shown.