馃 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 const dkimPrivateKey = process.env.DKIM_PRIVATE_KEY;
32 const mailchannelsApiKey = process.env.MAILCHANNELS_API_KEY;
33
34 if (!dkimPrivateKey) {
35 throw new Error(
36 "DKIM_PRIVATE_KEY environment variable is required for sending emails",
37 );
38 }
39
40 if (!mailchannelsApiKey) {
41 throw new Error(
42 "MAILCHANNELS_API_KEY environment variable is required for sending emails",
43 );
44 }
45
46 // Normalize recipient
47 const recipient =
48 typeof options.to === "string" ? { email: options.to } : options.to;
49
50 // Build content array
51 const content: EmailContent[] = [];
52 if (options.text) {
53 content.push({ type: "text/plain", value: options.text });
54 }
55 if (options.html) {
56 content.push({ type: "text/html", value: options.html });
57 }
58
59 if (content.length === 0) {
60 throw new Error("At least one of 'text' or 'html' must be provided");
61 }
62
63 const payload = {
64 personalizations: [
65 {
66 to: [recipient],
67 ...(options.replyTo && {
68 reply_to: { email: options.replyTo },
69 }),
70 dkim_domain: dkimDomain,
71 dkim_selector: "mailchannels",
72 dkim_private_key: dkimPrivateKey,
73 },
74 ],
75 from: {
76 email: fromEmail,
77 name: fromName,
78 },
79 subject: options.subject,
80 content,
81 };
82
83 const response = await fetch("https://api.mailchannels.net/tx/v1/send", {
84 method: "POST",
85 headers: {
86 "content-type": "application/json",
87 "X-Api-Key": mailchannelsApiKey,
88 },
89 body: JSON.stringify(payload),
90 });
91
92 if (!response.ok) {
93 const errorText = await response.text();
94 throw new Error(
95 `MailChannels API error (${response.status}): ${errorText}`,
96 );
97 }
98
99 console.log(`[Email] Sent "${options.subject}" to ${recipient.email}`);
100}