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