馃 distributed transcription service
thistle.dunkirk.sh
1/**
2 * Email service using MailChannels API
3 * Docs: https://api.mailchannels.net/tx/v1/documentation
4 */
5
6interface EmailAddress {
7 email: string;
8 name?: string;
9}
10
11interface EmailContent {
12 type: "text/plain" | "text/html";
13 value: string;
14}
15
16interface SendEmailOptions {
17 to: string | EmailAddress;
18 subject: string;
19 html?: string;
20 text?: string;
21 replyTo?: string;
22}
23
24/**
25 * Send an email via MailChannels
26 */
27export async function sendEmail(options: SendEmailOptions): Promise<void> {
28 const fromEmail = process.env.SMTP_FROM_EMAIL || "noreply@thistle.app";
29 const fromName = process.env.SMTP_FROM_NAME || "Thistle";
30 const dkimDomain = process.env.DKIM_DOMAIN || "thistle.app";
31 // Validated at startup
32 const dkimPrivateKey = process.env.DKIM_PRIVATE_KEY as string;
33 const mailchannelsApiKey = process.env.MAILCHANNELS_API_KEY as string;
34
35 // Normalize recipient
36 const recipient =
37 typeof options.to === "string" ? { email: options.to } : options.to;
38
39 // Build content array
40 const content: EmailContent[] = [];
41 if (options.text) {
42 content.push({ type: "text/plain", value: options.text });
43 }
44 if (options.html) {
45 content.push({ type: "text/html", value: options.html });
46 }
47
48 if (content.length === 0) {
49 throw new Error("At least one of 'text' or 'html' must be provided");
50 }
51
52 const payload = {
53 personalizations: [
54 {
55 to: [recipient],
56 ...(options.replyTo && {
57 reply_to: { email: options.replyTo },
58 }),
59 dkim_domain: dkimDomain,
60 dkim_selector: "mailchannels",
61 dkim_private_key: dkimPrivateKey,
62 },
63 ],
64 from: {
65 email: fromEmail,
66 name: fromName,
67 },
68 subject: options.subject,
69 content,
70 };
71
72 const response = await fetch("https://api.mailchannels.net/tx/v1/send", {
73 method: "POST",
74 headers: {
75 "content-type": "application/json",
76 "X-Api-Key": mailchannelsApiKey,
77 },
78 body: JSON.stringify(payload),
79 });
80
81 if (!response.ok) {
82 const errorText = await response.text();
83 throw new Error(
84 `MailChannels API error (${response.status}): ${errorText}`,
85 );
86 }
87
88 console.log(`[Email] Sent "${options.subject}" to ${recipient.email}`);
89}