Files
admin b1486a0b2a 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>
2026-06-02 23:07:26 -04:00

72 lines
2.1 KiB
TypeScript

// 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 */ }
}