馃 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}