agents
Version:
A home for your AI agents
323 lines (322 loc) • 11.3 kB
JavaScript
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