Microsoft Outlook / M365 email adapter for the Vercel Chat SDK. Treats email conversations as Chat SDK threads using Microsoft Graph change notifications for inbound and Graph mail APIs for outbound.
Supports both dedicated bot mailboxes and shared mailboxes.
pnpm add chat-adapter-outlook-email chat @chat-adapter/sharedimport { Chat } from "chat";
import { createOutlookEmailAdapter } from "chat-adapter-outlook-email";
const adapter = createOutlookEmailAdapter({
tenantId: process.env.OUTLOOK_TENANT_ID,
clientId: process.env.OUTLOOK_CLIENT_ID,
clientSecret: process.env.OUTLOOK_CLIENT_SECRET,
mailbox: process.env.OUTLOOK_MAILBOX,
notificationUrl: process.env.OUTLOOK_NOTIFICATION_URL,
clientState: process.env.OUTLOOK_CLIENT_STATE,
});
const chat = new Chat({
adapter,
async onMessage({ message, thread, reply }) {
// Echo the message back as a threaded reply
await reply(`You said: ${message.text}`);
},
});
// Mount the webhook handler on your HTTP server
// POST /api/webhooks/outlook-email → chat.handleWebhook(request)| Variable | Required | Description |
|---|---|---|
OUTLOOK_TENANT_ID |
Yes | Azure AD / Entra ID tenant ID |
OUTLOOK_CLIENT_ID |
Yes | Azure AD application (client) ID |
OUTLOOK_CLIENT_SECRET |
Yes | Azure AD client secret |
OUTLOOK_MAILBOX |
Yes | Mailbox to monitor (comma-separated for multiple) |
OUTLOOK_NOTIFICATION_URL |
Yes | Public HTTPS URL for Graph webhook notifications |
OUTLOOK_CLIENT_STATE |
Yes | Secret for webhook verification (max 128 chars) |
OUTLOOK_BOT_NAME |
No | Bot display name (defaults to "Bot") |
All configuration can also be passed directly via the OutlookEmailAdapterConfig object.
- Register an application in Azure AD / Entra ID
- Add application permissions:
Mail.ReadWrite+Mail.Send - Grant admin consent for the permissions
- Create a client secret
- (Recommended) Scope access to your bot mailbox using RBAC for Applications in Exchange Online to avoid tenant-wide mailbox access
Inbound emails are received via Microsoft Graph change notifications and delivered as normalized Chat SDK Message objects. The adapter handles subscription creation, renewal, and reconciliation automatically.
Replies are sent as properly threaded responses using Graph's createReply API:
async onMessage({ message, reply }) {
await reply("Thanks for your email! We'll get back to you shortly.");
}Start new outbound email threads using openDM:
const threadId = await chat.openDM("recipient@example.com");
await chat.postMessage(threadId, "Hello from the bot!");Send formatted messages using markdown:
await reply({ markdown: "**Important:** Please review the attached document." });Or pass an mdast AST directly:
await reply({ ast: myMdastRoot });Inbound attachments are mapped to Chat SDK Attachment objects with type detection (image, video, audio, file). Outbound files can be attached via the files property:
await reply({
markdown: "Here's the report you requested.",
files: [{ data: pdfBuffer, filename: "report.pdf", mimeType: "application/pdf" }],
});Retrieve the full conversation history for an email thread:
const { messages, nextCursor } = await chat.fetchMessages(threadId, { limit: 50 });Inbound email bodies are automatically cleaned up:
- Quoted reply chains are stripped (Outlook, Gmail, Apple Mail formats)
- Signatures are removed (
--,Sent from my iPhone, etc.) - HTML is converted to plain text
- The original HTML is preserved in
message.rawfor handlers that need full fidelity
Shared mailboxes work identically to dedicated mailboxes — just configure the shared mailbox address in OUTLOOK_MAILBOX. No additional code or permissions are needed (application permissions access shared mailboxes the same way via Graph API).
These methods throw NotImplementedError as they have no email equivalent:
editMessage()— email cannot be edited after senddeleteMessage()— email cannot be recalled reliablyaddReaction()/removeReaction()— no email equivalentstartTyping()— no email equivalent
The adapter uses multiple layers to prevent reply loops:
- From-address check — messages from the bot's own mailbox are skipped
- Custom header — outbound messages include
X-Chat-SDK-Bot: true; inbound messages with this header are skipped - Auto-reply detection — OOF replies, read receipts, and Exchange-generated messages are filtered via
Auto-Submittedand related headers
MIT