Files
Primora/apps/auth/src/lib/mail.ts
T
Tomas DvorakandDevin 2fbde04563 fix: don't require email infra on self-hosted, improve login form detection
- sendTransactionalEmail no-ops (with a warning) when no mail transport is
  configured instead of throwing — sign-up 500'd on SMTP-less instances
- sendVerificationEmail catches transport failures so a broken SMTP can't
  block account creation; verification stays optional (requireEmailVerification
  was never set)
- login form gains id/name attributes and label for/id pairing so password
  managers (Proton Pass et al.) detect the fields; email input uses
  autocomplete=username

Authored By: TDvorak <[email protected]>

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-22 19:46:14 +02:00

70 lines
2.1 KiB
TypeScript

import nodemailer from "nodemailer";
import { Resend } from "resend";
import { env } from "./env.js";
import { getStringSetting } from "./settings.js";
// Mail config resolves per send: in-app setting → env var. This keeps
// SMTP/Resend editable from the dashboard without restarting the service.
async function resolveMailConfig() {
const from = (await getStringSetting("mail.from", env.MAIL_FROM))!;
const resendApiKey = await getStringSetting("mail.resend_api_key", env.RESEND_API_KEY);
if (resendApiKey) {
return { from, resendApiKey } as const;
}
const host = await getStringSetting("mail.smtp_host", env.SMTP_HOST);
const portRaw = await getStringSetting("mail.smtp_port", String(env.SMTP_PORT));
const user = await getStringSetting("mail.smtp_user", env.SMTP_USER);
const pass = await getStringSetting("mail.smtp_password", env.SMTP_PASSWORD);
const secureRaw = await getStringSetting("mail.smtp_secure", String(env.SMTP_SECURE));
return {
from,
smtp: host
? {
host,
port: Number(portRaw) || 1025,
secure: secureRaw === "true",
auth: user ? { user, pass } : undefined,
}
: undefined,
} as const;
}
export async function sendTransactionalEmail(input: {
to: string;
subject: string;
text: string;
html: string;
}) {
const cfg = await resolveMailConfig();
if ("resendApiKey" in cfg && cfg.resendApiKey) {
const resend = new Resend(cfg.resendApiKey);
await resend.emails.send({
from: cfg.from,
to: [input.to],
subject: input.subject,
text: input.text,
html: input.html,
});
return;
}
if (!("smtp" in cfg) || !cfg.smtp) {
// Self-hosted instances may run without mail — auth still works, the
// email simply never goes out.
console.warn(
JSON.stringify({ level: "warn", msg: "mail_skipped_no_transport", to: input.to, subject: input.subject }),
);
return;
}
await nodemailer.createTransport(cfg.smtp).sendMail({
from: cfg.from,
to: input.to,
subject: input.subject,
text: input.text,
html: input.html,
});
}