mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
SMTP-less self-hosted instances still marked every account emailVerified=false and attempted a verification mail that could never arrive. Now: no transport → accounts are created verified, the send is skipped, and existing unverified rows are backfilled on startup. .env.example stops defaulting SMTP_HOST to the dev-only mailpit service. Verified live: fresh signup returns emailVerified=true, legacy unverified users flip on restart, sign-in unaffected. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
78 lines
2.4 KiB
TypeScript
78 lines
2.4 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;
|
|
}
|
|
|
|
// True when a mail transport exists (Resend key or SMTP host). Verification
|
|
// and reset mails only make sense when mail can actually leave the box.
|
|
export async function hasMailTransport(): Promise<boolean> {
|
|
const cfg = await resolveMailConfig();
|
|
if ("resendApiKey" in cfg && cfg.resendApiKey) return true;
|
|
return "smtp" in cfg && Boolean(cfg.smtp);
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|