CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | /**
* Email sending — provider-pluggable, never-throws.
*
* Providers:
* log — writes a formatted message to stderr (default, dev-safe)
* resend — POSTs to api.resend.com using RESEND_API_KEY
*
* Configured via:
* EMAIL_PROVIDER=log|resend
* EMAIL_FROM="gluecron <no-reply@gluecron.app>"
* RESEND_API_KEY=...
* APP_BASE_URL=https://gluecron.com
*
* Contract: sendEmail() must never reject. Failures are logged and swallowed
* so a downed email provider never breaks the primary request path. Callers
* await the returned promise to preserve ordering but may ignore the result.
*/
import { config } from "./config";
export interface EmailMessage {
to: string;
subject: string;
text: string;
html?: string;
}
export interface EmailResult {
ok: boolean;
provider: "log" | "resend" | "none";
skipped?: string;
error?: string;
id?: string;
}
function looksLikeEmail(s: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s.trim());
}
function renderPlainFallback(text: string): string {
// Minimal HTML fallback when caller didn't supply one.
const escaped = text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
return `<pre style="font-family:ui-monospace,SF-Mono,Menlo,monospace;font-size:13px;white-space:pre-wrap;color:#c9d1d9;background:#0d1117;padding:16px;border-radius:6px">${escaped}</pre>`;
}
async function sendViaResend(msg: EmailMessage): Promise<EmailResult> {
if (!config.resendApiKey) {
return { ok: false, provider: "resend", skipped: "RESEND_API_KEY unset" };
}
try {
const res = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
authorization: `Bearer ${config.resendApiKey}`,
"content-type": "application/json",
},
body: JSON.stringify({
from: config.emailFrom,
to: [msg.to],
subject: msg.subject,
text: msg.text,
html: msg.html || renderPlainFallback(msg.text),
}),
});
if (!res.ok) {
const body = await res.text();
return {
ok: false,
provider: "resend",
error: `resend ${res.status}: ${body.slice(0, 200)}`,
};
}
const body = (await res.json().catch(() => ({}))) as { id?: string };
return { ok: true, provider: "resend", id: body.id };
} catch (err) {
return {
ok: false,
provider: "resend",
error: String((err as Error)?.message || err),
};
}
}
function sendViaLog(msg: EmailMessage): EmailResult {
// Structured, grep-able log. Written to stderr so prod log collectors pick it up.
console.error(
`[email:log] to=${msg.to} subject=${JSON.stringify(msg.subject)}\n` +
msg.text.split("\n").map((l) => " " + l).join("\n")
);
return { ok: true, provider: "log" };
}
/**
* Send an email. Always resolves — never throws, never rejects.
* Returns { ok, provider, ... } so callers can surface errors in admin UIs
* without having to wrap in try/catch.
*/
export async function sendEmail(msg: EmailMessage): Promise<EmailResult> {
if (!msg.to || !looksLikeEmail(msg.to)) {
return { ok: false, provider: "none", skipped: "invalid recipient" };
}
if (!msg.subject || !msg.text) {
return { ok: false, provider: "none", skipped: "missing subject or body" };
}
try {
if (config.emailProvider === "resend") {
return await sendViaResend(msg);
}
return sendViaLog(msg);
} catch (err) {
// Defence-in-depth — provider handlers already swallow, but just in case.
return {
ok: false,
provider: config.emailProvider,
error: String((err as Error)?.message || err),
};
}
}
/**
* Build a fully-qualified URL from a path, using APP_BASE_URL.
* Safe with relative or absolute inputs.
*/
export function absoluteUrl(pathOrUrl: string | undefined | null): string {
if (!pathOrUrl) return config.appBaseUrl;
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
const suffix = pathOrUrl.startsWith("/") ? pathOrUrl : "/" + pathOrUrl;
return config.appBaseUrl + suffix;
}
|