Microsoft Teams

The Microsoft Teams connector is a Palantir-provided driver for Microsoft Teams.

To create a new Microsoft Teams source, follow the standard setup flow for Palantir-provided drivers, then use the sections below for Microsoft Teams-specific configuration and networking. For the complete property reference, see the official Microsoft Teams driver documentation ↗.

Supported capabilities

CapabilityStatus
Exploration🟢 Generally available
Batch syncs🟢 Generally available
Incremental🟢 Generally available
OAuth 2.0 authentication🟢 Generally available
Table exports🟢 Generally available

Introduction

The Microsoft Teams connector reads your organization's Teams over the Microsoft Graph API. It surfaces teams, channels, chats, members, and messages as relational tables that you browse in the source explorer.

For scheduled reads, use a batch sync to write Teams data to a dataset. For interactive reads or writes, call Microsoft Graph in code from a function.

Authentication and Microsoft Graph permissions determine which Teams data the connector can access. See Authentication to choose an authentication method and Permissions by capability for the permissions required by each operation.

Authentication

The Microsoft Teams connector authenticates to Microsoft Graph through Microsoft Entra ID. Use AuthScheme to select an authentication method:

AuthSchemeAuthentication methodUse when
AzureADDelegated user authenticationSource access should follow the permissions of a signed-in user, and interactive authorization is acceptable.
AzureServicePrincipalService principal with a client secretSource access should not be scoped to a signed-in user. Microsoft Graph permissions can provide tenant-wide access or access restricted to selected teams through resource-specific consent.
AzureServicePrincipalCertService principal with a certificateThe use case is the same as AzureServicePrincipal, but your organization requires certificate-based credentials.

Delegated user authentication (AzureAD)

With AuthScheme=AzureAD the source signs in as an interactive user and reads Graph as that user.

Register an application

Register an application in the Microsoft Entra admin center ↗, then record its client ID, generate a client secret, and note your tenant (ID or a domain such as contoso.onmicrosoft.com).

  • Grant the delegated Microsoft Graph permissions listed under Permissions by capability for your use case.
  • If your tenant restricts user consent for these scopes, a Global Administrator grants admin consent for the application first.
  • When you configure the source, add the CallbackURL property. Copy the URL that Foundry fills in and add it to the application as a Web redirect URI.

Configure the source

Set AuthScheme=AzureAD, AzureTenant, OAuthClientId, OAuthClientSecret, and InitiateOAuth=REFRESH, then add the auto-filled CallbackURL property. Select Authorize and sign in as the user whose Teams data you want to read. Foundry completes the interactive OAuth exchange and refreshes the token automatically.

The Foundry Microsoft Teams source connection settings with AuthScheme set to AzureAD, alongside its approved network egress to graph.microsoft.com and login.microsoftonline.com.

Service principal authentication (AzureServicePrincipal)

With AuthScheme=AzureServicePrincipal, the source authenticates as an application rather than as a signed-in user. The application can read across the tenant, depending on its granted permissions.

Register an application

Register an application in the Microsoft Entra admin center ↗ and record its client ID and your tenant, as for the delegated scheme. The permissions differ:

  • Under API permissions, add the application Microsoft Graph permissions your use case needs, then have a Global Administrator select Grant admin consent. Application permissions have no effect until an administrator consents to them.
  • Under Certificates & secrets, generate a client secret, or upload a certificate if your organization requires certificate credentials.
  • No redirect URI is required; there is no interactive sign-in.

Microsoft documents the full flow under get access without a user ↗.

Configure the source

Set AuthScheme=AzureServicePrincipal, AzureTenant, OAuthClientId, and OAuthClientSecret. There is no Authorize step. For certificate credentials, set AuthScheme=AzureServicePrincipalCert instead and configure the certificate properties from the official Microsoft Teams driver documentation ↗.

Permissions by capability

