// Stands in for https://esm.town/v/std/email: sends through exe.dev's // email gateway (https://exe.dev/docs/send-email). With no `to`, mail goes // to the VM's owner, as on Val Town. The gateway sends plain text, from the // VM's own address, to you, your team, people you share the VM with, and // people who emailed the VM; `from` and custom headers are ignored. type Address = string | { email: string; name?: string }; interface Attachment { content: string; // base64 filename: string; type?: string; } export interface EmailOptions { to?: Address | Address[]; cc?: Address | Address[]; bcc?: Address | Address[]; from?: Address; replyTo?: Address | Address[]; subject?: string; text?: string; html?: string; attachments?: Attachment[]; headers?: Record; } let owner: string | undefined; async function ownerEmail() { if (!owner) { const res = await fetch("https://reflection.int.exe.xyz/email"); if (!res.ok) throw new Error(`reflection: ${res.status}`); const body = (await res.text()).trim(); owner = body.startsWith("{") ? JSON.parse(body).email : body; } return owner!; } const list = (a?: Address | Address[]) => (Array.isArray(a) ? a : a ? [a] : []).map((x) => (typeof x === "string" ? x : x.email)); const htmlToText = (html: string) => html.replace(/<(br|\/p|\/div|\/h\d|\/li|\/tr)\b[^>]*>/gi, "\n").replace(/<[^>]+>/g, "") .replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">") .replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&") .replace(/\n{3,}/g, "\n\n").trim(); export async function email(data: EmailOptions) { const to = [...list(data.to), ...list(data.cc), ...list(data.bcc)]; if (!to.length) to.push(await ownerEmail()); const body = data.text ?? (data.html ? htmlToText(data.html) : ""); for (const recipient of to) { const res = await fetch("http://169.254.169.254/gateway/email/send", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ to: recipient, subject: data.subject ?? `Message from ${Deno.env.get("VAL_NAME") ?? "a val"}`, body, reply_to: list(data.replyTo)[0], attachments: data.attachments?.map((a) => ({ filename: a.filename, content: a.content, content_type: a.type, })), }), }); const result = await res.json().catch(() => ({})); if (!res.ok || result.error) { throw new Error(`exe.dev email error: ${result.error ?? res.status}`); } } return { message: "Email accepted to be sent" }; } export default email;