/** * MedOS email service. * * Backend selection (first one that has its env set wins): * * 1. Resend HTTP API — when RESEND_API_KEY is set. Preferred on * serverless because it's just HTTPS to * api.resend.com (port 443 is never blocked) * and errors come back as actionable JSON. * 2. nodemailer SMTP — when SMTP_HOST + SMTP_USER + SMTP_PASS are * all set. Works with any provider. * 3. Console fallback — when nothing above is configured. Dumps the * email body to stdout so dev work doesn't * require any email setup at all. The * verification code is visible in the server * logs. * * Errors from the actual send (auth rejected, domain not verified, * rate-limited, network) are logged to stderr with a clear prefix so * they're greppable in container logs. The callers (register, * forgot-password) treat email as best-effort and never block the * user on it. */ import nodemailer from 'nodemailer'; const FROM_EMAIL = process.env.FROM_EMAIL || 'MedOS '; const APP_NAME = 'MedOS'; const APP_URL = process.env.APP_URL || 'https://ruslanmv-medibot.hf.space'; // ---------- Resend HTTP transport ---------- const RESEND_API_KEY = process.env.RESEND_API_KEY; const RESEND_URL = 'https://api.resend.com/emails'; async function sendViaResend( to: string, subject: string, html: string, ): Promise { try { const res = await fetch(RESEND_URL, { method: 'POST', headers: { Authorization: `Bearer ${RESEND_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from: FROM_EMAIL, to, subject, html }), }); if (!res.ok) { const errBody = await res.text().catch(() => ''); console.error( `[EMAIL Resend] ${res.status} ${res.statusText} → to=${to}: ${errBody.slice(0, 300)}`, ); return false; } const data = (await res.json().catch(() => ({}))) as { id?: string }; console.log(`[EMAIL Resend] ok id=${data.id ?? '?'} to=${to} subject="${subject}"`); return true; } catch (err: any) { console.error(`[EMAIL Resend] network error to=${to}: ${err?.message ?? err}`); return false; } } // ---------- SMTP transport (nodemailer) ---------- const SMTP_HOST = process.env.SMTP_HOST; const SMTP_PORT = parseInt(process.env.SMTP_PORT || '587', 10); const SMTP_USER = process.env.SMTP_USER; const SMTP_PASS = process.env.SMTP_PASS; const smtpConfigured = !!(SMTP_HOST && SMTP_USER && SMTP_PASS); const smtpTransporter = smtpConfigured ? nodemailer.createTransport({ host: SMTP_HOST, port: SMTP_PORT, secure: SMTP_PORT === 465, auth: { user: SMTP_USER, pass: SMTP_PASS }, }) : null; async function sendViaSmtp( to: string, subject: string, html: string, ): Promise { if (!smtpTransporter) return false; try { const info = await smtpTransporter.sendMail({ from: FROM_EMAIL, to, subject, html }); console.log(`[EMAIL SMTP] ok messageId=${info.messageId} to=${to} subject="${subject}"`); return true; } catch (err: any) { console.error(`[EMAIL SMTP] failed to=${to}: ${err?.message ?? err}`); return false; } } // ---------- Console fallback (dev only) ---------- function logToConsole(to: string, subject: string, html: string): boolean { console.log( `\n[EMAIL stdout-fallback] No RESEND_API_KEY or SMTP_* configured. Set one to actually send mail.`, ); console.log(`[EMAIL] To: ${to}`); console.log(`[EMAIL] Subject: ${subject}`); console.log(`[EMAIL] Body (text): ${html.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim()}\n`); return true; } // ---------- Dispatcher ---------- async function sendEmail(to: string, subject: string, html: string): Promise { if (RESEND_API_KEY) return sendViaResend(to, subject, html); if (smtpConfigured) return sendViaSmtp(to, subject, html); return logToConsole(to, subject, html); } /** * Which transport is active. Logged at boot from app/api/auth/register * so operators can confirm the wiring from a single container-log line * instead of having to guess from absent email deliveries. */ export function emailTransportName(): 'resend' | 'smtp' | 'console' { if (RESEND_API_KEY) return 'resend'; if (smtpConfigured) return 'smtp'; return 'console'; } // ============================================================ // Email templates — clean, mobile-friendly, brand-consistent // ============================================================ function wrap(content: string): string { return `

${APP_NAME}

${content}

This email was sent by ${APP_NAME}. If you didn't request this, you can safely ignore it.

`; } // ============================================================ // Public API // ============================================================ export async function sendVerificationEmail( to: string, code: string, ): Promise { return sendEmail( to, `${APP_NAME} — verify your email`, wrap(`

Verify your email

Enter this code in the app to verify your email address and secure your account.

${code}

This code expires in 15 minutes.

`), ); } export async function sendPasswordResetEmail( to: string, code: string, ): Promise { // One-click reset link — the frontend (both Vercel and HF) parses // ?action=reset&email=…&code=… on mount and drops the user straight // into the "set new password" step with everything pre-filled. // The 6-digit code is still shown so users who can't click (some // mail clients strip query strings) can paste it manually. const linkUrl = `${APP_URL}?action=reset` + `&email=${encodeURIComponent(to)}` + `&code=${encodeURIComponent(code)}`; return sendEmail( to, `${APP_NAME} — reset your password`, wrap(`

Reset your password

Someone requested a password reset for your ${APP_NAME} account. Click the button below to set a new password, or enter the 6-digit code in the app.

Or enter this code manually:

${code}

This code expires in 1 hour. If you didn't request this, ignore this email — your password will stay the same.

`), ); } export async function sendWelcomeEmail(to: string): Promise { return sendEmail( to, `Welcome to ${APP_NAME}`, wrap(`

Welcome to ${APP_NAME}

Your account is ready. You can now track medications, appointments, vitals, and access your health data from any device.

Free forever. Private. No ads.

`), ); }