@neschadin/sendgrid-mcp 0.0.0-stage → 3.0.0
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/LICENSE +21 -0
- package/MCP_TOOLS.md +457 -0
- package/README.md +175 -2
- package/bin/sendgrid-mcp +2 -0
- package/mcp.json.example +9 -0
- package/package.json +63 -4
- package/src/client.ts +1585 -0
- package/src/config.ts +197 -0
- package/src/http.ts +132 -0
- package/src/index.ts +147 -0
- package/src/logger.ts +43 -0
- package/src/redact.ts +91 -0
- package/src/tool_signal.ts +50 -0
- package/src/tools/account.ts +566 -0
- package/src/tools/classify_error.ts +133 -0
- package/src/tools/console_settings.ts +329 -0
- package/src/tools/delivery_trace.ts +291 -0
- package/src/tools/diagnostics.ts +1557 -0
- package/src/tools/email.ts +486 -0
- package/src/tools/output_schemas.ts +488 -0
- package/src/tools/preflight.ts +768 -0
- package/src/tools/sync.ts +139 -0
- package/src/tools/templates.ts +602 -0
- package/src/tools/tool_utils.ts +341 -0
- package/src/webhook_receiver.ts +467 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type {
|
|
4
|
+
MailSettingName,
|
|
5
|
+
SendGridClient,
|
|
6
|
+
TrackingSettingName,
|
|
7
|
+
} from '../client';
|
|
8
|
+
import {
|
|
9
|
+
EnforcedTlsOutputSchema,
|
|
10
|
+
InboundParseSettingsListOutputSchema,
|
|
11
|
+
jsonReadResult,
|
|
12
|
+
jsonText,
|
|
13
|
+
MailSettingsListOutputSchema,
|
|
14
|
+
paginateArray,
|
|
15
|
+
SettingDetailOutputSchema,
|
|
16
|
+
TrackingSettingsListOutputSchema,
|
|
17
|
+
} from './output_schemas';
|
|
18
|
+
import {
|
|
19
|
+
ensureSafeToolRegistration,
|
|
20
|
+
ListPagingInputFields,
|
|
21
|
+
ReadInputFields,
|
|
22
|
+
} from './tool_utils';
|
|
23
|
+
|
|
24
|
+
const ConfirmTokenSchema = z
|
|
25
|
+
.literal('CONFIRM')
|
|
26
|
+
.describe('Safety token required for mutating SendGrid console settings');
|
|
27
|
+
|
|
28
|
+
const MailSettingNameSchema = z.enum([
|
|
29
|
+
'address_whitelist',
|
|
30
|
+
'bcc',
|
|
31
|
+
'bounce_purge',
|
|
32
|
+
'footer',
|
|
33
|
+
'forward_bounce',
|
|
34
|
+
'forward_spam',
|
|
35
|
+
'plain_content',
|
|
36
|
+
'spam_check',
|
|
37
|
+
'template',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const TrackingSettingNameSchema = z.enum([
|
|
41
|
+
'click',
|
|
42
|
+
'open',
|
|
43
|
+
'subscription',
|
|
44
|
+
'google_analytics',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const SettingValueSchema = z.union([
|
|
48
|
+
z.string(),
|
|
49
|
+
z.number(),
|
|
50
|
+
z.boolean(),
|
|
51
|
+
z.null(),
|
|
52
|
+
z.array(z.string()),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
export function registerConsoleSettingsTools(
|
|
56
|
+
server: McpServer,
|
|
57
|
+
client: SendGridClient,
|
|
58
|
+
) {
|
|
59
|
+
ensureSafeToolRegistration(server);
|
|
60
|
+
|
|
61
|
+
server.registerTool(
|
|
62
|
+
'get_enforced_tls',
|
|
63
|
+
{
|
|
64
|
+
description:
|
|
65
|
+
'Read Enforced TLS settings (GET /v3/user/settings/enforced_tls). require_tls drops mail when the recipient MX does not offer TLS.',
|
|
66
|
+
inputSchema: z.object({ ...ReadInputFields }),
|
|
67
|
+
outputSchema: EnforcedTlsOutputSchema,
|
|
68
|
+
},
|
|
69
|
+
async ({ response_format }) => {
|
|
70
|
+
const settings = await client.getEnforcedTls();
|
|
71
|
+
return jsonReadResult(
|
|
72
|
+
settings,
|
|
73
|
+
`require_tls=${settings.require_tls} require_valid_cert=${settings.require_valid_cert} version=${settings.version}`,
|
|
74
|
+
response_format,
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
server.registerTool(
|
|
80
|
+
'list_mail_settings',
|
|
81
|
+
{
|
|
82
|
+
description:
|
|
83
|
+
'List SendGrid mail settings summaries (footer, bounce purge, spam check, etc.).',
|
|
84
|
+
inputSchema: z.object({ ...ListPagingInputFields }),
|
|
85
|
+
outputSchema: MailSettingsListOutputSchema,
|
|
86
|
+
},
|
|
87
|
+
async (params) => {
|
|
88
|
+
const response = await client.listMailSettings({
|
|
89
|
+
limit: params.limit,
|
|
90
|
+
offset: params.offset,
|
|
91
|
+
});
|
|
92
|
+
const settings = response.result ?? [];
|
|
93
|
+
const { items, pagination } = paginateArray(
|
|
94
|
+
settings,
|
|
95
|
+
params.limit,
|
|
96
|
+
params.offset,
|
|
97
|
+
);
|
|
98
|
+
return jsonReadResult(
|
|
99
|
+
{ ...pagination, settings: items },
|
|
100
|
+
jsonText(response),
|
|
101
|
+
params.response_format,
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
server.registerTool(
|
|
107
|
+
'get_mail_setting',
|
|
108
|
+
{
|
|
109
|
+
description: 'Read one SendGrid mail setting by name.',
|
|
110
|
+
inputSchema: z.object({
|
|
111
|
+
setting: MailSettingNameSchema,
|
|
112
|
+
...ReadInputFields,
|
|
113
|
+
}),
|
|
114
|
+
outputSchema: SettingDetailOutputSchema,
|
|
115
|
+
},
|
|
116
|
+
async ({ setting, response_format }) =>
|
|
117
|
+
jsonReadResult(
|
|
118
|
+
await client.getMailSetting(setting as MailSettingName),
|
|
119
|
+
undefined,
|
|
120
|
+
response_format,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
server.registerTool(
|
|
125
|
+
'list_tracking_settings',
|
|
126
|
+
{
|
|
127
|
+
description:
|
|
128
|
+
'List SendGrid tracking settings summaries (click, open, subscription, etc.).',
|
|
129
|
+
inputSchema: z.object({ ...ListPagingInputFields }),
|
|
130
|
+
outputSchema: TrackingSettingsListOutputSchema,
|
|
131
|
+
},
|
|
132
|
+
async (params) => {
|
|
133
|
+
const response = await client.listTrackingSettings();
|
|
134
|
+
const settings = response.result ?? [];
|
|
135
|
+
const { items, pagination } = paginateArray(
|
|
136
|
+
settings,
|
|
137
|
+
params.limit,
|
|
138
|
+
params.offset,
|
|
139
|
+
);
|
|
140
|
+
return jsonReadResult(
|
|
141
|
+
{ ...pagination, settings: items },
|
|
142
|
+
jsonText(response),
|
|
143
|
+
params.response_format,
|
|
144
|
+
);
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
server.registerTool(
|
|
149
|
+
'get_tracking_setting',
|
|
150
|
+
{
|
|
151
|
+
description: 'Read one SendGrid tracking setting by name.',
|
|
152
|
+
inputSchema: z.object({
|
|
153
|
+
setting: TrackingSettingNameSchema,
|
|
154
|
+
...ReadInputFields,
|
|
155
|
+
}),
|
|
156
|
+
outputSchema: SettingDetailOutputSchema,
|
|
157
|
+
},
|
|
158
|
+
async ({ setting, response_format }) =>
|
|
159
|
+
jsonReadResult(
|
|
160
|
+
await client.getTrackingSetting(setting as TrackingSettingName),
|
|
161
|
+
undefined,
|
|
162
|
+
response_format,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
server.registerTool(
|
|
167
|
+
'list_inbound_parse_settings',
|
|
168
|
+
{
|
|
169
|
+
description: 'List inbound parse (receive email webhook) settings.',
|
|
170
|
+
inputSchema: z.object({ ...ListPagingInputFields }),
|
|
171
|
+
outputSchema: InboundParseSettingsListOutputSchema,
|
|
172
|
+
},
|
|
173
|
+
async (params) => {
|
|
174
|
+
const settings = await client.listInboundParseSettings();
|
|
175
|
+
const { items, pagination } = paginateArray(
|
|
176
|
+
settings,
|
|
177
|
+
params.limit,
|
|
178
|
+
params.offset,
|
|
179
|
+
);
|
|
180
|
+
return jsonReadResult(
|
|
181
|
+
{ ...pagination, settings: items },
|
|
182
|
+
jsonText(items),
|
|
183
|
+
params.response_format,
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
server.registerTool(
|
|
189
|
+
'update_mail_setting',
|
|
190
|
+
{
|
|
191
|
+
description:
|
|
192
|
+
'Update one SendGrid mail setting (PATCH /mail_settings/{name}). Payload fields depend on setting type.',
|
|
193
|
+
inputSchema: z.object({
|
|
194
|
+
confirmToken: ConfirmTokenSchema,
|
|
195
|
+
setting: MailSettingNameSchema,
|
|
196
|
+
settings: z
|
|
197
|
+
.record(z.string(), SettingValueSchema)
|
|
198
|
+
.describe(
|
|
199
|
+
'Setting payload, e.g. { "enabled": true } or footer { "enabled", "html_content", "plain_content" }',
|
|
200
|
+
),
|
|
201
|
+
}),
|
|
202
|
+
},
|
|
203
|
+
async ({ setting, settings }) => ({
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: 'text',
|
|
207
|
+
text: jsonText(
|
|
208
|
+
await client.updateMailSetting(
|
|
209
|
+
setting as MailSettingName,
|
|
210
|
+
settings,
|
|
211
|
+
),
|
|
212
|
+
),
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
server.registerTool(
|
|
219
|
+
'update_tracking_setting',
|
|
220
|
+
{
|
|
221
|
+
description:
|
|
222
|
+
'Update one SendGrid tracking setting (click/open/subscription/google_analytics).',
|
|
223
|
+
inputSchema: z.object({
|
|
224
|
+
confirmToken: ConfirmTokenSchema,
|
|
225
|
+
setting: TrackingSettingNameSchema,
|
|
226
|
+
settings: z
|
|
227
|
+
.record(z.string(), SettingValueSchema)
|
|
228
|
+
.describe(
|
|
229
|
+
'Tracking payload, commonly { "enabled": true }. Open tracking may include substitution_tag.',
|
|
230
|
+
),
|
|
231
|
+
}),
|
|
232
|
+
},
|
|
233
|
+
async ({ setting, settings }) => ({
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: jsonText(
|
|
238
|
+
await client.updateTrackingSetting(
|
|
239
|
+
setting as TrackingSettingName,
|
|
240
|
+
settings,
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
server.registerTool(
|
|
249
|
+
'create_inbound_parse_setting',
|
|
250
|
+
{
|
|
251
|
+
description:
|
|
252
|
+
'Create inbound parse setting. Hostname MX must point to SendGrid.',
|
|
253
|
+
inputSchema: z.object({
|
|
254
|
+
confirmToken: ConfirmTokenSchema,
|
|
255
|
+
url: z.url(),
|
|
256
|
+
hostname: z.string().min(1),
|
|
257
|
+
spamCheck: z.boolean().optional(),
|
|
258
|
+
sendRaw: z.boolean().optional(),
|
|
259
|
+
}),
|
|
260
|
+
},
|
|
261
|
+
async ({ url, hostname, spamCheck, sendRaw }) => ({
|
|
262
|
+
content: [
|
|
263
|
+
{
|
|
264
|
+
type: 'text',
|
|
265
|
+
text: jsonText(
|
|
266
|
+
await client.createInboundParseSetting({
|
|
267
|
+
url,
|
|
268
|
+
hostname,
|
|
269
|
+
spam_check: spamCheck,
|
|
270
|
+
send_raw: sendRaw,
|
|
271
|
+
}),
|
|
272
|
+
),
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
server.registerTool(
|
|
279
|
+
'update_inbound_parse_setting',
|
|
280
|
+
{
|
|
281
|
+
description: 'Update inbound parse setting for a hostname.',
|
|
282
|
+
inputSchema: z.object({
|
|
283
|
+
confirmToken: ConfirmTokenSchema,
|
|
284
|
+
hostname: z.string().min(1),
|
|
285
|
+
url: z.url().optional(),
|
|
286
|
+
newHostname: z.string().min(1).optional(),
|
|
287
|
+
spamCheck: z.boolean().optional(),
|
|
288
|
+
sendRaw: z.boolean().optional(),
|
|
289
|
+
}),
|
|
290
|
+
},
|
|
291
|
+
async ({ hostname, url, newHostname, spamCheck, sendRaw }) => ({
|
|
292
|
+
content: [
|
|
293
|
+
{
|
|
294
|
+
type: 'text',
|
|
295
|
+
text: jsonText(
|
|
296
|
+
await client.updateInboundParseSetting(hostname, {
|
|
297
|
+
url,
|
|
298
|
+
hostname: newHostname,
|
|
299
|
+
spam_check: spamCheck,
|
|
300
|
+
send_raw: sendRaw,
|
|
301
|
+
}),
|
|
302
|
+
),
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
server.registerTool(
|
|
309
|
+
'delete_inbound_parse_setting',
|
|
310
|
+
{
|
|
311
|
+
description: 'Delete inbound parse setting by hostname.',
|
|
312
|
+
inputSchema: z.object({
|
|
313
|
+
confirmToken: ConfirmTokenSchema,
|
|
314
|
+
hostname: z.string().min(1),
|
|
315
|
+
}),
|
|
316
|
+
},
|
|
317
|
+
async ({ hostname }) => {
|
|
318
|
+
await client.deleteInboundParseSetting(hostname);
|
|
319
|
+
return {
|
|
320
|
+
content: [
|
|
321
|
+
{
|
|
322
|
+
type: 'text',
|
|
323
|
+
text: `Inbound parse setting for hostname "${hostname}" deleted.`,
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
};
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSendGridApiError,
|
|
3
|
+
type EmailLogMessage,
|
|
4
|
+
type SendGridClient,
|
|
5
|
+
type SendGridMessageActivity,
|
|
6
|
+
} from '../client';
|
|
7
|
+
|
|
8
|
+
export function activityQueryForXMessageId(xMessageId: string): string {
|
|
9
|
+
const id = xMessageId.trim();
|
|
10
|
+
if (id.length === 0) {
|
|
11
|
+
throw new Error('xMessageId is empty');
|
|
12
|
+
}
|
|
13
|
+
if (id.includes("'") || id.includes('"') || /\s/u.test(id)) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'xMessageId contains quotes or whitespace and cannot be placed in an Email Activity query',
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return `msg_id LIKE '${id}%'`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function compileActivitySearch(input: {
|
|
22
|
+
query?: string;
|
|
23
|
+
xMessageId?: string;
|
|
24
|
+
}): string {
|
|
25
|
+
const like = input.xMessageId
|
|
26
|
+
? activityQueryForXMessageId(input.xMessageId)
|
|
27
|
+
: undefined;
|
|
28
|
+
const query = input.query?.trim();
|
|
29
|
+
if (like && query) return `(${query}) AND ${like}`;
|
|
30
|
+
if (like) return like;
|
|
31
|
+
if (query) return query;
|
|
32
|
+
throw new Error('Provide query or xMessageId');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const EMAIL_LOGS_FIELDS = new Set([
|
|
36
|
+
'sg_message_id',
|
|
37
|
+
'subject',
|
|
38
|
+
'to_email',
|
|
39
|
+
'status',
|
|
40
|
+
'reason',
|
|
41
|
+
'categories',
|
|
42
|
+
'sg_message_id_created_at',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/** Activity queries using msg_id/from_email/LIKE are rejected by POST /v3/logs. */
|
|
46
|
+
export function activityQueryCanFallbackToLogs(query: string): boolean {
|
|
47
|
+
if (/\bLIKE\b/iu.test(query)) return false;
|
|
48
|
+
const identifiers = query.match(/[a-z_][a-z0-9_]*(?=\s*(?:=|IN\b|<|>))/giu) ?? [];
|
|
49
|
+
if (identifiers.length === 0) return false;
|
|
50
|
+
return identifiers.every((identifier) =>
|
|
51
|
+
EMAIL_LOGS_FIELDS.has(identifier.toLowerCase()),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function rejectActivityOffset(offset: number | undefined): void {
|
|
56
|
+
if (offset !== undefined && offset > 0) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
'GET /v3/messages does not document offset. A request that includes it still returns 200 and is not a reliable next page. Narrow the query with to_email, last_event_time, or msg_id LIKE.',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function activityListMeta(count: number, limit: number): {
|
|
64
|
+
total_count: number;
|
|
65
|
+
count: number;
|
|
66
|
+
offset: number;
|
|
67
|
+
has_more: boolean;
|
|
68
|
+
next_offset: null;
|
|
69
|
+
note?: string;
|
|
70
|
+
} {
|
|
71
|
+
const hasMore = count >= limit && count > 0;
|
|
72
|
+
return {
|
|
73
|
+
total_count: count,
|
|
74
|
+
count,
|
|
75
|
+
offset: 0,
|
|
76
|
+
has_more: hasMore,
|
|
77
|
+
next_offset: null,
|
|
78
|
+
...(hasMore
|
|
79
|
+
? {
|
|
80
|
+
note: 'Email Activity does not page with offset. Narrow the query (to_email, last_event_time, status) to see other messages.',
|
|
81
|
+
}
|
|
82
|
+
: {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function logsEqualityQuery(
|
|
87
|
+
field: 'to_email' | 'sg_message_id',
|
|
88
|
+
value: string,
|
|
89
|
+
): string {
|
|
90
|
+
const trimmed = value.trim();
|
|
91
|
+
if (trimmed.length === 0 || trimmed.includes("'") || trimmed.includes('"')) {
|
|
92
|
+
throw new Error(`${field} cannot be quoted into an Email Logs query`);
|
|
93
|
+
}
|
|
94
|
+
return `${field}='${trimmed}'`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function summarizeActivityMessage(
|
|
98
|
+
message: SendGridMessageActivity,
|
|
99
|
+
): string {
|
|
100
|
+
const lines = [
|
|
101
|
+
`msg_id=${message.msg_id}`,
|
|
102
|
+
`status=${message.status ?? 'n/a'}`,
|
|
103
|
+
`from=${message.from_email ?? 'n/a'}`,
|
|
104
|
+
`to=${message.to_email ?? 'n/a'}`,
|
|
105
|
+
`subject=${message.subject ?? 'n/a'}`,
|
|
106
|
+
`last_event_time=${message.last_event_time ?? 'n/a'}`,
|
|
107
|
+
];
|
|
108
|
+
const asmGroupId = message['asm_group_id'];
|
|
109
|
+
if (typeof asmGroupId === 'number' || typeof asmGroupId === 'string') {
|
|
110
|
+
lines.push(`asm_group_id=${asmGroupId}`);
|
|
111
|
+
}
|
|
112
|
+
const outboundIp = message['outbound_ip'];
|
|
113
|
+
if (typeof outboundIp === 'string') {
|
|
114
|
+
lines.push(
|
|
115
|
+
`outbound_ip=${outboundIp} type=${String(message['outbound_ip_type'] ?? 'n/a')}`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const reason = message['reason'];
|
|
119
|
+
if (typeof reason === 'string' && reason.length > 0) {
|
|
120
|
+
lines.push(`reason=${reason}`);
|
|
121
|
+
}
|
|
122
|
+
const events = message['events'];
|
|
123
|
+
if (Array.isArray(events) && events.length > 0) {
|
|
124
|
+
const last = events[events.length - 1];
|
|
125
|
+
if (last && typeof last === 'object') {
|
|
126
|
+
const event = last as Record<string, unknown>;
|
|
127
|
+
lines.push(
|
|
128
|
+
`last_event=${String(event['event_name'] ?? 'n/a')} reason=${String(event['reason'] ?? 'n/a')} bounce_type=${String(event['bounce_type'] ?? 'n/a')} mx=${String(event['mx_server'] ?? 'n/a')}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
lines.push(`events=${events.length}`);
|
|
132
|
+
}
|
|
133
|
+
return lines.join(', ');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function mapLogMessage(row: EmailLogMessage): SendGridMessageActivity | undefined {
|
|
137
|
+
if (!row.sg_message_id) return undefined;
|
|
138
|
+
return {
|
|
139
|
+
msg_id: row.sg_message_id,
|
|
140
|
+
from_email: row.from_email,
|
|
141
|
+
to_email: row.to_email,
|
|
142
|
+
subject: row.subject,
|
|
143
|
+
status: row.status,
|
|
144
|
+
...(row.reason ? { reason: row.reason } : {}),
|
|
145
|
+
...(row.sg_message_id_created_at
|
|
146
|
+
? { last_event_time: row.sg_message_id_created_at }
|
|
147
|
+
: {}),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function logsMessages(
|
|
152
|
+
client: SendGridClient,
|
|
153
|
+
query: string,
|
|
154
|
+
limit: number,
|
|
155
|
+
): Promise<SendGridMessageActivity[]> {
|
|
156
|
+
const logs = await client.searchEmailLogs({ query, limit });
|
|
157
|
+
return (logs.messages ?? []).flatMap((row) => {
|
|
158
|
+
const mapped = mapLogMessage(row);
|
|
159
|
+
return mapped ? [mapped] : [];
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function searchActivityOrLogs(
|
|
164
|
+
client: SendGridClient,
|
|
165
|
+
query: string,
|
|
166
|
+
limit: number,
|
|
167
|
+
): Promise<{
|
|
168
|
+
messages: SendGridMessageActivity[];
|
|
169
|
+
source: 'activity' | 'logs';
|
|
170
|
+
note?: string;
|
|
171
|
+
}> {
|
|
172
|
+
try {
|
|
173
|
+
const response = await client.filterMessages(query, limit);
|
|
174
|
+
const messages = response.messages ?? [];
|
|
175
|
+
if (messages.length === 0 && client.preferEmailLogs) {
|
|
176
|
+
if (!activityQueryCanFallbackToLogs(query)) {
|
|
177
|
+
return {
|
|
178
|
+
messages,
|
|
179
|
+
source: 'activity',
|
|
180
|
+
note: 'Email Activity returned no rows. This query cannot be sent to Email Logs: POST /v3/logs rejects msg_id, from_email, and LIKE.',
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const fromLogs = await logsMessages(client, query, limit);
|
|
185
|
+
if (fromLogs.length > 0) {
|
|
186
|
+
return {
|
|
187
|
+
messages: fromLogs,
|
|
188
|
+
source: 'logs',
|
|
189
|
+
note: 'Email Activity returned no rows. SENDGRID_REGION=eu often has no Activity detail, so these rows are from POST /v3/logs.',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
} catch (logsError) {
|
|
193
|
+
const logsText =
|
|
194
|
+
logsError instanceof Error ? logsError.message : String(logsError);
|
|
195
|
+
return {
|
|
196
|
+
messages: [],
|
|
197
|
+
source: 'activity',
|
|
198
|
+
note: `Email Activity returned no rows. Email Logs fallback failed: ${logsText}`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { messages, source: 'activity' };
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (
|
|
205
|
+
!isSendGridApiError(error) ||
|
|
206
|
+
(error.status !== 403 && error.status !== 404)
|
|
207
|
+
) {
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
if (!activityQueryCanFallbackToLogs(query)) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Email Activity failed (${error.status}). Email Logs was not queried because POST /v3/logs rejects msg_id, from_email, and LIKE. Use to_email='...' with sendgrid_search_email_logs.`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const fromLogs = await logsMessages(client, query, limit);
|
|
217
|
+
return {
|
|
218
|
+
messages: fromLogs,
|
|
219
|
+
source: 'logs',
|
|
220
|
+
note: `Email Activity returned ${error.status}. Rows are from POST /v3/logs. Logs only accepts equality/IN on sg_message_id, subject, to_email, status, reason, categories, and sg_message_id_created_at.`,
|
|
221
|
+
};
|
|
222
|
+
} catch (logsError) {
|
|
223
|
+
const logsText =
|
|
224
|
+
logsError instanceof Error ? logsError.message : String(logsError);
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Email Activity failed (${error.status}). Email Logs fallback also failed.\n${logsText}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function traceMessage(
|
|
233
|
+
client: SendGridClient,
|
|
234
|
+
msgId: string,
|
|
235
|
+
): Promise<{ message: SendGridMessageActivity; note?: string }> {
|
|
236
|
+
try {
|
|
237
|
+
return { message: await client.getMessageById(msgId) };
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (
|
|
240
|
+
!isSendGridApiError(error) ||
|
|
241
|
+
(error.status !== 404 && error.status !== 403)
|
|
242
|
+
) {
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const isXMessageId = !msgId.includes('.');
|
|
247
|
+
if (isXMessageId && error.status === 404) {
|
|
248
|
+
const found = await client.filterMessages(
|
|
249
|
+
activityQueryForXMessageId(msgId),
|
|
250
|
+
10,
|
|
251
|
+
);
|
|
252
|
+
const rows = found.messages ?? [];
|
|
253
|
+
const only = rows[0];
|
|
254
|
+
if (rows.length === 1 && only?.msg_id) {
|
|
255
|
+
return {
|
|
256
|
+
message: await client.getMessageById(only.msg_id),
|
|
257
|
+
note: `Resolved x-message-id ${msgId} to msg_id ${only.msg_id} via msg_id LIKE.`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
if (rows.length > 1) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`x-message-id ${msgId} matches ${rows.length} messages. Call get_message_activity with one full msg_id:\n${rows.map((row) => row.msg_id).join('\n')}`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
throw new Error(
|
|
266
|
+
`No Email Activity row for x-message-id ${msgId}. It may not be indexed yet, or this account has no Activity detail. Try search_email_logs with to_email equality.`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (msgId.includes('.')) {
|
|
271
|
+
try {
|
|
272
|
+
const fromLogs = await logsMessages(
|
|
273
|
+
client,
|
|
274
|
+
logsEqualityQuery('sg_message_id', msgId),
|
|
275
|
+
5,
|
|
276
|
+
);
|
|
277
|
+
const message = fromLogs[0];
|
|
278
|
+
if (message) {
|
|
279
|
+
return {
|
|
280
|
+
message,
|
|
281
|
+
note: `Email Activity returned ${error.status}. Loaded from POST /v3/logs, which has status and reason but no event chain.`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// Keep the original Activity error below.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|