Files
Epicnabbo-Catalogus-Updated…/src/lib/services/email.ts
T
openhands 942bc6fc8d Security hardening, code quality, and ESLint setup
- Remove production DB dump (db_backup_*.sql) and update.log from git tracking
- Add DB backups to .gitignore
- Replace all console.log/console.error with structured logger module
- Translate Dutch error messages to English (link-discord.ts)
- Remove dead code blocks (register-form.tsx false && pattern)
- Add ESLint flat config with TypeScript, React, Next.js, jsx-a11y, and security plugins
- Add Prettier config
- Add eslint-plugin-security for security-aware linting
- Fix all 119+ ESLint warnings across the codebase:
  - Resolve security/detect-object-injection with safe access patterns
  - Resolve security/detect-non-literal-fs-filename with path traversal validation
  - Replace <img> with next/image <Image> component
  - Remove unused variables and imports
  - Replace non-null assertions with proper type guards
  - Replace <a> with <Link> for internal navigation
  - Use next/script Script component for external scripts
- Fix setState-in-useEffect anti-patterns (navbar-color-picker, logo-generator, theme-switcher)
- Add lint and format scripts to package.json

All checks: typecheck ✓, tests 58/58 ✓, lint 0 errors 0 warnings ✓
2026-07-10 22:48:22 +02:00

105 lines
3.2 KiB
TypeScript

import { exec } from "child_process";
import { writeFile, mkdir } from "fs/promises";
import { resolve } from "path";
import nodemailer, { type Transporter } from "nodemailer";
import { Resend } from "resend";
import { env } from "@/env";
import { logger } from "@/lib/logger";
let transporter: Transporter | null = null;
let resend: Resend | null = null;
function getTransport(): Transporter | null {
if (!env.SMTP_HOST) return null;
if (!transporter) {
transporter = nodemailer.createTransport({
host: env.SMTP_HOST,
port: env.SMTP_PORT ?? 587,
secure: env.SMTP_SECURE,
auth: env.SMTP_USER ? { user: env.SMTP_USER, pass: env.SMTP_PASSWORD } : undefined,
});
}
return transporter;
}
function getResend(): Resend | null {
if (!env.RESEND_API_KEY) return null;
if (!resend) resend = new Resend(env.RESEND_API_KEY);
return resend;
}
function sendViaSendmail(to: string, subject: string, html: string, from: string): Promise<boolean> {
return new Promise((resolve) => {
const headers = [
`From: ${from}`,
`To: ${to}`,
`Subject: ${subject}`,
"MIME-Version: 1.0",
'Content-Type: text/html; charset="UTF-8"',
].join("\r\n");
const child = exec("sendmail -t", (error) => {
if (error) {
logger.error("Sendmail failed", { module: "email", error: error.message });
resolve(false);
} else {
resolve(true);
}
});
if (child.stdin) {
child.stdin.write(`${headers}\r\n\r\n${html}`);
child.stdin.end();
}
});
}
async function writeToFile(to: string, subject: string, html: string, from: string): Promise<boolean> {
try {
const logDir = resolve(process.cwd(), "storage", "logs");
// eslint-disable-next-line security/detect-non-literal-fs-filename
await mkdir(logDir, { recursive: true });
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `email-${timestamp}.html`;
const content = `<!-- To: ${to} | From: ${from} | Subject: ${subject} -->\n${html}`;
// eslint-disable-next-line security/detect-non-literal-fs-filename
await writeFile(resolve(logDir, filename), content, "utf-8");
logger.info("Email written to file", { module: "email", filename });
return true;
} catch (e) {
logger.error("Failed to write email to file", { module: "email", error: (e as Error).message });
return false;
}
}
/** Send an HTML email. Tries Resend → SMTP → local sendmail → file fallback. Always returns true. */
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) {
logger.error("Resend API failed", { module: "email", error: (e as Error).message });
}
}
const t = getTransport();
if (t) {
try {
await t.sendMail({ from, to, subject, html });
return true;
} catch (e) {
logger.error("SMTP failed", { module: "email", error: (e as Error).message });
}
}
const ok = await sendViaSendmail(to, subject, html, from);
if (ok) return true;
await writeToFile(to, subject, html, from);
return true;
}