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
+34 -6
View File
@@ -6,6 +6,7 @@ import { useTranslations } from "next-intl";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useAuthStore } from "@/stores/auth-store";
import { useConfig } from "@/hooks/use-config";
import { Mail, AlertCircle, Loader2, X } from "lucide-react";
export default function LoginPage() {
@@ -13,9 +14,7 @@ export default function LoginPage() {
const params = useParams();
const t = useTranslations("login");
const { login, isLoading, error, clearError, isAuthenticated } = useAuthStore();
const serverUrl = process.env.NEXT_PUBLIC_JMAP_SERVER_URL;
const appName = process.env.NEXT_PUBLIC_APP_NAME || 'Webmail';
const { appName, jmapServerUrl: serverUrl, isLoading: configLoading, error: configError } = useConfig();
// All hooks must be called unconditionally at the top
const [formData, setFormData] = useState({
@@ -100,6 +99,35 @@ export default function LoginPage() {
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [serverUrl]);
// Show loading state while config is being fetched
if (configLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background via-background to-muted/20">
<div className="w-full max-w-sm mx-auto px-4 text-center" role="status">
<Loader2 className="w-8 h-8 animate-spin text-primary mx-auto" />
<span className="sr-only">{t("loading")}</span>
</div>
</div>
);
}
// Show error if config fetch failed
if (configError) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-background via-background to-muted/20">
<div className="w-full max-w-sm mx-auto px-4 text-center">
<div className="inline-flex items-center justify-center w-20 h-20 rounded-2xl bg-red-500/10 mb-6">
<AlertCircle className="w-10 h-10 text-red-500" />
</div>
<h1 className="text-xl font-medium text-foreground mb-2">{t("config_error.title")}</h1>
<p className="text-muted-foreground text-sm">
{t("config_error.fetch_failed")}
</p>
</div>
</div>
);
}
// Show error if JMAP server URL is not configured
if (!serverUrl) {
return (
@@ -108,9 +136,9 @@ export default function LoginPage() {
<div className="inline-flex items-center justify-center w-20 h-20 rounded-2xl bg-red-500/10 mb-6">
<AlertCircle className="w-10 h-10 text-red-500" />
</div>
<h1 className="text-xl font-medium text-foreground mb-2">Configuration Error</h1>
<h1 className="text-xl font-medium text-foreground mb-2">{t("config_error.title")}</h1>
<p className="text-muted-foreground text-sm">
NEXT_PUBLIC_JMAP_SERVER_URL environment variable is not set.
{t("config_error.server_not_configured")}
</p>
</div>
</div>
@@ -268,7 +296,7 @@ export default function LoginPage() {
type="button"
onClick={(e) => removeUsername(username, e)}
className="p-1 hover:bg-background rounded transition-colors"
title="Remove from history"
title={t("remove_from_history")}
>
<X className="w-3 h-3 text-muted-foreground" />
</button>
+4 -2
View File
@@ -353,11 +353,13 @@ export default function Home() {
subject: string;
body: string;
draftId?: string;
fromEmail?: string;
identityId?: string;
}) => {
if (!client) return;
try {
await sendEmail(client, data.to, data.subject, data.body, data.cc, data.bcc, data.draftId);
await sendEmail(client, data.to, data.subject, data.body, data.cc, data.bcc, data.draftId, data.fromEmail, data.identityId);
setShowComposer(false);
} catch (error) {
console.error("Failed to send email:", error);
@@ -810,7 +812,7 @@ export default function Home() {
{showComposer && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4 md:p-0">
<div className={cn(
"w-full h-full md:h-auto md:max-w-3xl md:max-h-[600px]",
"w-full h-full md:h-[600px] md:max-w-3xl",
"max-md:flex max-md:flex-col"
)}>
<ErrorBoundary
+20
View File
@@ -0,0 +1,20 @@
import { NextResponse } from 'next/server';
/**
* Runtime configuration endpoint
*
* This endpoint serves configuration values that can be set at runtime
* via environment variables, enabling post-build configuration for
* Docker deployments.
*
* Priority order:
* 1. Runtime env vars (APP_NAME, JMAP_SERVER_URL)
* 2. Build-time env vars (NEXT_PUBLIC_APP_NAME, NEXT_PUBLIC_JMAP_SERVER_URL)
* 3. Default values
*/
export async function GET() {
return NextResponse.json({
appName: process.env.APP_NAME || process.env.NEXT_PUBLIC_APP_NAME || 'Webmail',
jmapServerUrl: process.env.JMAP_SERVER_URL || process.env.NEXT_PUBLIC_JMAP_SERVER_URL || '',
});
}