ultisuite-client/components/gmail/settings/hosted-mail-setup-card.tsx
R3D347HR4Y 7ee1a66942
Some checks are pending
E2E / Playwright e2e (push) Waiting to run
feat(ai-assistant): enhance AI assistant configuration and integration
- Added support for managing AI models within the AI assistant settings.
- Introduced new hosted mail setup component for streamlined email configuration.
- Updated environment variables for local development and proxy settings.
- Enhanced error handling and user feedback in the chat page for API connectivity issues.
- Improved routing for AI-related API calls in the Next.js configuration.
- Added documentation for local development and agent management in CLAUDE.md.
2026-06-13 20:38:15 +02:00

227 lines
8.2 KiB
TypeScript

"use client"
import { useEffect, useMemo, useState } from "react"
import { Loader2, MailCheck, Server } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { ApiRequestError } from "@/lib/api/client"
import {
useCheckMailAddress,
useHostedMailStatus,
useSetupHostedMailbox,
} from "@/lib/api/hooks/use-hosted-mail"
import { useAuthStore } from "@/lib/api/auth-store"
function suggestLocalPart(platformEmail: string | undefined, domain: string): string {
if (!platformEmail?.includes("@")) return ""
const [local, emailDomain] = platformEmail.split("@")
if (!local) return ""
if (emailDomain?.toLowerCase() === domain.toLowerCase()) {
return local.toLowerCase()
}
return local.toLowerCase().replace(/[^a-z0-9._+-]/g, "")
}
function setupErrorMessage(error: unknown): string {
if (error instanceof ApiRequestError) {
switch (error.code) {
case "address_taken":
return "Cette adresse est déjà utilisée."
case "domain_not_active":
return "Le domaine mail n'est pas encore actif."
}
return error.message
}
if (error instanceof Error) return error.message
return "Échec de la configuration."
}
export function HostedMailSetupCard() {
const platformEmail = useAuthStore((s) => s.user?.email)
const { data: status, isPending } = useHostedMailStatus()
const setup = useSetupHostedMailbox()
const domain = status?.platform_domain ?? ""
const alreadyConnected = Boolean(
status?.hosted_mail_account_id || status?.mailbox?.mail_account_id
)
const connectedEmail =
status?.hosted_mail_account_email ?? status?.mailbox?.email ?? ""
const [localPart, setLocalPart] = useState("")
const [displayName, setDisplayName] = useState("")
const [password, setPassword] = useState("")
const [confirmPassword, setConfirmPassword] = useState("")
useEffect(() => {
if (!domain || localPart) return
const suggested = suggestLocalPart(platformEmail, domain)
if (suggested) setLocalPart(suggested)
}, [domain, localPart, platformEmail])
const fullEmail = useMemo(() => {
const local = localPart.trim().toLowerCase()
return local && domain ? `${local}@${domain}` : ""
}, [localPart, domain])
const addressCheck = useCheckMailAddress(localPart.trim(), domain)
const addressTaken =
addressCheck.data?.available === false &&
addressCheck.data.reason !== "hosted_mail_disabled"
const passwordsMatch = password === confirmPassword
const passwordOk = password.length >= 8
const canSubmit =
!setup.isPending &&
localPart.trim().length > 0 &&
passwordOk &&
passwordsMatch &&
addressCheck.data?.available !== false
if (isPending || !status?.enabled) return null
const endpoints = status.endpoints
async function handleSubmit() {
if (!canSubmit) return
try {
await setup.mutateAsync({
local_part: localPart.trim().toLowerCase(),
password,
display_name: displayName.trim() || undefined,
})
setPassword("")
setConfirmPassword("")
} catch {
/* message below */
}
}
return (
<Card className="border-primary/20 bg-primary/[0.03]">
<CardHeader>
<div className="flex items-start gap-3">
<Server className="mt-0.5 size-5 shrink-0 text-primary" />
<div>
<CardTitle className="text-base">Mail hébergé Ultimail (Stalwart)</CardTitle>
<CardDescription>
Créez ou connectez votre boîte @{domain} IMAP/SMTP préconfigurés, sans saisie
manuelle des serveurs.
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{endpoints ? (
<p className="text-xs text-muted-foreground">
Serveurs : IMAP {endpoints.imap_host}:{endpoints.imap_port}
{endpoints.imap_tls ? " (TLS)" : ""} · SMTP {endpoints.smtp_host}:{endpoints.smtp_port}
{endpoints.smtp_tls ? " (TLS)" : ""}
</p>
) : null}
{alreadyConnected ? (
<div className="flex items-start gap-2 rounded-lg border border-border bg-background px-3 py-2">
<MailCheck className="mt-0.5 size-4 shrink-0 text-green-600 dark:text-green-500" />
<div>
<p className="text-sm font-medium">Boîte hébergée connectée</p>
<p className="text-sm text-muted-foreground">{connectedEmail}</p>
</div>
</div>
) : (
<div className="space-y-4 max-w-lg">
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-1.5 sm:col-span-2">
<Label htmlFor="hosted-local-part">Adresse mail</Label>
<div className="flex items-center gap-2">
<Input
id="hosted-local-part"
value={localPart}
autoComplete="username"
placeholder="prenom.nom"
onChange={(e) => setLocalPart(e.target.value)}
/>
<span className="shrink-0 text-sm text-muted-foreground">@{domain}</span>
</div>
{fullEmail ? (
<p className="text-xs text-muted-foreground">{fullEmail}</p>
) : null}
{addressCheck.isFetching ? (
<p className="text-xs text-muted-foreground">Vérification de la disponibilité</p>
) : addressTaken ? (
<p className="text-xs text-destructive">Adresse déjà prise.</p>
) : addressCheck.data?.available === true && localPart.trim() ? (
<p className="text-xs text-green-600 dark:text-green-500">Adresse disponible.</p>
) : null}
</div>
<div className="space-y-1.5">
<Label htmlFor="hosted-display-name">Nom affiché</Label>
<Input
id="hosted-display-name"
value={displayName}
placeholder="Optionnel"
onChange={(e) => setDisplayName(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="hosted-password">Mot de passe boîte</Label>
<Input
id="hosted-password"
type="password"
value={password}
autoComplete="new-password"
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="space-y-1.5 sm:col-span-2">
<Label htmlFor="hosted-password-confirm">Confirmer le mot de passe</Label>
<Input
id="hosted-password-confirm"
type="password"
value={confirmPassword}
autoComplete="new-password"
onChange={(e) => setConfirmPassword(e.target.value)}
/>
{password && !passwordOk ? (
<p className="text-xs text-destructive">8 caractères minimum.</p>
) : null}
{confirmPassword && !passwordsMatch ? (
<p className="text-xs text-destructive">Les mots de passe ne correspondent pas.</p>
) : null}
</div>
</div>
{setup.isError ? (
<p className="text-sm text-destructive">{setupErrorMessage(setup.error)}</p>
) : null}
{setup.isSuccess ? (
<p className="text-sm text-green-600 dark:text-green-500">
Boîte configurée synchronisation IMAP en cours.
</p>
) : null}
<Button type="button" disabled={!canSubmit} onClick={() => void handleSubmit()}>
{setup.isPending ? (
<>
<Loader2 className="mr-2 size-4 animate-spin" />
Configuration
</>
) : (
"Activer ma boîte hébergée"
)}
</Button>
</div>
)}
</CardContent>
</Card>
)
}