mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 09:50:01 +00:00
Migrate email pipeline off Lovable + branded auth emails
Replace Lovable-bound email transport and auth webhook so the platform sends all automated email through its own infrastructure. - process-email-queue: drop sendLovableEmail/LOVABLE_API_KEY; send via the Hostinger Email API (primary) with automatic SMTP fallback. Shared transports added in _shared/hostinger-mail.ts and _shared/smtp-send.ts. - auth-email-hook: verify Supabase's native Send Email hook signature (Standard Webhooks via SEND_EMAIL_HOOK_SECRET) instead of Lovable's libs; build the GoTrue verify URL; keep enqueue → process-email-queue. - Recreate the 6 auth email templates under _shared/email-templates/ that previously only existed in the deployed function. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
// Hostinger Email API transport (https://api.mail.hostinger.com).
|
||||
//
|
||||
// Send endpoint: POST /api/v1/mailboxes/{mailboxResourceId}/send
|
||||
// Auth: Authorization: Bearer <token> (token + mailbox are scoped to one order)
|
||||
// The From identity is implicit — it is the mailbox the token is authorized for,
|
||||
// so the body carries no `from` field.
|
||||
//
|
||||
// `sendViaHostingerMail` throws on any non-2xx so the caller can fall back to
|
||||
// SMTP and/or let the queue retry. The thrown Error carries `.status` so HTTP
|
||||
// 429 can be mapped to the dispatcher's existing rate-limit backoff.
|
||||
|
||||
export type HostingerMailConfig = {
|
||||
token: string;
|
||||
mailboxResourceId: string;
|
||||
};
|
||||
|
||||
export type HostingerMessage = {
|
||||
to: string;
|
||||
subject: string;
|
||||
html: string;
|
||||
text?: string;
|
||||
};
|
||||
|
||||
const BASE_URL = 'https://api.mail.hostinger.com';
|
||||
|
||||
export class HostingerMailError extends Error {
|
||||
status: number;
|
||||
constructor(message: string, status: number) {
|
||||
super(message);
|
||||
this.name = 'HostingerMailError';
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendViaHostingerMail(
|
||||
cfg: HostingerMailConfig,
|
||||
msg: HostingerMessage,
|
||||
): Promise<void> {
|
||||
if (!cfg.token || !cfg.mailboxResourceId) {
|
||||
throw new Error('Hostinger mail config incomplete (token and mailboxResourceId required)');
|
||||
}
|
||||
|
||||
const url = `${BASE_URL}/api/v1/mailboxes/${encodeURIComponent(cfg.mailboxResourceId)}/send`;
|
||||
const body = {
|
||||
to: [msg.to],
|
||||
subject: msg.subject,
|
||||
html: msg.html,
|
||||
text: msg.text && msg.text.trim() ? msg.text : undefined,
|
||||
};
|
||||
|
||||
const resp = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${cfg.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
let detail = '';
|
||||
try { detail = (await resp.text()).slice(0, 500); } catch { /* ignore */ }
|
||||
throw new HostingerMailError(
|
||||
`Hostinger mail API ${resp.status} ${resp.statusText}: ${detail}`,
|
||||
resp.status,
|
||||
);
|
||||
}
|
||||
// Drain the body so the connection can be reused.
|
||||
try { await resp.text(); } catch { /* ignore */ }
|
||||
}
|
||||
Reference in New Issue
Block a user