import escapeHtml from "escape-html" import { convert } from "html-to-text" import type { Address } from "postal-mime" import * as z from "zod" import { defineAction } from "../../../automation/actions" import { EMAIL_ORIGIN_TOKEN } from "../../../lib/email-origin" import { buildRawMessage, formatAddress, getGmailApi, parseRawMessage, toMessageMetadata, } from "@automate.ax/integration-contracts/gmail" import { GMAIL_SEND_AND_READ_REQUIREMENT } from "../lib/scopes" import { ATTACHMENT_INPUT_SCHEMA, COMPOSE_OPTIONS_SCHEMA, MESSAGE_METADATA_SCHEMA, RECIPIENTS_SCHEMA, } from "@automate.ax/integration-contracts/gmail" /** * Forwards a Gmail message with optional introductory content. * * Original attachments are preserved by default. Text and HTML introductions * receive the same automatic plain-text fallback as newly composed messages. */ export const forwardGmailMessage = defineAction("Forward Gmail message") .describe("Forwards a Gmail message with optional introductory content.") .account("google", GMAIL_SEND_AND_READ_REQUIREMENT) .input( COMPOSE_OPTIONS_SCHEMA.extend({ /** Optional HTML introduction inserted before the forwarded message. */ html: z.string().optional(), /** Whether to preserve the original message's attachments. */ includeAttachments: z.boolean().optional(), /** Immutable Gmail ID of the message to forward. */ messageId: z.string().min(1), /** * Optional subject override; defaults to the original subject prefixed * with `Fwd:`. */ subject: z.string().optional(), /** Optional plain-text introduction inserted before the forwarded message. */ text: z.string().optional(), /** One or more primary recipients. */ to: RECIPIENTS_SCHEMA, }), ) .output(MESSAGE_METADATA_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input, runtime }) => { const gmailApi = getGmailApi(account.secret) const { data: original } = await gmailApi.users.messages.get({ format: "raw", id: input.messageId, userId: "me", }) if (!original.raw) throw new Error("Gmail did not return the raw message.") const parsed = await parseRawMessage(original.raw) const originalSubject = parsed.subject ?? "" const forwardedHeaders = [ "---------- Forwarded message ---------", `From: ${formatAddresses(parsed.from)}`, `Date: ${parsed.date ?? ""}`, `Subject: ${originalSubject}`, `To: ${formatAddresses(parsed.to)}`, ] const originalText = parsed.text ?? (parsed.html === undefined ? undefined : convert(parsed.html, { wordwrap: false })) const introHtml = input.html ?? (input.text === undefined ? undefined : textToHtml(input.text)) const originalHtml = parsed.html ?? (parsed.text === undefined ? undefined : textToHtml(parsed.text)) // Keep the generated MIME alternatives named so their fallback behavior is readable. const html = introHtml || originalHtml ? [ introHtml, `
${forwardedHeaders.map(escapeHtml).join("
")}
`, originalHtml, ] .filter(Boolean) .join("

") : undefined // See `html` above; this is the matching plain-text MIME alternative. const text = input.text !== undefined || originalText !== undefined ? [input.text, forwardedHeaders.join("\r\n"), originalText] .filter((part) => part !== undefined) .join("\r\n\r\n") : undefined const forwardedAttachments: z.input[] = input.includeAttachments === false ? [] : parsed.attachments.map((attachment) => ({ contentId: attachment.contentId, disposition: attachment.disposition ?? undefined, file: new File( [ attachment.content instanceof ArrayBuffer ? new Uint8Array(attachment.content) : typeof attachment.content === "string" ? attachment.content : Uint8Array.from(attachment.content), ], attachment.filename || "attachment", { type: attachment.mimeType }, ), })) return toMessageMetadata( ( await gmailApi.users.messages.send({ requestBody: { raw: await buildRawMessage({ automationOriginToken: runtime[EMAIL_ORIGIN_TOKEN], attachments: [ ...(input.attachments ?? []), ...forwardedAttachments, ], bcc: input.bcc, cc: input.cc, from: input.from, headers: input.headers, html, priority: input.priority, replyTo: input.replyTo, subject: input.subject ?? (originalSubject.startsWith("Fwd:") ? originalSubject : `Fwd: ${originalSubject}`), text, to: input.to, }), }, userId: "me", }) ).data, "forward", ) }) /** * Converts plain text to safe HTML for a multipart forwarded message. * * @param text - Plain-text content. */ function textToHtml(text: string) { return escapeHtml(text).replaceAll(/\r?\n/g, "
") } /** * Formats PostalMime addresses for a forwarded-message preamble. * * @param addresses - One address or an address list. */ function formatAddresses(addresses: Address | Address[] | undefined) { return (Array.isArray(addresses) ? addresses : addresses ? [addresses] : []) .map(formatAddress) .join(", ") }