diff --git a/apps/docs/content/docs/guides/slack-nextjs.mdx b/apps/docs/content/docs/guides/slack-nextjs.mdx
index 42850559..20b88418 100644
--- a/apps/docs/content/docs/guides/slack-nextjs.mdx
+++ b/apps/docs/content/docs/guides/slack-nextjs.mdx
@@ -59,6 +59,7 @@ oauth_config:
- reactions:read
- reactions:write
- users:read
+ - users:read.email
settings:
event_subscriptions:
@@ -142,7 +143,7 @@ type Platform = keyof typeof bot.webhooks;
export async function POST(
request: Request,
- context: RouteContext<"/api/webhooks/[platform]">
+ context: RouteContext<"/api/webhooks/[platform]">,
) {
const { platform } = await context.params;
@@ -209,7 +210,9 @@ bot.onAction("info", async (event) => {
```
- The file extension must be `.tsx` (not `.ts`) when using JSX components like `Card` and `Button`. Make sure your `tsconfig.json` has `"jsx": "react-jsx"` and `"jsxImportSource": "chat"`.
+ The file extension must be `.tsx` (not `.ts`) when using JSX components like
+ `Card` and `Button`. Make sure your `tsconfig.json` has `"jsx": "react-jsx"`
+ and `"jsxImportSource": "chat"`.
## Deploy to Vercel
diff --git a/examples/nextjs-chat/slack-manifest.yml b/examples/nextjs-chat/slack-manifest.yml
index 2df83dde..790c4a2e 100644
--- a/examples/nextjs-chat/slack-manifest.yml
+++ b/examples/nextjs-chat/slack-manifest.yml
@@ -26,6 +26,7 @@ oauth_config:
- reactions:write
# User info for display names
- users:read
+ - users:read.email
settings:
event_subscriptions:
diff --git a/packages/adapter-slack/README.md b/packages/adapter-slack/README.md
index 13971cf9..3618ef93 100644
--- a/packages/adapter-slack/README.md
+++ b/packages/adapter-slack/README.md
@@ -132,6 +132,7 @@ oauth_config:
- reactions:read
- reactions:write
- users:read
+ - users:read.email
settings:
event_subscriptions:
@@ -180,17 +181,17 @@ After creating the app, go to **Basic Information** → **App Credentials** and
All options are auto-detected from environment variables when not provided. You can call `createSlackAdapter()` with no arguments if the env vars are set.
-| Option | Required | Description |
-|--------|----------|-------------|
-| `botToken` | No | Bot token (`xoxb-...`). Auto-detected from `SLACK_BOT_TOKEN` |
-| `signingSecret` | No* | Signing secret for webhook verification. Auto-detected from `SLACK_SIGNING_SECRET` |
-| `clientId` | No | App client ID for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_ID` |
-| `clientSecret` | No | App client secret for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_SECRET` |
-| `encryptionKey` | No | AES-256-GCM key for encrypting stored tokens. Auto-detected from `SLACK_ENCRYPTION_KEY` |
-| `installationKeyPrefix` | No | Prefix for the state key used to store workspace installations. Defaults to `slack:installation`. The full key is `{prefix}:{teamId}` |
-| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |
+| Option | Required | Description |
+| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
+| `botToken` | No | Bot token (`xoxb-...`). Auto-detected from `SLACK_BOT_TOKEN` |
+| `signingSecret` | No\* | Signing secret for webhook verification. Auto-detected from `SLACK_SIGNING_SECRET` |
+| `clientId` | No | App client ID for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_ID` |
+| `clientSecret` | No | App client secret for multi-workspace OAuth. Auto-detected from `SLACK_CLIENT_SECRET` |
+| `encryptionKey` | No | AES-256-GCM key for encrypting stored tokens. Auto-detected from `SLACK_ENCRYPTION_KEY` |
+| `installationKeyPrefix` | No | Prefix for the state key used to store workspace installations. Defaults to `slack:installation`. The full key is `{prefix}:{teamId}` |
+| `logger` | No | Logger instance (defaults to `ConsoleLogger("info")`) |
-*`signingSecret` is required — either via config or `SLACK_SIGNING_SECRET` env var.
+\*`signingSecret` is required — either via config or `SLACK_SIGNING_SECRET` env var.
## Environment variables
@@ -206,59 +207,59 @@ SLACK_ENCRYPTION_KEY=... # Optional, for token encryption
### Messaging
-| Feature | Supported |
-|---------|-----------|
-| Post message | Yes |
-| Edit message | Yes |
-| Delete message | Yes |
-| File uploads | Yes |
-| Streaming | Native API |
+| Feature | Supported |
+| ------------------ | ------------------------- |
+| Post message | Yes |
+| Edit message | Yes |
+| Delete message | Yes |
+| File uploads | Yes |
+| Streaming | Native API |
| Scheduled messages | Yes (native, with cancel) |
### Rich content
-| Feature | Supported |
-|---------|-----------|
-| Card format | Block Kit |
-| Buttons | Yes |
-| Link buttons | Yes |
-| Select menus | Yes |
-| Tables | Block Kit |
-| Fields | Yes |
-| Images in cards | Yes |
-| Modals | Yes |
+| Feature | Supported |
+| --------------- | --------- |
+| Card format | Block Kit |
+| Buttons | Yes |
+| Link buttons | Yes |
+| Select menus | Yes |
+| Tables | Block Kit |
+| Fields | Yes |
+| Images in cards | Yes |
+| Modals | Yes |
### Conversations
-| Feature | Supported |
-|---------|-----------|
-| Slash commands | Yes |
-| Mentions | Yes |
-| Add reactions | Yes |
-| Remove reactions | Yes |
-| Typing indicator | Yes |
-| DMs | Yes |
+| Feature | Supported |
+| ------------------ | ------------ |
+| Slash commands | Yes |
+| Mentions | Yes |
+| Add reactions | Yes |
+| Remove reactions | Yes |
+| Typing indicator | Yes |
+| DMs | Yes |
| Ephemeral messages | Yes (native) |
### Message history
-| Feature | Supported |
-|---------|-----------|
-| Fetch messages | Yes |
-| Fetch single message | Yes |
-| Fetch thread info | Yes |
-| Fetch channel messages | Yes |
-| List threads | Yes |
-| Fetch channel info | Yes |
-| Post channel message | Yes |
+| Feature | Supported |
+| ---------------------- | --------- |
+| Fetch messages | Yes |
+| Fetch single message | Yes |
+| Fetch thread info | Yes |
+| Fetch channel messages | Yes |
+| List threads | Yes |
+| Fetch channel info | Yes |
+| Post channel message | Yes |
### Platform-specific
-| Feature | Supported |
-|---------|-----------|
-| Assistants API | Yes |
-| Member joined channel | Yes |
-| App Home tab | Yes |
+| Feature | Supported |
+| --------------------- | --------- |
+| Assistants API | Yes |
+| Member joined channel | Yes |
+| App Home tab | Yes |
## Slack Assistants API
@@ -286,13 +287,13 @@ bot.onAssistantContextChanged(async (event) => {
The `SlackAdapter` exposes these methods for the Assistants API:
-| Method | Description |
-|--------|-------------|
-| `setSuggestedPrompts(channelId, threadTs, prompts, title?)` | Show prompt suggestions in the thread |
-| `setAssistantStatus(channelId, threadTs, status)` | Show a thinking/status indicator |
-| `setAssistantTitle(channelId, threadTs, title)` | Set the thread title (shown in History) |
-| `publishHomeView(userId, view)` | Publish a Home tab view for a user |
-| `startTyping(threadId, status)` | Show a custom loading status (requires `assistant:write` scope) |
+| Method | Description |
+| ----------------------------------------------------------- | --------------------------------------------------------------- |
+| `setSuggestedPrompts(channelId, threadTs, prompts, title?)` | Show prompt suggestions in the thread |
+| `setAssistantStatus(channelId, threadTs, status)` | Show a thinking/status indicator |
+| `setAssistantTitle(channelId, threadTs, title)` | Set the thread title (shown in History) |
+| `publishHomeView(userId, view)` | Publish a Home tab view for a user |
+| `startTyping(threadId, status)` | Show a custom loading status (requires `assistant:write` scope) |
### Required scopes and events
@@ -318,7 +319,16 @@ When streaming in an assistant thread, you can attach Block Kit elements to the
```typescript
await thread.stream(textStream, {
stopBlocks: [
- { type: "actions", elements: [{ type: "button", text: { type: "plain_text", text: "Retry" }, action_id: "retry" }] },
+ {
+ type: "actions",
+ elements: [
+ {
+ type: "button",
+ text: { type: "plain_text", text: "Retry" },
+ action_id: "retry",
+ },
+ ],
+ },
],
});
```
diff --git a/packages/adapter-slack/src/index.test.ts b/packages/adapter-slack/src/index.test.ts
index 91cd0989..3614830e 100644
--- a/packages/adapter-slack/src/index.test.ts
+++ b/packages/adapter-slack/src/index.test.ts
@@ -3018,6 +3018,49 @@ describe("fetchMessages", () => {
})
);
});
+
+ it("fetches author email via lookupUser when fetching messages", async () => {
+ const adapter = createSlackAdapter({
+ botToken: "xoxb-test-token",
+ signingSecret: secret,
+ logger: mockLogger,
+ botUserId: "U_BOT",
+ });
+
+ mockClientMethod(
+ adapter,
+ "conversations.replies",
+ vi.fn().mockResolvedValue({
+ ok: true,
+ messages: [
+ {
+ type: "message",
+ user: "U1",
+ text: "test message",
+ ts: "1000.000",
+ channel: "C123",
+ },
+ ],
+ has_more: false,
+ })
+ );
+ mockClientMethod(
+ adapter,
+ "users.info",
+ vi.fn().mockResolvedValue({
+ ok: true,
+ user: { name: "user1", profile: { email: "user1@example.com" } },
+ })
+ );
+
+ const state = createMockState();
+ await adapter.initialize(createMockChatInstance(state));
+
+ const result = await adapter.fetchMessages("slack:C123:1234567890.000000");
+
+ expect(result.messages.length).toBe(1);
+ expect(result.messages[0].author.email).toBe("user1@example.com");
+ });
});
// ============================================================================
diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts
index ebc419d4..59e51fe6 100644
--- a/packages/adapter-slack/src/index.ts
+++ b/packages/adapter-slack/src/index.ts
@@ -351,6 +351,7 @@ type SlackInteractivePayload =
/** Cached user info */
interface CachedUser {
displayName: string;
+ email?: string;
realName: string;
}
@@ -700,14 +701,18 @@ export class SlackAdapter implements Adapter {
*/
private async lookupUser(
userId: string
- ): Promise<{ displayName: string; realName: string }> {
+ ): Promise<{ displayName: string; realName: string; email?: string }> {
const cacheKey = `slack:user:${userId}`;
// Check cache first (via state adapter for serverless compatibility)
if (this.chat) {
const cached = await this.chat.getState().get(cacheKey);
if (cached) {
- return { displayName: cached.displayName, realName: cached.realName };
+ return {
+ displayName: cached.displayName,
+ realName: cached.realName,
+ email: cached.email,
+ };
}
}
@@ -718,7 +723,7 @@ export class SlackAdapter implements Adapter {
const user = result.user as {
name?: string;
real_name?: string;
- profile?: { display_name?: string; real_name?: string };
+ profile?: { display_name?: string; real_name?: string; email?: string };
};
// Slack user naming: profile.display_name > profile.real_name > real_name > name > userId
@@ -730,6 +735,7 @@ export class SlackAdapter implements Adapter {
userId;
const realName =
user?.real_name || user?.profile?.real_name || displayName;
+ const email = user?.profile?.email;
// Cache the result via state adapter
if (this.chat) {
@@ -737,7 +743,7 @@ export class SlackAdapter implements Adapter {
.getState()
.set(
cacheKey,
- { displayName, realName },
+ { displayName, realName, email },
SlackAdapter.USER_CACHE_TTL_MS
);
@@ -757,8 +763,9 @@ export class SlackAdapter implements Adapter {
userId,
displayName,
realName,
+ email,
});
- return { displayName, realName };
+ return { displayName, realName, email };
} catch (error) {
this.logger.warn("Could not fetch user info", { userId, error });
// Fall back to user ID
@@ -1868,12 +1875,14 @@ export class SlackAdapter implements Adapter {
// since Slack events only include the user ID, not the username
let userName = event.username || "unknown";
let fullName = event.username || "unknown";
+ let userEmail = "unknown";
// If we have a user ID but no username, look up the user info
if (event.user && !event.username) {
const userInfo = await this.lookupUser(event.user);
userName = userInfo.displayName;
fullName = userInfo.realName;
+ userEmail = userInfo.email || "unknown";
}
// Track thread participants for outgoing mention resolution (skip dupes)
@@ -1911,6 +1920,7 @@ export class SlackAdapter implements Adapter {
userId: event.user || event.bot_id || "unknown",
userName,
fullName,
+ email: userEmail,
isBot: !!event.bot_id,
isMe,
},
@@ -3372,6 +3382,7 @@ export class SlackAdapter implements Adapter {
userId: event.user || event.bot_id || "unknown",
userName,
fullName,
+ email: "unknown",
isBot: !!event.bot_id,
isMe,
},
diff --git a/packages/chat/src/types.ts b/packages/chat/src/types.ts
index 3f95dd40..8326ef00 100644
--- a/packages/chat/src/types.ts
+++ b/packages/chat/src/types.ts
@@ -1051,6 +1051,8 @@ export interface RawMessage {
}
export interface Author {
+ /** Email address (if supported by platform and scopes) */
+ email?: string;
/** Display name */
fullName: string;
/** Whether the author is a bot */