postmark-mcp 1.0.11 → 1.0.13
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 +92 -10
- 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,30 +161,79 @@ 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().url()).optional().describe("Array of attachment URLs (optional)")
|
132
166
|
},
|
133
|
-
async ({ to, subject, textBody, htmlBody, from, tag }) => {
|
134
|
-
const headers = [
|
135
|
-
{ "Name": "In-Reply-To", "Value": inReplyTo },
|
136
|
-
{ "Name": "References", "Value": inReplyTo }
|
137
|
-
];
|
167
|
+
async ({ to, subject, textBody, htmlBody, from, tag, inReplyTo, attachmentUrls }) => {
|
138
168
|
const emailData = {
|
139
169
|
From: from || defaultSender,
|
140
170
|
To: to,
|
141
171
|
Subject: subject,
|
142
172
|
TextBody: textBody,
|
143
|
-
Headers: inReplyTo ? headers : undefined,
|
144
173
|
MessageStream: defaultMessageStream,
|
145
174
|
TrackOpens: true,
|
146
175
|
TrackLinks: "HtmlAndText"
|
147
176
|
};
|
148
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
|
+
|
149
231
|
if (htmlBody) emailData.HtmlBody = htmlBody;
|
150
232
|
if (tag) emailData.Tag = tag;
|
151
233
|
|
152
|
-
console.error(
|
234
|
+
console.error("Sending email...", { to, subject });
|
153
235
|
const result = await postmarkClient.sendEmail(emailData);
|
154
|
-
console.error(
|
236
|
+
console.error("Email sent successfully:", result.MessageID);
|
155
237
|
|
156
238
|
return {
|
157
239
|
content: [{
|
@@ -195,7 +277,7 @@ function registerTools(server, postmarkClient) {
|
|
195
277
|
|
196
278
|
if (tag) emailData.Tag = tag;
|
197
279
|
|
198
|
-
console.error('Sending template email...', { to,
|
280
|
+
console.error('Sending template email...', { to, template: templateId || templateAlias });
|
199
281
|
const result = await postmarkClient.sendEmailWithTemplate(emailData);
|
200
282
|
console.error('Template email sent successfully:', result.MessageID);
|
201
283
|
|