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.
Files changed (2) hide show
  1. package/index.js +92 -10
  2. 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 ID this email is in reply to (optional)")
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('Sending email...', { to, subject });
234
+ console.error("Sending email...", { to, subject });
153
235
  const result = await postmarkClient.sendEmail(emailData);
154
- console.error('Email sent successfully:', result.MessageID);
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, templateId: templateId || templateAlias });
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postmark-mcp",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Universal Postmark MCP server using official SDK",
5
5
  "main": "index.js",
6
6
  "bin": {