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