postmark-mcp 1.0.12 → 1.0.14
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/index.js +90 -9
- package/package.json +1 -1
package/index.js
CHANGED
@@ -118,6 +118,39 @@ process.on('unhandledRejection', (reason) => {
|
|
118
118
|
|
119
119
|
// Move tool registration to a separate function for better organization
|
120
120
|
function registerTools(server, postmarkClient) {
|
121
|
+
// Helpers (scoped to this registrar)
|
122
|
+
const MAX_EMAIL_SIZE_B64 = 10 * 1024 * 1024; // Postmark limit (after base64)
|
123
|
+
const FORBIDDEN_EXTS = new Set([
|
124
|
+
'vbs', 'exe', 'bin', 'bat', 'chm', 'com', 'cpl', 'crt', 'hlp', 'hta', 'inf', 'ins', 'isp', 'jse', 'lnk',
|
125
|
+
'mdb', 'pcd', 'pif', 'reg', 'scr', 'sct', 'shs', 'vbe', 'vba', 'wsf', 'wsh', 'wsl', 'msc', 'msi', 'msp', 'mst'
|
126
|
+
]);
|
127
|
+
|
128
|
+
function pickFilename(url, contentDisposition) {
|
129
|
+
// RFC 5987: filename*=UTF-8''encoded%20name.pdf
|
130
|
+
if (contentDisposition) {
|
131
|
+
const star = contentDisposition.match(/filename\*=([^;]+)/i);
|
132
|
+
if (star && star[1]) {
|
133
|
+
const v = star[1].trim().replace(/^UTF-8''/i, '');
|
134
|
+
try { return decodeURIComponent(v); } catch { }
|
135
|
+
}
|
136
|
+
const quoted = contentDisposition.match(/filename="?([^"]+)"?/i);
|
137
|
+
if (quoted && quoted[1]) return quoted[1];
|
138
|
+
}
|
139
|
+
try {
|
140
|
+
const u = new URL(url);
|
141
|
+
const last = u.pathname.split('/').pop();
|
142
|
+
if (last) return last;
|
143
|
+
} catch { }
|
144
|
+
return 'attachment';
|
145
|
+
}
|
146
|
+
|
147
|
+
function isForbiddenExt(name) {
|
148
|
+
const ext = (name.split('.').pop() || '').toLowerCase();
|
149
|
+
return FORBIDDEN_EXTS.has(ext);
|
150
|
+
}
|
151
|
+
|
152
|
+
const fmtMsgId = (id) => /^<.*>$/.test(id) ? id : `<${id}>`;
|
153
|
+
|
121
154
|
// Define and register the sendEmail tool
|
122
155
|
server.tool(
|
123
156
|
"sendEmail",
|
@@ -128,25 +161,73 @@ function registerTools(server, postmarkClient) {
|
|
128
161
|
htmlBody: z.string().optional().describe("HTML body of the email (optional)"),
|
129
162
|
from: z.string().optional().describe("Sender email address (optional, uses default if not provided)"),
|
130
163
|
tag: z.string().optional().describe("Optional tag for categorization"),
|
131
|
-
inReplyTo: z.string().optional().describe("Message
|
164
|
+
inReplyTo: z.string().optional().describe("SMTP Message-ID this email replies to (e.g. <id@host>)"),
|
165
|
+
attachmentUrls: z.array(z.string()).optional().describe("Array of attachment URLs (optional)")
|
132
166
|
},
|
133
|
-
async ({ to, subject, textBody, htmlBody, from, tag, inReplyTo }) => {
|
167
|
+
async ({ to, subject, textBody, htmlBody, from, tag, inReplyTo, attachmentUrls }) => {
|
134
168
|
const emailData = {
|
135
169
|
From: from || defaultSender,
|
136
170
|
To: to,
|
137
171
|
Subject: subject,
|
138
172
|
TextBody: textBody,
|
139
|
-
Headers: inReplyTo
|
140
|
-
? [
|
141
|
-
{ Name: "In-Reply-To", Value: inReplyTo },
|
142
|
-
{ Name: "References", Value: inReplyTo }
|
143
|
-
]
|
144
|
-
: undefined,
|
145
173
|
MessageStream: defaultMessageStream,
|
146
174
|
TrackOpens: true,
|
147
175
|
TrackLinks: "HtmlAndText"
|
148
176
|
};
|
149
177
|
|
178
|
+
if (inReplyTo) {
|
179
|
+
emailData.Headers = [
|
180
|
+
{ Name: "In-Reply-To", Value: fmtMsgId(inReplyTo) },
|
181
|
+
{ Name: "References", Value: fmtMsgId(inReplyTo) }
|
182
|
+
];
|
183
|
+
}
|
184
|
+
|
185
|
+
// Fetch attachments and convert to base64 (with limits and safer filename parsing)
|
186
|
+
if (attachmentUrls && attachmentUrls.length > 0) {
|
187
|
+
let attachmentsSize = 0;
|
188
|
+
const attachments = [];
|
189
|
+
|
190
|
+
for (const url of attachmentUrls) {
|
191
|
+
const response = await fetch(url);
|
192
|
+
if (!response.ok) {
|
193
|
+
throw new Error(`Failed to fetch attachment from ${url}: ${response.status} ${response.statusText}`);
|
194
|
+
}
|
195
|
+
|
196
|
+
const arrayBuf = await response.arrayBuffer();
|
197
|
+
const buf = Buffer.from(arrayBuf);
|
198
|
+
const base64 = buf.toString('base64');
|
199
|
+
|
200
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
201
|
+
const contentDisposition = response.headers.get("content-disposition") || "";
|
202
|
+
const filename = pickFilename(url, contentDisposition);
|
203
|
+
|
204
|
+
if (isForbiddenExt(filename)) {
|
205
|
+
throw new Error(`Attachment "${filename}" has a forbidden file extension.`);
|
206
|
+
}
|
207
|
+
|
208
|
+
attachments.push({
|
209
|
+
Name: filename,
|
210
|
+
Content: base64,
|
211
|
+
ContentType: contentType
|
212
|
+
// ContentID: 'cid:your-inline-id' // <- enable if you need inline images later
|
213
|
+
});
|
214
|
+
|
215
|
+
// Postmark counts after base64; track the growing size
|
216
|
+
attachmentsSize += Buffer.byteLength(base64, 'utf8');
|
217
|
+
}
|
218
|
+
|
219
|
+
// Conservative guard that also counts bodies
|
220
|
+
const bodySize =
|
221
|
+
(textBody ? Buffer.byteLength(textBody, 'utf8') : 0) +
|
222
|
+
(htmlBody ? Buffer.byteLength(htmlBody, 'utf8') : 0);
|
223
|
+
|
224
|
+
if (attachmentsSize + bodySize > MAX_EMAIL_SIZE_B64) {
|
225
|
+
throw new Error('Attachments + body exceed Postmark’s 10 MB limit.');
|
226
|
+
}
|
227
|
+
|
228
|
+
emailData.Attachments = attachments;
|
229
|
+
}
|
230
|
+
|
150
231
|
if (htmlBody) emailData.HtmlBody = htmlBody;
|
151
232
|
if (tag) emailData.Tag = tag;
|
152
233
|
|
@@ -196,7 +277,7 @@ function registerTools(server, postmarkClient) {
|
|
196
277
|
|
197
278
|
if (tag) emailData.Tag = tag;
|
198
279
|
|
199
|
-
console.error('Sending template email...', { to,
|
280
|
+
console.error('Sending template email...', { to, template: templateId || templateAlias });
|
200
281
|
const result = await postmarkClient.sendEmailWithTemplate(emailData);
|
201
282
|
console.error('Template email sent successfully:', result.MessageID);
|
202
283
|
|