feat: Add runtime config, trusted senders, JMAP identities, and UI improvements

- Runtime environment variables for Docker-friendly configuration
- Trusted senders list for automatic image loading
- JMAP identities for proper sender address
- Improved email composer readability
- Horizontal scroll for wide HTML emails
This commit is contained in:
Matthieu MALVACHE
2026-01-08 02:08:18 +01:00
committed by Matthieu MALVACHE
parent dbffaf2a15
commit 58cfe09dc6
18 changed files with 778 additions and 100 deletions
+13
View File
@@ -2,6 +2,7 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { JMAPClient } from '@/lib/jmap/client';
import { useEmailStore } from './email-store';
import type { Identity } from '@/lib/jmap/types';
interface AuthState {
isAuthenticated: boolean;
@@ -10,6 +11,8 @@ interface AuthState {
serverUrl: string | null;
username: string | null;
client: JMAPClient | null;
identities: Identity[];
primaryIdentity: Identity | null;
login: (serverUrl: string, username: string, password: string) => Promise<boolean>;
logout: () => void;
@@ -26,6 +29,8 @@ export const useAuthStore = create<AuthState>()(
serverUrl: null,
username: null,
client: null,
identities: [],
primaryIdentity: null,
login: async (serverUrl, username, password) => {
set({ isLoading: true, error: null });
@@ -37,6 +42,10 @@ export const useAuthStore = create<AuthState>()(
// Try to connect
await client.connect();
// Fetch identities from the server
const identities = await client.getIdentities();
const primaryIdentity = identities.length > 0 ? identities[0] : null;
// Success - save state (but NOT the password)
set({
isAuthenticated: true,
@@ -44,6 +53,8 @@ export const useAuthStore = create<AuthState>()(
serverUrl,
username,
client,
identities,
primaryIdentity,
error: null,
});
@@ -87,6 +98,8 @@ export const useAuthStore = create<AuthState>()(
serverUrl: null,
username: null,
client: null,
identities: [],
primaryIdentity: null,
error: null,
});
+3 -3
View File
@@ -46,7 +46,7 @@ interface EmailStore {
loadMoreEmails: (client: JMAPClient) => Promise<void>;
fetchEmailContent: (client: JMAPClient, emailId: string) => Promise<Email | null>;
fetchQuota: (client: JMAPClient) => Promise<void>;
sendEmail: (client: JMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], draftId?: string) => Promise<void>;
sendEmail: (client: JMAPClient, to: string[], subject: string, body: string, cc?: string[], bcc?: string[], draftId?: string, fromEmail?: string, identityId?: string) => Promise<void>;
deleteEmail: (client: JMAPClient, emailId: string) => Promise<void>;
markAsRead: (client: JMAPClient, emailId: string, read: boolean) => Promise<void>;
moveToMailbox: (client: JMAPClient, emailId: string, mailboxId: string) => Promise<void>;
@@ -282,10 +282,10 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
}
},
sendEmail: async (client, to, subject, body, cc, bcc, draftId) => {
sendEmail: async (client, to, subject, body, cc, bcc, draftId, fromEmail, identityId) => {
set({ isLoading: true, error: null });
try {
await client.sendEmail(to, subject, body, cc, bcc, draftId);
await client.sendEmail(to, subject, body, cc, bcc, draftId, fromEmail, identityId);
// Refresh emails after sending
await get().fetchEmails(client);
set({ isLoading: false });
+29
View File
@@ -35,6 +35,7 @@ interface SettingsState {
// Privacy & Security
sessionTimeout: number; // minutes (0 = never)
trustedSenders: string[]; // Email addresses that can load external content
// Advanced
debugMode: boolean;
@@ -47,6 +48,11 @@ interface SettingsState {
resetToDefaults: () => void;
exportSettings: () => string;
importSettings: (json: string) => boolean;
// Trusted senders
addTrustedSender: (email: string) => void;
removeTrustedSender: (email: string) => void;
isSenderTrusted: (email: string) => boolean;
}
const DEFAULT_SETTINGS = {
@@ -74,6 +80,7 @@ const DEFAULT_SETTINGS = {
// Privacy & Security
sessionTimeout: 0, // Never
trustedSenders: [] as string[],
// Advanced
debugMode: false,
@@ -124,6 +131,7 @@ export const useSettingsStore = create<SettingsState>()(
showPreview: state.showPreview,
emailsPerPage: state.emailsPerPage,
externalContentPolicy: state.externalContentPolicy,
trustedSenders: state.trustedSenders,
autoSaveDraftInterval: state.autoSaveDraftInterval,
sendConfirmation: state.sendConfirmation,
defaultReplyMode: state.defaultReplyMode,
@@ -160,6 +168,27 @@ export const useSettingsStore = create<SettingsState>()(
return false;
}
},
// Trusted senders methods
addTrustedSender: (email: string) => {
const normalizedEmail = email.toLowerCase().trim();
const current = get().trustedSenders;
if (!current.includes(normalizedEmail)) {
set({ trustedSenders: [...current, normalizedEmail] });
}
},
removeTrustedSender: (email: string) => {
const normalizedEmail = email.toLowerCase().trim();
set({
trustedSenders: get().trustedSenders.filter(e => e !== normalizedEmail)
});
},
isSenderTrusted: (email: string) => {
const normalizedEmail = email.toLowerCase().trim();
return get().trustedSenders.includes(normalizedEmail);
},
}),
{
name: 'settings-storage',