feat(tools): created outlook tools/block (#409)

* feat: first round of tools for outlook

* added more

* outlook finished

* added bun and docs

* fix: added greptile comments

* added greptile and bun lint

* got rid of HTML

---------

Co-authored-by: Adam Gough <adamgough@Adams-MacBook-Pro.local>
This commit is contained in:
Adam Gough
2025-05-24 20:57:49 -07:00
committed by GitHub
parent 45c92067e2
commit 79b761c022
18 changed files with 1252 additions and 62 deletions

View File

@@ -27,6 +27,7 @@
"microsoft_teams",
"notion",
"openai",
"outlook",
"perplexity",
"pinecone",
"reddit",

View File

@@ -9,30 +9,70 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
type="microsoft_teams"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2228.833 2073.333"
>
<path fill="#5059C9" d="M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z"/>
<circle fill="#5059C9" cx="1943.75" cy="440.583" r="233.25"/>
<circle fill="#7B83EB" cx="1218.083" cy="336.917" r="336.917"/>
<path fill="#7B83EB" d="M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z"/>
<path opacity=".1" d="M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z"/>
<path opacity=".2" d="M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"/>
<path opacity=".2" d="M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z"/>
<path opacity=".2" d="M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z"/>
<path opacity=".1" d="M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z"/>
<path opacity=".2" d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"/>
<path opacity=".2" d="M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z"/>
<path opacity=".2" d="M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z"/>
<linearGradient id="a" gradientUnits="userSpaceOnUse" x1="198.099" y1="1683.0726" x2="942.2344" y2="394.2607" gradientTransform="matrix(1 0 0 -1 0 2075.3333)">
<stop offset="0" stop-color="#5a62c3"/>
<stop offset=".5" stop-color="#4d55bd"/>
<stop offset="1" stop-color="#3940ab"/>
</linearGradient>
<path fill="url(#a)" d="M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z"/>
<path fill="#FFF" d="M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z"/>
iconSvg={`<svg className="block-icon" xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2228.833 2073.333'>
<path
fill='#5059C9'
d='M1554.637,777.5h575.713c54.391,0,98.483,44.092,98.483,98.483c0,0,0,0,0,0v524.398 c0,199.901-162.051,361.952-361.952,361.952h0h-1.711c-199.901,0.028-361.975-162-362.004-361.901c0-0.017,0-0.034,0-0.052V828.971 C1503.167,800.544,1526.211,777.5,1554.637,777.5L1554.637,777.5z'
/>
<circle fill='#5059C9' cx='1943.75' cy='440.583' r='233.25' />
<circle fill='#7B83EB' cx='1218.083' cy='336.917' r='336.917' />
<path
fill='#7B83EB'
d='M1667.323,777.5H717.01c-53.743,1.33-96.257,45.931-95.01,99.676v598.105 c-7.505,322.519,247.657,590.16,570.167,598.053c322.51-7.893,577.671-275.534,570.167-598.053V877.176 C1763.579,823.431,1721.066,778.83,1667.323,777.5z'
/>
<path
opacity='.1'
d='M1244,777.5v838.145c-0.258,38.435-23.549,72.964-59.09,87.598 c-11.316,4.787-23.478,7.254-35.765,7.257H667.613c-6.738-17.105-12.958-34.21-18.142-51.833 c-18.144-59.477-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1244z'
/>
<path
opacity='.2'
d='M1192.167,777.5v889.978c-0.002,12.287-2.47,24.449-7.257,35.765 c-14.634,35.541-49.163,58.833-87.598,59.09H691.975c-8.812-17.105-17.105-34.21-24.362-51.833 c-7.257-17.623-12.958-34.21-18.142-51.833c-18.144-59.476-27.402-121.307-27.472-183.49V877.02 c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z'
/>
<path
opacity='.2'
d='M1192.167,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855h-447.84 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1192.167z'
/>
<path
opacity='.2'
d='M1140.333,777.5v786.312c-0.395,52.223-42.632,94.46-94.855,94.855H649.472 c-18.144-59.476-27.402-121.307-27.472-183.49V877.02c-1.246-53.659,41.198-98.19,94.855-99.52H1140.333z'
/>
<path
opacity='.1'
d='M1244,509.522v163.275c-8.812,0.518-17.105,1.037-25.917,1.037 c-8.812,0-17.105-0.518-25.917-1.037c-17.496-1.161-34.848-3.937-51.833-8.293c-104.963-24.857-191.679-98.469-233.25-198.003 c-7.153-16.715-12.706-34.071-16.587-51.833h258.648C1201.449,414.866,1243.801,457.217,1244,509.522z'
/>
<path
opacity='.2'
d='M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z'
/>
<path
opacity='.2'
d='M1192.167,561.355v111.442c-17.496-1.161-34.848-3.937-51.833-8.293 c-104.963-24.857-191.679-98.469-233.25-198.003h190.228C1149.616,466.699,1191.968,509.051,1192.167,561.355z'
/>
<path
opacity='.2'
d='M1140.333,561.355v103.148c-104.963-24.857-191.679-98.469-233.25-198.003 h138.395C1097.783,466.699,1140.134,509.051,1140.333,561.355z'
/>
<linearGradient
id='a'
gradientUnits='userSpaceOnUse'
x1='198.099'
y1='1683.0726'
x2='942.2344'
y2='394.2607'
gradientTransform='matrix(1 0 0 -1 0 2075.3333)'
>
<stop offset='0' stop-color='#5a62c3' />
<stop offset='.5' stop-color='#4d55bd' />
<stop offset='1' stop-color='#3940ab' />
</linearGradient>
<path
fill='url(#a)'
d='M95.01,466.5h950.312c52.473,0,95.01,42.538,95.01,95.01v950.312c0,52.473-42.538,95.01-95.01,95.01 H95.01c-52.473,0-95.01-42.538-95.01-95.01V561.51C0,509.038,42.538,466.5,95.01,466.5z'
/>
<path
fill='#FFF'
d='M820.211,828.193H630.241v517.297H509.211V828.193H320.123V727.844h500.088V828.193z'
/>
</svg>`}
/>
@@ -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.

View File

@@ -0,0 +1,166 @@
---
title: Outlook
description: Access Outlook
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="outlook"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="Livello_1"
x="0px" y="0px"
viewBox="0 0 1831.085 1703.335"
enable-background="new 0 0 1831.085 1703.335"
>
<path fill="#0A2767" d="M1831.083,894.25c0.1-14.318-7.298-27.644-19.503-35.131h-0.213l-0.767-0.426l-634.492-375.585 c-2.74-1.851-5.583-3.543-8.517-5.067c-24.498-12.639-53.599-12.639-78.098,0c-2.934,1.525-5.777,3.216-8.517,5.067L446.486,858.693 l-0.766,0.426c-19.392,12.059-25.337,37.556-13.278,56.948c3.553,5.714,8.447,10.474,14.257,13.868l634.492,375.585 c2.749,1.835,5.592,3.527,8.517,5.068c24.498,12.639,53.599,12.639,78.098,0c2.925-1.541,5.767-3.232,8.517-5.068l634.492-375.585 C1823.49,922.545,1831.228,908.923,1831.083,894.25z"/>
<path fill="#0364B8" d="M520.453,643.477h416.38v381.674h-416.38V643.477z M1745.917,255.5V80.908 c1-43.652-33.552-79.862-77.203-80.908H588.204C544.552,1.046,510,37.256,511,80.908V255.5l638.75,170.333L1745.917,255.5z"/>
<path fill="#0078D4" d="M511,255.5h425.833v383.25H511V255.5z"/>
<path fill="#28A8EA" d="M1362.667,255.5H936.833v383.25L1362.667,1022h383.25V638.75L1362.667,255.5z"/>
<path fill="#0078D4" d="M936.833,638.75h425.833V1022H936.833V638.75z"/>
<path fill="#0364B8" d="M936.833,1022h425.833v383.25H936.833V1022z"/>
<path fill="#14447D" d="M520.453,1025.151h416.38v346.969h-416.38V1025.151z"/>
<path fill="#0078D4" d="M1362.667,1022h383.25v383.25h-383.25V1022z"/>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="1128.4584" y1="811.0833" x2="1128.4584" y2="1.9982" gradientTransform="matrix(1 0 0 -1 0 1705.3334)">
<stop offset="0" style={{ stopColor: '#35B8F1' }} />
<stop offset="1" style={{ stopColor: '#28A8EA' }} />
</linearGradient>
<path fill="url(#SVGID_1_)" d="M1811.58,927.593l-0.809,0.426l-634.492,356.848c-2.768,1.703-5.578,3.321-8.517,4.769 c-10.777,5.132-22.481,8.029-34.407,8.517l-34.663-20.27c-2.929-1.47-5.773-3.105-8.517-4.897L447.167,906.003h-0.298 l-21.036-11.753v722.384c0.328,48.196,39.653,87.006,87.849,86.7h1230.914c0.724,0,1.363-0.341,2.129-0.341 c10.18-0.651,20.216-2.745,29.808-6.217c4.145-1.756,8.146-3.835,11.966-6.217c2.853-1.618,7.75-5.152,7.75-5.152 c21.814-16.142,34.726-41.635,34.833-68.772V894.25C1831.068,908.067,1823.616,920.807,1811.58,927.593z"/>
<path opacity="0.5" fill="#0A2767" enable-background="new " d="M1797.017,891.397v44.287l-663.448,456.791L446.699,906.301 c0-0.235-0.191-0.426-0.426-0.426l0,0l-63.023-37.899v-31.938l25.976-0.426l54.932,31.512l1.277,0.426l4.684,2.981 c0,0,645.563,368.346,647.267,369.197l24.698,14.478c2.129-0.852,4.258-1.703,6.813-2.555 c1.278-0.852,640.879-360.681,640.879-360.681L1797.017,891.397z"/>
<path fill="#1490DF" d="M1811.58,927.593l-0.809,0.468l-634.492,356.848c-2.768,1.703-5.578,3.321-8.517,4.769 c-24.641,12.038-53.457,12.038-78.098,0c-2.918-1.445-5.76-3.037-8.517-4.769L446.657,928.061l-0.766-0.468 c-12.25-6.642-19.93-19.409-20.057-33.343v722.384c0.305,48.188,39.616,87.004,87.803,86.7c0.001,0,0.002,0,0.004,0h1229.636 c48.188,0.307,87.5-38.509,87.807-86.696c0-0.001,0-0.002,0-0.004V894.25C1831.068,908.067,1823.616,920.807,1811.58,927.593z"/>
<path opacity="0.1" enable-background="new " d="M1185.52,1279.629l-9.496,5.323c-2.752,1.752-5.595,3.359-8.517,4.812 c-10.462,5.135-21.838,8.146-33.47,8.857l241.405,285.479l421.107,101.476c11.539-8.716,20.717-20.178,26.7-33.343L1185.52,1279.629 z"/>
<path opacity="0.05" enable-background="new " d="M1228.529,1255.442l-52.505,29.51c-2.752,1.752-5.595,3.359-8.517,4.812 c-10.462,5.135-21.838,8.146-33.47,8.857l113.101,311.838l549.538,74.989c21.649-16.254,34.394-41.743,34.407-68.815v-9.326 L1228.529,1255.442z"/>
<path fill="#28A8EA" d="M514.833,1703.333h1228.316c18.901,0.096,37.335-5.874,52.59-17.033l-697.089-408.331 c-2.929-1.47-5.773-3.105-8.517-4.897L447.125,906.088h-0.298l-20.993-11.838v719.914 C425.786,1663.364,465.632,1703.286,514.833,1703.333C514.832,1703.333,514.832,1703.333,514.833,1703.333z"/>
<path opacity="0.1" enable-background="new " d="M1022,418.722v908.303c-0.076,31.846-19.44,60.471-48.971,72.392 c-9.148,3.931-19,5.96-28.957,5.962H425.833V383.25H511v-42.583h433.073C987.092,340.83,1021.907,375.702,1022,418.722z"/>
<path opacity="0.2" enable-background="new " d="M979.417,461.305v908.302c0.107,10.287-2.074,20.469-6.388,29.808 c-11.826,29.149-40.083,48.273-71.54,48.417H425.833V383.25h475.656c12.356-0.124,24.533,2.958,35.344,8.943 C962.937,405.344,979.407,432.076,979.417,461.305z"/>
<path opacity="0.2" enable-background="new " d="M979.417,461.305v823.136c-0.208,43-34.928,77.853-77.927,78.225H425.833V383.25 h475.656c12.356-0.124,24.533,2.958,35.344,8.943C962.937,405.344,979.407,432.076,979.417,461.305z"/>
<path opacity="0.2" enable-background="new " d="M936.833,461.305v823.136c-0.046,43.067-34.861,78.015-77.927,78.225H425.833 V383.25h433.072c43.062,0.023,77.951,34.951,77.927,78.013C936.833,461.277,936.833,461.291,936.833,461.305z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="162.7469" y1="1383.0741" x2="774.0864" y2="324.2592" gradientTransform="matrix(1 0 0 -1 0 1705.3334)">
<stop offset="0" style={{ stopColor: '#1784D9' }} />
<stop offset="0.5" style={{ stopColor: '#107AD5' }} />
<stop offset="1" style={{ stopColor: '#0A63C9' }} />
</linearGradient>
<path fill="url(#SVGID_2_)" d="M78.055,383.25h780.723c43.109,0,78.055,34.947,78.055,78.055v780.723 c0,43.109-34.946,78.055-78.055,78.055H78.055c-43.109,0-78.055-34.947-78.055-78.055V461.305 C0,418.197,34.947,383.25,78.055,383.25z"/>
<path fill="#FFFFFF" d="M243.96,710.631c19.238-40.988,50.29-75.289,89.17-98.495c43.057-24.651,92.081-36.94,141.675-35.515 c45.965-0.997,91.321,10.655,131.114,33.683c37.414,22.312,67.547,55.004,86.742,94.109c20.904,43.09,31.322,90.512,30.405,138.396 c1.013,50.043-9.706,99.628-31.299,144.783c-19.652,40.503-50.741,74.36-89.425,97.388c-41.327,23.734-88.367,35.692-136.011,34.578 c-46.947,1.133-93.303-10.651-134.01-34.067c-37.738-22.341-68.249-55.07-87.892-94.28c-21.028-42.467-31.57-89.355-30.745-136.735 C212.808,804.859,223.158,755.686,243.96,710.631z M339.006,941.858c10.257,25.912,27.651,48.385,50.163,64.812 c22.93,16.026,50.387,24.294,78.353,23.591c29.783,1.178,59.14-7.372,83.634-24.358c22.227-16.375,39.164-38.909,48.715-64.812 c10.677-28.928,15.946-59.572,15.543-90.404c0.33-31.127-4.623-62.084-14.649-91.554c-8.855-26.607-25.246-50.069-47.182-67.537 c-23.88-17.79-53.158-26.813-82.91-25.55c-28.572-0.74-56.644,7.593-80.184,23.804c-22.893,16.496-40.617,39.168-51.1,65.365 c-23.255,60.049-23.376,126.595-0.341,186.728L339.006,941.858z"/>
<path fill="#50D9FF" d="M1362.667,255.5h383.25v383.25h-383.25V255.5z"/>
</svg>`}
/>
{/* 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, and send 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` | json |
## 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`

View File

@@ -0,0 +1,132 @@
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')
interface OutlookFolder {
id: string
displayName: string
totalItemCount?: number
unreadItemCount?: number
}
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: OutlookFolder) => ({
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 }
)
}
}

