feat: store trusted senders in a dedicated JMAP address book #176
This commit is contained in:
@@ -3,6 +3,7 @@ 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';
|
||||
import { debug } from '@/lib/debug';
|
||||
|
||||
export function getContactDisplayName(contact: ContactCard): string {
|
||||
if (contact.name) {
|
||||
@@ -44,6 +45,8 @@ export function getContactPhotoUri(contact: ContactCard): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const TRUSTED_SENDERS_BOOK_NAME = 'Trusted Senders';
|
||||
|
||||
interface ContactStore {
|
||||
contacts: ContactCard[];
|
||||
addressBooks: AddressBook[];
|
||||
@@ -53,6 +56,12 @@ interface ContactStore {
|
||||
error: string | null;
|
||||
supportsSync: boolean;
|
||||
|
||||
// Trusted senders address book cache (runtime only, not persisted)
|
||||
trustedSenderEmails: string[];
|
||||
trustedSendersBookId: string | null;
|
||||
trustedSendersLoaded: boolean;
|
||||
trustedSendersLoading: boolean;
|
||||
|
||||
selectedContactIds: Set<string>;
|
||||
lastSelectedContactId: string | null;
|
||||
activeTab: 'all' | 'groups';
|
||||
@@ -95,6 +104,12 @@ interface ContactStore {
|
||||
renameKeyword: (client: IJMAPClient | null, oldKeyword: string, newKeyword: string) => Promise<void>;
|
||||
|
||||
importContacts: (client: IJMAPClient | null, contacts: ContactCard[]) => Promise<number>;
|
||||
|
||||
// Trusted senders address book
|
||||
loadTrustedSendersBook: (client: IJMAPClient) => Promise<void>;
|
||||
addToTrustedSendersBook: (client: IJMAPClient, email: string) => Promise<void>;
|
||||
removeFromTrustedSendersBook: (client: IJMAPClient, email: string) => Promise<void>;
|
||||
isTrustedAddressBookSender: (email: string) => boolean;
|
||||
}
|
||||
|
||||
export const useContactStore = create<ContactStore>()(
|
||||
@@ -139,6 +154,10 @@ export const useContactStore = create<ContactStore>()(
|
||||
isLoading: false,
|
||||
error: null,
|
||||
supportsSync: false,
|
||||
trustedSenderEmails: [],
|
||||
trustedSendersBookId: null,
|
||||
trustedSendersLoaded: false,
|
||||
trustedSendersLoading: false,
|
||||
selectedContactIds: new Set<string>(),
|
||||
lastSelectedContactId: null,
|
||||
activeTab: 'all' as const,
|
||||
@@ -667,6 +686,74 @@ export const useContactStore = create<ContactStore>()(
|
||||
}
|
||||
},
|
||||
|
||||
loadTrustedSendersBook: async (client) => {
|
||||
if (get().trustedSendersLoading) return;
|
||||
set({ trustedSendersLoading: true });
|
||||
try {
|
||||
debug.log('contacts', 'Loading trusted senders address book');
|
||||
const books = await client.getAddressBooks();
|
||||
let book = books.find(b => b.name === TRUSTED_SENDERS_BOOK_NAME);
|
||||
if (!book) {
|
||||
debug.log('contacts', 'Creating new trusted senders address book');
|
||||
book = await client.createAddressBook(TRUSTED_SENDERS_BOOK_NAME);
|
||||
}
|
||||
const bookId = book.id;
|
||||
debug.log('contacts', 'Trusted senders book id:', bookId);
|
||||
const contacts = await client.getContacts(bookId);
|
||||
debug.log('contacts', 'Loaded', contacts.length, 'trusted sender contacts');
|
||||
const emails = contacts.flatMap(c =>
|
||||
c.emails ? Object.values(c.emails).map(e => e.address.toLowerCase().trim()) : []
|
||||
).filter(Boolean);
|
||||
set({ trustedSendersBookId: bookId, trustedSenderEmails: emails, trustedSendersLoaded: true, trustedSendersLoading: false });
|
||||
} catch (error) {
|
||||
debug.error('Failed to load trusted senders address book:', error);
|
||||
set({ trustedSendersLoaded: true, trustedSendersLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
addToTrustedSendersBook: async (client, email) => {
|
||||
const normalizedEmail = email.toLowerCase().trim();
|
||||
const { trustedSenderEmails } = get();
|
||||
if (trustedSenderEmails.includes(normalizedEmail)) return;
|
||||
|
||||
let bookId = get().trustedSendersBookId;
|
||||
if (!bookId) {
|
||||
await get().loadTrustedSendersBook(client);
|
||||
bookId = get().trustedSendersBookId;
|
||||
}
|
||||
if (!bookId) throw new Error('Could not find or create trusted senders address book');
|
||||
|
||||
debug.log('contacts', 'Adding trusted sender:', normalizedEmail, 'to book:', bookId);
|
||||
await client.createContact({
|
||||
addressBookIds: { [bookId]: true },
|
||||
emails: { email: { address: normalizedEmail } },
|
||||
});
|
||||
set((state) => ({ trustedSenderEmails: [...state.trustedSenderEmails, normalizedEmail] }));
|
||||
debug.log('contacts', 'Trusted sender added successfully');
|
||||
},
|
||||
|
||||
removeFromTrustedSendersBook: async (client, email) => {
|
||||
const normalizedEmail = email.toLowerCase().trim();
|
||||
const { trustedSendersBookId } = get();
|
||||
if (!trustedSendersBookId) return;
|
||||
|
||||
debug.log('contacts', 'Removing trusted sender:', normalizedEmail);
|
||||
const contacts = await client.getContacts(trustedSendersBookId);
|
||||
const match = contacts.find(c =>
|
||||
c.emails && Object.values(c.emails).some(e => e.address.toLowerCase().trim() === normalizedEmail)
|
||||
);
|
||||
if (match) {
|
||||
await client.deleteContact(match.id);
|
||||
debug.log('contacts', 'Trusted sender removed');
|
||||
}
|
||||
set((state) => ({ trustedSenderEmails: state.trustedSenderEmails.filter(e => e !== normalizedEmail) }));
|
||||
},
|
||||
|
||||
isTrustedAddressBookSender: (email) => {
|
||||
const normalizedEmail = email.toLowerCase().trim();
|
||||
return get().trustedSenderEmails.includes(normalizedEmail);
|
||||
},
|
||||
|
||||
importContacts: async (client, contacts) => {
|
||||
const { supportsSync } = get();
|
||||
let imported = 0;
|
||||
|
||||
@@ -49,7 +49,7 @@ export const ALL_HOVER_ACTIONS: { id: HoverAction; labelKey: string }[] = [
|
||||
{ id: 'spam', labelKey: 'spam' },
|
||||
];
|
||||
|
||||
export type DebugCategory = 'jmap' | 'calendar' | 'tasks' | 'auth' | 'filters' | 'email' | 'push';
|
||||
export type DebugCategory = 'jmap' | 'calendar' | 'tasks' | 'auth' | 'filters' | 'email' | 'push' | 'contacts';
|
||||
|
||||
export const ALL_DEBUG_CATEGORIES: { id: DebugCategory; labelKey: string }[] = [
|
||||
{ id: 'jmap', labelKey: 'jmap' },
|
||||
@@ -59,6 +59,7 @@ export const ALL_DEBUG_CATEGORIES: { id: DebugCategory; labelKey: string }[] = [
|
||||
{ id: 'filters', labelKey: 'filters' },
|
||||
{ id: 'email', labelKey: 'email' },
|
||||
{ id: 'push', labelKey: 'push' },
|
||||
{ id: 'contacts', labelKey: 'contacts' },
|
||||
];
|
||||
|
||||
export interface KeywordDefinition {
|
||||
@@ -140,6 +141,7 @@ interface SettingsState {
|
||||
// Privacy & Security
|
||||
sessionTimeout: number; // minutes (0 = never)
|
||||
trustedSenders: string[]; // Email addresses that can load external content
|
||||
trustedSendersAddressBook: boolean; // Store trusted senders in a dedicated JMAP address book
|
||||
|
||||
// Filters
|
||||
expandedFilterView: boolean;
|
||||
@@ -273,6 +275,7 @@ const DEFAULT_SETTINGS = {
|
||||
// Privacy & Security
|
||||
sessionTimeout: 0, // Never
|
||||
trustedSenders: [] as string[],
|
||||
trustedSendersAddressBook: false,
|
||||
|
||||
// Filters
|
||||
expandedFilterView: false,
|
||||
|
||||
Reference in New Issue
Block a user