Files
temetro/backend/src/lib/email.ts
T
Khalid Abdi 90e6ec4cc0 feat: email provider, admin password reset, portal new-patient, chat pill
Chat: history pill now shows a History icon + a Start-new-chat (SquarePen)
button; removed the duplicate chat-history list from the sidebar.

Email: deployment-wide email provider config (Resend/Postmark/SendGrid/SMTP) in
Settings → Developers, with encrypted API key and a Send-test action. sendEmail
dispatches via the chosen provider (REST via fetch; SMTP via nodemailer).

Forgot password with no provider: alert the clinic admin(s) via a "System"
message card in Messages + a bell notification (seeded system user + per-clinic
System conversation); clicking deep-links to /settings?tab=careTeam&member=<id>.
Admins can set a member's password directly from the employee dialog
(PATCH /api/staff/:id/password via Better Auth's internal context — no admin
plugin needed).

Patient Portal: "New patient" booking path registers a demographics-only patient
then books; bookings reject double-booked slots (409).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:52:55 +03:00

150 lines
4.2 KiB
TypeScript

import nodemailer from "nodemailer";
import { env } from "../env.js";
import { getActiveConfig } from "../services/email-config.js";
type SendArgs = {
to: string;
subject: string;
text: string;
html?: string;
};
// SMTP transport (built from env) — used when the active provider is "smtp" or,
// for backward compatibility, when SMTP_HOST is set and no provider is chosen.
const smtpTransport = env.SMTP_HOST
? nodemailer.createTransport({
host: env.SMTP_HOST,
port: env.SMTP_PORT ?? 587,
secure: (env.SMTP_PORT ?? 587) === 465,
auth:
env.SMTP_USER && env.SMTP_PASS
? { user: env.SMTP_USER, pass: env.SMTP_PASS }
: undefined,
})
: null;
function logToConsole({ to, subject, text }: SendArgs): void {
console.info(
[
"",
"✉️ [email:console] No email provider configured — printing instead of sending.",
` to: ${to}`,
` subject: ${subject}`,
` body: ${text}`,
"",
].join("\n"),
);
}
async function sendViaResend(
apiKey: string,
from: string,
{ to, subject, text, html }: SendArgs,
): Promise<void> {
const res = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ from, to, subject, text, html: html ?? text }),
});
if (!res.ok) throw new Error(`Resend failed: ${res.status} ${await res.text()}`);
}
async function sendViaPostmark(
apiKey: string,
from: string,
{ to, subject, text, html }: SendArgs,
): Promise<void> {
const res = await fetch("https://api.postmarkapp.com/email", {
method: "POST",
headers: {
"X-Postmark-Server-Token": apiKey,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
From: from,
To: to,
Subject: subject,
TextBody: text,
HtmlBody: html ?? text,
MessageStream: "outbound",
}),
});
if (!res.ok)
throw new Error(`Postmark failed: ${res.status} ${await res.text()}`);
}
async function sendViaSendgrid(
apiKey: string,
from: string,
{ to, subject, text, html }: SendArgs,
): Promise<void> {
const res = await fetch("https://api.sendgrid.com/v3/mail/send", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
personalizations: [{ to: [{ email: to }] }],
from: { email: from },
subject,
content: [
{ type: "text/plain", value: text },
{ type: "text/html", value: html ?? text },
],
}),
});
if (!res.ok)
throw new Error(`SendGrid failed: ${res.status} ${await res.text()}`);
}
// Send an email via the deployment's configured provider. Falls back to logging
// when nothing is configured so local/open-source dev needs zero setup.
export async function sendEmail(args: SendArgs): Promise<void> {
const cfg = await getActiveConfig();
// A real "from": the configured address, else the SMTP default.
const from = cfg.fromAddress || env.SMTP_FROM;
switch (cfg.provider) {
case "resend":
if (!cfg.credentials) return logToConsole(args);
return sendViaResend(cfg.credentials, from, args);
case "postmark":
if (!cfg.credentials) return logToConsole(args);
return sendViaPostmark(cfg.credentials, from, args);
case "sendgrid":
if (!cfg.credentials) return logToConsole(args);
return sendViaSendgrid(cfg.credentials, from, args);
case "smtp": {
if (!smtpTransport) return logToConsole(args);
await smtpTransport.sendMail({
from,
to: args.to,
subject: args.subject,
text: args.text,
html: args.html ?? args.text,
});
return;
}
default: {
// No provider chosen — honour a pre-existing SMTP env setup if present.
if (smtpTransport) {
await smtpTransport.sendMail({
from,
to: args.to,
subject: args.subject,
text: args.text,
html: args.html ?? args.text,
});
return;
}
return logToConsole(args);
}
}
}