Grant only the Microsoft Graph permissions required for your use case. Your tenant's consent policies may impose additional restrictions. For the complete list, see the Microsoft Graph permissions reference ↗.

What you want to doDelegatedApplication
Discover teams and channelsTeam.ReadBasic.All and Channel.ReadBasic.All. Requires user consent.Team.ReadBasic.All and Channel.ReadBasic.All. Requires administrator consent.
Read channel message contentChannelMessage.Read.All. Requires administrator consent.ChannelMessage.Read.All provides tenant-wide access and requires administrator consent. ChannelMessage.Read.Group provides access to an installed team and requires resource-specific consent for that team.
Read chats and chat messages—Chat.Read.All. Requires administrator consent.
List members and ownersGroupMember.Read.All, TeamMember.Read.All, and ChannelMember.Read.All. Requires administrator consent.GroupMember.Read.All, TeamMember.Read.All, and ChannelMember.Read.All. Requires administrator consent.
Send a channel messageChannelMessage.Send. Requires user consent.ChannelMessage.Send.Group. Requires resource-specific consent for each team where the application is installed.

Reading data

To read Microsoft Teams data into Foundry, select a table in the source explorer and set up a batch sync. With delegated authentication, the sync reads data available to the signed-in user. With service principal authentication, access depends on the permissions granted to the application and can cover the tenant or selected teams. See Authentication to configure access.

Rate limits

Microsoft throttles GET /teams/{team-id}/channels/{channel-id}/messages at 200 requests per second per application, 20 requests per second per application per tenant, and 1 request per second per channel. See the Microsoft Teams service limits ↗.

Configuration

The properties below are mandatory or recommended.

PropertyRequired?DescriptionDefault
AuthScheme ↗RecommendedSpecifies the type of authentication to use when connecting to Microsoft Teams. If this property is left blank, the default authentication is used.AzureServicePrincipal
AzureTenant ↗RecommendedIdentifies the Microsoft Teams tenant being used to access data, either by name (for example, contoso.onmicrosoft.com) or ID. (Conditional).—
InitiateOAuth ↗RecommendedSpecifies the process for obtaining or refreshing the OAuth access token, which maintains user access while an authenticated, authorized user is working.GETANDREFRESH
OAuthClientId ↗RecommendedSpecifies the client Id that was assigned when the custom OAuth application was created. (Also known as the consumer key.) This ID registers the custom application with the OAuth authorization server.—
OAuthClientSecret ↗RecommendedSpecifies the client secret that was assigned when the custom OAuth application was created. (Also known as the consumer secret). This secret registers the custom application with the OAuth authorization server.—
OAuthGrantType ↗RecommendedSpecifies the grant type for the chosen OAuth flow. This value should be the same as the grant_type that was set during OAuth custom application creation.CLIENT

Networking

The table below lists the domains that the source must be able to access to run.

For each domain, add a corresponding egress policy. If the source is hosted on-premises and not directly reachable from Foundry, use an agent proxy egress policy instead. The agent host itself must also be able to reach the listed domains. See using an agent as a proxy for details.

DomainRequired
graph.microsoft.comIf AzureEnvironment=GLOBAL (default)
login.microsoftonline.comIf AuthScheme=AzureAD (default), AzureServicePrincipal, AzureServicePrincipalCert AND AzureEnvironment=GLOBAL (default)
microsoftgraph.chinacloudapi.cnIf AzureEnvironment=CHINA
login.chinacloudapi.cnIf AuthScheme=AzureAD (default), AzureServicePrincipal, AzureServicePrincipalCert AND AzureEnvironment=CHINA
graph.microsoft.usIf AzureEnvironment=USGOVT
login.microsoftonline.usIf AuthScheme=AzureAD (default), AzureServicePrincipal, AzureServicePrincipalCert AND AzureEnvironment=USGOVT or USGOVTDOD
dod-graph.microsoft.usIf AzureEnvironment=USGOVTDOD

OAuth 2.0 authentication

