From 30725202c5b6a5684bcc1bc0d8624e824c0e7293 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Fri, 23 May 2025 22:18:50 -0700 Subject: [PATCH 1/7] feat: first round of tools for outlook --- apps/sim/blocks/blocks/outlook.ts | 139 ++++++++++++++++++++++++++++++ apps/sim/components/icons.tsx | 45 ++++++++++ apps/sim/tools/outlook/draft.ts | 89 +++++++++++++++++++ apps/sim/tools/outlook/index.ts | 7 ++ apps/sim/tools/outlook/read.ts | 95 ++++++++++++++++++++ apps/sim/tools/outlook/send.ts | 89 +++++++++++++++++++ apps/sim/tools/outlook/types.ts | 42 +++++++++ 7 files changed, 506 insertions(+) create mode 100644 apps/sim/blocks/blocks/outlook.ts create mode 100644 apps/sim/tools/outlook/draft.ts create mode 100644 apps/sim/tools/outlook/index.ts create mode 100644 apps/sim/tools/outlook/read.ts create mode 100644 apps/sim/tools/outlook/send.ts create mode 100644 apps/sim/tools/outlook/types.ts diff --git a/apps/sim/blocks/blocks/outlook.ts b/apps/sim/blocks/blocks/outlook.ts new file mode 100644 index 00000000000..3edaeb5c8f6 --- /dev/null +++ b/apps/sim/blocks/blocks/outlook.ts @@ -0,0 +1,139 @@ +import { OutlookIcon } from '@/components/icons' +import { OutlookReadResponse, OutlookSendResponse, OutlookDraftResponse } from '@/tools/outlook/types' +import { BlockConfig } from '../types' + +export const OutlookBlock: BlockConfig = { + type: 'outlook', + name: 'Outlook', + description: 'Send Outlook', + longDescription: + 'Integrate Outlook functionality to read, draft, andsend email messages within your workflow. Automate email communications and process email content using OAuth authentication.', + docsLink: 'https://docs.simstudio.ai/tools/outlook', + category: 'tools', + bgColor: '#E0E0E0', + icon: OutlookIcon, + subBlocks: [ + // Operation selector + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'Send Email', id: 'send_outlook' }, + { label: 'Draft Email', id: 'draft_outlook' }, + { label: 'Read Email', id: 'read_outlook' }, + ], + }, + // Gmail Credentials + { + id: 'credential', + title: 'Outlook Account', + type: 'oauth-input', + layout: 'full', + provider: 'outlook', + serviceId: 'outlook', + requiredScopes: [ + + ], + placeholder: 'Select Outlook account', + }, + // Send Email Fields + { + id: 'to', + title: 'To', + type: 'short-input', + layout: 'full', + placeholder: 'Recipient email address', + condition: { field: 'operation', value: ['send_outlook', 'draft_outlook'] }, + }, + { + id: 'subject', + title: 'Subject', + type: 'short-input', + layout: 'full', + placeholder: 'Email subject', + condition: { field: 'operation', value: ['send_outlook', 'draft_outlook'] }, + }, + { + id: 'body', + title: 'Body', + type: 'long-input', + layout: 'full', + placeholder: 'Email content', + condition: { field: 'operation', value: ['send_outlook', 'draft_outlook'] }, + }, + // Read Email Fields - Add folder selector + { + id: 'folder', + title: 'Label', + type: 'folder-selector', + layout: 'full', + provider: 'outlook', + serviceId: 'outlook', + requiredScopes: [ + + ], + placeholder: 'Select Outlook label/folder', + condition: { field: 'operation', value: 'read_outlook' }, + }, + { + id: 'maxResults', + title: 'Number of Emails', + type: 'short-input', + layout: 'full', + placeholder: 'Number of emails to retrieve (default: 1, max: 10)', + condition: { field: 'operation', value: 'read_outlook' }, + }, + ], + tools: { + access: ['outlook_send', 'outlook_draft', 'outlook_read'], + config: { + tool: (params) => { + switch (params.operation) { + case 'send_outlook': + return 'outlook_send' + case 'read_outlook': + return 'outlook_read' + case 'draft_outlook': + return 'outlook_draft' + default: + throw new Error(`Invalid Outlook operation: ${params.operation}`) + } + }, + params: (params) => { + // Pass the credential directly from the credential field + const { credential, ...rest } = params + + // Set default folder to INBOX if not specified + if (rest.operation === 'read_outlook' && !rest.folder) { + rest.folder = 'INBOX' + } + + return { + ...rest, + credential, // Keep the credential parameter + } + }, + }, + }, + inputs: { + operation: { type: 'string', required: true }, + credential: { type: 'string', required: true }, + // Send operation inputs + to: { type: 'string', required: false }, + subject: { type: 'string', required: false }, + body: { type: 'string', required: false }, + // Read operation inputs + folder: { type: 'string', required: false }, + maxResults: { type: 'number', required: false }, + }, + outputs: { + response: { + type: { + message: 'string', + results: 'json', + }, + }, + }, +} diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 0d0b8e26c99..6070d88a823 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2556,3 +2556,48 @@ export function MicrosoftTeamsIcon(props: SVGProps) { ) } + +export function OutlookIcon(props: SVGProps) { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/apps/sim/tools/outlook/draft.ts b/apps/sim/tools/outlook/draft.ts new file mode 100644 index 00000000000..f4766bc27ea --- /dev/null +++ b/apps/sim/tools/outlook/draft.ts @@ -0,0 +1,89 @@ +import { ToolConfig } from '../types' +import { OutlookDraftParams, OutlookDraftResponse } from './types' + + +export const outlookDraftTool: ToolConfig = { + id: 'outlook_draft', + name: 'Outlook Draft', + description: 'Draft emails using Outlook', + version: '1.0.0', + + oauth: { + required: true, + provider: 'outlook', + }, + + params: { + accessToken: { + type: 'string', + required: true, + description: 'Access token for Outlook API', + }, + to: { + type: 'string', + required: true, + description: 'Recipient email address', + }, + subject: { + type: 'string', + required: true, + description: 'Email subject', + }, + body: { + type: 'string', + required: true, + description: 'Email body content', + }, + }, + + request: { + url: (params) => { + return `https://graph.microsoft.com/v1.0/me/messages` + }, + method: 'POST', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to draft Outlook mail: ${errorText}`) + } + + return { + success: true, + output: { + message: 'Email drafted successfully', + results: [], + }, + } + }, + transformError: (error) => { + // If it's an Error instance with a message, use that + if (error instanceof Error) { + return error.message + } + + // If it's an object with an error or message property + if (typeof error === 'object' && error !== null) { + if (error.error) { + return typeof error.error === 'string' ? error.error : JSON.stringify(error.error) + } + if (error.message) { + return error.message + } + } + + // Default fallback message + return 'An error occurred while reading Microsoft Teams chat' + }, + } \ No newline at end of file diff --git a/apps/sim/tools/outlook/index.ts b/apps/sim/tools/outlook/index.ts new file mode 100644 index 00000000000..b0d3757f2dd --- /dev/null +++ b/apps/sim/tools/outlook/index.ts @@ -0,0 +1,7 @@ +import { sendTool } from './send' +import { readTool } from './read' +import { draftTool } from './draft' + +export const outlookSendTool = sendTool +export const outlookReadTool = readTool +export const outlookDraftTool = draftTool \ No newline at end of file diff --git a/apps/sim/tools/outlook/read.ts b/apps/sim/tools/outlook/read.ts new file mode 100644 index 00000000000..5059b9101f3 --- /dev/null +++ b/apps/sim/tools/outlook/read.ts @@ -0,0 +1,95 @@ +import { ToolConfig } from '../types' +import { OutlookReadParams, OutlookReadResponse } from './types' + +export const outlookReadTool: ToolConfig = { + id: 'outlook_read', + name: 'Outlook Read', + description: 'Read emails from Outlook', + version: '1.0.0', + + oauth: { + required: true, + provider: 'outlook', + }, + params: { + accessToken: { + type: 'string', + required: true, + description: 'OAuth access token for Outlook', + }, + messageId: { + type: 'string', + required: false, + description: 'Message ID to read', + } + }, + request: { + url: (params) => { + // If messageId is provided, fetch that specific message + if (params.messageId) { + return `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` + } + // Otherwise fetch the most recent messages + return `https://graph.microsoft.com/v1.0/me/messages?$top=50&$orderby=createdDateTime desc` + }, + method: 'GET', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + } + }, + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to read Outlook mail: ${errorText}`) + } + + const data = await response.json() + + // Microsoft Graph API returns messages in a 'value' array + const messages = data.value || [] + + if (messages.length === 0) { + return { + success: true, + output: { + message: 'No mail found.', + results: [], + }, + } + } + + return { + success: true, + output: { + message: 'Email read successfully', + results: messages, + }, + } + }, + transformError: (error) => { + // If it's an Error instance with a message, use that + if (error instanceof Error) { + return error.message + } + + // If it's an object with an error or message property + if (typeof error === 'object' && error !== null) { + if (error.error) { + return typeof error.error === 'string' ? error.error : JSON.stringify(error.error) + } + if (error.message) { + return error.message + } + } + + // Default fallback message + return 'An error occurred while reading Microsoft Teams chat' + }, + } \ No newline at end of file diff --git a/apps/sim/tools/outlook/send.ts b/apps/sim/tools/outlook/send.ts new file mode 100644 index 00000000000..fb42c4d18ea --- /dev/null +++ b/apps/sim/tools/outlook/send.ts @@ -0,0 +1,89 @@ +import { ToolConfig } from '../types' +import { OutlookSendParams, OutlookSendResponse } from './types' + + +export const outlookSendTool: ToolConfig = { + id: 'outlook_send', + name: 'Outlook Send', + description: 'Send emails using Outlook', + version: '1.0.0', + + oauth: { + required: true, + provider: 'outlook', + }, + + params: { + accessToken: { + type: 'string', + required: true, + description: 'Access token for Outlook API', + }, + to: { + type: 'string', + required: true, + description: 'Recipient email address', + }, + subject: { + type: 'string', + required: true, + description: 'Email subject', + }, + body: { + type: 'string', + required: true, + description: 'Email body content', + }, + }, + + request: { + url: (params) => { + return `https://graph.microsoft.com/v1.0/me/sendMail` + }, + method: 'POST', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } + }, + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to send Outlook mail: ${errorText}`) + } + + return { + success: true, + output: { + message: 'Email sent successfully', + results: [], + }, + } + }, + transformError: (error) => { + // If it's an Error instance with a message, use that + if (error instanceof Error) { + return error.message + } + + // If it's an object with an error or message property + if (typeof error === 'object' && error !== null) { + if (error.error) { + return typeof error.error === 'string' ? error.error : JSON.stringify(error.error) + } + if (error.message) { + return error.message + } + } + + // Default fallback message + return 'An error occurred while reading Microsoft Teams chat' + }, + } \ No newline at end of file diff --git a/apps/sim/tools/outlook/types.ts b/apps/sim/tools/outlook/types.ts new file mode 100644 index 00000000000..82e101d12c6 --- /dev/null +++ b/apps/sim/tools/outlook/types.ts @@ -0,0 +1,42 @@ +import { ToolResponse } from '../types' + +export interface OutlookSendParams { + accessToken: string + to: string + subject: string + body: string +} + +export interface OutlookSendResponse extends ToolResponse { + output: { + message: string + results: any + } +} + +export interface OutlookReadParams { + accessToken: string + folder: string + maxResults: number +} + +export interface OutlookReadResponse extends ToolResponse { + output: { + message: string + results: any + } +} + +export interface OutlookDraftParams { + accessToken: string + to: string + subject: string + body: string +} + +export interface OutlookDraftResponse extends ToolResponse { + output: { + message: string + results: any + } +} \ No newline at end of file From b3333e460425179606ad6ba006751fb10b1378af Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Sat, 24 May 2025 14:21:23 -0700 Subject: [PATCH 2/7] added more --- apps/sim/blocks/blocks/outlook.ts | 2 +- apps/sim/blocks/registry.ts | 2 ++ apps/sim/tools/outlook/index.ts | 10 ++++------ apps/sim/tools/outlook/types.ts | 1 + apps/sim/tools/registry.ts | 4 ++++ 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/sim/blocks/blocks/outlook.ts b/apps/sim/blocks/blocks/outlook.ts index 3edaeb5c8f6..7ad6026dcd1 100644 --- a/apps/sim/blocks/blocks/outlook.ts +++ b/apps/sim/blocks/blocks/outlook.ts @@ -5,7 +5,7 @@ import { BlockConfig } from '../types' export const OutlookBlock: BlockConfig = { type: 'outlook', name: 'Outlook', - description: 'Send Outlook', + description: 'Access Outlook', longDescription: 'Integrate Outlook functionality to read, draft, andsend email messages within your workflow. Automate email communications and process email content using OAuth authentication.', docsLink: 'https://docs.simstudio.ai/tools/outlook', diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index 86f698af842..4571e9910cc 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -36,6 +36,7 @@ import { MicrosoftTeamsBlock } from './blocks/microsoft_teams' import { MistralParseBlock } from './blocks/mistral_parse' import { NotionBlock } from './blocks/notion' import { OpenAIBlock } from './blocks/openai' +import { OutlookBlock } from './blocks/outlook' import { PerplexityBlock } from './blocks/perplexity' import { PineconeBlock } from './blocks/pinecone' import { RedditBlock } from './blocks/reddit' @@ -92,6 +93,7 @@ export const registry: Record = { mistral_parse: MistralParseBlock, notion: NotionBlock, openai: OpenAIBlock, + outlook: OutlookBlock, perplexity: PerplexityBlock, pinecone: PineconeBlock, reddit: RedditBlock, diff --git a/apps/sim/tools/outlook/index.ts b/apps/sim/tools/outlook/index.ts index b0d3757f2dd..1cbb65c0524 100644 --- a/apps/sim/tools/outlook/index.ts +++ b/apps/sim/tools/outlook/index.ts @@ -1,7 +1,5 @@ -import { sendTool } from './send' -import { readTool } from './read' -import { draftTool } from './draft' +import { outlookDraftTool } from './draft' +import { outlookReadTool } from './read' +import { outlookSendTool } from './send' -export const outlookSendTool = sendTool -export const outlookReadTool = readTool -export const outlookDraftTool = draftTool \ No newline at end of file +export { outlookDraftTool, outlookReadTool, outlookSendTool } \ No newline at end of file diff --git a/apps/sim/tools/outlook/types.ts b/apps/sim/tools/outlook/types.ts index 82e101d12c6..9d6dd81f2df 100644 --- a/apps/sim/tools/outlook/types.ts +++ b/apps/sim/tools/outlook/types.ts @@ -18,6 +18,7 @@ export interface OutlookReadParams { accessToken: string folder: string maxResults: number + messageId?: string } export interface OutlookReadResponse extends ToolResponse { diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 83578a844df..56625e80278 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -68,6 +68,7 @@ import { import { redditGetCommentsTool, redditGetPostsTool, redditHotPostsTool } from './reddit' import { s3GetObjectTool } from './s3' import { opportunitiesTool as salesforceOpportunities } from './salesforce/opportunities' +import { outlookReadTool, outlookSendTool, outlookDraftTool } from './outlook' import { searchTool as serperSearch } from './serper' import { slackMessageTool } from './slack' import { stagehandAgentTool, stagehandExtractTool } from './stagehand' @@ -183,4 +184,7 @@ export const tools: Record = { microsoft_teams_write_chat: microsoftTeamsWriteChatTool, microsoft_teams_read_channel: microsoftTeamsReadChannelTool, microsoft_teams_write_channel: microsoftTeamsWriteChannelTool, + outlook_read: outlookReadTool, + outlook_send: outlookSendTool, + outlook_draft: outlookDraftTool, } From 2b453b75cf4ac4a0bcac8ea016010eb958d39da8 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Sat, 24 May 2025 18:57:26 -0700 Subject: [PATCH 3/7] outlook finished --- .../api/auth/oauth/outlook/folders/route.ts | 121 ++++++++++++++++++ .../components/oauth-required-modal.tsx | 4 + .../credential-selector.tsx | 1 - .../folder-selector/folder-selector.tsx | 83 ++++++------ apps/sim/blocks/blocks/outlook.ts | 21 ++- apps/sim/lib/auth.ts | 25 ++++ apps/sim/lib/oauth.ts | 35 +++++ apps/sim/tools/outlook/draft.ts | 88 ++++++++----- apps/sim/tools/outlook/read.ts | 67 ++++++++-- apps/sim/tools/outlook/send.ts | 87 ++++++++----- 10 files changed, 416 insertions(+), 116 deletions(-) create mode 100644 apps/sim/app/api/auth/oauth/outlook/folders/route.ts diff --git a/apps/sim/app/api/auth/oauth/outlook/folders/route.ts b/apps/sim/app/api/auth/oauth/outlook/folders/route.ts new file mode 100644 index 00000000000..2518fbf2a61 --- /dev/null +++ b/apps/sim/app/api/auth/oauth/outlook/folders/route.ts @@ -0,0 +1,121 @@ +import { NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console-logger' +import { refreshAccessTokenIfNeeded } from '../../utils' + +export const dynamic = 'force-dynamic' + +const logger = createLogger('OutlookFoldersAPI') + +export async function GET(request: Request) { + try { + const session = await getSession() + const { searchParams } = new URL(request.url) + const credentialId = searchParams.get('credentialId') + + if (!credentialId) { + logger.error('Missing credentialId in request') + return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) + } + + try { + // Get the userId from the session + const userId = session?.user?.id || '' + + if (!userId) { + logger.error('No user ID found in session') + return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) + } + + const accessToken = await refreshAccessTokenIfNeeded(credentialId, userId, crypto.randomUUID().slice(0, 8)) + + if (!accessToken) { + logger.error('Failed to get access token', { credentialId, userId }) + return NextResponse.json( + { + error: 'Could not retrieve access token', + authRequired: true, + }, + { status: 401 } + ) + } + + const response = await fetch('https://graph.microsoft.com/v1.0/me/mailFolders', { + method: 'GET', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, + }) + + if (!response.ok) { + const errorData = await response.json() + logger.error('Microsoft Graph API error getting folders', { + status: response.status, + error: errorData, + endpoint: 'https://graph.microsoft.com/v1.0/me/mailFolders', + }) + + // Check for auth errors specifically + if (response.status === 401) { + return NextResponse.json( + { + error: 'Authentication failed. Please reconnect your Outlook account.', + authRequired: true, + }, + { status: 401 } + ) + } + + throw new Error(`Microsoft Graph API error: ${JSON.stringify(errorData)}`) + } + + const data = await response.json() + const folders = data.value || [] + + // Transform folders to match the expected format + const transformedFolders = folders.map((folder: any) => ({ + id: folder.id, + name: folder.displayName, + type: 'folder', + messagesTotal: folder.totalItemCount || 0, + messagesUnread: folder.unreadItemCount || 0, + })) + + return NextResponse.json({ + folders: transformedFolders, + }) + } catch (innerError) { + logger.error('Error during API requests:', innerError) + + // Check if it's an authentication error + const errorMessage = innerError instanceof Error ? innerError.message : String(innerError) + if ( + errorMessage.includes('auth') || + errorMessage.includes('token') || + errorMessage.includes('unauthorized') || + errorMessage.includes('unauthenticated') + ) { + return NextResponse.json( + { + error: 'Authentication failed. Please reconnect your Outlook account.', + authRequired: true, + details: errorMessage, + }, + { status: 401 } + ) + } + + throw innerError + } + } catch (error) { + logger.error('Error processing Outlook folders request:', error) + return NextResponse.json( + { + error: 'Failed to retrieve Outlook folders', + details: (error as Error).message, + }, + { status: 500 } + ) + } +} \ No newline at end of file diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 2c7ef7fddf5..59fbd6798db 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -97,6 +97,10 @@ const SCOPE_DESCRIPTIONS: Record = { 'Group.Read.All': 'Read your Microsoft groups', 'Group.ReadWrite.All': 'Write to your Microsoft groups', 'Team.ReadBasic.All': 'Read your Microsoft teams', + 'Mail.ReadWrite': 'Write to your Microsoft emails', + 'Mail.ReadBasic': 'Read your Microsoft emails', + 'Mail.Read': 'Read your Microsoft emails', + 'Mail.Send': 'Send emails on your behalf', identify: 'Read your Discord user', bot: 'Read your Discord bot', 'messages.read': 'Read your Discord messages', diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx index 94cf46f422f..0420835b42e 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/credential-selector/credential-selector.tsx @@ -168,7 +168,6 @@ export function CredentialSelector({ if (!baseProviderConfig) { return } - // Always use the base provider icon for a more consistent UI return baseProviderConfig.icon({ className: 'h-4 w-4' }) } diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx index 85e0220e27f..a6e8cfb5176 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { Check, ChevronDown, RefreshCw } from 'lucide-react' -import { GmailIcon } from '@/components/icons' +import { GmailIcon, OutlookIcon } from '@/components/icons' import { Button } from '@/components/ui/button' import { Command, @@ -112,7 +112,7 @@ export function FolderSelector({ // Fetch a single folder by ID when we have a selectedFolderId but no metadata const fetchFolderById = useCallback( async (folderId: string) => { - if (!selectedCredentialId || !folderId) return null + if (!selectedCredentialId || !folderId || provider === 'outlook') return null setIsLoadingSelectedFolder(true) try { @@ -144,10 +144,10 @@ export function FolderSelector({ setIsLoadingSelectedFolder(false) } }, - [selectedCredentialId, onFolderInfoChange] + [selectedCredentialId, onFolderInfoChange, provider] ) - // Fetch folders from Gmail + // Fetch folders from Gmail or Outlook const fetchFolders = useCallback( async (searchQuery?: string) => { if (!selectedCredentialId) return @@ -163,22 +163,32 @@ export function FolderSelector({ queryParams.append('query', searchQuery) } - const response = await fetch(`/api/auth/oauth/gmail/labels?${queryParams.toString()}`) + // Determine the API endpoint based on provider + let apiEndpoint: string + if (provider === 'outlook') { + apiEndpoint = `/api/auth/oauth/outlook/folders?${queryParams.toString()}` + } else { + // Default to Gmail + apiEndpoint = `/api/auth/oauth/gmail/labels?${queryParams.toString()}` + } + + const response = await fetch(apiEndpoint) if (response.ok) { const data = await response.json() - setFolders(data.labels || []) + const folderList = provider === 'outlook' ? data.folders : data.labels + setFolders(folderList || []) // If we have a selected folder ID, find the folder info if (selectedFolderId) { - const folderInfo = data.labels.find( + const folderInfo = folderList.find( (folder: FolderInfo) => folder.id === selectedFolderId ) if (folderInfo) { setSelectedFolder(folderInfo) onFolderInfoChange?.(folderInfo) - } else if (!searchQuery) { - // Only try to fetch by ID if this is not a search query + } else if (!searchQuery && provider !== 'outlook') { + // Only try to fetch by ID for Gmail if this is not a search query // and we couldn't find the folder in the list fetchFolderById(selectedFolderId) } @@ -196,7 +206,7 @@ export function FolderSelector({ setIsLoading(false) } }, - [selectedCredentialId, selectedFolderId, onFolderInfoChange, fetchFolderById] + [selectedCredentialId, selectedFolderId, onFolderInfoChange, fetchFolderById, provider] ) // Fetch credentials on initial mount @@ -221,12 +231,12 @@ export function FolderSelector({ } }, [value]) - // Fetch the selected folder metadata once credentials are ready + // Fetch the selected folder metadata once credentials are ready (Gmail only) useEffect(() => { - if (value && selectedCredentialId && !selectedFolder) { + if (value && selectedCredentialId && !selectedFolder && provider !== 'outlook') { fetchFolderById(value) } - }, [value, selectedCredentialId, selectedFolder, fetchFolderById]) + }, [value, selectedCredentialId, selectedFolder, fetchFolderById, provider]) // Handle folder selection const handleSelectFolder = (folder: FolderInfo) => { @@ -263,7 +273,22 @@ export function FolderSelector({ const getFolderIcon = (size: 'sm' | 'md' = 'sm') => { const iconSize = size === 'sm' ? 'h-4 w-4' : 'h-5 w-5' - return + if (provider === 'gmail') { + return + } else if (provider === 'outlook') { + return + } + return null + } + + const getProviderName = () => { + if (provider === 'outlook') return 'Outlook' + return 'Gmail' + } + + const getFolderLabel = () => { + if (provider === 'outlook') return 'folders' + return 'labels' } return ( @@ -283,11 +308,6 @@ export function FolderSelector({ {getFolderIcon('sm')} {selectedFolder.name} - ) : selectedFolderId && (isLoadingSelectedFolder || !selectedCredentialId) ? ( -
- - Loading label... -
) : (
{getFolderIcon('sm')} @@ -321,24 +341,24 @@ export function FolderSelector({ )} - + {isLoading ? (
- Loading labels... + Loading {getFolderLabel()}...
) : credentials.length === 0 ? (

No accounts connected.

- Connect a Gmail account to continue. + Connect a {getProviderName()} account to continue.

) : (
-

No labels found.

+

No {getFolderLabel()} found.

Try a different search or account.

@@ -371,7 +391,7 @@ export function FolderSelector({ {folders.length > 0 && (
- Labels + {getFolderLabel().charAt(0).toUpperCase() + getFolderLabel().slice(1)}
{folders.map((folder) => (
- Connect Gmail account + Connect {getProviderName()} account
)} - - {/* Add another account option */} - {/* {credentials.length > 0 && ( - - -
- Connect Another Account -
-
-
- )} */} @@ -421,7 +430,7 @@ export function FolderSelector({ isOpen={showOAuthModal} onClose={() => setShowOAuthModal(false)} provider={provider} - toolName='Gmail' + toolName={getProviderName()} requiredScopes={requiredScopes} serviceId={getServiceId()} /> diff --git a/apps/sim/blocks/blocks/outlook.ts b/apps/sim/blocks/blocks/outlook.ts index 7ad6026dcd1..01408b66dd2 100644 --- a/apps/sim/blocks/blocks/outlook.ts +++ b/apps/sim/blocks/blocks/outlook.ts @@ -28,15 +28,22 @@ export const OutlookBlock: BlockConfig = { 'offline_access', ], }, + outlook: { + id: 'outlook', + name: 'Outlook', + description: 'Connect to Outlook and manage emails.', + providerId: 'outlook', + icon: (props) => OutlookIcon(props), + baseProviderIcon: (props) => MicrosoftIcon(props), + scopes: [ + 'openid', + 'profile', + 'email', + 'Mail.ReadWrite', + 'Mail.ReadBasic', + 'Mail.Read', + 'Mail.Send', + 'offline_access', + ], + }, }, defaultService: 'microsoft', }, @@ -356,6 +376,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[] } } else if (provider === 'microsoft-teams') { return 'microsoft-teams' + } else if (provider === 'outlook') { + return 'outlook' } else if (provider === 'github') { return 'github' } else if (provider === 'supabase') { @@ -412,6 +434,14 @@ export interface ProviderConfig { * This is a server-safe utility that can be used in both client and server code */ export function parseProvider(provider: OAuthProvider): ProviderConfig { + // Handle special cases first + if (provider === 'outlook') { + return { + baseProvider: 'microsoft', + featureType: 'outlook', + } + } + // Handle compound providers (e.g., 'google-email' -> { baseProvider: 'google', featureType: 'email' }) const [base, feature] = provider.split('-') @@ -506,6 +536,11 @@ export async function refreshOAuthToken( clientId = env.MICROSOFT_CLIENT_ID clientSecret = env.MICROSOFT_CLIENT_SECRET break + case 'outlook': + tokenEndpoint = 'https://login.microsoftonline.com/common/oauth2/v2.0/token' + clientId = env.MICROSOFT_CLIENT_ID + clientSecret = env.MICROSOFT_CLIENT_SECRET + break default: throw new Error(`Unsupported provider: ${provider}`) } diff --git a/apps/sim/tools/outlook/draft.ts b/apps/sim/tools/outlook/draft.ts index f4766bc27ea..05510a1d6b3 100644 --- a/apps/sim/tools/outlook/draft.ts +++ b/apps/sim/tools/outlook/draft.ts @@ -52,38 +52,62 @@ export const outlookDraftTool: ToolConfig => { + return { + subject: params.subject, + body: { + contentType: "Text", + content: params.body, + }, + toRecipients: [ + { + emailAddress: { + address: params.to, + }, + }, + ], + } + }, }, - transformResponse: async (response: Response) => { - if (!response.ok) { - const errorText = await response.text() - throw new Error(`Failed to draft Outlook mail: ${errorText}`) - } - - return { - success: true, - output: { - message: 'Email drafted successfully', - results: [], - }, - } - }, - transformError: (error) => { - // If it's an Error instance with a message, use that - if (error instanceof Error) { - return error.message - } - - // If it's an object with an error or message property - if (typeof error === 'object' && error !== null) { - if (error.error) { - return typeof error.error === 'string' ? error.error : JSON.stringify(error.error) + transformResponse: async (response) => { + if (!response.ok) { + let errorData + try { + errorData = await response.json() + } catch { + throw new Error('Failed to draft email') + } + throw new Error(errorData.error?.message || 'Failed to draft email') } - if (error.message) { - return error.message + + // Outlook draft API returns the created message object + const data = await response.json() + + return { + success: true, + output: { + message: 'Email drafted successfully', + results: { + id: data.id, + subject: data.subject, + status: 'drafted', + timestamp: new Date().toISOString(), + }, + }, } - } - - // Default fallback message - return 'An error occurred while reading Microsoft Teams chat' - }, - } \ No newline at end of file + }, + + transformError: (error) => { + // Handle Google API error format + if (error.error?.message) { + if (error.error.message.includes('invalid authentication credentials')) { + return 'Invalid or expired access token. Please reauthenticate.' + } + if (error.error.message.includes('quota')) { + return 'Outlook API quota exceeded. Please try again later.' + } + return error.error.message + } + return error.message || 'An unexpected error occurred while sending email' + }, + } diff --git a/apps/sim/tools/outlook/read.ts b/apps/sim/tools/outlook/read.ts index 5059b9101f3..6d08f62c166 100644 --- a/apps/sim/tools/outlook/read.ts +++ b/apps/sim/tools/outlook/read.ts @@ -17,10 +17,20 @@ export const outlookReadTool: ToolConfig required: true, description: 'OAuth access token for Outlook', }, + folder: { + type: 'string', + required: false, + description: 'Folder ID to read emails from (default: Inbox)', + }, + maxResults: { + type: 'number', + required: false, + description: 'Maximum number of emails to retrieve (default: 1, max: 10)', + }, messageId: { - type: 'string', - required: false, - description: 'Message ID to read', + type: 'string', + required: false, + description: 'Message ID to read', } }, request: { @@ -29,8 +39,17 @@ export const outlookReadTool: ToolConfig if (params.messageId) { return `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` } - // Otherwise fetch the most recent messages - return `https://graph.microsoft.com/v1.0/me/messages?$top=50&$orderby=createdDateTime desc` + + // Set max results (default to 1 for simplicity, max 10) + const maxResults = params.maxResults ? Math.min(params.maxResults, 10) : 1 + + // If folder is provided, read from that specific folder + if (params.folder) { + return `https://graph.microsoft.com/v1.0/me/mailFolders/${params.folder}/messages?$top=${maxResults}&$orderby=createdDateTime desc` + } + + // Otherwise fetch from all messages (default behavior) + return `https://graph.microsoft.com/v1.0/me/messages?$top=${maxResults}&$orderby=createdDateTime desc` }, method: 'GET', headers: (params) => { @@ -63,13 +82,45 @@ export const outlookReadTool: ToolConfig results: [], }, } - } + } + + // Clean up the message data to only include essential fields + const cleanedMessages = messages.map((message: any) => ({ + id: message.id, + subject: message.subject, + bodyPreview: message.bodyPreview, + body: { + contentType: message.body?.contentType, + content: message.body?.content, + }, + sender: { + name: message.sender?.emailAddress?.name, + address: message.sender?.emailAddress?.address, + }, + from: { + name: message.from?.emailAddress?.name, + address: message.from?.emailAddress?.address, + }, + toRecipients: message.toRecipients?.map((recipient: any) => ({ + name: recipient.emailAddress?.name, + address: recipient.emailAddress?.address, + })) || [], + ccRecipients: message.ccRecipients?.map((recipient: any) => ({ + name: recipient.emailAddress?.name, + address: recipient.emailAddress?.address, + })) || [], + receivedDateTime: message.receivedDateTime, + sentDateTime: message.sentDateTime, + hasAttachments: message.hasAttachments, + isRead: message.isRead, + importance: message.importance, + })) return { success: true, output: { - message: 'Email read successfully', - results: messages, + message: `Successfully read ${cleanedMessages.length} email${cleanedMessages.length === 1 ? '' : 's'}`, + results: cleanedMessages, }, } }, diff --git a/apps/sim/tools/outlook/send.ts b/apps/sim/tools/outlook/send.ts index fb42c4d18ea..0a03092480b 100644 --- a/apps/sim/tools/outlook/send.ts +++ b/apps/sim/tools/outlook/send.ts @@ -52,38 +52,61 @@ export const outlookSendTool: ToolConfig 'Content-Type': 'application/json', } }, + body: (params: OutlookSendParams): Record => { + return { + message: { + subject: params.subject, + body: { + contentType: "Text", + content: params.body, + }, + toRecipients: [ + { + emailAddress: { + address: params.to, + }, + }, + ], + }, + saveToSentItems: true, + } + }, }, - transformResponse: async (response: Response) => { - if (!response.ok) { - const errorText = await response.text() - throw new Error(`Failed to send Outlook mail: ${errorText}`) - } - - return { - success: true, - output: { - message: 'Email sent successfully', - results: [], - }, - } - }, - transformError: (error) => { - // If it's an Error instance with a message, use that - if (error instanceof Error) { - return error.message - } - - // If it's an object with an error or message property - if (typeof error === 'object' && error !== null) { - if (error.error) { - return typeof error.error === 'string' ? error.error : JSON.stringify(error.error) + transformResponse: async (response) => { + if (!response.ok) { + let errorData + try { + errorData = await response.json() + } catch { + throw new Error('Failed to send email') + } + throw new Error(errorData.error?.message || 'Failed to send email') } - if (error.message) { - return error.message + + // Outlook sendMail API returns empty body on success + return { + success: true, + output: { + message: 'Email sent successfully', + results: { + status: 'sent', + timestamp: new Date().toISOString(), + }, + }, } - } - - // Default fallback message - return 'An error occurred while reading Microsoft Teams chat' - }, - } \ No newline at end of file + }, + + transformError: (error) => { + // Handle Google API error format + if (error.error?.message) { + if (error.error.message.includes('invalid authentication credentials')) { + return 'Invalid or expired access token. Please reauthenticate.' + } + if (error.error.message.includes('quota')) { + return 'Outlook API quota exceeded. Please try again later.' + } + return error.error.message + } + return error.message || 'An unexpected error occurred while sending email' + }, + } \ No newline at end of file From 8bf7eba00a1b64c25ee1acf250249573db52fa7e Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Sat, 24 May 2025 19:07:03 -0700 Subject: [PATCH 4/7] added bun and docs --- apps/docs/content/docs/tools/meta.json | 1 + .../content/docs/tools/microsoft_teams.mdx | 89 +++++--- apps/docs/content/docs/tools/outlook.mdx | 166 +++++++++++++++ .../api/auth/oauth/outlook/folders/route.ts | 8 +- .../folder-selector/folder-selector.tsx | 8 +- apps/sim/blocks/blocks/outlook.ts | 50 ++--- apps/sim/components/icons.tsx | 138 +++++++++--- apps/sim/lib/oauth.ts | 2 +- apps/sim/tools/outlook/draft.ts | 139 ++++++------ apps/sim/tools/outlook/index.ts | 2 +- apps/sim/tools/outlook/read.ts | 198 +++++++++--------- apps/sim/tools/outlook/send.ts | 141 +++++++------ apps/sim/tools/outlook/types.ts | 4 +- apps/sim/tools/registry.ts | 2 +- 14 files changed, 618 insertions(+), 330 deletions(-) create mode 100644 apps/docs/content/docs/tools/outlook.mdx diff --git a/apps/docs/content/docs/tools/meta.json b/apps/docs/content/docs/tools/meta.json index 166703b7e82..f849bd071a7 100644 --- a/apps/docs/content/docs/tools/meta.json +++ b/apps/docs/content/docs/tools/meta.json @@ -27,6 +27,7 @@ "microsoft_teams", "notion", "openai", + "outlook", "perplexity", "pinecone", "reddit", diff --git a/apps/docs/content/docs/tools/microsoft_teams.mdx b/apps/docs/content/docs/tools/microsoft_teams.mdx index 6c7f92d43f1..1e5e7631684 100644 --- a/apps/docs/content/docs/tools/microsoft_teams.mdx +++ b/apps/docs/content/docs/tools/microsoft_teams.mdx @@ -9,30 +9,70 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" type="microsoft_teams" color="#E0E0E0" icon={true} - iconSvg={` - - - - - - - - - - - - - - - - - - - + iconSvg={` + + + + + + + + + + + + + + + + + + + `} /> @@ -52,6 +92,7 @@ With Microsoft Teams, you can: In Sim Studio, the Microsoft Teams integration enables your agents to interact directly with chat messages programmatically. This allows for powerful automation scenarios such as sending updates, posting alerts, coordinating tasks, and responding to conversations in real time. Your agents can write new messages to chats or channels, update content based on workflow data, and engage with users where collaboration happens. By integrating Sim Studio with Microsoft Teams, you bridge the gap between intelligent workflows and team communication — empowering your agents to streamline collaboration, automate communication tasks, and keep your teams aligned. {/* MANUAL-CONTENT-END */} + ## Usage Instructions Integrate Microsoft Teams functionality to manage messages. Read content from existing messages and write to messages using OAuth authentication. Supports text content manipulation for message creation and editing. diff --git a/apps/docs/content/docs/tools/outlook.mdx b/apps/docs/content/docs/tools/outlook.mdx new file mode 100644 index 00000000000..5abe88ade15 --- /dev/null +++ b/apps/docs/content/docs/tools/outlook.mdx @@ -0,0 +1,166 @@ +--- +title: Outlook +description: Access Outlook +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `} +/> + +{/* MANUAL-CONTENT-START:intro */} +[Microsoft Outlook](https://outlook.office365.com) is a comprehensive email and calendar platform that helps users manage communications, schedules, and tasks efficiently. As part of Microsoft's productivity suite, Outlook offers robust tools for sending and organizing emails, coordinating meetings, and integrating seamlessly with Microsoft 365 applications — enabling individuals and teams to stay organized and connected across devices. + +With Microsoft Outlook, you can: + +- **Send and receive emails**: Communicate clearly and professionally with individuals or distribution lists +- **Manage calendars and events**: Schedule meetings, set reminders, and view availability +- **Organize your inbox**: Use folders, categories, and rules to keep your email streamlined +- **Access contacts and tasks**: Keep track of key people and action items in one place +- **Integrate with Microsoft 365**: Work seamlessly with Word, Excel, Teams, and other Microsoft apps +- **Access across devices**: Use Outlook on desktop, web, and mobile with real-time sync +- **Maintain privacy and security**: Leverage enterprise-grade encryption and compliance controls + +In Sim Studio, the Microsoft Outlook integration enables your agents to interact directly with email and calendar data programmatically. This allows for powerful automation scenarios such as sending custom email updates, parsing incoming messages for workflow triggers, creating calendar events, and managing task reminders. By connecting Sim Studio with Microsoft Outlook, you enable intelligent agents to automate communications, streamline scheduling, and maintain visibility into organizational correspondence — all within your workflow ecosystem. +{/* MANUAL-CONTENT-END */} + +## Usage Instructions + +Integrate Outlook functionality to read, draft, andsend email messages within your workflow. Automate email communications and process email content using OAuth authentication. + + + +## Tools + +### `outlook_send` + +Send emails using Outlook + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | Access token for Outlook API | +| `to` | string | Yes | Recipient email address | +| `subject` | string | Yes | Email subject | +| `body` | string | Yes | Email body content | + +#### Output + +| Parameter | Type | +| --------- | ---- | +| `message` | string | +| `results` | string | +| `timestamp` | string | + +### `outlook_draft` + +Draft emails using Outlook + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | Access token for Outlook API | +| `to` | string | Yes | Recipient email address | +| `subject` | string | Yes | Email subject | +| `body` | string | Yes | Email body content | + +#### Output + +| Parameter | Type | +| --------- | ---- | +| `message` | string | +| `results` | string | +| `subject` | string | +| `status` | string | +| `timestamp` | string | + +### `outlook_read` + +Read emails from Outlook + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `accessToken` | string | Yes | OAuth access token for Outlook | +| `folder` | string | No | Folder ID to read emails from \(default: Inbox\) | +| `maxResults` | number | No | Maximum number of emails to retrieve \(default: 1, max: 10\) | +| `messageId` | string | No | Message ID to read | + +#### Output + +| Parameter | Type | +| --------- | ---- | +| `message` | string | +| `results` | string | + + + +## Block Configuration + +### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `operation` | string | Yes | Operation | + + + +### Outputs + +| Output | Type | Description | +| ------ | ---- | ----------- | +| `response` | object | Output from response | +| ↳ `message` | string | message of the response | +| ↳ `results` | json | results of the response | + + +## Notes + +- Category: `tools` +- Type: `outlook` diff --git a/apps/sim/app/api/auth/oauth/outlook/folders/route.ts b/apps/sim/app/api/auth/oauth/outlook/folders/route.ts index 2518fbf2a61..1c4daa648e5 100644 --- a/apps/sim/app/api/auth/oauth/outlook/folders/route.ts +++ b/apps/sim/app/api/auth/oauth/outlook/folders/route.ts @@ -27,7 +27,11 @@ export async function GET(request: Request) { return NextResponse.json({ error: 'Authentication required' }, { status: 401 }) } - const accessToken = await refreshAccessTokenIfNeeded(credentialId, userId, crypto.randomUUID().slice(0, 8)) + const accessToken = await refreshAccessTokenIfNeeded( + credentialId, + userId, + crypto.randomUUID().slice(0, 8) + ) if (!accessToken) { logger.error('Failed to get access token', { credentialId, userId }) @@ -118,4 +122,4 @@ export async function GET(request: Request) { { status: 500 } ) } -} \ No newline at end of file +} diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx index a6e8cfb5176..c86334540b3 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx @@ -275,7 +275,8 @@ export function FolderSelector({ const iconSize = size === 'sm' ? 'h-4 w-4' : 'h-5 w-5' if (provider === 'gmail') { return - } else if (provider === 'outlook') { + } + if (provider === 'outlook') { return } return null @@ -341,7 +342,10 @@ export function FolderSelector({ )} - + {isLoading ? ( diff --git a/apps/sim/blocks/blocks/outlook.ts b/apps/sim/blocks/blocks/outlook.ts index 01408b66dd2..4c2c8247392 100644 --- a/apps/sim/blocks/blocks/outlook.ts +++ b/apps/sim/blocks/blocks/outlook.ts @@ -1,8 +1,14 @@ import { OutlookIcon } from '@/components/icons' -import { OutlookReadResponse, OutlookSendResponse, OutlookDraftResponse } from '@/tools/outlook/types' -import { BlockConfig } from '../types' +import type { + OutlookDraftResponse, + OutlookReadResponse, + OutlookSendResponse, +} from '@/tools/outlook/types' +import type { BlockConfig } from '../types' -export const OutlookBlock: BlockConfig = { +export const OutlookBlock: BlockConfig< + OutlookReadResponse | OutlookSendResponse | OutlookDraftResponse +> = { type: 'outlook', name: 'Outlook', description: 'Access Outlook', @@ -78,37 +84,33 @@ export const OutlookBlock: BlockConfig { - switch (params.operation) { - case 'send_outlook': - return 'outlook_send' - case 'read_outlook': - return 'outlook_read' - case 'draft_outlook': - return 'outlook_draft' - default: - throw new Error(`Invalid Outlook operation: ${params.operation}`) - } + switch (params.operation) { + case 'send_outlook': + return 'outlook_send' + case 'read_outlook': + return 'outlook_read' + case 'draft_outlook': + return 'outlook_draft' + default: + throw new Error(`Invalid Outlook operation: ${params.operation}`) + } }, params: (params) => { // Pass the credential directly from the credential field diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 6070d88a823..66f4568983b 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -2561,43 +2561,113 @@ export function OutlookIcon(props: SVGProps) { return ( + + + + + + + + + - - - - - - - - - - - + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - + + + ) } diff --git a/apps/sim/lib/oauth.ts b/apps/sim/lib/oauth.ts index 9d7a0736d94..ba295661b05 100644 --- a/apps/sim/lib/oauth.ts +++ b/apps/sim/lib/oauth.ts @@ -13,8 +13,8 @@ import { JiraIcon, MicrosoftIcon, MicrosoftTeamsIcon, - OutlookIcon, NotionIcon, + OutlookIcon, SupabaseIcon, xIcon, } from '@/components/icons' diff --git a/apps/sim/tools/outlook/draft.ts b/apps/sim/tools/outlook/draft.ts index 05510a1d6b3..62152f98eca 100644 --- a/apps/sim/tools/outlook/draft.ts +++ b/apps/sim/tools/outlook/draft.ts @@ -1,6 +1,5 @@ -import { ToolConfig } from '../types' -import { OutlookDraftParams, OutlookDraftResponse } from './types' - +import type { ToolConfig } from '../types' +import type { OutlookDraftParams, OutlookDraftResponse } from './types' export const outlookDraftTool: ToolConfig = { id: 'outlook_draft', @@ -38,76 +37,76 @@ export const outlookDraftTool: ToolConfig { - return `https://graph.microsoft.com/v1.0/me/messages` + return `https://graph.microsoft.com/v1.0/me/messages` }, - method: 'POST', - headers: (params) => { - // Validate access token - if (!params.accessToken) { - throw new Error('Access token is required') - } - - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - body: (params: OutlookDraftParams): Record => { - return { - subject: params.subject, - body: { - contentType: "Text", - content: params.body, - }, - toRecipients: [ - { - emailAddress: { - address: params.to, - }, - }, - ], - } - }, + method: 'POST', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } }, - transformResponse: async (response) => { - if (!response.ok) { - let errorData - try { - errorData = await response.json() - } catch { - throw new Error('Failed to draft email') - } - throw new Error(errorData.error?.message || 'Failed to draft email') - } - - // Outlook draft API returns the created message object - const data = await response.json() - - return { - success: true, - output: { - message: 'Email drafted successfully', - results: { - id: data.id, - subject: data.subject, - status: 'drafted', - timestamp: new Date().toISOString(), + body: (params: OutlookDraftParams): Record => { + return { + subject: params.subject, + body: { + contentType: 'Text', + content: params.body, + }, + toRecipients: [ + { + emailAddress: { + address: params.to, }, }, - } - }, - - transformError: (error) => { - // Handle Google API error format - if (error.error?.message) { - if (error.error.message.includes('invalid authentication credentials')) { - return 'Invalid or expired access token. Please reauthenticate.' - } - if (error.error.message.includes('quota')) { - return 'Outlook API quota exceeded. Please try again later.' - } - return error.error.message - } - return error.message || 'An unexpected error occurred while sending email' + ], + } + }, + }, + transformResponse: async (response) => { + if (!response.ok) { + let errorData + try { + errorData = await response.json() + } catch { + throw new Error('Failed to draft email') + } + throw new Error(errorData.error?.message || 'Failed to draft email') + } + + // Outlook draft API returns the created message object + const data = await response.json() + + return { + success: true, + output: { + message: 'Email drafted successfully', + results: { + id: data.id, + subject: data.subject, + status: 'drafted', + timestamp: new Date().toISOString(), + }, }, } + }, + + transformError: (error) => { + // Handle Google API error format + if (error.error?.message) { + if (error.error.message.includes('invalid authentication credentials')) { + return 'Invalid or expired access token. Please reauthenticate.' + } + if (error.error.message.includes('quota')) { + return 'Outlook API quota exceeded. Please try again later.' + } + return error.error.message + } + return error.message || 'An unexpected error occurred while sending email' + }, +} diff --git a/apps/sim/tools/outlook/index.ts b/apps/sim/tools/outlook/index.ts index 1cbb65c0524..6cd4ed142fa 100644 --- a/apps/sim/tools/outlook/index.ts +++ b/apps/sim/tools/outlook/index.ts @@ -2,4 +2,4 @@ import { outlookDraftTool } from './draft' import { outlookReadTool } from './read' import { outlookSendTool } from './send' -export { outlookDraftTool, outlookReadTool, outlookSendTool } \ No newline at end of file +export { outlookDraftTool, outlookReadTool, outlookSendTool } diff --git a/apps/sim/tools/outlook/read.ts b/apps/sim/tools/outlook/read.ts index 6d08f62c166..fe1cbf26a89 100644 --- a/apps/sim/tools/outlook/read.ts +++ b/apps/sim/tools/outlook/read.ts @@ -1,5 +1,5 @@ -import { ToolConfig } from '../types' -import { OutlookReadParams, OutlookReadResponse } from './types' +import type { ToolConfig } from '../types' +import type { OutlookReadParams, OutlookReadResponse } from './types' export const outlookReadTool: ToolConfig = { id: 'outlook_read', @@ -31,116 +31,118 @@ export const outlookReadTool: ToolConfig type: 'string', required: false, description: 'Message ID to read', - } + }, }, request: { url: (params) => { - // If messageId is provided, fetch that specific message - if (params.messageId) { - return `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` - } - - // Set max results (default to 1 for simplicity, max 10) - const maxResults = params.maxResults ? Math.min(params.maxResults, 10) : 1 - - // If folder is provided, read from that specific folder - if (params.folder) { - return `https://graph.microsoft.com/v1.0/me/mailFolders/${params.folder}/messages?$top=${maxResults}&$orderby=createdDateTime desc` - } - - // Otherwise fetch from all messages (default behavior) - return `https://graph.microsoft.com/v1.0/me/messages?$top=${maxResults}&$orderby=createdDateTime desc` - }, - method: 'GET', - headers: (params) => { - // Validate access token - if (!params.accessToken) { - throw new Error('Access token is required') - } - - return { - Authorization: `Bearer ${params.accessToken}`, - } - }, + // If messageId is provided, fetch that specific message + if (params.messageId) { + return `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` + } + + // Set max results (default to 1 for simplicity, max 10) + const maxResults = params.maxResults ? Math.min(params.maxResults, 10) : 1 + + // If folder is provided, read from that specific folder + if (params.folder) { + return `https://graph.microsoft.com/v1.0/me/mailFolders/${params.folder}/messages?$top=${maxResults}&$orderby=createdDateTime desc` + } + + // Otherwise fetch from all messages (default behavior) + return `https://graph.microsoft.com/v1.0/me/messages?$top=${maxResults}&$orderby=createdDateTime desc` }, - transformResponse: async (response: Response) => { - if (!response.ok) { - const errorText = await response.text() - throw new Error(`Failed to read Outlook mail: ${errorText}`) + method: 'GET', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') } - - const data = await response.json() - - // Microsoft Graph API returns messages in a 'value' array - const messages = data.value || [] - - if (messages.length === 0) { - return { - success: true, - output: { - message: 'No mail found.', - results: [], - }, - } + + return { + Authorization: `Bearer ${params.accessToken}`, } + }, + }, + transformResponse: async (response: Response) => { + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to read Outlook mail: ${errorText}`) + } - // Clean up the message data to only include essential fields - const cleanedMessages = messages.map((message: any) => ({ - id: message.id, - subject: message.subject, - bodyPreview: message.bodyPreview, - body: { - contentType: message.body?.contentType, - content: message.body?.content, - }, - sender: { - name: message.sender?.emailAddress?.name, - address: message.sender?.emailAddress?.address, - }, - from: { - name: message.from?.emailAddress?.name, - address: message.from?.emailAddress?.address, + const data = await response.json() + + // Microsoft Graph API returns messages in a 'value' array + const messages = data.value || [] + + if (messages.length === 0) { + return { + success: true, + output: { + message: 'No mail found.', + results: [], }, - toRecipients: message.toRecipients?.map((recipient: any) => ({ + } + } + + // Clean up the message data to only include essential fields + const cleanedMessages = messages.map((message: any) => ({ + id: message.id, + subject: message.subject, + bodyPreview: message.bodyPreview, + body: { + contentType: message.body?.contentType, + content: message.body?.content, + }, + sender: { + name: message.sender?.emailAddress?.name, + address: message.sender?.emailAddress?.address, + }, + from: { + name: message.from?.emailAddress?.name, + address: message.from?.emailAddress?.address, + }, + toRecipients: + message.toRecipients?.map((recipient: any) => ({ name: recipient.emailAddress?.name, address: recipient.emailAddress?.address, })) || [], - ccRecipients: message.ccRecipients?.map((recipient: any) => ({ + ccRecipients: + message.ccRecipients?.map((recipient: any) => ({ name: recipient.emailAddress?.name, address: recipient.emailAddress?.address, })) || [], - receivedDateTime: message.receivedDateTime, - sentDateTime: message.sentDateTime, - hasAttachments: message.hasAttachments, - isRead: message.isRead, - importance: message.importance, - })) - - return { - success: true, - output: { - message: `Successfully read ${cleanedMessages.length} email${cleanedMessages.length === 1 ? '' : 's'}`, - results: cleanedMessages, - }, + receivedDateTime: message.receivedDateTime, + sentDateTime: message.sentDateTime, + hasAttachments: message.hasAttachments, + isRead: message.isRead, + importance: message.importance, + })) + + return { + success: true, + output: { + message: `Successfully read ${cleanedMessages.length} email${cleanedMessages.length === 1 ? '' : 's'}`, + results: cleanedMessages, + }, + } + }, + transformError: (error) => { + // If it's an Error instance with a message, use that + if (error instanceof Error) { + return error.message + } + + // If it's an object with an error or message property + if (typeof error === 'object' && error !== null) { + if (error.error) { + return typeof error.error === 'string' ? error.error : JSON.stringify(error.error) } - }, - transformError: (error) => { - // If it's an Error instance with a message, use that - if (error instanceof Error) { + if (error.message) { return error.message } - - // If it's an object with an error or message property - if (typeof error === 'object' && error !== null) { - if (error.error) { - return typeof error.error === 'string' ? error.error : JSON.stringify(error.error) - } - if (error.message) { - return error.message - } - } - - // Default fallback message - return 'An error occurred while reading Microsoft Teams chat' - }, - } \ No newline at end of file + } + + // Default fallback message + return 'An error occurred while reading Microsoft Teams chat' + }, +} diff --git a/apps/sim/tools/outlook/send.ts b/apps/sim/tools/outlook/send.ts index 0a03092480b..72400a16f5d 100644 --- a/apps/sim/tools/outlook/send.ts +++ b/apps/sim/tools/outlook/send.ts @@ -1,6 +1,5 @@ -import { ToolConfig } from '../types' -import { OutlookSendParams, OutlookSendResponse } from './types' - +import type { ToolConfig } from '../types' +import type { OutlookSendParams, OutlookSendResponse } from './types' export const outlookSendTool: ToolConfig = { id: 'outlook_send', @@ -38,75 +37,75 @@ export const outlookSendTool: ToolConfig request: { url: (params) => { - return `https://graph.microsoft.com/v1.0/me/sendMail` + return `https://graph.microsoft.com/v1.0/me/sendMail` }, - method: 'POST', - headers: (params) => { - // Validate access token - if (!params.accessToken) { - throw new Error('Access token is required') - } - - return { - Authorization: `Bearer ${params.accessToken}`, - 'Content-Type': 'application/json', - } - }, - body: (params: OutlookSendParams): Record => { - return { - message: { - subject: params.subject, - body: { - contentType: "Text", - content: params.body, - }, - toRecipients: [ - { - emailAddress: { - address: params.to, - }, - }, - ], - }, - saveToSentItems: true, - } - }, + method: 'POST', + headers: (params) => { + // Validate access token + if (!params.accessToken) { + throw new Error('Access token is required') + } + + return { + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + } }, - transformResponse: async (response) => { - if (!response.ok) { - let errorData - try { - errorData = await response.json() - } catch { - throw new Error('Failed to send email') - } - throw new Error(errorData.error?.message || 'Failed to send email') - } - - // Outlook sendMail API returns empty body on success - return { - success: true, - output: { - message: 'Email sent successfully', - results: { - status: 'sent', - timestamp: new Date().toISOString(), - }, + body: (params: OutlookSendParams): Record => { + return { + message: { + subject: params.subject, + body: { + contentType: 'Text', + content: params.body, }, - } - }, - - transformError: (error) => { - // Handle Google API error format - if (error.error?.message) { - if (error.error.message.includes('invalid authentication credentials')) { - return 'Invalid or expired access token. Please reauthenticate.' - } - if (error.error.message.includes('quota')) { - return 'Outlook API quota exceeded. Please try again later.' - } - return error.error.message - } - return error.message || 'An unexpected error occurred while sending email' + toRecipients: [ + { + emailAddress: { + address: params.to, + }, + }, + ], + }, + saveToSentItems: true, + } + }, + }, + transformResponse: async (response) => { + if (!response.ok) { + let errorData + try { + errorData = await response.json() + } catch { + throw new Error('Failed to send email') + } + throw new Error(errorData.error?.message || 'Failed to send email') + } + + // Outlook sendMail API returns empty body on success + return { + success: true, + output: { + message: 'Email sent successfully', + results: { + status: 'sent', + timestamp: new Date().toISOString(), + }, }, - } \ No newline at end of file + } + }, + + transformError: (error) => { + // Handle Google API error format + if (error.error?.message) { + if (error.error.message.includes('invalid authentication credentials')) { + return 'Invalid or expired access token. Please reauthenticate.' + } + if (error.error.message.includes('quota')) { + return 'Outlook API quota exceeded. Please try again later.' + } + return error.error.message + } + return error.message || 'An unexpected error occurred while sending email' + }, +} diff --git a/apps/sim/tools/outlook/types.ts b/apps/sim/tools/outlook/types.ts index 9d6dd81f2df..b698b711ddf 100644 --- a/apps/sim/tools/outlook/types.ts +++ b/apps/sim/tools/outlook/types.ts @@ -1,4 +1,4 @@ -import { ToolResponse } from '../types' +import type { ToolResponse } from '../types' export interface OutlookSendParams { accessToken: string @@ -40,4 +40,4 @@ export interface OutlookDraftResponse extends ToolResponse { message: string results: any } -} \ No newline at end of file +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 56625e80278..46b6aeebf4d 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -57,6 +57,7 @@ import { import { mistralParserTool } from './mistral' import { notionReadTool, notionWriteTool } from './notion' import { imageTool, embeddingsTool as openAIEmbeddings } from './openai' +import { outlookDraftTool, outlookReadTool, outlookSendTool } from './outlook' import { perplexityChatTool } from './perplexity' import { pineconeFetchTool, @@ -68,7 +69,6 @@ import { import { redditGetCommentsTool, redditGetPostsTool, redditHotPostsTool } from './reddit' import { s3GetObjectTool } from './s3' import { opportunitiesTool as salesforceOpportunities } from './salesforce/opportunities' -import { outlookReadTool, outlookSendTool, outlookDraftTool } from './outlook' import { searchTool as serperSearch } from './serper' import { slackMessageTool } from './slack' import { stagehandAgentTool, stagehandExtractTool } from './stagehand' From 7dba9eaaa654eb07483e69ace837f95976a05a9b Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Sat, 24 May 2025 19:25:15 -0700 Subject: [PATCH 5/7] fix: added greptile comments --- apps/docs/content/docs/tools/outlook.mdx | 4 ++-- apps/sim/app/api/auth/oauth/outlook/folders/route.ts | 9 ++++++++- apps/sim/tools/outlook/read.ts | 9 --------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/apps/docs/content/docs/tools/outlook.mdx b/apps/docs/content/docs/tools/outlook.mdx index 5abe88ade15..fa7415c8a42 100644 --- a/apps/docs/content/docs/tools/outlook.mdx +++ b/apps/docs/content/docs/tools/outlook.mdx @@ -69,7 +69,7 @@ In Sim Studio, the Microsoft Outlook integration enables your agents to interact ## Usage Instructions -Integrate Outlook functionality to read, draft, andsend email messages within your workflow. Automate email communications and process email content using OAuth authentication. +Integrate Outlook functionality to read, draft, and send email messages within your workflow. Automate email communications and process email content using OAuth authentication. @@ -137,7 +137,7 @@ Read emails from Outlook | Parameter | Type | | --------- | ---- | | `message` | string | -| `results` | string | +| `results` | json | diff --git a/apps/sim/app/api/auth/oauth/outlook/folders/route.ts b/apps/sim/app/api/auth/oauth/outlook/folders/route.ts index 1c4daa648e5..fc76a86063b 100644 --- a/apps/sim/app/api/auth/oauth/outlook/folders/route.ts +++ b/apps/sim/app/api/auth/oauth/outlook/folders/route.ts @@ -7,6 +7,13 @@ export const dynamic = 'force-dynamic' const logger = createLogger('OutlookFoldersAPI') +interface OutlookFolder { + id: string; + displayName: string; + totalItemCount?: number; + unreadItemCount?: number; + } + export async function GET(request: Request) { try { const session = await getSession() @@ -78,7 +85,7 @@ export async function GET(request: Request) { const folders = data.value || [] // Transform folders to match the expected format - const transformedFolders = folders.map((folder: any) => ({ + const transformedFolders = folders.map((folder: OutlookFolder) => ({ id: folder.id, name: folder.displayName, type: 'folder', diff --git a/apps/sim/tools/outlook/read.ts b/apps/sim/tools/outlook/read.ts index fe1cbf26a89..e8c936b1c7a 100644 --- a/apps/sim/tools/outlook/read.ts +++ b/apps/sim/tools/outlook/read.ts @@ -27,18 +27,9 @@ export const outlookReadTool: ToolConfig required: false, description: 'Maximum number of emails to retrieve (default: 1, max: 10)', }, - messageId: { - type: 'string', - required: false, - description: 'Message ID to read', - }, }, request: { url: (params) => { - // If messageId is provided, fetch that specific message - if (params.messageId) { - return `https://graph.microsoft.com/v1.0/me/messages/${params.messageId}` - } // Set max results (default to 1 for simplicity, max 10) const maxResults = params.maxResults ? Math.min(params.maxResults, 10) : 1 From 8ba716fa38bf3ee99dfd73851fd75580ee997682 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Sat, 24 May 2025 19:35:48 -0700 Subject: [PATCH 6/7] added greptile and bun lint --- .../api/auth/oauth/outlook/folders/route.ts | 10 +-- apps/sim/tools/outlook/draft.ts | 6 +- apps/sim/tools/outlook/read.ts | 27 +++--- apps/sim/tools/outlook/send.ts | 4 +- apps/sim/tools/outlook/types.ts | 88 ++++++++++++++++++- 5 files changed, 114 insertions(+), 21 deletions(-) diff --git a/apps/sim/app/api/auth/oauth/outlook/folders/route.ts b/apps/sim/app/api/auth/oauth/outlook/folders/route.ts index fc76a86063b..df0897ebbdf 100644 --- a/apps/sim/app/api/auth/oauth/outlook/folders/route.ts +++ b/apps/sim/app/api/auth/oauth/outlook/folders/route.ts @@ -8,11 +8,11 @@ export const dynamic = 'force-dynamic' const logger = createLogger('OutlookFoldersAPI') interface OutlookFolder { - id: string; - displayName: string; - totalItemCount?: number; - unreadItemCount?: number; - } + id: string + displayName: string + totalItemCount?: number + unreadItemCount?: number +} export async function GET(request: Request) { try { diff --git a/apps/sim/tools/outlook/draft.ts b/apps/sim/tools/outlook/draft.ts index 62152f98eca..bdbb4888e92 100644 --- a/apps/sim/tools/outlook/draft.ts +++ b/apps/sim/tools/outlook/draft.ts @@ -55,7 +55,7 @@ export const outlookDraftTool: ToolConfig { - // Handle Google API error format + // Handle Outlook API error format if (error.error?.message) { if (error.error.message.includes('invalid authentication credentials')) { return 'Invalid or expired access token. Please reauthenticate.' @@ -107,6 +107,6 @@ export const outlookDraftTool: ToolConfig = { id: 'outlook_read', @@ -30,9 +36,10 @@ export const outlookReadTool: ToolConfig }, request: { url: (params) => { - - // Set max results (default to 1 for simplicity, max 10) - const maxResults = params.maxResults ? Math.min(params.maxResults, 10) : 1 + // Set max results (default to 1 for simplicity, max 10) with no negative values + const maxResults = params.maxResults + ? Math.max(1, Math.min(Math.abs(params.maxResults), 10)) + : 1 // If folder is provided, read from that specific folder if (params.folder) { @@ -60,7 +67,7 @@ export const outlookReadTool: ToolConfig throw new Error(`Failed to read Outlook mail: ${errorText}`) } - const data = await response.json() + const data: OutlookMessagesResponse = await response.json() // Microsoft Graph API returns messages in a 'value' array const messages = data.value || [] @@ -76,7 +83,7 @@ export const outlookReadTool: ToolConfig } // Clean up the message data to only include essential fields - const cleanedMessages = messages.map((message: any) => ({ + const cleanedMessages: CleanedOutlookMessage[] = messages.map((message: OutlookMessage) => ({ id: message.id, subject: message.subject, bodyPreview: message.bodyPreview, @@ -93,12 +100,12 @@ export const outlookReadTool: ToolConfig address: message.from?.emailAddress?.address, }, toRecipients: - message.toRecipients?.map((recipient: any) => ({ + message.toRecipients?.map((recipient) => ({ name: recipient.emailAddress?.name, address: recipient.emailAddress?.address, })) || [], ccRecipients: - message.ccRecipients?.map((recipient: any) => ({ + message.ccRecipients?.map((recipient) => ({ name: recipient.emailAddress?.name, address: recipient.emailAddress?.address, })) || [], @@ -112,7 +119,7 @@ export const outlookReadTool: ToolConfig return { success: true, output: { - message: `Successfully read ${cleanedMessages.length} email${cleanedMessages.length === 1 ? '' : 's'}`, + message: `Successfully read ${cleanedMessages.length} email(s).`, results: cleanedMessages, }, } @@ -134,6 +141,6 @@ export const outlookReadTool: ToolConfig } // Default fallback message - return 'An error occurred while reading Microsoft Teams chat' + return 'An error occurred while reading Outlook email' }, } diff --git a/apps/sim/tools/outlook/send.ts b/apps/sim/tools/outlook/send.ts index 72400a16f5d..64bdff42b81 100644 --- a/apps/sim/tools/outlook/send.ts +++ b/apps/sim/tools/outlook/send.ts @@ -56,7 +56,7 @@ export const outlookSendTool: ToolConfig message: { subject: params.subject, body: { - contentType: 'Text', + contentType: ['Text', 'HTML'], content: params.body, }, toRecipients: [ @@ -96,7 +96,7 @@ export const outlookSendTool: ToolConfig }, transformError: (error) => { - // Handle Google API error format + // Handle Outlook API error format if (error.error?.message) { if (error.error.message.includes('invalid authentication credentials')) { return 'Invalid or expired access token. Please reauthenticate.' diff --git a/apps/sim/tools/outlook/types.ts b/apps/sim/tools/outlook/types.ts index b698b711ddf..5ad7cae54f4 100644 --- a/apps/sim/tools/outlook/types.ts +++ b/apps/sim/tools/outlook/types.ts @@ -24,7 +24,7 @@ export interface OutlookReadParams { export interface OutlookReadResponse extends ToolResponse { output: { message: string - results: any + results: CleanedOutlookMessage[] } } @@ -41,3 +41,89 @@ export interface OutlookDraftResponse extends ToolResponse { results: any } } + +// Outlook API response interfaces +export interface OutlookEmailAddress { + name?: string + address: string +} + +export interface OutlookRecipient { + emailAddress: OutlookEmailAddress +} + +export interface OutlookMessageBody { + contentType?: string + content?: string +} + +export interface OutlookMessage { + id: string + subject?: string + bodyPreview?: string + body?: OutlookMessageBody + sender?: OutlookRecipient + from?: OutlookRecipient + toRecipients?: OutlookRecipient[] + ccRecipients?: OutlookRecipient[] + bccRecipients?: OutlookRecipient[] + receivedDateTime?: string + sentDateTime?: string + hasAttachments?: boolean + isRead?: boolean + importance?: string + // Add other common fields + '@odata.etag'?: string + createdDateTime?: string + lastModifiedDateTime?: string + changeKey?: string + categories?: string[] + internetMessageId?: string + parentFolderId?: string + conversationId?: string + conversationIndex?: string + isDeliveryReceiptRequested?: boolean | null + isReadReceiptRequested?: boolean + isDraft?: boolean + webLink?: string + inferenceClassification?: string + replyTo?: OutlookRecipient[] +} + +export interface OutlookMessagesResponse { + '@odata.context'?: string + '@odata.nextLink'?: string + value: OutlookMessage[] +} + +// Cleaned message interface for our response +export interface CleanedOutlookMessage { + id: string + subject?: string + bodyPreview?: string + body?: { + contentType?: string + content?: string + } + sender?: { + name?: string + address?: string + } + from?: { + name?: string + address?: string + } + toRecipients: Array<{ + name?: string + address?: string + }> + ccRecipients: Array<{ + name?: string + address?: string + }> + receivedDateTime?: string + sentDateTime?: string + hasAttachments?: boolean + isRead?: boolean + importance?: string +} From 6d2379012eb70674be5af40ba0a7e29b07766904 Mon Sep 17 00:00:00 2001 From: Adam Gough Date: Sat, 24 May 2025 19:40:12 -0700 Subject: [PATCH 7/7] got rid of HTML --- apps/sim/tools/outlook/draft.ts | 2 +- apps/sim/tools/outlook/send.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/tools/outlook/draft.ts b/apps/sim/tools/outlook/draft.ts index bdbb4888e92..677ef488f0e 100644 --- a/apps/sim/tools/outlook/draft.ts +++ b/apps/sim/tools/outlook/draft.ts @@ -55,7 +55,7 @@ export const outlookDraftTool: ToolConfig message: { subject: params.subject, body: { - contentType: ['Text', 'HTML'], + contentType: 'Text', content: params.body, }, toRecipients: [