/** * Turn one ready order into one delivered email. * * The interesting part is getting the QR out of whatever the operator's * fulfilment handed us and into an attachment. `EsimOrder.qrCode` is * documented as "a data: URI or an https URL" because both are things a * fulfilment API plausibly returns, and neither can be dropped into an email * as-is: * * - a `data:` URI in `` is stripped by Gmail and shows as a broken * image, which is the single most expensive way for this email to fail; * - an `https` URL survives, but only while that host is up and only if the * reader's client loads remote images, which many do not by default. * * So both are normalised to bytes and attached with a `content_id`, and the * HTML points at `cid:`. An attachment is part of the message; it cannot 404 * next month and it does not need remote images enabled. */ import brand from "@/brand.config"; import type { EsimOrder } from "@/vendor/carrier/types"; import { buildQrDeliveryEmail, type QrImage } from "@/vendor/email/qr-delivery"; import { emailConfigured, fromAddress, sendEmail, type EmailSendResult } from "@/vendor/email/resend"; /** A QR code is a few kB. Anything this size is not one, and is not attached. */ const MAX_QR_BYTES = 2 * 1024 * 1024; // `[\s\S]` rather than the `s` flag: the template's tsconfig targets an older // ES level than dotAll, and a base64 payload can contain newlines. const DATA_URI = /^data:([^;,]+)?(;base64)?,([\s\S]*)$/; function toBase64(bytes: Uint8Array): string { // Chunked so a large-ish buffer cannot blow the argument limit of // String.fromCharCode. btoa exists in both Workers and Node 18+. let binary = ""; const chunk = 0x8000; for (let i = 0; i < bytes.length; i += chunk) { binary += String.fromCharCode(...bytes.subarray(i, i + chunk)); } return btoa(binary); } /** * Fetch or decode the QR into base64 bytes. Null whenever that cannot be done * — the email is still worth sending, because the manual credentials and the * written steps below the QR are what most people use anyway. */ export async function resolveQrImage(qrCode: string | undefined): Promise { if (!qrCode) return null; const data = DATA_URI.exec(qrCode); if (data) { const [, contentType, isBase64, payload] = data; if (!isBase64 || !payload) return null; return { base64: payload.replace(/\s+/g, ""), contentType: contentType || "image/png" }; } if (!/^https:\/\//i.test(qrCode)) return null; try { const res = await fetch(qrCode, { cache: "no-store" }); if (!res.ok) return null; const buffer = await res.arrayBuffer(); if (buffer.byteLength === 0 || buffer.byteLength > MAX_QR_BYTES) return null; return { base64: toBase64(new Uint8Array(buffer)), contentType: res.headers.get("content-type")?.split(";")[0]?.trim() || "image/png", }; } catch { return null; } } export type QrDeliveryResult = | { sent: true; id?: string; withQrImage: boolean } | { sent: false; reason: "unconfigured" | "no-from-address" | "rejected" | "network-error"; detail?: string }; /** * Send the delivery email for one ready order. * * Never throws and never reports success it did not get. A storefront with no * Resend key returns `unconfigured`, which the caller shows as a plain * sentence — the order is complete either way, and pretending an email went * out is worse than saying none did. */ export async function deliverQrEmail(order: EsimOrder, to: string): Promise { if (!emailConfigured()) return { sent: false, reason: "unconfigured" }; if (!fromAddress()) return { sent: false, reason: "no-from-address" }; const qr = await resolveQrImage(order.qrCode); const message = buildQrDeliveryEmail({ to, order, brand: { name: brand.name, supportEmail: brand.supportEmail, supportUrl: brand.supportUrl, appUrl: brand.appUrl, colors: { accent: brand.colors.accent }, }, qr, }); const result: EmailSendResult = await sendEmail(message); if (!result.ok) return { sent: false, reason: result.reason, detail: result.detail }; return { sent: true, id: result.id, withQrImage: qr !== null }; }