This connector supports OAuth 2.0 authentication. Follow the OAuth 2.0 guidance for Palantir-provided drivers to configure and authorize the connection.

Table exports

This connector supports table exports. Learn how to set up a table export.

Use Microsoft Teams sources in code

To read channel messages from a pipeline or function, call Microsoft Graph using a Microsoft Teams source. To send messages as a signed-in user, use a REST API source configured with an outbound application. The examples below demonstrate both workflows in Python and TypeScript.

Read channel messages (service principal)

To run these examples, import your Microsoft Teams source into your code repository. Configure the source with service principal authentication and the permissions required to read channel messages.

Replace the placeholders with your source, tenant, application, team, and channel identifiers. You can find the team and channel identifiers in the Teams and Channels tables in the source explorer. In the TypeScript v1 example, replace MicrosoftTeamsSource with the identifier generated from your source's API name.

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 """Read Microsoft Teams channel-message metadata with app-only Microsoft Graph OAuth.""" import pandas as pd import requests from transforms.api import Output, transform from transforms.external.systems import ResolvedSource, Source, external_systems APP_ONLY_SOURCE_RID = "<source_rid>" TENANT_ID = "<tenant_id>" CLIENT_ID = "<client_id>" TEAM_ID = "<team_id>" CHANNEL_ID = "<channel_id>" TOKEN_URL = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token" MESSAGES_URL = ( f"https://graph.microsoft.com/v1.0/teams/{TEAM_ID}/channels/" f"{CHANNEL_ID}/messages" ) OUTPUT_PATH = "<output_dataset_rid>" def session_and_access_token(source: ResolvedSource) -> tuple[requests.Session, str]: session = requests.Session() proxy_uri = source.get_https_proxy_uri() if proxy_uri: session.proxies.update({"http": proxy_uri, "https": proxy_uri}) token_response = session.post( TOKEN_URL, data={ "client_id": CLIENT_ID, "client_secret": source.get_secret("OAuthClientSecret"), "scope": "https://graph.microsoft.com/.default", "grant_type": "client_credentials", }, timeout=30, ) token_response.raise_for_status() access_token = token_response.json().get("access_token") if not access_token: raise RuntimeError("Microsoft token response did not contain an access token") return session, access_token @external_systems(teams=Source(APP_ONLY_SOURCE_RID)) @transform.using(output=Output(OUTPUT_PATH)) def msteams_app_only_read(teams: ResolvedSource, output) -> None: """Write stable, non-content message metadata for the fixed Teams channel.""" session, access_token = session_and_access_token(teams) response = session.get( MESSAGES_URL, params={"$top": 20}, headers={"Authorization": f"Bearer {access_token}"}, timeout=30, ) response.raise_for_status() rows = [] for message in response.json().get("value", []): body = message.get("body") or {} rows.append( { "http_status": response.status_code, "id": message.get("id"), "reply_to_id": message.get("replyToId"), "etag": message.get("etag"), "created_date_time": message.get("createdDateTime"), "last_modified_date_time": message.get("lastModifiedDateTime"), "deleted_date_time": message.get("deletedDateTime"), "message_type": message.get("messageType"), "importance": message.get("importance"), "locale": message.get("locale"), "content_type": body.get("contentType"), "web_url": message.get("webUrl"), } ) output.write_table(pd.DataFrame(rows))
Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 """Read Microsoft Teams channel-message metadata with app-only Microsoft Graph OAuth.""" import json import requests from functions.api import UserFacingError, function from functions.sources import get_source SOURCE_ALIAS = "<source_alias>" TENANT_ID = "<tenant_id>" CLIENT_ID = "<client_id>" TEAM_ID = "<team_id>" CHANNEL_ID = "<channel_id>" TOKEN_URL = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token" MESSAGES_URL = ( f"https://graph.microsoft.com/v1.0/teams/{TEAM_ID}/channels/" f"{CHANNEL_ID}/messages" ) @function(sources=[SOURCE_ALIAS]) def list_channel_messages() -> str: """Return stable, non-content metadata for recent messages in the fixed channel.""" source = get_source(SOURCE_ALIAS) session = requests.Session() proxy_uri = source.get_https_proxy_uri() if proxy_uri: session.proxies.update({"http": proxy_uri, "https": proxy_uri}) token_response = session.post( TOKEN_URL, data={ "client_id": CLIENT_ID, "client_secret": source.get_secret("OAuthClientSecret"), "scope": "https://graph.microsoft.com/.default", "grant_type": "client_credentials", }, timeout=30, ) if not token_response.ok: raise UserFacingError( f"Microsoft token exchange failed with HTTP {token_response.status_code}." ) access_token = token_response.json().get("access_token") if not access_token: raise UserFacingError("Microsoft token response did not contain an access token.") response = session.get( MESSAGES_URL, params={"$top": 20}, headers={"Authorization": f"Bearer {access_token}"}, timeout=30, ) if not response.ok: raise UserFacingError( f"Microsoft Graph message read failed with HTTP {response.status_code}." ) messages = [] for message in response.json().get("value", []): body = message.get("body") or {} messages.append( { "id": message.get("id"), "replyToId": message.get("replyToId"), "etag": message.get("etag"), "createdDateTime": message.get("createdDateTime"), "lastModifiedDateTime": message.get("lastModifiedDateTime"), "deletedDateTime": message.get("deletedDateTime"), "messageType": message.get("messageType"), "importance": message.get("importance"), "locale": message.get("locale"), "contentType": body.get("contentType"), "webUrl": message.get("webUrl"), } ) return json.dumps( { "httpStatus": response.status_code, "messageCount": len(messages), "messages": messages, } )
Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 import { ExternalSystems, Function, UserFacingError, } from "@foundry/functions-api"; import { MicrosoftTeamsSource, } from "@foundry/external-systems/sources"; const TENANT_ID = "<tenant_id>"; const CLIENT_ID = "<client_id>"; const TEAM_ID = "<team_id>"; const CHANNEL_ID = "<channel_id>"; const TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`; const MESSAGES_URL = `https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/channels/${encodeURIComponent(CHANNEL_ID)}/messages?$top=20`; interface TokenResponse { access_token?: string; } interface ChannelMessage { id?: string; replyToId?: string; etag?: string; createdDateTime?: string; lastModifiedDateTime?: string; deletedDateTime?: string; messageType?: string; importance?: string; locale?: string; body?: { contentType?: string }; webUrl?: string; } interface ChannelMessagesResponse { value?: ChannelMessage[]; } export class TeamsGraphFunctions { @ExternalSystems({ sources: [MicrosoftTeamsSource] }) @Function() public async listChannelMessages(): Promise<string> { const tokenResponse = await MicrosoftTeamsSource.fetch(TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: MicrosoftTeamsSource.getSecret("OAuthClientSecret"), scope: "https://graph.microsoft.com/.default", grant_type: "client_credentials", }).toString(), }); if (!tokenResponse.ok) { throw new UserFacingError( `Microsoft token exchange failed with HTTP ${tokenResponse.status}.`, ); } const tokenPayload = await tokenResponse.json() as TokenResponse; if (!tokenPayload.access_token) { throw new UserFacingError("Microsoft token response did not contain an access token."); } const response = await MicrosoftTeamsSource.fetch(MESSAGES_URL, { method: "GET", headers: { Authorization: `Bearer ${tokenPayload.access_token}` }, }); if (!response.ok) { throw new UserFacingError( `Microsoft Graph message read failed with HTTP ${response.status}.`, ); } const payload = await response.json() as ChannelMessagesResponse; const messages = (payload.value ?? []).map((message) => ({ id: message.id, replyToId: message.replyToId, etag: message.etag, createdDateTime: message.createdDateTime, lastModifiedDateTime: message.lastModifiedDateTime, deletedDateTime: message.deletedDateTime, messageType: message.messageType, importance: message.importance, locale: message.locale, contentType: message.body?.contentType, webUrl: message.webUrl, })); return JSON.stringify({ httpStatus: response.status, messageCount: messages.length, messages, }); } }
Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 import { UserFacingError } from "@osdk/functions"; import { getFetch, getSource } from "@palantir/functions-sources"; const APP_ONLY_SOURCE_RID = "<source_rid>"; const TENANT_ID = "<tenant_id>"; const CLIENT_ID = "<client_id>"; const TEAM_ID = "<team_id>"; const CHANNEL_ID = "<channel_id>"; const TOKEN_URL = `https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token`; const MESSAGES_URL = `https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/channels/${encodeURIComponent(CHANNEL_ID)}/messages?$top=20`; export const config = { sources: [APP_ONLY_SOURCE_RID], }; interface TokenResponse { access_token?: string; } interface ChannelMessage { id?: string; replyToId?: string; etag?: string; createdDateTime?: string; lastModifiedDateTime?: string; deletedDateTime?: string; messageType?: string; importance?: string; locale?: string; body?: { contentType?: string }; webUrl?: string; } interface ChannelMessagesResponse { value?: ChannelMessage[]; } /** Returns stable, non-content metadata for recent messages in the fixed channel. */ export default async function listChannelMessages(): Promise<string> { const source = await getSource({ rid: APP_ONLY_SOURCE_RID }); const clientSecret = source.secrets.OAuthClientSecret; if (!clientSecret) { throw new UserFacingError("Microsoft Teams source is missing OAuthClientSecret."); } const sourceFetch = await getFetch(source); const tokenResponse = await sourceFetch(TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: clientSecret, scope: "https://graph.microsoft.com/.default", grant_type: "client_credentials", }).toString(), }); if (!tokenResponse.ok) { throw new UserFacingError( `Microsoft token exchange failed with HTTP ${tokenResponse.status}.`, ); } const tokenPayload = (await tokenResponse.json()) as TokenResponse; if (!tokenPayload.access_token) { throw new UserFacingError("Microsoft token response did not contain an access token."); } const response = await sourceFetch(MESSAGES_URL, { method: "GET", headers: { Authorization: `Bearer ${tokenPayload.access_token}` }, }); if (!response.ok) { throw new UserFacingError( `Microsoft Graph message read failed with HTTP ${response.status}.`, ); } const payload = (await response.json()) as ChannelMessagesResponse; const messages = (payload.value ?? []).map((message) => ({ id: message.id, replyToId: message.replyToId, etag: message.etag, createdDateTime: message.createdDateTime, lastModifiedDateTime: message.lastModifiedDateTime, deletedDateTime: message.deletedDateTime, messageType: message.messageType, importance: message.importance, locale: message.locale, contentType: message.body?.contentType, webUrl: message.webUrl, })); return JSON.stringify({ httpStatus: response.status, messageCount: messages.length, messages, }); }

Send a channel message (delegated user)

To send a channel message as a signed-in user, configure a REST API source for https://graph.microsoft.com with an outbound application. Grant the permissions required to send channel messages, then authorize the outbound application as the user who will send the messages.

The examples below send a message from a Python or TypeScript v2 function. Import the REST API source into your Functions repository and replace the source, team, and channel identifiers. Each example retrieves the user's access token and includes it in the request's Authorization header.

For application access to selected teams, use ChannelMessage.Send.Group with resource-specific consent and the application-token pattern from the read examples. Teamwork.Migrate.All is intended for historical message import ↗, not for sending new channel messages.

Before using this function directly in Workshop or in an automated workflow, authorize the outbound application through a function-backed action. The function uses the calling user's authorization. If that authorization expires or is revoked, the user must authorize again through an interactive workflow.

Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 import json import requests from external_systems.sources import OauthCredentials from functions.api import function from functions.sources import get_source SOURCE_ALIAS = "<source_alias>" TEAM_ID = "<team_id>" CHANNEL_ID = "<channel_id>" MESSAGES_URL = ( f"https://graph.microsoft.com/v1.0/teams/{TEAM_ID}/channels/" f"{CHANNEL_ID}/messages" ) @function(sources=[SOURCE_ALIAS]) def send_channel_message(text: str) -> str: """Post a message and return only sanitized status and identifiers.""" content = text.strip() if not content: return json.dumps( { "httpStatus": None, "errorCode": "InvalidInput", "errorMessage": "Message text must not be empty.", } ) source = get_source(SOURCE_ALIAS) credentials = source.get_session_credentials().get() if not isinstance(credentials, OauthCredentials) or not credentials.access_token: return json.dumps( { "httpStatus": None, "errorCode": "DelegatedOAuthUnavailable", "errorMessage": "Delegated Microsoft Graph OAuth is unavailable.", } ) session = requests.Session() proxy_uri = source.get_https_proxy_uri() if proxy_uri: session.proxies.update({"http": proxy_uri, "https": proxy_uri}) response = session.post( MESSAGES_URL, headers={ "Authorization": f"Bearer {credentials.access_token}", "Content-Type": "application/json", }, json={"body": {"content": content}}, timeout=30, ) if not response.ok: error_code = None error_message = f"Microsoft Graph returned HTTP {response.status_code}." try: error = response.json().get("error") or {} error_code = error.get("code") if error.get("message"): error_message = error["message"][:1000] except (AttributeError, ValueError): pass return json.dumps( { "httpStatus": response.status_code, "errorCode": error_code, "errorMessage": error_message, } ) message = response.json() return json.dumps( { "httpStatus": response.status_code, "messageId": message.get("id"), "webUrl": message.get("webUrl"), } )
Copied!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 import { getFetch, getSource } from "@palantir/functions-sources"; const DELEGATED_SOURCE_RID = "<source_rid>"; const TEAM_ID = "<team_id>"; const CHANNEL_ID = "<channel_id>"; const MESSAGES_URL = `https://graph.microsoft.com/v1.0/teams/${TEAM_ID}/channels/${encodeURIComponent(CHANNEL_ID)}/messages`; export const config = { sources: [DELEGATED_SOURCE_RID], }; interface PostedMessageResponse { id?: string; webUrl?: string; } interface GraphErrorResponse { error?: { code?: string; message?: string; }; } /** Posts one message and returns only sanitized status and identifiers. */ export default async function sendChannelMessage(text: string): Promise<string> { const content = text.trim(); if (!content) { return JSON.stringify({ httpStatus: null, errorCode: "InvalidInput", errorMessage: "Message text must not be empty.", }); } const source = await getSource({ rid: DELEGATED_SOURCE_RID }); const credentials = await source.sessionCredentials?.get(); if (!credentials || credentials.type !== "oauth" || !credentials.accessToken) { return JSON.stringify({ httpStatus: null, errorCode: "DelegatedOAuthUnavailable", errorMessage: "Delegated Microsoft Graph OAuth is unavailable.", }); } const sourceFetch = await getFetch(source); const response = await sourceFetch(MESSAGES_URL, { method: "POST", headers: { Authorization: `Bearer ${credentials.accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ body: { content } }), }); if (!response.ok) { let errorCode: string | undefined; let errorMessage = `Microsoft Graph returned HTTP ${response.status}.`; try { const payload = (await response.json()) as GraphErrorResponse; errorCode = payload.error?.code; errorMessage = payload.error?.message?.slice(0, 1000) ?? errorMessage; } catch { // Keep the bounded, status-only error when Graph does not return JSON. } return JSON.stringify({ httpStatus: response.status, errorCode, errorMessage, }); } const message = (await response.json()) as PostedMessageResponse; return JSON.stringify({ httpStatus: response.status, messageId: message.id, webUrl: message.webUrl, }); }