122 lines
4.5 KiB
TypeScript
122 lines
4.5 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState } from 'react';
|
||
|
|
import { Send, CheckCircle2, AlertCircle } from 'lucide-react';
|
||
|
|
import { toast } from 'sonner';
|
||
|
|
|
||
|
|
import { Button } from '@/components/ui/button';
|
||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||
|
|
import { Input } from '@/components/ui/input';
|
||
|
|
import { Label } from '@/components/ui/label';
|
||
|
|
import { apiFetch } from '@/lib/api/client';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* SMTP connectivity test card. Distinct from the branding-page "Send a
|
||
|
|
* test" affordance:
|
||
|
|
* - This one isolates SMTP — plaintext + minimal HTML, no logo, no
|
||
|
|
* branded shell — so the failure mode is pure transport.
|
||
|
|
* - The branding-preview send exercises the full rendering pipeline.
|
||
|
|
*
|
||
|
|
* Surfaces the SMTP error inline (under the input) instead of toasting,
|
||
|
|
* because the whole point is to read it. ENOTFOUND, EAUTH, connection
|
||
|
|
* refused, etc. land here as a useful diagnostic.
|
||
|
|
*/
|
||
|
|
export function SmtpTestSendCard() {
|
||
|
|
const [recipient, setRecipient] = useState('');
|
||
|
|
const [sending, setSending] = useState(false);
|
||
|
|
const [result, setResult] = useState<
|
||
|
|
| { kind: 'ok'; recipient: string; messageId: string | null }
|
||
|
|
| { kind: 'err'; message: string }
|
||
|
|
| null
|
||
|
|
>(null);
|
||
|
|
|
||
|
|
async function send() {
|
||
|
|
if (!recipient) return;
|
||
|
|
setSending(true);
|
||
|
|
setResult(null);
|
||
|
|
try {
|
||
|
|
const res = await apiFetch<{
|
||
|
|
data: { sent: boolean; recipient: string; messageId: string | null };
|
||
|
|
}>('/api/v1/admin/email/test-send', {
|
||
|
|
method: 'POST',
|
||
|
|
body: { recipient },
|
||
|
|
});
|
||
|
|
setResult({
|
||
|
|
kind: 'ok',
|
||
|
|
recipient: res.data.recipient,
|
||
|
|
messageId: res.data.messageId,
|
||
|
|
});
|
||
|
|
toast.success(`Test email accepted by SMTP for ${res.data.recipient}`);
|
||
|
|
} catch (err) {
|
||
|
|
const message = err instanceof Error ? err.message : 'SMTP send failed';
|
||
|
|
setResult({ kind: 'err', message });
|
||
|
|
} finally {
|
||
|
|
setSending(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Card>
|
||
|
|
<CardHeader>
|
||
|
|
<CardTitle>SMTP test send</CardTitle>
|
||
|
|
<CardDescription>
|
||
|
|
Fires a minimal plaintext email through the configured SMTP host. Use this to confirm
|
||
|
|
credentials reach the wire before a real notification flow depends on them. Errors land
|
||
|
|
inline below so you can read the SMTP response (auth failure, connection refused, etc.).
|
||
|
|
</CardDescription>
|
||
|
|
</CardHeader>
|
||
|
|
<CardContent className="space-y-3">
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="smtp-test-recipient">Recipient</Label>
|
||
|
|
<div className="flex flex-wrap items-center gap-2">
|
||
|
|
<Input
|
||
|
|
id="smtp-test-recipient"
|
||
|
|
type="email"
|
||
|
|
value={recipient}
|
||
|
|
onChange={(e) => setRecipient(e.target.value)}
|
||
|
|
placeholder="you@example.com"
|
||
|
|
className="flex-1 min-w-[240px]"
|
||
|
|
/>
|
||
|
|
<Button onClick={send} disabled={sending || !recipient}>
|
||
|
|
<Send className="mr-1.5 h-4 w-4" aria-hidden />
|
||
|
|
{sending ? 'Sending…' : 'Send test'}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
{result?.kind === 'ok' && (
|
||
|
|
<div
|
||
|
|
role="status"
|
||
|
|
className="flex items-start gap-2 rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-900"
|
||
|
|
>
|
||
|
|
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" aria-hidden />
|
||
|
|
<div>
|
||
|
|
<div className="font-medium">Accepted by SMTP for {result.recipient}.</div>
|
||
|
|
{result.messageId ? (
|
||
|
|
<div className="text-xs text-emerald-800/80">
|
||
|
|
Message-ID: <span className="font-mono">{result.messageId}</span>
|
||
|
|
</div>
|
||
|
|
) : null}
|
||
|
|
<div className="text-xs text-emerald-800/80">
|
||
|
|
Acceptance doesn't guarantee inbox delivery. Check the recipient's mailbox
|
||
|
|
(and spam folder) to confirm.
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{result?.kind === 'err' && (
|
||
|
|
<div
|
||
|
|
role="alert"
|
||
|
|
className="flex items-start gap-2 rounded-md border border-destructive/40 bg-destructive/5 px-3 py-2 text-sm text-destructive"
|
||
|
|
>
|
||
|
|
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" aria-hidden />
|
||
|
|
<div>
|
||
|
|
<div className="font-medium">SMTP send failed</div>
|
||
|
|
<div className="font-mono text-xs break-all">{result.message}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</CardContent>
|
||
|
|
</Card>
|
||
|
|
);
|
||
|
|
}
|