@sendinel/mcp-server 1.0.78 → 1.0.80
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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertSendAccepted, SendRefusedError } from "@sendinel/domain";
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { appendPhysicalAddressFooter, appendUnsubscribeFooter, buildListUnsubscribeHeaders, createUnsubscribeUrl, messageIds, normalizeSubject } from '@sendinel/domain';
|
|
3
4
|
import { db, requestContext } from '../db.js';
|
|
@@ -141,6 +142,7 @@ export async function sendAgentReply(mailbox, input, approvalId) {
|
|
|
141
142
|
replyTo: `${mailbox.local_part}@${mailbox.domain}`,
|
|
142
143
|
headers: { 'Message-ID': message.message_id_header, ...(parentId ? { 'In-Reply-To': parentId, References: references.join(' ') } : {}), ...message.unsubscribeHeaders },
|
|
143
144
|
});
|
|
145
|
+
assertSendAccepted(result);
|
|
144
146
|
sent.push(message.id);
|
|
145
147
|
checked(await mailboxDb().from('email_log').update({ status: 'sent', sent_at: new Date().toISOString(), provider_message_id: result.messageId })
|
|
146
148
|
.eq('id', message.log_id).eq('project_id', getProjectId()));
|
|
@@ -148,7 +150,10 @@ export async function sendAgentReply(mailbox, input, approvalId) {
|
|
|
148
150
|
.eq('id', message.id).eq('mailbox_id', mailbox.id).eq('project_id', getProjectId()));
|
|
149
151
|
await mailboxAudit('reply_sent', mailbox, { approval_id: approvalId, thread_id: threadId, message_id: message.id });
|
|
150
152
|
}
|
|
151
|
-
catch {
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error instanceof SendRefusedError) {
|
|
155
|
+
checked(await mailboxDb().from('email_log').update({ status: 'failed', error_message: error.message }).eq('id', message.log_id).eq('project_id', getProjectId()));
|
|
156
|
+
}
|
|
152
157
|
// Preserve reservation and queued log on ambiguous failures; never retry automatically or claim no send.
|
|
153
158
|
await mailboxAudit('reply_needs_human', mailbox, { approval_id: approvalId, thread_id: threadId, message_id: message.id });
|
|
154
159
|
return { sent: false, sent_message_ids: sent, thread_id: threadId,
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
import { type SendRefusal } from "@sendinel/domain";
|
|
1
2
|
export declare const EMAIL_PROVIDER_NAMES: readonly ["resend", "sendgrid", "ses", "postmark", "mailgun", "elastic-email"];
|
|
2
3
|
export type EmailProviderName = (typeof EMAIL_PROVIDER_NAMES)[number];
|
|
3
4
|
type SendArgs = {
|
|
4
5
|
from: string;
|
|
5
|
-
to: string;
|
|
6
|
+
to: string | string[];
|
|
7
|
+
cc?: string[];
|
|
8
|
+
bcc?: string[];
|
|
6
9
|
subject: string;
|
|
7
10
|
html?: string;
|
|
8
11
|
text?: string;
|
|
9
12
|
replyTo?: string;
|
|
10
13
|
headers?: Record<string, string>;
|
|
11
14
|
};
|
|
12
|
-
type SendResult = {
|
|
15
|
+
type SendResult = SendRefusal | {
|
|
16
|
+
refused?: false;
|
|
13
17
|
provider: EmailProviderName;
|
|
14
18
|
messageId: string;
|
|
15
19
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { checkProjectSendAllowlist } from "@sendinel/domain";
|
|
1
2
|
import { Resend } from "resend";
|
|
2
|
-
import { db } from "../db.js";
|
|
3
|
+
import { adminDb, db } from "../db.js";
|
|
3
4
|
import { decrypt } from "../crypto.js";
|
|
4
5
|
export const EMAIL_PROVIDER_NAMES = [
|
|
5
6
|
"resend",
|
|
@@ -125,6 +126,10 @@ async function sendViaResend(apiKey, args) {
|
|
|
125
126
|
payload.html = args.html;
|
|
126
127
|
if (args.text)
|
|
127
128
|
payload.text = args.text;
|
|
129
|
+
if (args.cc?.length)
|
|
130
|
+
payload.cc = args.cc;
|
|
131
|
+
if (args.bcc?.length)
|
|
132
|
+
payload.bcc = args.bcc;
|
|
128
133
|
if (args.replyTo)
|
|
129
134
|
payload.reply_to = args.replyTo;
|
|
130
135
|
if (args.headers)
|
|
@@ -143,7 +148,7 @@ async function sendViaElasticEmail(apiKey, args) {
|
|
|
143
148
|
"X-ElasticEmail-ApiKey": apiKey,
|
|
144
149
|
},
|
|
145
150
|
body: JSON.stringify({
|
|
146
|
-
Recipients: { To: [args.to] },
|
|
151
|
+
Recipients: { To: Array.isArray(args.to) ? args.to : [args.to], ...(args.cc?.length ? { CC: args.cc } : {}), ...(args.bcc?.length ? { BCC: args.bcc } : {}) },
|
|
147
152
|
Content: {
|
|
148
153
|
From: from.email,
|
|
149
154
|
FromName: from.name,
|
|
@@ -171,7 +176,7 @@ async function sendViaSendGrid(apiKey, args) {
|
|
|
171
176
|
"Content-Type": "application/json",
|
|
172
177
|
},
|
|
173
178
|
body: JSON.stringify({
|
|
174
|
-
personalizations: [{ to: [{ email: args.
|
|
179
|
+
personalizations: [{ to: (Array.isArray(args.to) ? args.to : [args.to]).map((email) => ({ email })), ...(args.cc?.length ? { cc: args.cc.map((email) => ({ email })) } : {}), ...(args.bcc?.length ? { bcc: args.bcc.map((email) => ({ email })) } : {}) }],
|
|
175
180
|
from: { email: from.email, name: from.name },
|
|
176
181
|
subject: args.subject,
|
|
177
182
|
content: [
|
|
@@ -196,7 +201,9 @@ async function sendViaPostmark(apiKey, args) {
|
|
|
196
201
|
},
|
|
197
202
|
body: JSON.stringify({
|
|
198
203
|
From: args.from,
|
|
199
|
-
To: args.to,
|
|
204
|
+
To: Array.isArray(args.to) ? args.to.join(",") : args.to,
|
|
205
|
+
...(args.cc?.length ? { Cc: args.cc.join(",") } : {}),
|
|
206
|
+
...(args.bcc?.length ? { Bcc: args.bcc.join(",") } : {}),
|
|
200
207
|
Subject: args.subject,
|
|
201
208
|
HtmlBody: args.html,
|
|
202
209
|
TextBody: args.text,
|
|
@@ -212,7 +219,11 @@ async function sendViaPostmark(apiKey, args) {
|
|
|
212
219
|
async function sendViaMailgun(apiKey, args) {
|
|
213
220
|
const form = new FormData();
|
|
214
221
|
form.set("from", args.from);
|
|
215
|
-
form.set("to", args.to);
|
|
222
|
+
form.set("to", Array.isArray(args.to) ? args.to.join(",") : args.to);
|
|
223
|
+
if (args.cc?.length)
|
|
224
|
+
form.set("cc", args.cc.join(","));
|
|
225
|
+
if (args.bcc?.length)
|
|
226
|
+
form.set("bcc", args.bcc.join(","));
|
|
216
227
|
form.set("subject", args.subject);
|
|
217
228
|
if (args.html)
|
|
218
229
|
form.set("html", args.html);
|
|
@@ -237,6 +248,9 @@ async function sendViaMailgun(apiKey, args) {
|
|
|
237
248
|
return id || `mg_${Date.now()}`;
|
|
238
249
|
}
|
|
239
250
|
export async function sendEmailForSite(projectId, siteId, args) {
|
|
251
|
+
const refusal = await checkProjectSendAllowlist(adminDb(), projectId, args, "mcp");
|
|
252
|
+
if (refusal)
|
|
253
|
+
return refusal;
|
|
240
254
|
const config = await resolveProviderConfig(projectId, siteId);
|
|
241
255
|
switch (config.name) {
|
|
242
256
|
case "resend":
|
package/build/tools/campaigns.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import Anthropic from '@anthropic-ai/sdk';
|
|
3
|
-
import { BLOCKING_CONTENT_ISSUE_KINDS, computeSendContentKey, findContentIssues, sendingHostOf, NAME_FALLBACKS, ONE_SEND_MATCH_STATUSES, ONE_SEND_SENT_STATUSES, ONE_SEND_WINDOW_DAYS, normalizeTriggerConfig, renderTemplateString as renderTemplateVars, resolveBrandTokens, readAllRows, } from '@sendinel/domain';
|
|
3
|
+
import { assertSendAccepted, SendRefusedError, BLOCKING_CONTENT_ISSUE_KINDS, computeSendContentKey, findContentIssues, sendingHostOf, NAME_FALLBACKS, ONE_SEND_MATCH_STATUSES, ONE_SEND_SENT_STATUSES, ONE_SEND_WINDOW_DAYS, normalizeTriggerConfig, renderTemplateString as renderTemplateVars, resolveBrandTokens, readAllRows, } from '@sendinel/domain';
|
|
4
4
|
// Subpath import, not the barrel above -- reply-actions.ts uses node:crypto,
|
|
5
5
|
// and MCP-server-side that's fine (this never runs in an Edge Function), but
|
|
6
6
|
// keeping it off the shared barrel avoids re-creating the Edge-Function
|
|
@@ -2450,8 +2450,12 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2450
2450
|
let sendResult;
|
|
2451
2451
|
try {
|
|
2452
2452
|
sendResult = await sendEmailForSite(projectId, site_id, payload);
|
|
2453
|
+
assertSendAccepted(sendResult);
|
|
2453
2454
|
}
|
|
2454
2455
|
catch (error) {
|
|
2456
|
+
if (error instanceof SendRefusedError) {
|
|
2457
|
+
await db().from('email_log').insert({ project_id: projectId, site_id, to_email: to, from_email: site.from_email, subject: resolvedSubject, category: 'transactional', status: 'failed', error_message: error.message });
|
|
2458
|
+
}
|
|
2455
2459
|
return txt(`Error: Send failed — ${error instanceof Error ? error.message : 'unknown provider error'}`);
|
|
2456
2460
|
}
|
|
2457
2461
|
// Log to email_log
|
|
@@ -2692,6 +2696,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2692
2696
|
let sendResult;
|
|
2693
2697
|
try {
|
|
2694
2698
|
sendResult = await sendEmailForSite(projectId, site_id, payload);
|
|
2699
|
+
assertSendAccepted(sendResult);
|
|
2695
2700
|
}
|
|
2696
2701
|
catch (error) {
|
|
2697
2702
|
await db().from('email_log').update({ status: 'failed' }).eq('id', logRow.id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.80",
|
|
4
4
|
"description": "AI-controlled email operations control plane for campaigns, contacts, segments, analytics, and multi-provider sending via MCP.",
|
|
5
5
|
"mcpName": "ai.sendinel/mcp-server",
|
|
6
6
|
"type": "module",
|