Add Resend mailer as primary with SMTP fallback for reliable email delivery

This commit is contained in:
openhands committed 2026-07-07 20:12:56 +02:00
1 parent f386ae2b25
commit bf4075233d
4 files changed
+74 -14

No files matched your search

+1
View File
@@ -32,6 +32,7 @@
"otplib": "^12.0.1", "otplib": "^12.0.1",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"resend": "^6.17.1",
"sanitize-html": "^2.17.5", "sanitize-html": "^2.17.5",
"zod": "^3.24.0" "zod": "^3.24.0"
}, },
+40
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -11,6 +11,8 @@ const schema = z.object({
DATABASE_CONNECT_TIMEOUT_MS: z.coerce.number().int().positive().default(10_000), DATABASE_CONNECT_TIMEOUT_MS: z.coerce.number().int().positive().default(10_000),
HOTEL_NAME: z.string().default("Atom"), HOTEL_NAME: z.string().default("Atom"),
APP_URL: z.string().url().default("http://localhost:3000"), APP_URL: z.string().url().default("http://localhost:3000"),
// Resend API key (preferred — simpler HTTP API, always works).
RESEND_API_KEY: z.string().optional(),
// SMTP (password reset / notifications). Email features no-op if unset. // SMTP (password reset / notifications). Email features no-op if unset.
SMTP_HOST: z.string().optional(), SMTP_HOST: z.string().optional(),
SMTP_PORT: z.coerce.number().int().positive().optional(), SMTP_PORT: z.coerce.number().int().positive().optional(),
+31 -14
View File
@@ -1,7 +1,9 @@
import nodemailer, { type Transporter } from "nodemailer"; import nodemailer, { type Transporter } from "nodemailer";
import { Resend } from "resend";
import { env } from "@/env"; import { env } from "@/env";
let transporter: Transporter | null = null; let transporter: Transporter | null = null;
let resend: Resend | null = null;
function getTransport(): Transporter | null { function getTransport(): Transporter | null {
if (!env.SMTP_HOST) return null; if (!env.SMTP_HOST) return null;
@@ -16,23 +18,38 @@ function getTransport(): Transporter | null {
return transporter; return transporter;
} }
/** Send an HTML email. No-ops (returns false) when SMTP isn't configured. */ function getResend(): Resend | null {
export async function sendMail(to: string, subject: string, html: string): Promise<boolean> { if (!env.RESEND_API_KEY) return null;
const t = getTransport(); if (!resend) resend = new Resend(env.RESEND_API_KEY);
if (!t) { return resend;
console.warn("[email] SMTP not configured — skipping mail to", to);
return false;
} }
/** Send an HTML email. Tries Resend first, then SMTP. Returns false if both are unconfigured. */
export async function sendMail(to: string, subject: string, html: string): Promise<boolean> {
const from = env.SMTP_FROM ?? `no-reply@${env.HOTEL_NAME}`;
const r = getResend();
if (r) {
try { try {
await t.sendMail({ await r.emails.send({ from, to, subject, html });
from: env.SMTP_FROM ?? `no-reply@${env.HOTEL_NAME}`,
to,
subject,
html,
});
return true; return true;
} catch (e) { } catch (e) {
console.error("[email] send failed:", (e as Error).message); console.error("[email] Resend failed:", (e as Error).message);
}
}
const t = getTransport();
if (t) {
try {
await t.sendMail({ from, to, subject, html });
return true;
} catch (e) {
console.error("[email] SMTP failed:", (e as Error).message);
}
}
if (!r && !t) {
console.warn("[email] No mailer configured — set RESEND_API_KEY or SMTP_HOST");
}
return false; return false;
} }
}