- PHP 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .gitignore | ||
| config.php.example | ||
| docker-compose.yml | ||
| README.md | ||
| TeamsMessenger.php | ||
| TeamsPooling.php | ||
| TeamsSender.php | ||
TeamsMessenger
TeamsMessenger is a PHP helper for working with Microsoft Teams 1:1 chat messages over Microsoft Graph. It is designed for a single delegated user and supports sending messages, polling for unread messages, reading reactions, and marking chats as read so the same messages are not returned on every poll.
The intended use case is a small internal integration, script, or daemon where one Microsoft 365 user account is authorized once and then reused silently through cached tokens. The recommended modern authentication model for that setup is delegated OAuth with Device Code flow and offline_access, not username/password or app passwords.
What it does
The code is meant to act as a lightweight Teams chat client wrapper around Microsoft Graph for PHP. In practical terms, it can:
- Send a chat message to a user in a 1:1 Teams conversation.
- Read a user's chats and recent messages.
- Detect unread chats using the chat
viewpoint.lastMessageReadDateTimefield. - Mark chats as read after processing so the same unread messages do not keep appearing on every run.
- Return message reactions as part of the normalized message data.
This project is not a full Teams client and does not aim to render every rich message type. Teams messages can contain plain text, reactions.
Authentication model
Teams chat send/read scenarios generally require delegated permissions because the app is acting on behalf of a user, and sending chat messages is documented primarily under delegated permissions rather than normal application-only auth.
For a single fixed user that should authenticate once and then run unattended, the recommended pattern is:
- Use Device Code flow for the initial sign-in.
- Request
offline_accessso a refresh token is issued. - Store the access token, refresh token, expiry, and
/meprofile in the token cache. - Refresh silently on later runs until the refresh token is revoked or expires.
This avoids storing the user's password and works better with modern Microsoft identity controls than ROPC.
Microsoft tenant setup
The following Microsoft Entra / Azure app registration setup is required for this code to work with Device Code flow and Microsoft Graph Teams chat APIs.
1. Create an app registration
In the Microsoft Entra admin center:
- Go to Microsoft Entra ID.
- Open App registrations.
- Click New registration.
- Give the app a name such as
TeamsMessenger. - Choose the tenant scope that matches the environment, usually Accounts in this organizational directory only for a single-tenant internal tool.
After creation, note these values:
- Application (client) ID → used as
clientIdin the PHP code (update the config.php file). - Directory (tenant) ID → used as
tenantIdin the PHP code (update the config.php file).
2. Enable public client flows
Because Device Code flow is a public client flow, the app registration must allow public client flows. If this is not enabled, the token polling step fails with AADSTS7000218 and Microsoft asks for client_secret or client_assertion instead.
In the app registration:
- Open Authentication.
- Scroll to Advanced settings.
- Set Allow public client flows to Yes.
This is one of the most important setup steps for Device Code flow.
3. Add Microsoft Graph delegated permissions
In the app registration:
- Open API permissions.
- Click Add a permission.
- Choose Microsoft Graph.
- Choose Delegated permissions.
- Add the permissions required by the code.
Permissions for this project:
| Permission | Why it is needed |
|---|---|
User.Read |
Read /me so the app can identify the signed-in user and cache their profile. |
Chat.Read or Chat.ReadWrite |
Read chats, messages, and chat state for the delegated user. |
ChatMessage.Send |
Send Teams chat messages as the delegated user. |
offline_access |
Receive a refresh token for silent long-term reuse. |
openid and profile |
Standard identity scopes commonly used with delegated sign-in flows. |
If the application needs to mark chats as read with markChatReadForUser, the delegated chat permissions must also allow that Graph call in the user's context.
4. Grant admin consent
Depending on tenant policy, some delegated permissions may require admin consent before the app can be used. If users are not allowed to self-consent in the tenant, a tenant administrator must approve the requested Microsoft Graph permissions in the app registration.
In the app registration:
- Open API permissions.
- Click Grant admin consent if required by tenant policy.
5. Device Code bootstrap
On the first run, the app should start Device Code flow and show a message similar to:
To sign in, use a web browser to open the page
https://login.microsoft.com/deviceand enter the provided code.
The user then:
- Opens the device login page.
- Enters the shown code.
- Signs in with the Microsoft 365 account that should own the integration.
- Accepts consent if prompted.
The PHP script must print this message and flush output immediately so the user can actually see the code before the polling loop waits for authorization.
6. Store token cache
After the first successful Device Code sign-in, the code should save a token cache file containing at least:
access_tokenrefresh_tokenexpires_attenant_id- cached
/meprofile
This lets later runs silently reuse the token cache or refresh the access token when it expires, instead of forcing the user through browser login every time.
Unread message logic
This project uses Teams chat read state from the viewpoint.lastMessageReadDateTime property on a chat to determine whether a chat has unread content. Microsoft documents that this caller-specific read-state information is available on the chat object and can be compared with lastMessagePreview.createdDateTime to decide whether the chat is unread.
A good polling flow is:
- List chats with
lastMessagePreview,members, andviewpoint. - Keep only chats where the last preview is newer than the read timestamp.
- Fetch messages for those chats.
- Filter to message items newer than the read timestamp.
- Mark the chat as read after processing so the same messages are not returned again.
Message content limitations
Teams messages are not always plain text. Some messages contain HTML, reactions, file attachments, or hosted content such as inline images and GIF-like content, and Microsoft exposes hosted content through dedicated resources and endpoints.
For this reason, the code should be described as a text-first Teams chat helper rather than a full-fidelity Teams renderer. Core read/send/reaction support is the main scope; advanced media handling can be added later if needed.
Security notes
Security recommendations:
- Keep the token cache outside the web root and protect file permissions.
- Do not commit token cache files to version control.
- Use a dedicated Entra app registration for this integration rather than sharing one across unrelated tools.
Setup checklist
Use this checklist when deploying the tool to a tenant:
- Create a Microsoft Entra app registration.
- Enable Allow public client flows.
- Copy the tenant ID and client ID into the PHP config.
- Add delegated Microsoft Graph permissions:
User.Read,Chat.ReadorChat.ReadWrite,ChatMessage.Send, and requestoffline_accessduring sign-in. - Grant admin consent if tenant policy requires it.
- Run first-time Device Code authentication and complete the browser sign-in.
- Confirm the token cache file contains access token, refresh token, expiry, and
/meprofile. - Run polling and sending scripts without interactive login on subsequent executions.