View File

@@ -97,6 +97,10 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'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',

View File

@@ -168,7 +168,6 @@ export function CredentialSelector({
if (!baseProviderConfig) {
return <ExternalLink className='h-4 w-4' />
}
// Always use the base provider icon for a more consistent UI
return baseProviderConfig.icon({ className: 'h-4 w-4' })
}

View File

@@ -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,23 @@ export function FolderSelector({
const getFolderIcon = (size: 'sm' | 'md' = 'sm') => {
const iconSize = size === 'sm' ? 'h-4 w-4' : 'h-5 w-5'
return <GmailIcon className={iconSize} />
if (provider === 'gmail') {
return <GmailIcon className={iconSize} />
}
if (provider === 'outlook') {
return <OutlookIcon className={iconSize} />
}
return null
}
const getProviderName = () => {
if (provider === 'outlook') return 'Outlook'
return 'Gmail'
}
const getFolderLabel = () => {
if (provider === 'outlook') return 'folders'
return 'labels'
}
return (
@@ -283,11 +309,6 @@ export function FolderSelector({
{getFolderIcon('sm')}
<span className='truncate font-normal'>{selectedFolder.name}</span>
</div>
) : selectedFolderId && (isLoadingSelectedFolder || !selectedCredentialId) ? (
<div className='flex items-center gap-2'>
<RefreshCw className='h-4 w-4 animate-spin' />
<span className='text-muted-foreground'>Loading label...</span>
</div>
) : (
<div className='flex items-center gap-2'>
{getFolderIcon('sm')}
@@ -321,24 +342,27 @@ export function FolderSelector({
)}
<Command>
<CommandInput placeholder='Search labels...' onValueChange={handleSearch} />
<CommandInput
placeholder={`Search ${getFolderLabel()}...`}
onValueChange={handleSearch}
/>
<CommandList>
<CommandEmpty>
{isLoading ? (
<div className='flex items-center justify-center p-4'>
<RefreshCw className='h-4 w-4 animate-spin' />
<span className='ml-2'>Loading labels...</span>
<span className='ml-2'>Loading {getFolderLabel()}...</span>
</div>
) : credentials.length === 0 ? (
<div className='p-4 text-center'>
<p className='font-medium text-sm'>No accounts connected.</p>
<p className='text-muted-foreground text-xs'>
Connect a Gmail account to continue.
Connect a {getProviderName()} account to continue.
</p>
</div>
) : (
<div className='p-4 text-center'>
<p className='font-medium text-sm'>No labels found.</p>
<p className='font-medium text-sm'>No {getFolderLabel()} found.</p>
<p className='text-muted-foreground text-xs'>
Try a different search or account.
</p>
@@ -371,7 +395,7 @@ export function FolderSelector({
{folders.length > 0 && (
<CommandGroup>
<div className='px-2 py-1.5 font-medium text-muted-foreground text-xs'>
Labels
{getFolderLabel().charAt(0).toUpperCase() + getFolderLabel().slice(1)}
</div>
{folders.map((folder) => (
<CommandItem
@@ -394,22 +418,11 @@ export function FolderSelector({
<CommandGroup>
<CommandItem onSelect={handleAddCredential}>
<div className='flex items-center gap-2 text-primary'>
<span>Connect Gmail account</span>
<span>Connect {getProviderName()} account</span>
</div>
</CommandItem>
</CommandGroup>
)}
{/* Add another account option */}
{/* {credentials.length > 0 && (
<CommandGroup>
<CommandItem onSelect={handleAddCredential}>
<div className="flex items-center gap-2 text-primary">
<span>Connect Another Account</span>
</div>
</CommandItem>
</CommandGroup>
)} */}
</CommandList>
</Command>
</PopoverContent>
@@ -421,7 +434,7 @@ export function FolderSelector({
isOpen={showOAuthModal}
onClose={() => setShowOAuthModal(false)}
provider={provider}
toolName='Gmail'
toolName={getProviderName()}
requiredScopes={requiredScopes}
serviceId={getServiceId()}
/>

View File

@@ -0,0 +1,150 @@
import { OutlookIcon } from '@/components/icons'
import type {
OutlookDraftResponse,
OutlookReadResponse,
OutlookSendResponse,
} from '@/tools/outlook/types'
import type { BlockConfig } from '../types'
export const OutlookBlock: BlockConfig<
OutlookReadResponse | OutlookSendResponse | OutlookDraftResponse
> = {
type: 'outlook',
name: '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',
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: 'Microsoft Account',
type: 'oauth-input',
layout: 'full',
provider: 'outlook',
serviceId: 'outlook',
requiredScopes: [
'Mail.ReadWrite',
'Mail.ReadBasic',
'Mail.Read',
'Mail.Send',
'offline_access',
'openid',
'profile',
'email',
],
placeholder: 'Select Microsoft 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: 'Folder',
type: 'folder-selector',
layout: 'full',
provider: 'outlook',
serviceId: 'outlook',
requiredScopes: ['Mail.ReadWrite', 'Mail.ReadBasic', 'Mail.Read'],
placeholder: 'Select Outlook 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',
},
},
},
}

View File

@@ -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<string, BlockConfig> = {
mistral_parse: MistralParseBlock,
notion: NotionBlock,
openai: OpenAIBlock,
outlook: OutlookBlock,
perplexity: PerplexityBlock,
pinecone: PineconeBlock,
reddit: RedditBlock,

View File

@@ -2556,3 +2556,118 @@ export function MicrosoftTeamsIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function OutlookIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
xmlns='http://www.w3.org/2000/svg'
version='1.1'
id='Livello_1'
x='0px'
y='0px'
viewBox='0 0 1831.085 1703.335'
enable-background='new 0 0 1831.085 1703.335'
>
<path
fill='#0A2767'
d='M1831.083,894.25c0.1-14.318-7.298-27.644-19.503-35.131h-0.213l-0.767-0.426l-634.492-375.585 c-2.74-1.851-5.583-3.543-8.517-5.067c-24.498-12.639-53.599-12.639-78.098,0c-2.934,1.525-5.777,3.216-8.517,5.067L446.486,858.693 l-0.766,0.426c-19.392,12.059-25.337,37.556-13.278,56.948c3.553,5.714,8.447,10.474,14.257,13.868l634.492,375.585 c2.749,1.835,5.592,3.527,8.517,5.068c24.498,12.639,53.599,12.639,78.098,0c2.925-1.541,5.767-3.232,8.517-5.068l634.492-375.585 C1823.49,922.545,1831.228,908.923,1831.083,894.25z'
/>
<path
fill='#0364B8'
d='M520.453,643.477h416.38v381.674h-416.38V643.477z M1745.917,255.5V80.908 c1-43.652-33.552-79.862-77.203-80.908H588.204C544.552,1.046,510,37.256,511,80.908V255.5l638.75,170.333L1745.917,255.5z'
/>
<path fill='#0078D4' d='M511,255.5h425.833v383.25H511V255.5z' />
<path
fill='#28A8EA'
d='M1362.667,255.5H936.833v383.25L1362.667,1022h383.25V638.75L1362.667,255.5z'
/>
<path fill='#0078D4' d='M936.833,638.75h425.833V1022H936.833V638.75z' />
<path fill='#0364B8' d='M936.833,1022h425.833v383.25H936.833V1022z' />
<path fill='#14447D' d='M520.453,1025.151h416.38v346.969h-416.38V1025.151z' />
<path fill='#0078D4' d='M1362.667,1022h383.25v383.25h-383.25V1022z' />
<linearGradient
id='SVGID_1_'
gradientUnits='userSpaceOnUse'
x1='1128.4584'
y1='811.0833'
x2='1128.4584'
y2='1.9982'
gradientTransform='matrix(1 0 0 -1 0 1705.3334)'
>
<stop offset='0' style={{ stopColor: '#35B8F1' }} />
<stop offset='1' style={{ stopColor: '#28A8EA' }} />
</linearGradient>
<path
fill='url(#SVGID_1_)'
d='M1811.58,927.593l-0.809,0.426l-634.492,356.848c-2.768,1.703-5.578,3.321-8.517,4.769 c-10.777,5.132-22.481,8.029-34.407,8.517l-34.663-20.27c-2.929-1.47-5.773-3.105-8.517-4.897L447.167,906.003h-0.298 l-21.036-11.753v722.384c0.328,48.196,39.653,87.006,87.849,86.7h1230.914c0.724,0,1.363-0.341,2.129-0.341 c10.18-0.651,20.216-2.745,29.808-6.217c4.145-1.756,8.146-3.835,11.966-6.217c2.853-1.618,7.75-5.152,7.75-5.152 c21.814-16.142,34.726-41.635,34.833-68.772V894.25C1831.068,908.067,1823.616,920.807,1811.58,927.593z'
/>
<path
opacity='0.5'
fill='#0A2767'
enable-background='new '
d='M1797.017,891.397v44.287l-663.448,456.791L446.699,906.301 c0-0.235-0.191-0.426-0.426-0.426l0,0l-63.023-37.899v-31.938l25.976-0.426l54.932,31.512l1.277,0.426l4.684,2.981 c0,0,645.563,368.346,647.267,369.197l24.698,14.478c2.129-0.852,4.258-1.703,6.813-2.555 c1.278-0.852,640.879-360.681,640.879-360.681L1797.017,891.397z'
/>
<path
fill='#1490DF'
d='M1811.58,927.593l-0.809,0.468l-634.492,356.848c-2.768,1.703-5.578,3.321-8.517,4.769 c-24.641,12.038-53.457,12.038-78.098,0c-2.918-1.445-5.76-3.037-8.517-4.769L446.657,928.061l-0.766-0.468 c-12.25-6.642-19.93-19.409-20.057-33.343v722.384c0.305,48.188,39.616,87.004,87.803,86.7c0.001,0,0.002,0,0.004,0h1229.636 c48.188,0.307,87.5-38.509,87.807-86.696c0-0.001,0-0.002,0-0.004V894.25C1831.068,908.067,1823.616,920.807,1811.58,927.593z'
/>
<path
opacity='0.1'
enable-background='new '
d='M1185.52,1279.629l-9.496,5.323c-2.752,1.752-5.595,3.359-8.517,4.812 c-10.462,5.135-21.838,8.146-33.47,8.857l241.405,285.479l421.107,101.476c11.539-8.716,20.717-20.178,26.7-33.343L1185.52,1279.629 z'
/>
<path
opacity='0.05'
enable-background='new '
d='M1228.529,1255.442l-52.505,29.51c-2.752,1.752-5.595,3.359-8.517,4.812 c-10.462,5.135-21.838,8.146-33.47,8.857l113.101,311.838l549.538,74.989c21.649-16.254,34.394-41.743,34.407-68.815v-9.326 L1228.529,1255.442z'
/>
<path
fill='#28A8EA'
d='M514.833,1703.333h1228.316c18.901,0.096,37.335-5.874,52.59-17.033l-697.089-408.331 c-2.929-1.47-5.773-3.105-8.517-4.897L447.125,906.088h-0.298l-20.993-11.838v719.914 C425.786,1663.364,465.632,1703.286,514.833,1703.333C514.832,1703.333,514.832,1703.333,514.833,1703.333z'
/>
<path
opacity='0.1'
enable-background='new '
d='M1022,418.722v908.303c-0.076,31.846-19.44,60.471-48.971,72.392 c-9.148,3.931-19,5.96-28.957,5.962H425.833V383.25H511v-42.583h433.073C987.092,340.83,1021.907,375.702,1022,418.722z'
/>
<path
opacity='0.2'
enable-background='new '
d='M979.417,461.305v908.302c0.107,10.287-2.074,20.469-6.388,29.808 c-11.826,29.149-40.083,48.273-71.54,48.417H425.833V383.25h475.656c12.356-0.124,24.533,2.958,35.344,8.943 C962.937,405.344,979.407,432.076,979.417,461.305z'
/>
<path
opacity='0.2'
enable-background='new '
d='M979.417,461.305v823.136c-0.208,43-34.928,77.853-77.927,78.225H425.833V383.25 h475.656c12.356-0.124,24.533,2.958,35.344,8.943C962.937,405.344,979.407,432.076,979.417,461.305z'
/>
<path
opacity='0.2'
enable-background='new '
d='M936.833,461.305v823.136c-0.046,43.067-34.861,78.015-77.927,78.225H425.833 V383.25h433.072c43.062,0.023,77.951,34.951,77.927,78.013C936.833,461.277,936.833,461.291,936.833,461.305z'
/>
<linearGradient
id='SVGID_2_'
gradientUnits='userSpaceOnUse'
x1='162.7469'
y1='1383.0741'
x2='774.0864'
y2='324.2592'
gradientTransform='matrix(1 0 0 -1 0 1705.3334)'
>
<stop offset='0' style={{ stopColor: '#1784D9' }} />
<stop offset='0.5' style={{ stopColor: '#107AD5' }} />
<stop offset='1' style={{ stopColor: '#0A63C9' }} />
</linearGradient>
<path
fill='url(#SVGID_2_)'
d='M78.055,383.25h780.723c43.109,0,78.055,34.947,78.055,78.055v780.723 c0,43.109-34.946,78.055-78.055,78.055H78.055c-43.109,0-78.055-34.947-78.055-78.055V461.305 C0,418.197,34.947,383.25,78.055,383.25z'
/>
<path
fill='#FFFFFF'
d='M243.96,710.631c19.238-40.988,50.29-75.289,89.17-98.495c43.057-24.651,92.081-36.94,141.675-35.515 c45.965-0.997,91.321,10.655,131.114,33.683c37.414,22.312,67.547,55.004,86.742,94.109c20.904,43.09,31.322,90.512,30.405,138.396 c1.013,50.043-9.706,99.628-31.299,144.783c-19.652,40.503-50.741,74.36-89.425,97.388c-41.327,23.734-88.367,35.692-136.011,34.578 c-46.947,1.133-93.303-10.651-134.01-34.067c-37.738-22.341-68.249-55.07-87.892-94.28c-21.028-42.467-31.57-89.355-30.745-136.735 C212.808,804.859,223.158,755.686,243.96,710.631z M339.006,941.858c10.257,25.912,27.651,48.385,50.163,64.812 c22.93,16.026,50.387,24.294,78.353,23.591c29.783,1.178,59.14-7.372,83.634-24.358c22.227-16.375,39.164-38.909,48.715-64.812 c10.677-28.928,15.946-59.572,15.543-90.404c0.33-31.127-4.623-62.084-14.649-91.554c-8.855-26.607-25.246-50.069-47.182-67.537 c-23.88-17.79-53.158-26.813-82.91-25.55c-28.572-0.74-56.644,7.593-80.184,23.804c-22.893,16.496-40.617,39.168-51.1,65.365 c-23.255,60.049-23.376,126.595-0.341,186.728L339.006,941.858z'
/>
<path fill='#50D9FF' d='M1362.667,255.5h383.25v383.25h-383.25V255.5z' />
</svg>
)
}

View File

@@ -397,6 +397,31 @@ export const auth = betterAuth({
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/microsoft-teams`,
},
{
providerId: 'outlook',
clientId: env.MICROSOFT_CLIENT_ID as string,
clientSecret: env.MICROSOFT_CLIENT_SECRET as string,
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
userInfoUrl: 'https://graph.microsoft.com/v1.0/me',
scopes: [
'openid',
'profile',
'email',
'Mail.ReadWrite',
'Mail.ReadBasic',
'Mail.Read',
'Mail.Send',
'offline_access',
],
responseType: 'code',
accessType: 'offline',
authentication: 'basic',
prompt: 'consent',
pkce: true,
redirectURI: `${env.NEXT_PUBLIC_APP_URL}/api/auth/oauth2/callback/outlook`,
},
// Supabase provider
{
providerId: 'supabase',

View File

@@ -14,6 +14,7 @@ import {
MicrosoftIcon,
MicrosoftTeamsIcon,
NotionIcon,
OutlookIcon,
SupabaseIcon,
xIcon,
} from '@/components/icons'
@@ -51,6 +52,7 @@ export type OAuthService =
| 'jira'
| 'discord'
| 'microsoft-teams'
| 'outlook'
// Define the interface for OAuth provider configuration
export interface OAuthProviderConfig {
id: OAuthProvider
@@ -163,6 +165,24 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'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}`)
}

View File

@@ -0,0 +1,112 @@
import type { ToolConfig } from '../types'
import type { OutlookDraftParams, OutlookDraftResponse } from './types'
export const outlookDraftTool: ToolConfig<OutlookDraftParams, OutlookDraftResponse> = {
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',
}
},
body: (params: OutlookDraftParams): Record<string, any> => {
return {
subject: params.subject,
body: {
contentType: 'Text',
content: params.body,
},
toRecipients: [
{
emailAddress: {
address: params.to,
},
},
],
}
},
},
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 Outlook 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 drafting email'
},
}

View File

@@ -0,0 +1,5 @@
import { outlookDraftTool } from './draft'
import { outlookReadTool } from './read'
import { outlookSendTool } from './send'
export { outlookDraftTool, outlookReadTool, outlookSendTool }

View File

@@ -0,0 +1,146 @@
import type { ToolConfig } from '../types'
import type {
CleanedOutlookMessage,
OutlookMessage,
OutlookMessagesResponse,
OutlookReadParams,
OutlookReadResponse,
} from './types'
export const outlookReadTool: ToolConfig<OutlookReadParams, OutlookReadResponse> = {
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',
},
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)',
},
},
request: {
url: (params) => {
// 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) {
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}`,
}
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
const errorText = await response.text()
throw new Error(`Failed to read Outlook mail: ${errorText}`)
}
const data: OutlookMessagesResponse = 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: [],
},
}
}
// Clean up the message data to only include essential fields
const cleanedMessages: CleanedOutlookMessage[] = messages.map((message: OutlookMessage) => ({
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) => ({
name: recipient.emailAddress?.name,
address: recipient.emailAddress?.address,
})) || [],
ccRecipients:
message.ccRecipients?.map((recipient) => ({
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(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)
}
if (error.message) {
return error.message
}
}
// Default fallback message
return 'An error occurred while reading Outlook email'
},
}

View File

@@ -0,0 +1,111 @@
import type { ToolConfig } from '../types'
import type { OutlookSendParams, OutlookSendResponse } from './types'
export const outlookSendTool: ToolConfig<OutlookSendParams, OutlookSendResponse> = {
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',
}
},
body: (params: OutlookSendParams): Record<string, any> => {
return {
message: {
subject: params.subject,
body: {
contentType: 'Text',
content: params.body,
},
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(),
},
},
}
},
transformError: (error) => {
// 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.'
}
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'
},
}

View File

@@ -0,0 +1,129 @@
import type { 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
messageId?: string
}
export interface OutlookReadResponse extends ToolResponse {
output: {
message: string
results: CleanedOutlookMessage[]
}
}
export interface OutlookDraftParams {
accessToken: string
to: string
subject: string
body: string
}
export interface OutlookDraftResponse extends ToolResponse {
output: {
message: string
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
}

View File

@@ -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,
@@ -183,4 +184,7 @@ export const tools: Record<string, ToolConfig> = {
microsoft_teams_write_chat: microsoftTeamsWriteChatTool,
microsoft_teams_read_channel: microsoftTeamsReadChannelTool,
microsoft_teams_write_channel: microsoftTeamsWriteChannelTool,
outlook_read: outlookReadTool,
outlook_send: outlookSendTool,
outlook_draft: outlookDraftTool,
}