mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
improvement(forwarding+excel): added forwarding and improve excel read (#1136)
* added forwarding for outlook * lint * improved excel sheet read * addressed greptile * fixed bodytext getting truncated * fixed any type * added html func --------- Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
This commit is contained in:
@@ -22,6 +22,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
|||||||
{ label: 'Send Email', id: 'send_outlook' },
|
{ label: 'Send Email', id: 'send_outlook' },
|
||||||
{ label: 'Draft Email', id: 'draft_outlook' },
|
{ label: 'Draft Email', id: 'draft_outlook' },
|
||||||
{ label: 'Read Email', id: 'read_outlook' },
|
{ label: 'Read Email', id: 'read_outlook' },
|
||||||
|
{ label: 'Forward Email', id: 'forward_outlook' },
|
||||||
],
|
],
|
||||||
value: () => 'send_outlook',
|
value: () => 'send_outlook',
|
||||||
},
|
},
|
||||||
@@ -51,9 +52,30 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
|||||||
type: 'short-input',
|
type: 'short-input',
|
||||||
layout: 'full',
|
layout: 'full',
|
||||||
placeholder: 'Recipient email address',
|
placeholder: 'Recipient email address',
|
||||||
condition: { field: 'operation', value: ['send_outlook', 'draft_outlook'] },
|
condition: {
|
||||||
|
field: 'operation',
|
||||||
|
value: ['send_outlook', 'draft_outlook', 'forward_outlook'],
|
||||||
|
},
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'messageId',
|
||||||
|
title: 'Message ID',
|
||||||
|
type: 'short-input',
|
||||||
|
layout: 'full',
|
||||||
|
placeholder: 'Message ID to forward',
|
||||||
|
condition: { field: 'operation', value: ['forward_outlook'] },
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'comment',
|
||||||
|
title: 'Comment',
|
||||||
|
type: 'long-input',
|
||||||
|
layout: 'full',
|
||||||
|
placeholder: 'Optional comment to include when forwarding',
|
||||||
|
condition: { field: 'operation', value: ['forward_outlook'] },
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'subject',
|
id: 'subject',
|
||||||
title: 'Subject',
|
title: 'Subject',
|
||||||
@@ -157,7 +179,7 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
tools: {
|
tools: {
|
||||||
access: ['outlook_send', 'outlook_draft', 'outlook_read'],
|
access: ['outlook_send', 'outlook_draft', 'outlook_read', 'outlook_forward'],
|
||||||
config: {
|
config: {
|
||||||
tool: (params) => {
|
tool: (params) => {
|
||||||
switch (params.operation) {
|
switch (params.operation) {
|
||||||
@@ -167,6 +189,8 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
|||||||
return 'outlook_read'
|
return 'outlook_read'
|
||||||
case 'draft_outlook':
|
case 'draft_outlook':
|
||||||
return 'outlook_draft'
|
return 'outlook_draft'
|
||||||
|
case 'forward_outlook':
|
||||||
|
return 'outlook_forward'
|
||||||
default:
|
default:
|
||||||
throw new Error(`Invalid Outlook operation: ${params.operation}`)
|
throw new Error(`Invalid Outlook operation: ${params.operation}`)
|
||||||
}
|
}
|
||||||
@@ -197,6 +221,9 @@ export const OutlookBlock: BlockConfig<OutlookResponse> = {
|
|||||||
to: { type: 'string', description: 'Recipient email address' },
|
to: { type: 'string', description: 'Recipient email address' },
|
||||||
subject: { type: 'string', description: 'Email subject' },
|
subject: { type: 'string', description: 'Email subject' },
|
||||||
body: { type: 'string', description: 'Email content' },
|
body: { type: 'string', description: 'Email content' },
|
||||||
|
// Forward operation inputs
|
||||||
|
messageId: { type: 'string', description: 'Message ID to forward' },
|
||||||
|
comment: { type: 'string', description: 'Optional comment for forwarding' },
|
||||||
// Read operation inputs
|
// Read operation inputs
|
||||||
folder: { type: 'string', description: 'Email folder' },
|
folder: { type: 'string', description: 'Email folder' },
|
||||||
manualFolder: { type: 'string', description: 'Manual folder name' },
|
manualFolder: { type: 'string', description: 'Manual folder name' },
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
|
import { htmlToText } from 'html-to-text'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import { createLogger } from '@/lib/logs/console/logger'
|
import { createLogger } from '@/lib/logs/console/logger'
|
||||||
import { hasProcessedMessage, markMessageAsProcessed } from '@/lib/redis'
|
import { hasProcessedMessage, markMessageAsProcessed } from '@/lib/redis'
|
||||||
@@ -79,6 +80,24 @@ export interface OutlookWebhookPayload {
|
|||||||
rawEmail?: OutlookEmail // Only included when includeRawEmail is true
|
rawEmail?: OutlookEmail // Only included when includeRawEmail is true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert HTML content to a readable plain-text representation.
|
||||||
|
* Keeps reasonable newlines and decodes common HTML entities.
|
||||||
|
*/
|
||||||
|
function convertHtmlToPlainText(html: string): string {
|
||||||
|
if (!html) return ''
|
||||||
|
return htmlToText(html, {
|
||||||
|
wordwrap: false,
|
||||||
|
selectors: [
|
||||||
|
{ selector: 'a', options: { hideLinkHrefIfSameAsText: true, noAnchorUrl: true } },
|
||||||
|
{ selector: 'img', format: 'skip' },
|
||||||
|
{ selector: 'script', format: 'skip' },
|
||||||
|
{ selector: 'style', format: 'skip' },
|
||||||
|
],
|
||||||
|
preserveNewlines: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function pollOutlookWebhooks() {
|
export async function pollOutlookWebhooks() {
|
||||||
logger.info('Starting Outlook webhook polling')
|
logger.info('Starting Outlook webhook polling')
|
||||||
|
|
||||||
@@ -357,7 +376,18 @@ async function processOutlookEmails(
|
|||||||
to: email.toRecipients?.map((r) => r.emailAddress.address).join(', ') || '',
|
to: email.toRecipients?.map((r) => r.emailAddress.address).join(', ') || '',
|
||||||
cc: email.ccRecipients?.map((r) => r.emailAddress.address).join(', ') || '',
|
cc: email.ccRecipients?.map((r) => r.emailAddress.address).join(', ') || '',
|
||||||
date: email.receivedDateTime,
|
date: email.receivedDateTime,
|
||||||
bodyText: email.bodyPreview || '',
|
bodyText: (() => {
|
||||||
|
const content = email.body?.content || ''
|
||||||
|
const type = (email.body?.contentType || '').toLowerCase()
|
||||||
|
if (!content) {
|
||||||
|
return email.bodyPreview || ''
|
||||||
|
}
|
||||||
|
if (type === 'text' || type === 'text/plain') {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
// Default to converting HTML or unknown types
|
||||||
|
return convertHtmlToPlainText(content)
|
||||||
|
})(),
|
||||||
bodyHtml: email.body?.content || '',
|
bodyHtml: email.body?.content || '',
|
||||||
hasAttachments: email.hasAttachments,
|
hasAttachments: email.hasAttachments,
|
||||||
isRead: email.isRead,
|
isRead: email.isRead,
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
"fuse.js": "7.1.0",
|
"fuse.js": "7.1.0",
|
||||||
"geist": "1.4.2",
|
"geist": "1.4.2",
|
||||||
"groq-sdk": "^0.15.0",
|
"groq-sdk": "^0.15.0",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"jose": "6.0.11",
|
"jose": "6.0.11",
|
||||||
@@ -133,6 +134,7 @@
|
|||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@trigger.dev/build": "4.0.0",
|
"@trigger.dev/build": "4.0.0",
|
||||||
|
"@types/html-to-text": "^9.0.4",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsdom": "21.1.7",
|
"@types/jsdom": "21.1.7",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
|
|||||||
@@ -12,6 +12,61 @@ import {
|
|||||||
|
|
||||||
const logger = createLogger('Tools')
|
const logger = createLogger('Tools')
|
||||||
|
|
||||||
|
// Extract a concise, meaningful error message from diverse API error shapes
|
||||||
|
function getDeepApiErrorMessage(errorInfo?: {
|
||||||
|
status?: number
|
||||||
|
statusText?: string
|
||||||
|
data?: any
|
||||||
|
}): string {
|
||||||
|
return (
|
||||||
|
// GraphQL errors (Linear API)
|
||||||
|
errorInfo?.data?.errors?.[0]?.message ||
|
||||||
|
// X/Twitter API specific pattern
|
||||||
|
errorInfo?.data?.errors?.[0]?.detail ||
|
||||||
|
// Generic details array
|
||||||
|
errorInfo?.data?.details?.[0]?.message ||
|
||||||
|
// Hunter API pattern
|
||||||
|
errorInfo?.data?.errors?.[0]?.details ||
|
||||||
|
// Direct errors array (when errors[0] is a string or simple object)
|
||||||
|
(Array.isArray(errorInfo?.data?.errors)
|
||||||
|
? typeof errorInfo.data.errors[0] === 'string'
|
||||||
|
? errorInfo.data.errors[0]
|
||||||
|
: errorInfo.data.errors[0]?.message
|
||||||
|
: undefined) ||
|
||||||
|
// Notion/Discord/GitHub/Twilio pattern
|
||||||
|
errorInfo?.data?.message ||
|
||||||
|
// SOAP/XML fault patterns
|
||||||
|
errorInfo?.data?.fault?.faultstring ||
|
||||||
|
errorInfo?.data?.faultstring ||
|
||||||
|
// Microsoft/OAuth error descriptions
|
||||||
|
errorInfo?.data?.error_description ||
|
||||||
|
// Airtable/Google fallback pattern
|
||||||
|
(typeof errorInfo?.data?.error === 'object'
|
||||||
|
? errorInfo?.data?.error?.message || JSON.stringify(errorInfo?.data?.error)
|
||||||
|
: errorInfo?.data?.error) ||
|
||||||
|
// HTTP status text fallback
|
||||||
|
errorInfo?.statusText ||
|
||||||
|
// Final fallback
|
||||||
|
`Request failed with status ${errorInfo?.status || 'unknown'}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an Error instance from errorInfo and attach useful context
|
||||||
|
function createTransformedErrorFromErrorInfo(errorInfo?: {
|
||||||
|
status?: number
|
||||||
|
statusText?: string
|
||||||
|
data?: any
|
||||||
|
}): Error {
|
||||||
|
const message = getDeepApiErrorMessage(errorInfo)
|
||||||
|
const transformed = new Error(message)
|
||||||
|
Object.assign(transformed, {
|
||||||
|
status: errorInfo?.status,
|
||||||
|
statusText: errorInfo?.statusText,
|
||||||
|
data: errorInfo?.data,
|
||||||
|
})
|
||||||
|
return transformed
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process file outputs for a tool result if execution context is available
|
* Process file outputs for a tool result if execution context is available
|
||||||
* Uses dynamic imports to avoid client-side bundling issues
|
* Uses dynamic imports to avoid client-side bundling issues
|
||||||
@@ -410,15 +465,46 @@ async function handleInternalRequest(
|
|||||||
|
|
||||||
const response = await fetch(fullUrl, requestOptions)
|
const response = await fetch(fullUrl, requestOptions)
|
||||||
|
|
||||||
// Parse response data once
|
// For non-OK responses, attempt JSON first; if parsing fails, preserve legacy error expected by tests
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorData: any
|
||||||
|
try {
|
||||||
|
errorData = await response.json()
|
||||||
|
} catch (jsonError) {
|
||||||
|
logger.error(`[${requestId}] JSON parse error for ${toolId}:`, {
|
||||||
|
error: jsonError instanceof Error ? jsonError.message : String(jsonError),
|
||||||
|
})
|
||||||
|
throw new Error(`Failed to parse response from ${toolId}: ${jsonError}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isError, errorInfo } = isErrorResponse(response, errorData)
|
||||||
|
if (isError) {
|
||||||
|
const errorToTransform = createTransformedErrorFromErrorInfo(errorInfo)
|
||||||
|
|
||||||
|
logger.error(`[${requestId}] Internal API error for ${toolId}:`, {
|
||||||
|
status: errorInfo?.status,
|
||||||
|
errorData: errorInfo?.data,
|
||||||
|
})
|
||||||
|
|
||||||
|
throw errorToTransform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response data once with guard for empty 202 bodies
|
||||||
let responseData
|
let responseData
|
||||||
try {
|
const status = response.status
|
||||||
responseData = await response.json()
|
if (status === 202) {
|
||||||
} catch (jsonError) {
|
// Many APIs (e.g., Microsoft Graph) return 202 with empty body
|
||||||
logger.error(`[${requestId}] JSON parse error for ${toolId}:`, {
|
responseData = { status }
|
||||||
error: jsonError instanceof Error ? jsonError.message : String(jsonError),
|
} else {
|
||||||
})
|
try {
|
||||||
throw new Error(`Failed to parse response from ${toolId}: ${jsonError}`)
|
responseData = await response.json()
|
||||||
|
} catch (jsonError) {
|
||||||
|
logger.error(`[${requestId}] JSON parse error for ${toolId}:`, {
|
||||||
|
error: jsonError instanceof Error ? jsonError.message : String(jsonError),
|
||||||
|
})
|
||||||
|
throw new Error(`Failed to parse response from ${toolId}: ${jsonError}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for error conditions
|
// Check for error conditions
|
||||||
@@ -426,44 +512,7 @@ async function handleInternalRequest(
|
|||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
// Handle error case
|
// Handle error case
|
||||||
const errorToTransform = new Error(
|
const errorToTransform = createTransformedErrorFromErrorInfo(errorInfo)
|
||||||
// GraphQL errors (Linear API)
|
|
||||||
errorInfo?.data?.errors?.[0]?.message ||
|
|
||||||
// X/Twitter API specific pattern
|
|
||||||
errorInfo?.data?.errors?.[0]?.detail ||
|
|
||||||
// Generic details array
|
|
||||||
errorInfo?.data?.details?.[0]?.message ||
|
|
||||||
// Hunter API pattern
|
|
||||||
errorInfo?.data?.errors?.[0]?.details ||
|
|
||||||
// Direct errors array (when errors[0] is a string or simple object)
|
|
||||||
(Array.isArray(errorInfo?.data?.errors)
|
|
||||||
? typeof errorInfo.data.errors[0] === 'string'
|
|
||||||
? errorInfo.data.errors[0]
|
|
||||||
: errorInfo.data.errors[0]?.message
|
|
||||||
: undefined) ||
|
|
||||||
// Notion/Discord/GitHub/Twilio pattern
|
|
||||||
errorInfo?.data?.message ||
|
|
||||||
// SOAP/XML fault patterns
|
|
||||||
errorInfo?.data?.fault?.faultstring ||
|
|
||||||
errorInfo?.data?.faultstring ||
|
|
||||||
// Microsoft/OAuth error descriptions
|
|
||||||
errorInfo?.data?.error_description ||
|
|
||||||
// Airtable/Google fallback pattern
|
|
||||||
(typeof errorInfo?.data?.error === 'object'
|
|
||||||
? errorInfo?.data?.error?.message || JSON.stringify(errorInfo?.data?.error)
|
|
||||||
: errorInfo?.data?.error) ||
|
|
||||||
// HTTP status text fallback
|
|
||||||
errorInfo?.statusText ||
|
|
||||||
// Final fallback
|
|
||||||
`Request failed with status ${errorInfo?.status || 'unknown'}`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Add error context
|
|
||||||
Object.assign(errorToTransform, {
|
|
||||||
status: errorInfo?.status,
|
|
||||||
statusText: errorInfo?.statusText,
|
|
||||||
data: errorInfo?.data,
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.error(`[${requestId}] Internal API error for ${toolId}:`, {
|
logger.error(`[${requestId}] Internal API error for ${toolId}:`, {
|
||||||
status: errorInfo?.status,
|
status: errorInfo?.status,
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
visibility: 'user-or-llm',
|
visibility: 'user-or-llm',
|
||||||
description: 'The range of cells to read from',
|
description:
|
||||||
|
'The range of cells to read from. Accepts "SheetName!A1:B2" for explicit ranges or just "SheetName" to read the used range of that sheet. If omitted, reads the used range of the first sheet.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -53,10 +54,19 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rangeInput = params.range.trim()
|
const rangeInput = params.range.trim()
|
||||||
|
|
||||||
|
// If the input contains no '!', treat it as a sheet name only and fetch usedRange
|
||||||
|
if (!rangeInput.includes('!')) {
|
||||||
|
const sheetOnly = encodeURIComponent(rangeInput)
|
||||||
|
return `https://graph.microsoft.com/v1.0/me/drive/items/${spreadsheetId}/workbook/worksheets('${sheetOnly}')/usedRange(valuesOnly=true)`
|
||||||
|
}
|
||||||
|
|
||||||
const match = rangeInput.match(/^([^!]+)!(.+)$/)
|
const match = rangeInput.match(/^([^!]+)!(.+)$/)
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
throw new Error(`Invalid range format: "${params.range}". Use the format "Sheet1!A1:B2"`)
|
throw new Error(
|
||||||
|
`Invalid range format: "${params.range}". Use "Sheet1!A1:B2" or just "Sheet1" to read the whole sheet`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const sheetName = encodeURIComponent(match[1])
|
const sheetName = encodeURIComponent(match[1])
|
||||||
@@ -104,7 +114,7 @@ export const readTool: ToolConfig<MicrosoftExcelToolParams, MicrosoftExcelReadRe
|
|||||||
if (!rangeResp.ok) {
|
if (!rangeResp.ok) {
|
||||||
// Normalize Microsoft Graph sheet/range errors to a friendly message
|
// Normalize Microsoft Graph sheet/range errors to a friendly message
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Invalid range provided or worksheet not found. Provide a range like "Sheet1!A1:B2"'
|
'Invalid range provided or worksheet not found. Provide a range like "Sheet1!A1:B2" or just the sheet name to read the whole sheet'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
154
apps/sim/tools/outlook/forward.ts
Normal file
154
apps/sim/tools/outlook/forward.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import type { OutlookForwardParams, OutlookForwardResponse } from '@/tools/outlook/types'
|
||||||
|
import type { ToolConfig } from '@/tools/types'
|
||||||
|
|
||||||
|
export const outlookForwardTool: ToolConfig<OutlookForwardParams, OutlookForwardResponse> = {
|
||||||
|
id: 'outlook_forward',
|
||||||
|
name: 'Outlook Forward',
|
||||||
|
description: 'Forward an existing Outlook message to specified recipients',
|
||||||
|
version: '1.0.0',
|
||||||
|
|
||||||
|
oauth: {
|
||||||
|
required: true,
|
||||||
|
provider: 'outlook',
|
||||||
|
},
|
||||||
|
|
||||||
|
params: {
|
||||||
|
accessToken: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
visibility: 'hidden',
|
||||||
|
description: 'OAuth access token for Outlook',
|
||||||
|
},
|
||||||
|
messageId: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
visibility: 'user-or-llm',
|
||||||
|
description: 'The ID of the message to forward',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
visibility: 'user-only',
|
||||||
|
description: 'Recipient email address(es), comma-separated',
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
visibility: 'user-or-llm',
|
||||||
|
description: 'Optional comment to include with the forwarded message',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
request: {
|
||||||
|
url: (params) => {
|
||||||
|
return `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}/forward`
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
headers: (params) => {
|
||||||
|
if (!params.accessToken) {
|
||||||
|
throw new Error('Access token is required')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
Authorization: `Bearer ${params.accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: (params: OutlookForwardParams): Record<string, any> => {
|
||||||
|
const parseEmails = (emailString?: string) => {
|
||||||
|
if (!emailString) return []
|
||||||
|
return emailString
|
||||||
|
.split(',')
|
||||||
|
.map((email) => email.trim())
|
||||||
|
.filter((email) => email.length > 0)
|
||||||
|
.map((email) => ({ emailAddress: { address: email } }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const toRecipients = parseEmails(params.to)
|
||||||
|
if (toRecipients.length === 0) {
|
||||||
|
throw new Error('At least one recipient is required to forward a message')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
comment: params.comment ?? '',
|
||||||
|
toRecipients,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
transformResponse: async (response: Response) => {
|
||||||
|
const status = response.status
|
||||||
|
const requestId =
|
||||||
|
response.headers?.get('request-id') || response.headers?.get('x-ms-request-id') || undefined
|
||||||
|
|
||||||
|
// Graph forward action typically returns 202/204 with no body. Try to read text safely.
|
||||||
|
let bodyText = ''
|
||||||
|
try {
|
||||||
|
bodyText = await response.text()
|
||||||
|
} catch (_) {
|
||||||
|
// ignore body read errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to parse JSON if present (rare for this endpoint). Extract message identifiers if available.
|
||||||
|
let parsed: any | undefined
|
||||||
|
if (bodyText && bodyText.trim().length > 0) {
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(bodyText)
|
||||||
|
} catch (_) {
|
||||||
|
// non-JSON body; ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageId = parsed?.id || parsed?.messageId || parsed?.internetMessageId
|
||||||
|
const internetMessageId = parsed?.internetMessageId
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: {
|
||||||
|
message:
|
||||||
|
status === 202 || status === 204
|
||||||
|
? 'Email forwarded successfully'
|
||||||
|
: `Email forwarded (HTTP ${status})`,
|
||||||
|
results: {
|
||||||
|
status: 'forwarded',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
httpStatus: status,
|
||||||
|
requestId,
|
||||||
|
...(messageId ? { messageId } : {}),
|
||||||
|
...(internetMessageId ? { internetMessageId } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
outputs: {
|
||||||
|
message: { type: 'string', description: 'Success or error message' },
|
||||||
|
results: {
|
||||||
|
type: 'object',
|
||||||
|
description: 'Delivery result details',
|
||||||
|
properties: {
|
||||||
|
status: { type: 'string', description: 'Delivery status of the email' },
|
||||||
|
timestamp: { type: 'string', description: 'Timestamp when email was forwarded' },
|
||||||
|
httpStatus: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'HTTP status code returned by the API',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
requestId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Microsoft Graph request-id header for tracing',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
messageId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Forwarded message ID if provided by API',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
internetMessageId: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'RFC 822 Message-ID if provided',
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { outlookDraftTool } from '@/tools/outlook/draft'
|
import { outlookDraftTool } from '@/tools/outlook/draft'
|
||||||
|
import { outlookForwardTool } from '@/tools/outlook/forward'
|
||||||
import { outlookReadTool } from '@/tools/outlook/read'
|
import { outlookReadTool } from '@/tools/outlook/read'
|
||||||
import { outlookSendTool } from '@/tools/outlook/send'
|
import { outlookSendTool } from '@/tools/outlook/send'
|
||||||
|
|
||||||
export { outlookDraftTool, outlookReadTool, outlookSendTool }
|
export { outlookDraftTool, outlookForwardTool, outlookReadTool, outlookSendTool }
|
||||||
|
|||||||
@@ -127,9 +127,7 @@ export const outlookReadTool: ToolConfig<OutlookReadParams, OutlookReadResponse>
|
|||||||
},
|
},
|
||||||
|
|
||||||
outputs: {
|
outputs: {
|
||||||
success: { type: 'boolean', description: 'Email read operation success status' },
|
|
||||||
messageCount: { type: 'number', description: 'Number of emails retrieved' },
|
|
||||||
messages: { type: 'array', description: 'Array of email message objects' },
|
|
||||||
message: { type: 'string', description: 'Success or status message' },
|
message: { type: 'string', description: 'Success or status message' },
|
||||||
|
results: { type: 'array', description: 'Array of email message objects' },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,3 +136,19 @@ export interface CleanedOutlookMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type OutlookResponse = OutlookReadResponse | OutlookSendResponse | OutlookDraftResponse
|
export type OutlookResponse = OutlookReadResponse | OutlookSendResponse | OutlookDraftResponse
|
||||||
|
|
||||||
|
export interface OutlookForwardParams {
|
||||||
|
accessToken: string
|
||||||
|
messageId: string
|
||||||
|
to: string
|
||||||
|
comment?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OutlookForwardResponse extends ToolResponse {
|
||||||
|
output: {
|
||||||
|
message: string
|
||||||
|
results: any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OutlookExtendedResponse = OutlookResponse | OutlookForwardResponse
|
||||||
|
|||||||
@@ -109,7 +109,12 @@ import {
|
|||||||
} from '@/tools/notion'
|
} from '@/tools/notion'
|
||||||
import { onedriveCreateFolderTool, onedriveListTool, onedriveUploadTool } from '@/tools/onedrive'
|
import { onedriveCreateFolderTool, onedriveListTool, onedriveUploadTool } from '@/tools/onedrive'
|
||||||
import { imageTool, embeddingsTool as openAIEmbeddings } from '@/tools/openai'
|
import { imageTool, embeddingsTool as openAIEmbeddings } from '@/tools/openai'
|
||||||
import { outlookDraftTool, outlookReadTool, outlookSendTool } from '@/tools/outlook'
|
import {
|
||||||
|
outlookDraftTool,
|
||||||
|
outlookForwardTool,
|
||||||
|
outlookReadTool,
|
||||||
|
outlookSendTool,
|
||||||
|
} from '@/tools/outlook'
|
||||||
import { parallelSearchTool } from '@/tools/parallel'
|
import { parallelSearchTool } from '@/tools/parallel'
|
||||||
import { perplexityChatTool } from '@/tools/perplexity'
|
import { perplexityChatTool } from '@/tools/perplexity'
|
||||||
import {
|
import {
|
||||||
@@ -302,6 +307,7 @@ export const tools: Record<string, ToolConfig> = {
|
|||||||
outlook_read: outlookReadTool,
|
outlook_read: outlookReadTool,
|
||||||
outlook_send: outlookSendTool,
|
outlook_send: outlookSendTool,
|
||||||
outlook_draft: outlookDraftTool,
|
outlook_draft: outlookDraftTool,
|
||||||
|
outlook_forward: outlookForwardTool,
|
||||||
linear_read_issues: linearReadIssuesTool,
|
linear_read_issues: linearReadIssuesTool,
|
||||||
linear_create_issue: linearCreateIssueTool,
|
linear_create_issue: linearCreateIssueTool,
|
||||||
onedrive_create_folder: onedriveCreateFolderTool,
|
onedrive_create_folder: onedriveCreateFolderTool,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const outlookPollingTrigger: TriggerConfig = {
|
|||||||
},
|
},
|
||||||
bodyText: {
|
bodyText: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Plain text email body (preview)',
|
description: 'Plain text email body',
|
||||||
},
|
},
|
||||||
bodyHtml: {
|
bodyHtml: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|||||||
4
bun.lock
4
bun.lock
@@ -118,6 +118,7 @@
|
|||||||
"fuse.js": "7.1.0",
|
"fuse.js": "7.1.0",
|
||||||
"geist": "1.4.2",
|
"geist": "1.4.2",
|
||||||
"groq-sdk": "^0.15.0",
|
"groq-sdk": "^0.15.0",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"jose": "6.0.11",
|
"jose": "6.0.11",
|
||||||
@@ -162,6 +163,7 @@
|
|||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@trigger.dev/build": "4.0.0",
|
"@trigger.dev/build": "4.0.0",
|
||||||
|
"@types/html-to-text": "^9.0.4",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsdom": "21.1.7",
|
"@types/jsdom": "21.1.7",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
@@ -1378,6 +1380,8 @@
|
|||||||
|
|
||||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||||
|
|
||||||
|
"@types/html-to-text": ["@types/html-to-text@9.0.4", "", {}, "sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ=="],
|
||||||
|
|
||||||
"@types/inquirer": ["@types/inquirer@8.2.12", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-YxURZF2ZsSjU5TAe06tW0M3sL4UI9AMPA6dd8I72uOtppzNafcY38xkYgCZ/vsVOAyNdzHmvtTpLWilOrbP0dQ=="],
|
"@types/inquirer": ["@types/inquirer@8.2.12", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-YxURZF2ZsSjU5TAe06tW0M3sL4UI9AMPA6dd8I72uOtppzNafcY38xkYgCZ/vsVOAyNdzHmvtTpLWilOrbP0dQ=="],
|
||||||
|
|
||||||
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||||
|
|||||||
Reference in New Issue
Block a user