import * as z from "zod" import { defineAction } from "../../automation/actions" import { EMAIL_BODY_SCHEMA, normalizeEmailBody } from "../../lib/email" import { getOutlookGraphApi, listOutlookGraphCollection, } from "@automate.ax/integration-contracts/outlook" import { GRAPH_ATTACHMENT_SCHEMA, GRAPH_MAIL_FOLDER_SCHEMA, GRAPH_MESSAGE_SCHEMA, GRAPH_PROFILE_SCHEMA, OUTLOOK_ATTACHMENT_SCHEMA, OUTLOOK_BODY_TYPE_SCHEMA, OUTLOOK_FLAG_STATUS_SCHEMA, OUTLOOK_IMPORTANCE_SCHEMA, OUTLOOK_MAIL_FOLDER_SCHEMA, OUTLOOK_MESSAGE_SCHEMA, OUTLOOK_PROFILE_SCHEMA, OUTLOOK_RECIPIENT_SCHEMA, OUTLOOK_RECIPIENTS_SCHEMA, } from "@automate.ax/integration-contracts/outlook" import { OUTLOOK_MAIL_METADATA_REQUIREMENT, OUTLOOK_MAIL_READ_REQUIREMENT, OUTLOOK_MAIL_SEND_REQUIREMENT, OUTLOOK_MAIL_WRITE_REQUIREMENT, } from "./lib/scopes" export * from "./extra-actions" const DEFAULT_LIMIT = 100 const MAX_LIMIT = 10_000 const GRAPH_PAGE_SIZE = 100 const MAX_DIRECT_ATTACHMENT_BYTES = 3_000_000 const MAX_ATTACHMENT_BYTES = 150_000_000 const UPLOAD_CHUNK_BYTES = 3_276_800 const MESSAGE_SELECT = [ "id", "createdDateTime", "lastModifiedDateTime", "receivedDateTime", "sentDateTime", "hasAttachments", "internetMessageId", "subject", "body", "bodyPreview", "importance", "parentFolderId", "conversationId", "isRead", "isDraft", "webLink", "inferenceClassification", "sender", "from", "toRecipients", "ccRecipients", "bccRecipients", "replyTo", "flag", "categories", ].join(",") const MESSAGE_METADATA_SELECT = [ "id", "createdDateTime", "lastModifiedDateTime", "receivedDateTime", "sentDateTime", "hasAttachments", "internetMessageId", "subject", "importance", "parentFolderId", "conversationId", "isRead", "isDraft", "webLink", "inferenceClassification", "sender", "from", "toRecipients", "ccRecipients", "bccRecipients", "replyTo", "flag", "categories", ].join(",") const LIMIT_SCHEMA = z.number().int().min(1).max(MAX_LIMIT).optional() const MESSAGE_ID_INPUT_SCHEMA = z.object({ /** Opaque Outlook message identifier. */ messageId: z.string().min(1), }) const MESSAGE_CONTENT_SCHEMA = z.object({ /** Message content. */ body: z.string(), /** Whether the content is plain text or HTML. */ bodyType: OUTLOOK_BODY_TYPE_SCHEMA.prefault("text"), }) const COMPOSE_OPTIONS_SCHEMA = z.object({ /** Files attached directly to the message, each limited to 3 MB. */ attachments: z.instanceof(File).array().optional(), /** Blind-copy recipients. */ bcc: OUTLOOK_RECIPIENTS_SCHEMA.optional(), /** Carbon-copy recipients. */ cc: OUTLOOK_RECIPIENTS_SCHEMA.optional(), /** Provider-defined importance. */ importance: OUTLOOK_IMPORTANCE_SCHEMA.optional(), /** Whether Outlook should request a delivery receipt. */ requestDeliveryReceipt: z.boolean().optional(), /** Whether Outlook should request a read receipt. */ requestReadReceipt: z.boolean().optional(), /** Addresses that should receive replies. */ replyTo: OUTLOOK_RECIPIENTS_SCHEMA.optional(), /** Message subject. */ subject: z.string(), /** One or more primary recipients. */ to: OUTLOOK_RECIPIENTS_SCHEMA, }) const COMPOSE_SCHEMA = COMPOSE_OPTIONS_SCHEMA.extend({ /** Message content. */ body: z.string(), /** Whether the content is plain text or HTML. */ bodyType: OUTLOOK_BODY_TYPE_SCHEMA.prefault("text"), }) const SEND_EMAIL_SCHEMA = COMPOSE_OPTIONS_SCHEMA.and(EMAIL_BODY_SCHEMA) /** Gets the connected Microsoft user's mail identity. */ export const getOutlookProfile = defineAction("Get Outlook profile") .describe("Gets the connected Microsoft user's mail identity.") .account("microsoft", "User.Read") .output(OUTLOOK_PROFILE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account }) => GRAPH_PROFILE_SCHEMA.parse( await getOutlookGraphApi(account.secret).request( "/me?$select=id,displayName,mail,userPrincipalName", ), ), ) /** Lists top-level or child Outlook mail folders. */ export const listOutlookMailFolders = defineAction("List Outlook mail folders") .describe("Lists Outlook mail folders and their message counts.") .account("microsoft", OUTLOOK_MAIL_METADATA_REQUIREMENT) .input( z.object({ /** Parent folder whose direct child folders should be returned. */ parentFolder: z.string().trim().min(1).optional(), /** Whether hidden folders should be included. */ includeHidden: z.boolean().optional(), /** Maximum folders returned across Graph pages. */ limit: LIMIT_SCHEMA, }), ) .output(OUTLOOK_MAIL_FOLDER_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { request } = getOutlookGraphApi(account.secret) const parentFolderId = input.parentFolder ? await resolveMailFolderId(request, input.parentFolder) : undefined const params = new URLSearchParams({ $top: String(Math.min(GRAPH_PAGE_SIZE, input.limit ?? DEFAULT_LIMIT)), includeHiddenFolders: String(input.includeHidden ?? false), }) return listOutlookGraphCollection( request, parentFolderId ? `/me/mailFolders/${encodeURIComponent(parentFolderId)}/childFolders?${params.toString()}` : `/me/mailFolders?${params.toString()}`, GRAPH_MAIL_FOLDER_SCHEMA, input.limit ?? DEFAULT_LIMIT, ) }) /** Lists message metadata without requesting bodies, previews, or attachments. */ export const listOutlookMessageMetadata = defineAction( "List Outlook message metadata", ) .describe("Lists Outlook message metadata with minimal mail access.") .account("microsoft", OUTLOOK_MAIL_METADATA_REQUIREMENT) .input( z.object({ /** OData filter expression. */ filter: z.string().min(1).optional(), /** Folder display path, ID, or well-known name such as `inbox`. */ folder: z.string().trim().min(1).optional(), /** Maximum messages returned across Graph pages. */ limit: LIMIT_SCHEMA, /** OData ordering expression. */ orderBy: z.string().min(1).optional(), }), ) .output(OUTLOOK_MESSAGE_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => listMessagesFromGraph(account.secret, input, false), ) /** Lists full Outlook messages, including bodies and previews. */ export const listOutlookMessages = defineAction("List Outlook messages") .describe("Lists full Outlook messages from a mailbox or folder.") .account("microsoft", OUTLOOK_MAIL_READ_REQUIREMENT) .input( z.object({ /** OData filter expression. */ filter: z.string().min(1).optional(), /** Folder display path, ID, or well-known name such as `inbox`. */ folder: z.string().trim().min(1).optional(), /** Maximum messages returned across Graph pages. */ limit: LIMIT_SCHEMA, /** OData ordering expression. */ orderBy: z.string().min(1).optional(), }), ) .output(OUTLOOK_MESSAGE_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => listMessagesFromGraph(account.secret, input, true), ) /** Searches Outlook mail using Microsoft Graph message search syntax. */ export const searchOutlookMessages = defineAction("Search Outlook messages") .describe("Searches Outlook mail and returns matching message metadata.") .account("microsoft", OUTLOOK_MAIL_METADATA_REQUIREMENT) .input( z.object({ /** Folder display path, ID, or well-known name to search within. */ folder: z.string().trim().min(1).optional(), /** Maximum messages returned across Graph pages. */ limit: LIMIT_SCHEMA, /** Microsoft Graph message search expression. */ query: z.string().min(1), }), ) .output(OUTLOOK_MESSAGE_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { request } = getOutlookGraphApi(account.secret) const limit = input.limit ?? DEFAULT_LIMIT return listOutlookGraphCollection( request, messageCollectionPath( input.folder ? await resolveMailFolderId(request, input.folder) : undefined, new URLSearchParams({ $search: `"${input.query.replaceAll('"', '\\"')}"`, $select: MESSAGE_METADATA_SELECT, $top: String(Math.min(GRAPH_PAGE_SIZE, limit)), }), ), GRAPH_MESSAGE_SCHEMA, limit, ) }) /** Gets one full Outlook message. */ export const getOutlookMessage = defineAction("Get Outlook message") .describe("Gets one Outlook message with its full body and metadata.") .account("microsoft", OUTLOOK_MAIL_READ_REQUIREMENT) .input(MESSAGE_ID_INPUT_SCHEMA) .output(OUTLOOK_MESSAGE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => GRAPH_MESSAGE_SCHEMA.parse( await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}?$select=${MESSAGE_SELECT}`, ), ), ) /** Sends a new Outlook email. */ export const sendOutlookEmail = defineAction("Send Outlook email") .describe("Sends a formatted Outlook email with optional small attachments.") .account("microsoft", OUTLOOK_MAIL_SEND_REQUIREMENT) .input(SEND_EMAIL_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getOutlookGraphApi(account.secret).request("/me/sendMail", { body: { message: await sendMessagePayload(input), saveToSentItems: true, }, method: "POST", }) }) /** Creates an unsent Outlook draft. */ export const draftOutlookEmail = defineAction("Draft Outlook email") .describe( "Creates a formatted Outlook draft with optional small attachments.", ) .account("microsoft", OUTLOOK_MAIL_WRITE_REQUIREMENT) .input(COMPOSE_SCHEMA) .output(OUTLOOK_MESSAGE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => GRAPH_MESSAGE_SCHEMA.parse( await getOutlookGraphApi(account.secret).request("/me/messages", { body: await messagePayload(input), method: "POST", }), ), ) /** Updates writable properties on an Outlook message or draft. */ export const updateOutlookMessage = defineAction("Update Outlook message") .describe("Updates mutable Outlook message properties.") .account("microsoft", OUTLOOK_MAIL_WRITE_REQUIREMENT) .input( MESSAGE_ID_INPUT_SCHEMA.extend({ /** Replacement blind-copy recipients for a draft. */ bcc: OUTLOOK_RECIPIENTS_SCHEMA.optional(), /** Replacement draft content. */ body: z.string().optional(), /** Content type used when replacing the draft body. */ bodyType: OUTLOOK_BODY_TYPE_SCHEMA.optional(), /** Replacement Outlook categories. */ categories: z.string().array().optional(), /** Replacement carbon-copy recipients for a draft. */ cc: OUTLOOK_RECIPIENTS_SCHEMA.optional(), /** Follow-up flag status. */ flagStatus: OUTLOOK_FLAG_STATUS_SCHEMA.optional(), /** Replacement importance. */ importance: OUTLOOK_IMPORTANCE_SCHEMA.optional(), /** Replacement read state. */ isRead: z.boolean().optional(), /** Replacement draft subject. */ subject: z.string().optional(), /** Replacement primary recipients for a draft. */ to: OUTLOOK_RECIPIENTS_SCHEMA.optional(), }), ) .output(OUTLOOK_MESSAGE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { bcc, body, bodyType, categories, cc, flagStatus, importance, isRead, messageId, subject, to, } = input return GRAPH_MESSAGE_SCHEMA.parse( await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(messageId)}`, { body: { ...(bcc && { bccRecipients: recipientsPayload(bcc) }), ...(body !== undefined && { body: { content: body, contentType: graphBodyType(bodyType ?? "text"), }, }), ...(categories && { categories }), ...(cc && { ccRecipients: recipientsPayload(cc) }), ...(flagStatus && { flag: { flagStatus } }), ...(importance && { importance }), ...(isRead !== undefined && { isRead }), ...(subject !== undefined && { subject }), ...(to && { toRecipients: recipientsPayload(to) }), }, method: "PATCH", }, ), ) }) /** Marks one Outlook message as read. */ export const markOutlookMessageAsRead = defineReadStateAction( "Mark Outlook message as read", true, ) /** Marks one Outlook message as unread. */ export const markOutlookMessageAsUnread = defineReadStateAction( "Mark Outlook message as unread", false, ) /** Flags one Outlook message for follow-up. */ export const flagOutlookMessage = defineFlagStateAction( "Flag Outlook message", "flagged", ) /** Marks one Outlook message's follow-up flag complete. */ export const completeOutlookMessageFlag = defineFlagStateAction( "Complete Outlook message flag", "complete", ) /** Clears the follow-up flag from one Outlook message. */ export const clearOutlookMessageFlag = defineFlagStateAction( "Clear Outlook message flag", "notFlagged", ) /** Replaces the Outlook categories assigned to one message. */ export const setOutlookMessageCategories = defineAction( "Set Outlook message categories", ) .describe("Replaces the Outlook categories assigned to one message.") .account("microsoft", OUTLOOK_MAIL_WRITE_REQUIREMENT) .input( MESSAGE_ID_INPUT_SCHEMA.extend({ /** Complete replacement category list; an empty list clears categories. */ categories: z.string().array(), }), ) .output(OUTLOOK_MESSAGE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => GRAPH_MESSAGE_SCHEMA.parse( await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}`, { body: { categories: input.categories }, method: "PATCH" }, ), ), ) /** Sends an existing Outlook draft. */ export const sendOutlookDraft = defineAction("Send Outlook draft") .describe("Sends an existing Outlook draft.") .account("microsoft", OUTLOOK_MAIL_SEND_REQUIREMENT) .input(MESSAGE_ID_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}/send`, { method: "POST" }, ) }) /** Deletes an Outlook message. */ export const deleteOutlookMessage = defineAction("Delete Outlook message") .describe("Deletes an Outlook message from its current folder.") .account("microsoft", OUTLOOK_MAIL_WRITE_REQUIREMENT) .input(MESSAGE_ID_INPUT_SCHEMA) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}`, { method: "DELETE" }, ) }) /** Moves an Outlook message to another folder. */ export const moveOutlookMessage = defineMessageRelocationAction( "Move Outlook message", "move", ) /** Copies an Outlook message to another folder. */ export const copyOutlookMessage = defineMessageRelocationAction( "Copy Outlook message", "copy", ) /** Replies to the sender of an Outlook message. */ export const replyToOutlookMessage = defineMessageResponseAction( "Reply to Outlook message", "reply", ) /** Replies to all recipients of an Outlook message. */ export const replyAllToOutlookMessage = defineMessageResponseAction( "Reply all to Outlook message", "replyAll", ) /** Forwards an Outlook message to new recipients. */ export const forwardOutlookMessage = defineAction("Forward Outlook message") .describe("Forwards an Outlook message to one or more recipients.") .account("microsoft", OUTLOOK_MAIL_SEND_REQUIREMENT) .input( MESSAGE_ID_INPUT_SCHEMA.extend({ ...MESSAGE_CONTENT_SCHEMA.shape, /** Forward recipients. */ to: OUTLOOK_RECIPIENTS_SCHEMA, }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}/forward`, { body: { message: { body: { content: input.body, contentType: graphBodyType(input.bodyType), }, toRecipients: recipientsPayload(input.to), }, }, method: "POST", }, ) }) /** Creates a reply draft that can be edited or given attachments before send. */ export const draftOutlookReply = defineResponseDraftAction( "Draft Outlook reply", "createReply", ) /** Creates a reply-all draft for later editing and sending. */ export const draftOutlookReplyAll = defineResponseDraftAction( "Draft Outlook reply all", "createReplyAll", ) /** Creates a forward draft for later editing and sending. */ export const draftOutlookForward = defineAction("Draft Outlook forward") .describe("Creates a forward draft for later editing and sending.") .account("microsoft", OUTLOOK_MAIL_WRITE_REQUIREMENT) .input( MESSAGE_ID_INPUT_SCHEMA.extend({ /** Optional comment placed above the forwarded message. */ comment: z.string().optional(), /** Optional forward recipients. */ to: OUTLOOK_RECIPIENTS_SCHEMA.optional(), }), ) .output(OUTLOOK_MESSAGE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => GRAPH_MESSAGE_SCHEMA.parse( await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}/createForward`, { body: { ...(input.comment !== undefined && { comment: input.comment }), ...(input.to && { message: { toRecipients: recipientsPayload(input.to) }, }), }, method: "POST", }, ), ), ) /** Lists attachments on one Outlook message. */ export const listOutlookAttachments = defineAction("List Outlook attachments") .describe("Lists attachment metadata without downloading file contents.") .account("microsoft", OUTLOOK_MAIL_READ_REQUIREMENT) .input( MESSAGE_ID_INPUT_SCHEMA.extend({ /** Maximum attachments returned across Graph pages. */ limit: LIMIT_SCHEMA, }), ) .output(OUTLOOK_ATTACHMENT_SCHEMA.array()) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { request } = getOutlookGraphApi(account.secret) const limit = input.limit ?? DEFAULT_LIMIT return listOutlookGraphCollection( request, `/me/messages/${encodeURIComponent(input.messageId)}/attachments?$select=id,name,size,isInline,contentType&$top=${Math.min(GRAPH_PAGE_SIZE, limit)}`, GRAPH_ATTACHMENT_SCHEMA, limit, ) }) /** Gets one Outlook attachment, including file contents when applicable. */ export const getOutlookAttachment = defineAction("Get Outlook attachment") .describe("Gets one Outlook attachment and decodes file contents.") .account("microsoft", OUTLOOK_MAIL_READ_REQUIREMENT) .input( MESSAGE_ID_INPUT_SCHEMA.extend({ /** Opaque Outlook attachment identifier. */ attachmentId: z.string().min(1), }), ) .output(OUTLOOK_ATTACHMENT_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => GRAPH_ATTACHMENT_SCHEMA.parse( await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}/attachments/${encodeURIComponent(input.attachmentId)}`, ), ), ) /** Adds one file to an Outlook draft. */ export const addOutlookAttachment = defineAction("Add Outlook attachment") .describe("Adds one file up to 150 MB to an Outlook draft.") .account("microsoft", OUTLOOK_MAIL_WRITE_REQUIREMENT) .input( MESSAGE_ID_INPUT_SCHEMA.extend({ /** Content-ID used to reference an inline attachment from HTML. */ contentId: z.string().min(1).optional(), /** File to attach. */ file: z.instanceof(File), /** Whether the file is an inline attachment. */ inline: z.boolean().optional(), }), ) .output(OUTLOOK_ATTACHMENT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { assertAttachmentSize(input.file) if (input.file.size > MAX_DIRECT_ATTACHMENT_BYTES) { return uploadOutlookAttachment(account.secret, input) } return GRAPH_ATTACHMENT_SCHEMA.parse( await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}/attachments`, { body: await attachmentPayload( input.file, input.inline, input.contentId, ), method: "POST", }, ), ) }) /** Deletes an attachment from an Outlook draft. */ export const deleteOutlookAttachment = defineAction("Delete Outlook attachment") .describe("Deletes an attachment from an Outlook draft.") .account("microsoft", OUTLOOK_MAIL_WRITE_REQUIREMENT) .input( MESSAGE_ID_INPUT_SCHEMA.extend({ /** Opaque Outlook attachment identifier. */ attachmentId: z.string().min(1), }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}/attachments/${encodeURIComponent(input.attachmentId)}`, { method: "DELETE" }, ) }) /** * Defines one fixed Outlook read-state mutation. * * @param name - Action display name. * @param isRead - Read state to apply. */ function defineReadStateAction(name: string, isRead: boolean) { return defineAction(name) .describe(`Marks one Outlook message as ${isRead ? "read" : "unread"}.`) .account("microsoft", OUTLOOK_MAIL_WRITE_REQUIREMENT) .input(MESSAGE_ID_INPUT_SCHEMA) .output(OUTLOOK_MESSAGE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => GRAPH_MESSAGE_SCHEMA.parse( await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}`, { body: { isRead }, method: "PATCH" }, ), ), ) } /** * Defines one fixed Outlook follow-up flag mutation. * * @param name - Action display name. * @param flagStatus - Follow-up flag state to apply. */ function defineFlagStateAction( name: string, flagStatus: "complete" | "flagged" | "notFlagged", ) { return defineAction(name) .describe( flagStatus === "flagged" ? "Flags one Outlook message for follow-up." : flagStatus === "complete" ? "Marks one Outlook message's follow-up flag complete." : "Clears the follow-up flag from one Outlook message.", ) .account("microsoft", OUTLOOK_MAIL_WRITE_REQUIREMENT) .input(MESSAGE_ID_INPUT_SCHEMA) .output(OUTLOOK_MESSAGE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => GRAPH_MESSAGE_SCHEMA.parse( await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}`, { body: { flag: { flagStatus } }, method: "PATCH" }, ), ), ) } /** * Defines a message copy or move action. * * @param name - Action display name. * @param operation - Graph relocation operation. */ function defineMessageRelocationAction( name: string, operation: "copy" | "move", ) { return defineAction(name) .describe( `${operation === "move" ? "Moves" : "Copies"} one Outlook message to another folder.`, ) .account("microsoft", OUTLOOK_MAIL_WRITE_REQUIREMENT) .input( MESSAGE_ID_INPUT_SCHEMA.extend({ /** Destination folder display path, ID, or well-known name. */ destinationFolder: z.string().trim().min(1), }), ) .output(OUTLOOK_MESSAGE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { request } = getOutlookGraphApi(account.secret) return GRAPH_MESSAGE_SCHEMA.parse( await request( `/me/messages/${encodeURIComponent(input.messageId)}/${operation}`, { body: { destinationId: await resolveMailFolderId( request, input.destinationFolder, ), }, method: "POST", }, ), ) }) } /** * Defines an immediate reply action. * * @param name - Action display name. * @param operation - Graph reply operation. */ function defineMessageResponseAction( name: string, operation: "reply" | "replyAll", ) { return defineAction(name) .describe( `${operation === "replyAll" ? "Replies to all recipients of" : "Replies to the sender of"} an Outlook message.`, ) .account("microsoft", OUTLOOK_MAIL_SEND_REQUIREMENT) .input(MESSAGE_ID_INPUT_SCHEMA.extend(MESSAGE_CONTENT_SCHEMA.shape)) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}/${operation}`, { body: { message: { body: { content: input.body, contentType: graphBodyType(input.bodyType), }, }, }, method: "POST", }, ) }) } /** * Defines a reply-draft action. * * @param name - Action display name. * @param operation - Graph draft operation. */ function defineResponseDraftAction( name: string, operation: "createReply" | "createReplyAll", ) { return defineAction(name) .describe("Creates an Outlook reply draft for later editing and sending.") .account("microsoft", OUTLOOK_MAIL_WRITE_REQUIREMENT) .input( MESSAGE_ID_INPUT_SCHEMA.extend({ /** Optional content placed above the quoted message. */ body: z.string().optional(), /** Whether the optional content is plain text or HTML. */ bodyType: OUTLOOK_BODY_TYPE_SCHEMA.optional(), }), ) .output(OUTLOOK_MESSAGE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => GRAPH_MESSAGE_SCHEMA.parse( await getOutlookGraphApi(account.secret).request( `/me/messages/${encodeURIComponent(input.messageId)}/${operation}`, { body: input.body === undefined ? {} : { message: { body: { content: input.body, contentType: graphBodyType(input.bodyType ?? "text"), }, }, }, method: "POST", }, ), ), ) } /** * Resolves an exact Outlook folder display path, ID, or well-known name. * * Unknown references are returned unchanged so provider-defined well-known * names and otherwise unlisted IDs remain usable. * * @param request - Authenticated Microsoft Graph request function. * @param reference - Folder display path, ID, or well-known name. */ async function resolveMailFolderId( request: ReturnType["request"], reference: string, ) { const folders = await listMailFolderReferences(request) const idMatch = folders.find(({ folderId }) => folderId === reference) if (idMatch) return idMatch.folderId const normalizedReference = reference.trim().toLowerCase() const pathMatches = folders.filter( ({ path }) => path.toLowerCase() === normalizedReference, ) if (pathMatches.length === 1) return pathMatches[0]!.folderId const nameMatches = folders.filter( ({ displayName }) => displayName.trim().toLowerCase() === normalizedReference, ) if (nameMatches.length === 1) return nameMatches[0]!.folderId const matches = pathMatches.length > 1 ? pathMatches : nameMatches if (matches.length > 1) { throw new Error( `Outlook folder "${reference}" is ambiguous; matching paths: ${matches.map(({ path }) => path).join(", ")}.`, ) } return reference } /** * Lists the complete Outlook folder hierarchy with human-readable paths. * * @param request - Authenticated Microsoft Graph request function. * @param parent - Optional parent folder path and ID. * @param parent.folderId - Parent Outlook folder ID. * @param parent.path - Human-readable parent folder path. */ async function listMailFolderReferences( request: ReturnType["request"], parent?: { folderId: string; path: string }, ): Promise<{ displayName: string; folderId: string; path: string }[]> { const folders = await listOutlookGraphCollection( request, parent ? `/me/mailFolders/${encodeURIComponent(parent.folderId)}/childFolders?$top=${GRAPH_PAGE_SIZE}` : `/me/mailFolders?$top=${GRAPH_PAGE_SIZE}`, GRAPH_MAIL_FOLDER_SCHEMA, MAX_LIMIT, ) return [ ...folders.map(({ displayName, folderId }) => ({ displayName, folderId, path: parent ? `${parent.path}/${displayName}` : displayName, })), ...( await Promise.all( folders .filter(({ childFolderCount }) => childFolderCount > 0) .map(({ displayName, folderId }) => listMailFolderReferences(request, { folderId, path: parent ? `${parent.path}/${displayName}` : displayName, }), ), ) ).flat(), ] } /** * Lists and validates Outlook messages. * * @param secret - Resolved Microsoft secret. * @param input - Folder, filtering, ordering, and limit options. * @param input.filter - Optional OData filter. * @param input.folder - Optional mail folder display path, ID, or well-known * name. * @param input.limit - Optional maximum result count. * @param input.orderBy - Optional OData ordering. * @param includeBody - Whether to request message body fields. */ async function listMessagesFromGraph( secret: Record, input: { filter?: string folder?: string limit?: number orderBy?: string }, includeBody: boolean, ) { const { request } = getOutlookGraphApi(secret) const limit = input.limit ?? DEFAULT_LIMIT return listOutlookGraphCollection( request, messageCollectionPath( input.folder ? await resolveMailFolderId(request, input.folder) : undefined, new URLSearchParams({ $select: includeBody ? MESSAGE_SELECT : MESSAGE_METADATA_SELECT, $top: String(Math.min(GRAPH_PAGE_SIZE, limit)), ...(input.filter && { $filter: input.filter }), ...(input.orderBy && { $orderby: input.orderBy }), }), ), GRAPH_MESSAGE_SCHEMA, limit, ) } /** * Builds a mailbox or folder message-collection path. * * @param folderId - Optional folder identifier. * @param params - OData query parameters. */ function messageCollectionPath( folderId: string | undefined, params: URLSearchParams, ) { return folderId ? `/me/mailFolders/${encodeURIComponent(folderId)}/messages?${params.toString()}` : `/me/messages?${params.toString()}` } /** * Converts send-email input to a Graph message payload. * * Graph accepts one body representation, so HTML and Markdown select HTML; * plain text remains text when it is the only supplied format. * * @param input - Parsed send-email input. */ async function sendMessagePayload( input: z.output, ): Promise> { const normalizedBody = normalizeEmailBody(input) const useHtml = input.html !== undefined || input.markdown !== undefined return await messagePayload({ ...input, body: useHtml ? normalizedBody.html : normalizedBody.text, bodyType: useHtml ? "html" : "text", }) } /** * Converts compose input to a Graph message payload. * * @param input - Parsed compose input. */ async function messagePayload( input: z.output, ): Promise> { input.attachments?.forEach(assertDirectAttachmentSize) return { attachments: await Promise.all( (input.attachments ?? []).map((file) => attachmentPayload(file)), ), bccRecipients: input.bcc ? recipientsPayload(input.bcc) : [], body: { content: input.body, contentType: graphBodyType(input.bodyType), }, ccRecipients: input.cc ? recipientsPayload(input.cc) : [], ...(input.importance && { importance: input.importance }), ...(input.requestDeliveryReceipt !== undefined && { isDeliveryReceiptRequested: input.requestDeliveryReceipt, }), ...(input.requestReadReceipt !== undefined && { isReadReceiptRequested: input.requestReadReceipt, }), ...(input.replyTo && { replyTo: recipientsPayload(input.replyTo) }), subject: input.subject, toRecipients: recipientsPayload(input.to), } } /** * Converts normalized recipients to Graph recipient objects. * * @param recipients - One or more email recipients. */ function recipientsPayload( recipients: | z.output | z.output[], ) { return (Array.isArray(recipients) ? recipients : [recipients]).map( (recipient) => ({ emailAddress: typeof recipient === "string" ? { address: recipient } : { address: recipient.address, ...(recipient.name && { name: recipient.name }), }, }), ) } /** * Converts a File to a Graph fileAttachment. * * @param file - File contents and metadata. * @param inline - Whether the file is displayed inline. * @param contentId - Optional HTML Content-ID. */ async function attachmentPayload( file: File, inline = false, contentId?: string, ) { return { "@odata.type": "#microsoft.graph.fileAttachment", contentBytes: Buffer.from(await file.arrayBuffer()).toString("base64"), contentType: file.type || "application/octet-stream", ...(contentId && { contentId }), isInline: inline, name: file.name, } } /** * Uploads one large Outlook attachment through a resumable session. * * @param secret - Microsoft account secret. * @param input - Message, file, and inline attachment fields. * @param input.contentId - Optional HTML Content-ID. * @param input.file - File contents and metadata. * @param input.inline - Whether the file is displayed inline. * @param input.messageId - Opaque Outlook message ID. */ async function uploadOutlookAttachment( secret: Record, input: { contentId?: string file: File inline?: boolean messageId: string }, ) { const { request } = getOutlookGraphApi(secret) const session = z.object({ uploadUrl: z.url() }).parse( await request( `/me/messages/${encodeURIComponent(input.messageId)}/attachments/createUploadSession`, { body: { AttachmentItem: { attachmentType: "file", ...(input.contentId && { contentId: input.contentId }), contentType: input.file.type || "application/octet-stream", isInline: input.inline ?? false, name: input.file.name, size: input.file.size, }, }, method: "POST", }, ), ) const bytes = new Uint8Array(await input.file.arrayBuffer()) let attachmentId: string | undefined for (let start = 0; start < input.file.size; start += UPLOAD_CHUNK_BYTES) { const end = Math.min(start + UPLOAD_CHUNK_BYTES, input.file.size) const response = await fetch(session.uploadUrl, { body: bytes.slice(start, end), headers: { "Content-Length": String(end - start), "Content-Range": `bytes ${start}-${end - 1}/${input.file.size}`, "Content-Type": "application/octet-stream", }, method: "PUT", }) if (!response.ok) throw new Error( `Microsoft Graph attachment upload failed with ${response.status} ${response.statusText}.`, ) if (response.status === 201) { attachmentId = outlookAttachmentIdFromLocation( response.headers.get("Location"), ) break } } if (!attachmentId) { throw new Error( "Microsoft Graph attachment upload completed without an attachment location.", ) } return GRAPH_ATTACHMENT_SCHEMA.parse( await request( `/me/messages/${encodeURIComponent(input.messageId)}/attachments/${encodeURIComponent(attachmentId)}`, ), ) } /** * Extracts the attachment ID from Graph's final upload Location header. * * @param location - Absolute Outlook attachment URL. * @throws When Graph omits or malforms the attachment location. */ function outlookAttachmentIdFromLocation(location: string | null) { if (!location) { throw new Error( "Microsoft Graph attachment upload omitted the attachment location.", ) } const match = /\/Attachments\('([^']+)'\)\/?$/i.exec( decodeURIComponent(new URL(location).pathname), ) if (!match?.[1]) { throw new Error( "Microsoft Graph attachment upload returned an invalid attachment location.", ) } return match[1] } /** * Enforces Graph's maximum attachment size. * * @param file - File to validate. * @throws When the file exceeds Graph's upload-session limit. */ function assertAttachmentSize(file: File) { if (file.size > MAX_ATTACHMENT_BYTES) { throw new Error( `Outlook attachments must be 150 MB or smaller; ${file.name} is ${file.size} bytes.`, ) } } /** * Enforces Graph's direct-attachment size limit. * * @param file - File to validate. * @throws When the file exceeds Graph's direct-attachment limit. */ function assertDirectAttachmentSize(file: File) { if (file.size > MAX_DIRECT_ATTACHMENT_BYTES) { throw new Error( `Outlook direct attachments must be 3 MB or smaller; ${file.name} is ${file.size} bytes.`, ) } } /** * Converts normalized body types to Graph casing. * * @param bodyType - Normalized body type. */ function graphBodyType(bodyType: z.output) { return bodyType === "html" ? "HTML" : "Text" }