Add Resend mailer as primary with SMTP fallback for reliable email delivery
This commit is contained in:
1 parent
f386ae2b25
commit
bf4075233d
4 files changed
+75
-15
No files matched your search
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+40
@@ -50,6 +50,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.2.0
|
specifier: ^19.2.0
|
||||||
version: 19.2.7([email protected])
|
version: 19.2.7([email protected])
|
||||||
|
resend:
|
||||||
|
specifier: ^6.17.1
|
||||||
|
version: 6.17.1
|
||||||
sanitize-html:
|
sanitize-html:
|
||||||
specifier: ^2.17.5
|
specifier: ^2.17.5
|
||||||
version: 2.17.5
|
version: 2.17.5
|
||||||
@@ -1381,6 +1384,9 @@ packages:
|
|||||||
'@schummar/[email protected]':
|
'@schummar/[email protected]':
|
||||||
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
|
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
|
||||||
|
|
||||||
|
'@stablelib/[email protected]':
|
||||||
|
resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==}
|
||||||
|
|
||||||
'@standard-schema/[email protected]':
|
'@standard-schema/[email protected]':
|
||||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
@@ -1946,6 +1952,9 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
|
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
|
||||||
|
|
||||||
@@ -2293,6 +2302,9 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==}
|
resolution: {integrity: sha512-ECF4zHLbUItpUgE3OTtLKlPjeBN+fKEczj2zYjDfCGOzicNs0GK3Vg2IoAYwx7LH/XYw43fZQP6xnZ4TkNxSLQ==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
|
resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -2369,6 +2381,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-QhHHIRPPM9Tq2uilWmNF8C8kd6nLkUmiFgDQCt1v4eR4o+6p86qrK+Vn12jnAs0jR8Gf6ABdxI18/90LGQ7GVA==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
peerDependencies:
|
||||||
|
'@react-email/render': '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@react-email/render':
|
||||||
|
optional: true
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
|
||||||
|
|
||||||
@@ -2452,6 +2473,9 @@ packages:
|
|||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
|
||||||
|
|
||||||
@@ -3415,6 +3439,8 @@ snapshots:
|
|||||||
|
|
||||||
'@schummar/[email protected]': {}
|
'@schummar/[email protected]': {}
|
||||||
|
|
||||||
|
'@stablelib/[email protected]': {}
|
||||||
|
|
||||||
'@standard-schema/[email protected]': {}
|
'@standard-schema/[email protected]': {}
|
||||||
|
|
||||||
'@swc/[email protected]':
|
'@swc/[email protected]':
|
||||||
@@ -3915,6 +3941,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
@@ -4225,6 +4253,8 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
cssesc: 3.0.0
|
cssesc: 3.0.0
|
||||||
@@ -4311,6 +4341,11 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
postal-mime: 2.7.4
|
||||||
|
standardwebhooks: 1.0.0
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
@@ -4429,6 +4464,11 @@ snapshots:
|
|||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
|
[email protected]:
|
||||||
|
dependencies:
|
||||||
|
'@stablelib/base64': 1.0.1
|
||||||
|
fast-sha256: 1.3.0
|
||||||
|
|
||||||
[email protected]: {}
|
[email protected]: {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
+32
-15
@@ -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 {
|
||||||
|
if (!env.RESEND_API_KEY) return null;
|
||||||
|
if (!resend) resend = new Resend(env.RESEND_API_KEY);
|
||||||
|
return resend;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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> {
|
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 {
|
||||||
|
await r.emails.send({ from, to, subject, html });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[email] Resend failed:", (e as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const t = getTransport();
|
const t = getTransport();
|
||||||
if (!t) {
|
if (t) {
|
||||||
console.warn("[email] SMTP not configured — skipping mail to", to);
|
try {
|
||||||
return false;
|
await t.sendMail({ from, to, subject, html });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[email] SMTP failed:", (e as Error).message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
await t.sendMail({
|
if (!r && !t) {
|
||||||
from: env.SMTP_FROM ?? `no-reply@${env.HOTEL_NAME}`,
|
console.warn("[email] No mailer configured — set RESEND_API_KEY or SMTP_HOST");
|
||||||
to,
|
|
||||||
subject,
|
|
||||||
html,
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[email] send failed:", (e as Error).message);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user