feat(admin-email): SMTP test-send card on /admin/email
Adds a plaintext-only SMTP connectivity test on the email-settings
page. Distinct from the branding-preview "Send a test" affordance:
- branding-preview exercises the full rendering pipeline (logo +
branded shell + colour) — useful for confirming the email *looks*
right.
- this test isolates SMTP — minimal HTML, plaintext alternative, no
logo dependency — so a failure is purely transport. Confirms the
configured credentials (env or per-port DB) reach the wire before
a real notification flow depends on them.
SMTP errors surface inline below the input (auth failure, ENOTFOUND,
connection refused, etc.) rather than as a passing toast — the whole
point of the test is to read them.
`/api/v1/admin/email/test-send` route reuses `sendEmail(...,
ctx.portId)` so per-port SMTP overrides are exercised the same way a
real notification would.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
61
src/app/api/v1/admin/email/test-send/route.ts
Normal file
61
src/app/api/v1/admin/email/test-send/route.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
import { errorResponse, ValidationError } from '@/lib/errors';
|
||||
import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { sendEmail } from '@/lib/email';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
const testSendSchema = z.object({
|
||||
recipient: z.string().email('Enter a valid email address'),
|
||||
});
|
||||
|
||||
/**
|
||||
* SMTP connectivity test. Sends a minimal plaintext-ish message so the
|
||||
* admin can verify the configured SMTP credentials (env or per-port DB)
|
||||
* reach the inbox WITHOUT depending on branding being uploaded.
|
||||
*
|
||||
* Separate from the branding-preview send (`/admin/branding/email-preview`):
|
||||
* - This one isolates the SMTP-host/port/user/pass surface.
|
||||
* - The branding one exercises the rendering pipeline + logo bytes.
|
||||
*
|
||||
* Surface SMTP errors to the caller directly (auth failure, ENOTFOUND,
|
||||
* connection refused) — the whole point of the test is to see them
|
||||
* inline in the admin UI.
|
||||
*/
|
||||
export const POST = withAuth(
|
||||
withPermission('admin', 'manage_settings', async (req, ctx) => {
|
||||
try {
|
||||
if (!ctx.portId) throw new ValidationError('No active port');
|
||||
const { recipient } = await parseBody(req, testSendSchema);
|
||||
|
||||
const subject = 'CRM SMTP test — connection verified';
|
||||
const html = `
|
||||
<div style="font-family:system-ui,-apple-system,sans-serif;font-size:14px;color:#1e293b;padding:24px;line-height:1.5;">
|
||||
<h1 style="font-size:18px;margin:0 0 12px;">SMTP test</h1>
|
||||
<p style="margin:0 0 12px;">
|
||||
If you're reading this, the SMTP credentials configured for this port
|
||||
are reaching ${recipient}.
|
||||
</p>
|
||||
<p style="margin:0;color:#64748b;font-size:13px;">
|
||||
Sent from /admin/email — Port Nimara CRM
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
const text = `SMTP test\n\nIf you're reading this, the SMTP credentials configured for this port are reaching ${recipient}.\n\nSent from /admin/email — Port Nimara CRM`;
|
||||
|
||||
const info = await sendEmail(recipient, subject, html, undefined, text, ctx.portId);
|
||||
logger.info(
|
||||
{ portId: ctx.portId, recipient, messageId: info.messageId },
|
||||
'Admin SMTP test send succeeded',
|
||||
);
|
||||
return NextResponse.json({
|
||||
data: { sent: true, recipient, messageId: info.messageId ?? null },
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn({ err: error, portId: ctx.portId }, 'Admin SMTP test send failed');
|
||||
return errorResponse(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user