UNPKG

agents

Version:

A home for your AI agents

323 lines (322 loc) 11.3 kB
import { l as utf8ByteLength, o as renderInput } from "../internal-CYlgHl1l.js"; import PostalMime from "postal-mime"; //#region src/channels/adapters/email/inbound-email.ts const EMAIL_HEADER_VALUE_LIMIT$1 = 2048; function normalizeReference(value) { return value?.trim().replace(/^<|>$/g, "") || void 0; } function emailReferences(value) { return (value?.match(/<([^<>]+)>/g) ?? value?.trim().split(/\s+/) ?? []).flatMap((reference) => { const normalized = normalizeReference(reference); return normalized ? [normalized] : []; }); } function firstReference(value) { return emailReferences(value)[0]; } function boundedReferences(references) { const unique = [...new Set(references.filter(Boolean))]; if (unique.length < 2) return unique; const selected = [unique[0]]; for (let index = unique.length - 1; index > 0; index -= 1) { const candidate = unique[index]; if (utf8ByteLength([...selected, candidate].map((reference) => `<${reference}>`).join(" ")) <= EMAIL_HEADER_VALUE_LIMIT$1) selected.splice(1, 0, candidate); } return selected; } function isAutoReply(headers) { return headers.some((header) => { const key = header.key.toLowerCase(); const value = header.value.trim().toLowerCase(); if (key === "auto-submitted") return value !== "no"; if (key === "x-auto-response-suppress") return true; return key === "precedence" && (value === "bulk" || value === "junk" || value === "list"); }); } function attachmentSize(content) { return typeof content === "string" ? utf8ByteLength(content) : content.byteLength; } function normalizedAttachment(attachment) { return { ...normalizeReference(attachment.contentId) && { id: normalizeReference(attachment.contentId) }, mediaType: attachment.mimeType, ...attachment.filename && { name: attachment.filename }, size: attachmentSize(attachment.content), ...typeof attachment.content === "string" && attachment.mimeType.startsWith("text/") && { text: attachment.content } }; } function replyRecipients(replyTo) { const recipients = (replyTo ?? []).flatMap((entry) => entry.address ? [{ email: entry.address, name: entry.name }] : (entry.group ?? []).map((mailbox) => ({ email: mailbox.address, name: mailbox.name }))); return recipients.length > 0 ? recipients : void 0; } function recipientLabel(recipients) { return (Array.isArray(recipients) ? recipients : [recipients]).map((recipient) => typeof recipient === "string" ? recipient : recipient.name ? `${recipient.name} <${recipient.email}>` : recipient.email).join(", "); } function normalizedDate(value) { if (!value) return void 0; const timestamp = Date.parse(value); return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : void 0; } async function rawEmail(email) { if (email.getRaw) return email.getRaw(); if (email.raw) return new Uint8Array(await new Response(email.raw).arrayBuffer()); throw new Error("Inbound email must provide raw or getRaw()"); } async function contentReference(content) { const bytes = new Uint8Array(content.byteLength); bytes.set(content); const digest = await crypto.subtle.digest("SHA-256", bytes); return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`; } /** Parse Workers Email events into normalized Channel ingress events. */ function inboundEmail(options = {}) { const recipient = options.to?.toLowerCase(); const senders = new Set((typeof options.from === "string" ? [options.from] : options.from ?? []).map((sender) => sender.toLowerCase())); return { async receive(email) { if (!((!recipient || email.to.toLowerCase() === recipient) && (senders.size === 0 || senders.has(email.from.toLowerCase())))) return null; const content = await rawEmail(email); const parsed = await PostalMime.parse(content); const text = parsed.text?.trim() ?? ""; const reference = normalizeReference(parsed.messageId) ?? await contentReference(content); const replyReference = normalizeReference(parsed.inReplyTo); const threadRoot = firstReference(parsed.references) ?? replyReference ?? reference; const senderAddress = parsed.from?.address ?? email.from; const sentAt = normalizedDate(parsed.date); const references = boundedReferences([...emailReferences(parsed.references), reference]); const replyTo = replyRecipients(parsed.replyTo) ?? senderAddress; const replySurface = { version: 1, address: { from: options.replyFrom ?? email.to, to: replyTo, ...parsed.subject && { subject: parsed.subject }, inReplyTo: reference, references: [...new Set(references)] }, label: `Email · ${recipientLabel(replyTo)}` }; return { events: [{ event: { type: "message", eventId: reference, thread: { id: threadRoot, isDirectMessage: "unknown" }, replySurface, actor: { id: senderAddress, identity: { subject: senderAddress.toLowerCase() }, ...parsed.from?.name && { fullName: parsed.from.name } }, message: { id: reference, text, ...parsed.subject && { title: parsed.subject }, attachments: parsed.attachments.map(normalizedAttachment), ...replyReference && { reply: { id: replyReference } }, metadata: { ...sentAt && { sentAt }, autoReply: isAutoReply(parsed.headers) } } }, raw: parsed }] }; } }; } //#endregion //#region src/channels/adapters/email/email.ts const DEFAULT_EMAIL_TITLE = "Agent message"; const EMAIL_HEADER_VALUE_LIMIT = 2048; function approvalLinksUnavailable(retryable = false) { return { status: "failed", retryable, error: { code: "APPROVAL_LINKS_UNAVAILABLE", message: "Email approval requests require caller-supplied approval links" } }; } const PERMANENT_EMAIL_ERRORS = /* @__PURE__ */ new Set([ "E_VALIDATION_ERROR", "E_FIELD_MISSING", "E_TOO_MANY_RECIPIENTS", "E_TOO_MANY_ATTACHMENTS", "E_SENDER_NOT_VERIFIED", "E_RECIPIENT_NOT_ALLOWED", "E_RECIPIENT_SUPPRESSED", "E_SENDER_DOMAIN_NOT_AVAILABLE", "E_CONTENT_TOO_LARGE", "E_DELIVERY_FAILED", "E_HEADER_NOT_ALLOWED", "E_HEADER_USE_API_FIELD", "E_HEADER_VALUE_INVALID", "E_HEADER_VALUE_TOO_LONG", "E_HEADER_NAME_INVALID", "E_HEADERS_TOO_LARGE", "E_HEADERS_TOO_MANY" ]); const RETRYABLE_EMAIL_ERRORS = /* @__PURE__ */ new Set(["E_RATE_LIMIT_EXCEEDED", "E_DAILY_LIMIT_EXCEEDED"]); const EMAIL_ERROR_MESSAGES = /* @__PURE__ */ new Map([["Email must have at least one recipient in \"to\", \"cc\", or \"bcc\".", "E_FIELD_MISSING"]]); function emailFailure(error) { if (error !== null && typeof error === "object") { const value = error; return { code: typeof value.code === "string" ? value.code : "EMAIL_DELIVERY_ERROR", message: typeof value.message === "string" ? value.message : "Email delivery failed" }; } return { code: "EMAIL_DELIVERY_ERROR", message: typeof error === "string" ? error : "Email delivery failed" }; } /** Classify an Email Service binding error as a model-visible result. */ function classifyEmailDeliveryError(error) { const failure = emailFailure(error); const inferredCode = EMAIL_ERROR_MESSAGES.get(failure.message); if (inferredCode) return { status: "failed", retryable: false, error: { ...failure, code: inferredCode } }; if (RETRYABLE_EMAIL_ERRORS.has(failure.code)) return { status: "failed", retryable: true, error: failure }; if (PERMANENT_EMAIL_ERRORS.has(failure.code)) return { status: "failed", retryable: false, error: failure }; return { status: "uncertain", error: failure }; } function emailSurface(surface) { return surface; } function mutableRecipients(value) { return Array.isArray(value) ? [...value] : value; } function replySubject(subject) { if (!subject) return void 0; return /^re:/i.test(subject) ? subject : `Re: ${subject}`; } function referenceHeader(references) { const unique = [...new Set(references.filter(Boolean))]; if (unique.length === 0) return void 0; const root = `<${unique[0]}>`; if (utf8ByteLength(root) > EMAIL_HEADER_VALUE_LIMIT) return void 0; const selected = [root]; for (let index = unique.length - 1; index > 0; index -= 1) { const candidate = `<${unique[index]}>`; if (utf8ByteLength([...selected, candidate].join(" ")) <= EMAIL_HEADER_VALUE_LIMIT) selected.splice(1, 0, candidate); } return selected.join(" "); } /** Create a configured email Channel. */ function email(options) { if (!options.binding) throw new Error("binding is required to create an email channel"); async function send(destinationValue, title, text) { const destination = emailSurface(destinationValue); const inReplyTo = destination.address.inReplyTo; const inReplyToHeader = inReplyTo ? `<${inReplyTo}>` : void 0; if (inReplyToHeader && utf8ByteLength(inReplyToHeader) > EMAIL_HEADER_VALUE_LIMIT) return { status: "failed", retryable: false, error: { code: "EMAIL_SURFACE_INVALID", message: "Email reply metadata exceeds provider header limits" } }; const references = destination.address.references; const referencesHeader = references ? referenceHeader(references) : void 0; if (references?.length && !referencesHeader) return { status: "failed", retryable: false, error: { code: "EMAIL_SURFACE_INVALID", message: "Email reply metadata exceeds provider header limits" } }; const replyHeaders = { ...inReplyToHeader && { "In-Reply-To": inReplyToHeader }, ...referencesHeader && { References: referencesHeader } }; try { return { status: "delivered", reference: (await options.binding.send({ from: destination.address.from, to: mutableRecipients(destination.address.to), subject: title ?? replySubject(destination.address.subject) ?? options.defaultTitle ?? DEFAULT_EMAIL_TITLE, text, replyTo: destination.address.replyTo, cc: destination.address.cc === void 0 ? void 0 : mutableRecipients(destination.address.cc), bcc: destination.address.bcc === void 0 ? void 0 : mutableRecipients(destination.address.bcc), headers: { ...destination.address.headers, ...replyHeaders } })).messageId }; } catch (error) { return classifyEmailDeliveryError(error); } } return { ...options.route && { route: options.route }, emailIngress: inboundEmail({ to: options.inbound?.to, from: options.inbound?.from, replyFrom: options.from }), contactSurface(identity) { if ((identity.scope ?? "default") !== "default") return null; return { version: 1, address: { from: options.from, to: identity.subject }, label: `Email · ${identity.subject}` }; }, deliver(destination, message) { return send(destination, message.title, message.markdown); }, async requestApproval(destination, { request, getApprovalLinks }) { if (!getApprovalLinks) return approvalLinksUnavailable(); let links; try { links = await getApprovalLinks(); } catch { return approvalLinksUnavailable(true); } const text = [ request.summary, `Input:\n${renderInput(request.input)}`, `Approve: ${links.approve}`, `Reject: ${links.reject}` ].join("\n\n"); return send(destination, request.title, text); } }; } //#endregion export { email, inboundEmail }; //# sourceMappingURL=email.js.map