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
|
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 {
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 {
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" };
}
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) {
return {
ok: false,
provider: config.emailProvider,
error: String((err as Error)?.message || err),
};
}
}
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;
}
|