gmail-workspace-mcp-server 0.4.7 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.integration-with-mock.js +3 -0
- package/node_modules/@pulsemcp/mcp-elicitation/build/config.d.ts +8 -6
- package/node_modules/@pulsemcp/mcp-elicitation/build/config.js +11 -6
- package/node_modules/@pulsemcp/mcp-elicitation/build/elicitation.d.ts +6 -1
- package/node_modules/@pulsemcp/mcp-elicitation/build/elicitation.js +30 -14
- package/node_modules/@pulsemcp/mcp-elicitation/build/types.d.ts +10 -0
- package/node_modules/@pulsemcp/mcp-elicitation/package.json +1 -1
- package/package.json +1 -1
- package/shared/server.d.ts +8 -0
- package/shared/server.js +3 -0
- package/shared/tools/get-email-conversation.js +14 -5
- package/shared/tools/list-email-conversations.js +13 -6
- package/shared/tools/search-email-conversations.js +13 -7
- package/shared/tools/send-email.js +10 -3
- package/shared/utils/email-helpers.d.ts +18 -2
- package/shared/utils/email-helpers.js +25 -3
|
@@ -3,11 +3,13 @@ import type { ElicitationConfig } from './types.js';
|
|
|
3
3
|
* Reads elicitation configuration from environment variables.
|
|
4
4
|
*
|
|
5
5
|
* Environment variables:
|
|
6
|
-
* ELICITATION_ENABLED
|
|
7
|
-
* ELICITATION_REQUEST_URL
|
|
8
|
-
* ELICITATION_POLL_URL
|
|
9
|
-
* ELICITATION_TTL_MS
|
|
10
|
-
* ELICITATION_POLL_INTERVAL_MS
|
|
11
|
-
* ELICITATION_SESSION_ID
|
|
6
|
+
* ELICITATION_ENABLED - "true" (default) or "false"
|
|
7
|
+
* ELICITATION_REQUEST_URL - POST endpoint for HTTP fallback
|
|
8
|
+
* ELICITATION_POLL_URL - GET endpoint for HTTP fallback polling
|
|
9
|
+
* ELICITATION_TTL_MS - Request TTL in milliseconds (default: 300000)
|
|
10
|
+
* ELICITATION_POLL_INTERVAL_MS - Poll interval in milliseconds (default: 5000, min: 1000)
|
|
11
|
+
* ELICITATION_SESSION_ID - Session identifier for HTTP fallback `_meta`
|
|
12
|
+
* ELICITATION_PREFER_HTTP_FALLBACK - "true" forces HTTP fallback over native elicitation
|
|
13
|
+
* when both are available. Default: "false".
|
|
12
14
|
*/
|
|
13
15
|
export declare function readElicitationConfig(env?: Record<string, string | undefined>): ElicitationConfig;
|
|
@@ -14,16 +14,20 @@ function parsePositiveInt(value, defaultValue) {
|
|
|
14
14
|
* Reads elicitation configuration from environment variables.
|
|
15
15
|
*
|
|
16
16
|
* Environment variables:
|
|
17
|
-
* ELICITATION_ENABLED
|
|
18
|
-
* ELICITATION_REQUEST_URL
|
|
19
|
-
* ELICITATION_POLL_URL
|
|
20
|
-
* ELICITATION_TTL_MS
|
|
21
|
-
* ELICITATION_POLL_INTERVAL_MS
|
|
22
|
-
* ELICITATION_SESSION_ID
|
|
17
|
+
* ELICITATION_ENABLED - "true" (default) or "false"
|
|
18
|
+
* ELICITATION_REQUEST_URL - POST endpoint for HTTP fallback
|
|
19
|
+
* ELICITATION_POLL_URL - GET endpoint for HTTP fallback polling
|
|
20
|
+
* ELICITATION_TTL_MS - Request TTL in milliseconds (default: 300000)
|
|
21
|
+
* ELICITATION_POLL_INTERVAL_MS - Poll interval in milliseconds (default: 5000, min: 1000)
|
|
22
|
+
* ELICITATION_SESSION_ID - Session identifier for HTTP fallback `_meta`
|
|
23
|
+
* ELICITATION_PREFER_HTTP_FALLBACK - "true" forces HTTP fallback over native elicitation
|
|
24
|
+
* when both are available. Default: "false".
|
|
23
25
|
*/
|
|
24
26
|
export function readElicitationConfig(env = process.env) {
|
|
25
27
|
const enabledRaw = env.ELICITATION_ENABLED;
|
|
26
28
|
const enabled = enabledRaw === undefined ? true : enabledRaw.toLowerCase() !== 'false';
|
|
29
|
+
const preferHttpFallbackRaw = env.ELICITATION_PREFER_HTTP_FALLBACK;
|
|
30
|
+
const preferHttpFallback = preferHttpFallbackRaw !== undefined && preferHttpFallbackRaw.toLowerCase() === 'true';
|
|
27
31
|
const pollIntervalMs = Math.max(MIN_POLL_INTERVAL_MS, parsePositiveInt(env.ELICITATION_POLL_INTERVAL_MS, DEFAULT_POLL_INTERVAL_MS));
|
|
28
32
|
return {
|
|
29
33
|
enabled,
|
|
@@ -32,5 +36,6 @@ export function readElicitationConfig(env = process.env) {
|
|
|
32
36
|
ttlMs: parsePositiveInt(env.ELICITATION_TTL_MS, DEFAULT_TTL_MS),
|
|
33
37
|
pollIntervalMs,
|
|
34
38
|
sessionId: env.ELICITATION_SESSION_ID,
|
|
39
|
+
preferHttpFallback,
|
|
35
40
|
};
|
|
36
41
|
}
|
|
@@ -2,12 +2,17 @@ import type { ElicitationConfig, ElicitationRequestedSchema, ElicitationResult,
|
|
|
2
2
|
/**
|
|
3
3
|
* Requests user confirmation through the best available mechanism.
|
|
4
4
|
*
|
|
5
|
-
* Decision tree:
|
|
5
|
+
* Decision tree (default):
|
|
6
6
|
* 1. If elicitation is disabled (`ELICITATION_ENABLED=false`), returns `accept` immediately.
|
|
7
7
|
* 2. If the client supports native elicitation, uses `server.elicitInput()`.
|
|
8
8
|
* 3. If HTTP fallback URLs are configured, posts to the external endpoint and polls.
|
|
9
9
|
* 4. Otherwise, throws an error indicating no elicitation mechanism is available.
|
|
10
10
|
*
|
|
11
|
+
* When `cfg.preferHttpFallback` is true (set via `ELICITATION_PREFER_HTTP_FALLBACK=true`)
|
|
12
|
+
* AND both fallback URLs are configured, tier 3 runs before tier 2. This is intended for
|
|
13
|
+
* headless agent runtimes that falsely advertise elicitation capability but cannot actually
|
|
14
|
+
* surface the prompt to a user.
|
|
15
|
+
*
|
|
11
16
|
* @param options - Configuration for the confirmation request.
|
|
12
17
|
* @param config - Elicitation config (defaults to reading from env vars).
|
|
13
18
|
* @returns The user's response.
|
|
@@ -98,15 +98,37 @@ async function pollElicitationStatus(config, requestId, expiresAt) {
|
|
|
98
98
|
}
|
|
99
99
|
return { action: 'expired' };
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Runs the HTTP fallback flow: POST a request, then poll until resolved or expired.
|
|
103
|
+
*/
|
|
104
|
+
async function httpFallbackElicit(cfg, options) {
|
|
105
|
+
const clientRequestId = randomUUID();
|
|
106
|
+
const expiresAt = Date.now() + cfg.ttlMs;
|
|
107
|
+
const meta = {
|
|
108
|
+
'com.pulsemcp/request-id': clientRequestId,
|
|
109
|
+
'com.pulsemcp/expires-at': new Date(expiresAt).toISOString(),
|
|
110
|
+
...(cfg.sessionId && { 'com.pulsemcp/session-id': cfg.sessionId }),
|
|
111
|
+
...options.meta,
|
|
112
|
+
};
|
|
113
|
+
const postResponse = await postElicitationRequest(cfg, options.message, options.requestedSchema, meta);
|
|
114
|
+
// Use the server-provided requestId if available, otherwise fall back to the client-generated one
|
|
115
|
+
const requestId = postResponse.requestId || clientRequestId;
|
|
116
|
+
return pollElicitationStatus(cfg, requestId, expiresAt);
|
|
117
|
+
}
|
|
101
118
|
/**
|
|
102
119
|
* Requests user confirmation through the best available mechanism.
|
|
103
120
|
*
|
|
104
|
-
* Decision tree:
|
|
121
|
+
* Decision tree (default):
|
|
105
122
|
* 1. If elicitation is disabled (`ELICITATION_ENABLED=false`), returns `accept` immediately.
|
|
106
123
|
* 2. If the client supports native elicitation, uses `server.elicitInput()`.
|
|
107
124
|
* 3. If HTTP fallback URLs are configured, posts to the external endpoint and polls.
|
|
108
125
|
* 4. Otherwise, throws an error indicating no elicitation mechanism is available.
|
|
109
126
|
*
|
|
127
|
+
* When `cfg.preferHttpFallback` is true (set via `ELICITATION_PREFER_HTTP_FALLBACK=true`)
|
|
128
|
+
* AND both fallback URLs are configured, tier 3 runs before tier 2. This is intended for
|
|
129
|
+
* headless agent runtimes that falsely advertise elicitation capability but cannot actually
|
|
130
|
+
* surface the prompt to a user.
|
|
131
|
+
*
|
|
110
132
|
* @param options - Configuration for the confirmation request.
|
|
111
133
|
* @param config - Elicitation config (defaults to reading from env vars).
|
|
112
134
|
* @returns The user's response.
|
|
@@ -117,24 +139,18 @@ export async function requestConfirmation(options, config) {
|
|
|
117
139
|
if (!cfg.enabled) {
|
|
118
140
|
return { action: 'accept' };
|
|
119
141
|
}
|
|
142
|
+
const httpFallbackAvailable = Boolean(cfg.requestUrl && cfg.pollUrl);
|
|
143
|
+
// Opt-in: prefer HTTP fallback over native elicitation when both are available.
|
|
144
|
+
if (cfg.preferHttpFallback && httpFallbackAvailable) {
|
|
145
|
+
return httpFallbackElicit(cfg, options);
|
|
146
|
+
}
|
|
120
147
|
// Tier 2: Native elicitation
|
|
121
148
|
if (clientSupportsElicitation(options.server)) {
|
|
122
149
|
return nativeElicit(options.server, options.message, options.requestedSchema);
|
|
123
150
|
}
|
|
124
151
|
// Tier 3: HTTP fallback
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
const expiresAt = Date.now() + cfg.ttlMs;
|
|
128
|
-
const meta = {
|
|
129
|
-
'com.pulsemcp/request-id': clientRequestId,
|
|
130
|
-
'com.pulsemcp/expires-at': new Date(expiresAt).toISOString(),
|
|
131
|
-
...(cfg.sessionId && { 'com.pulsemcp/session-id': cfg.sessionId }),
|
|
132
|
-
...options.meta,
|
|
133
|
-
};
|
|
134
|
-
const postResponse = await postElicitationRequest(cfg, options.message, options.requestedSchema, meta);
|
|
135
|
-
// Use the server-provided requestId if available, otherwise fall back to the client-generated one
|
|
136
|
-
const requestId = postResponse.requestId || clientRequestId;
|
|
137
|
-
return pollElicitationStatus(cfg, requestId, expiresAt);
|
|
152
|
+
if (httpFallbackAvailable) {
|
|
153
|
+
return httpFallbackElicit(cfg, options);
|
|
138
154
|
}
|
|
139
155
|
// Tier 4: No mechanism available
|
|
140
156
|
throw new Error('Elicitation is enabled but no mechanism is available. ' +
|
|
@@ -64,6 +64,16 @@ export interface ElicitationConfig {
|
|
|
64
64
|
pollIntervalMs: number;
|
|
65
65
|
/** Session identifier included as `com.pulsemcp/session-id` in `_meta` of HTTP fallback requests. */
|
|
66
66
|
sessionId?: string;
|
|
67
|
+
/**
|
|
68
|
+
* When true, prefer HTTP fallback (Tier 3) over native elicitation (Tier 2)
|
|
69
|
+
* when both are available. Default: false.
|
|
70
|
+
*
|
|
71
|
+
* Useful for headless agent runtimes (e.g., Claude Code under Agent Orchestrator)
|
|
72
|
+
* that advertise the `elicitation` client capability but have no real interactive
|
|
73
|
+
* user — native `elicitInput()` calls auto-cancel without ever surfacing a prompt.
|
|
74
|
+
* Forcing the HTTP fallback routes the request to an external approval UI instead.
|
|
75
|
+
*/
|
|
76
|
+
preferHttpFallback?: boolean;
|
|
67
77
|
}
|
|
68
78
|
/**
|
|
69
79
|
* The result of an elicitation request.
|
package/package.json
CHANGED
package/shared/server.d.ts
CHANGED
|
@@ -119,6 +119,13 @@ export interface IGmailClient {
|
|
|
119
119
|
data: string;
|
|
120
120
|
size: number;
|
|
121
121
|
}>;
|
|
122
|
+
/**
|
|
123
|
+
* Get the email address of the Gmail account this client reads from.
|
|
124
|
+
* Used to construct account-scoped Gmail web URLs so that links open in
|
|
125
|
+
* the correct mailbox regardless of which accounts the reader happens to
|
|
126
|
+
* be signed into in their browser.
|
|
127
|
+
*/
|
|
128
|
+
getAccountEmail(): Promise<string>;
|
|
122
129
|
}
|
|
123
130
|
/**
|
|
124
131
|
* Service account credentials structure
|
|
@@ -231,6 +238,7 @@ declare abstract class BaseGmailClient implements IGmailClient {
|
|
|
231
238
|
data: string;
|
|
232
239
|
size: number;
|
|
233
240
|
}>;
|
|
241
|
+
getAccountEmail(): Promise<string>;
|
|
234
242
|
}
|
|
235
243
|
/**
|
|
236
244
|
* Gmail API client implementation using service account with domain-wide delegation
|
package/shared/server.js
CHANGED
|
@@ -108,6 +108,9 @@ class BaseGmailClient {
|
|
|
108
108
|
const { getAttachment } = await import('./gmail-client/lib/get-attachment.js');
|
|
109
109
|
return getAttachment(this.baseUrl, headers, messageId, attachmentId);
|
|
110
110
|
}
|
|
111
|
+
async getAccountEmail() {
|
|
112
|
+
return this.getSenderEmail();
|
|
113
|
+
}
|
|
111
114
|
}
|
|
112
115
|
/**
|
|
113
116
|
* Gmail API client implementation using service account with domain-wide delegation
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { getHeader } from '../utils/email-helpers.js';
|
|
2
|
+
import { buildGmailUrl, getHeader } from '../utils/email-helpers.js';
|
|
3
3
|
const PARAM_DESCRIPTIONS = {
|
|
4
4
|
email_id: 'The unique identifier of the email to retrieve. ' +
|
|
5
5
|
'Obtain this from list_email_conversations or search_email_conversations.',
|
|
@@ -25,6 +25,7 @@ Full email details including:
|
|
|
25
25
|
- Raw HTML body (when include_html is true and HTML content is available)
|
|
26
26
|
- List of attachments (if any)
|
|
27
27
|
- Labels assigned to the email
|
|
28
|
+
- Account-scoped Gmail web URL (resolves to the correct mailbox regardless of the reader's browser session)
|
|
28
29
|
|
|
29
30
|
**Use cases:**
|
|
30
31
|
- Read the full content of an email after listing conversations
|
|
@@ -143,7 +144,11 @@ function formatFullEmail(email, options) {
|
|
|
143
144
|
let output = `# Email Details
|
|
144
145
|
|
|
145
146
|
**ID:** ${email.id}
|
|
146
|
-
**Thread ID:** ${email.threadId}
|
|
147
|
+
**Thread ID:** ${email.threadId}`;
|
|
148
|
+
if (options?.accountEmail) {
|
|
149
|
+
output += `\n**Gmail URL:** ${buildGmailUrl(options.accountEmail, email.id)}`;
|
|
150
|
+
}
|
|
151
|
+
output += `
|
|
147
152
|
|
|
148
153
|
## Headers
|
|
149
154
|
**Subject:** ${subject}
|
|
@@ -210,11 +215,15 @@ export function getEmailConversationTool(server, clientFactory) {
|
|
|
210
215
|
try {
|
|
211
216
|
const parsed = GetEmailConversationSchema.parse(args ?? {});
|
|
212
217
|
const client = clientFactory();
|
|
213
|
-
const email = await
|
|
214
|
-
|
|
215
|
-
|
|
218
|
+
const [email, accountEmail] = await Promise.all([
|
|
219
|
+
client.getMessage(parsed.email_id, {
|
|
220
|
+
format: 'full',
|
|
221
|
+
}),
|
|
222
|
+
client.getAccountEmail(),
|
|
223
|
+
]);
|
|
216
224
|
const formattedEmail = formatFullEmail(email, {
|
|
217
225
|
includeHtml: parsed.include_html,
|
|
226
|
+
accountEmail,
|
|
218
227
|
});
|
|
219
228
|
return {
|
|
220
229
|
content: [
|
|
@@ -37,6 +37,7 @@ A formatted list of email conversations with:
|
|
|
37
37
|
- Sender (From)
|
|
38
38
|
- Date received
|
|
39
39
|
- Snippet preview
|
|
40
|
+
- Account-scoped Gmail web URL (resolves to the correct mailbox regardless of the reader's browser session)
|
|
40
41
|
|
|
41
42
|
**Use cases:**
|
|
42
43
|
- Check recent inbox activity
|
|
@@ -113,18 +114,24 @@ export function listEmailConversationsTool(server, clientFactory) {
|
|
|
113
114
|
],
|
|
114
115
|
};
|
|
115
116
|
}
|
|
116
|
-
// Fetch full details for each message
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
// Fetch full details for each message, plus the account email for
|
|
118
|
+
// building account-scoped Gmail URLs.
|
|
119
|
+
const [emailDetails, accountEmail] = await Promise.all([
|
|
120
|
+
Promise.all(messages.map((msg) => client.getMessage(msg.id, {
|
|
121
|
+
format: 'metadata',
|
|
122
|
+
metadataHeaders: ['Subject', 'From', 'Date'],
|
|
123
|
+
}))),
|
|
124
|
+
client.getAccountEmail(),
|
|
125
|
+
]);
|
|
121
126
|
// Sort based on sort_by parameter
|
|
122
127
|
const sortedEmails = [...emailDetails].sort((a, b) => {
|
|
123
128
|
const dateA = parseInt(a.internalDate, 10);
|
|
124
129
|
const dateB = parseInt(b.internalDate, 10);
|
|
125
130
|
return parsed.sort_by === 'recent' ? dateB - dateA : dateA - dateB;
|
|
126
131
|
});
|
|
127
|
-
const formattedEmails = sortedEmails
|
|
132
|
+
const formattedEmails = sortedEmails
|
|
133
|
+
.map((email) => formatEmail(email, accountEmail))
|
|
134
|
+
.join('\n\n---\n\n');
|
|
128
135
|
return {
|
|
129
136
|
content: [
|
|
130
137
|
{
|
|
@@ -38,7 +38,7 @@ const TOOL_DESCRIPTION = `Search email conversations using Gmail's powerful sear
|
|
|
38
38
|
- Use - to exclude: "subject:meeting -subject:canceled"
|
|
39
39
|
|
|
40
40
|
**Returns:**
|
|
41
|
-
A formatted list of matching emails with ID, Thread ID, Subject, From, Date, and
|
|
41
|
+
A formatted list of matching emails with ID, Thread ID, Subject, From, Date, snippet, and an account-scoped Gmail web URL (resolves to the correct mailbox regardless of the reader's browser session).
|
|
42
42
|
|
|
43
43
|
**Note:** Use get_email_conversation with an email ID to retrieve full message content.`;
|
|
44
44
|
export function searchEmailConversationsTool(server, clientFactory) {
|
|
@@ -79,12 +79,18 @@ export function searchEmailConversationsTool(server, clientFactory) {
|
|
|
79
79
|
],
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
|
-
// Fetch full details for each message
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
82
|
+
// Fetch full details for each message, plus the account email for
|
|
83
|
+
// building account-scoped Gmail URLs.
|
|
84
|
+
const [emailDetails, accountEmail] = await Promise.all([
|
|
85
|
+
Promise.all(messages.map((msg) => client.getMessage(msg.id, {
|
|
86
|
+
format: 'metadata',
|
|
87
|
+
metadataHeaders: ['Subject', 'From', 'Date'],
|
|
88
|
+
}))),
|
|
89
|
+
client.getAccountEmail(),
|
|
90
|
+
]);
|
|
91
|
+
const formattedEmails = emailDetails
|
|
92
|
+
.map((email) => formatEmail(email, accountEmail))
|
|
93
|
+
.join('\n\n---\n\n');
|
|
88
94
|
return {
|
|
89
95
|
content: [
|
|
90
96
|
{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { getHeader } from '../utils/email-helpers.js';
|
|
2
|
+
import { buildGmailUrl, getHeader } from '../utils/email-helpers.js';
|
|
3
3
|
import { requestConfirmation, createConfirmationSchema, readElicitationConfig, } from '@pulsemcp/mcp-elicitation';
|
|
4
4
|
const PARAM_DESCRIPTIONS = {
|
|
5
5
|
to: 'Recipient email address(es). For multiple recipients, separate with commas.',
|
|
@@ -180,11 +180,15 @@ export function sendEmailTool(server, clientFactory) {
|
|
|
180
180
|
// Option 2: Send a draft
|
|
181
181
|
if (parsed.from_draft_id) {
|
|
182
182
|
const sentEmail = await client.sendDraft(parsed.from_draft_id);
|
|
183
|
+
const accountEmail = await client.getAccountEmail();
|
|
183
184
|
return {
|
|
184
185
|
content: [
|
|
185
186
|
{
|
|
186
187
|
type: 'text',
|
|
187
|
-
text: `Draft sent successfully!\n\n**Message ID:** ${sentEmail.id}\n
|
|
188
|
+
text: `Draft sent successfully!\n\n**Message ID:** ${sentEmail.id}\n` +
|
|
189
|
+
`**Thread ID:** ${sentEmail.threadId}\n` +
|
|
190
|
+
`**Gmail URL:** ${buildGmailUrl(accountEmail, sentEmail.id)}\n\n` +
|
|
191
|
+
`The draft has been sent and removed from Drafts.`,
|
|
188
192
|
},
|
|
189
193
|
],
|
|
190
194
|
};
|
|
@@ -220,7 +224,10 @@ export function sendEmailTool(server, clientFactory) {
|
|
|
220
224
|
inReplyTo,
|
|
221
225
|
references,
|
|
222
226
|
});
|
|
223
|
-
|
|
227
|
+
const accountEmail = await client.getAccountEmail();
|
|
228
|
+
let responseText = `Email sent successfully!\n\n**Message ID:** ${sentEmail.id}\n` +
|
|
229
|
+
`**Thread ID:** ${sentEmail.threadId}\n` +
|
|
230
|
+
`**Gmail URL:** ${buildGmailUrl(accountEmail, sentEmail.id)}`;
|
|
224
231
|
if (parsed.thread_id) {
|
|
225
232
|
responseText += '\n\nThis email was sent as a reply in an existing conversation.';
|
|
226
233
|
}
|
|
@@ -4,6 +4,22 @@ import type { Email } from '../types.js';
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function getHeader(email: Email, headerName: string): string | undefined;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
7
|
+
* Builds an account-scoped Gmail web URL for a given message.
|
|
8
|
+
*
|
|
9
|
+
* Uses the `/mail/u/<account-email>/#inbox/<messageId>` path form so the link
|
|
10
|
+
* opens in the correct mailbox regardless of which accounts the reader is
|
|
11
|
+
* signed into in their browser. The user-INDEX form `/mail/u/0/` would
|
|
12
|
+
* otherwise open whichever account happens to be at index 0 in the reader's
|
|
13
|
+
* browser session, which is rarely the impersonated/OAuth account this
|
|
14
|
+
* server reads from.
|
|
15
|
+
*
|
|
16
|
+
* Gmail also accepts `?authuser=<email>` as a query-parameter fallback.
|
|
8
17
|
*/
|
|
9
|
-
export declare function
|
|
18
|
+
export declare function buildGmailUrl(accountEmail: string, messageId: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Formats an email for display in tool output.
|
|
21
|
+
*
|
|
22
|
+
* When `accountEmail` is provided, an account-scoped Gmail web URL is
|
|
23
|
+
* appended so the user can click through to the correct mailbox.
|
|
24
|
+
*/
|
|
25
|
+
export declare function formatEmail(email: Email, accountEmail?: string): string;
|
|
@@ -6,17 +6,39 @@ export function getHeader(email, headerName) {
|
|
|
6
6
|
?.value;
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
9
|
+
* Builds an account-scoped Gmail web URL for a given message.
|
|
10
|
+
*
|
|
11
|
+
* Uses the `/mail/u/<account-email>/#inbox/<messageId>` path form so the link
|
|
12
|
+
* opens in the correct mailbox regardless of which accounts the reader is
|
|
13
|
+
* signed into in their browser. The user-INDEX form `/mail/u/0/` would
|
|
14
|
+
* otherwise open whichever account happens to be at index 0 in the reader's
|
|
15
|
+
* browser session, which is rarely the impersonated/OAuth account this
|
|
16
|
+
* server reads from.
|
|
17
|
+
*
|
|
18
|
+
* Gmail also accepts `?authuser=<email>` as a query-parameter fallback.
|
|
10
19
|
*/
|
|
11
|
-
export function
|
|
20
|
+
export function buildGmailUrl(accountEmail, messageId) {
|
|
21
|
+
return `https://mail.google.com/mail/u/${encodeURIComponent(accountEmail)}/#inbox/${messageId}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Formats an email for display in tool output.
|
|
25
|
+
*
|
|
26
|
+
* When `accountEmail` is provided, an account-scoped Gmail web URL is
|
|
27
|
+
* appended so the user can click through to the correct mailbox.
|
|
28
|
+
*/
|
|
29
|
+
export function formatEmail(email, accountEmail) {
|
|
12
30
|
const subject = getHeader(email, 'Subject') || '(No Subject)';
|
|
13
31
|
const from = getHeader(email, 'From') || 'Unknown';
|
|
14
32
|
const date = getHeader(email, 'Date') || 'Unknown date';
|
|
15
33
|
const snippet = email.snippet || '';
|
|
16
|
-
|
|
34
|
+
let output = `**ID:** ${email.id}
|
|
17
35
|
**Thread ID:** ${email.threadId}
|
|
18
36
|
**Subject:** ${subject}
|
|
19
37
|
**From:** ${from}
|
|
20
38
|
**Date:** ${date}
|
|
21
39
|
**Preview:** ${snippet}`;
|
|
40
|
+
if (accountEmail) {
|
|
41
|
+
output += `\n**Gmail URL:** ${buildGmailUrl(accountEmail, email.id)}`;
|
|
42
|
+
}
|
|
43
|
+
return output;
|
|
22
44
|
}
|