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.
Files changed (2) hide show
  1. package/index.js +90 -9
  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,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 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()).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, templateId: templateId || templateAlias });
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postmark-mcp",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "Universal Postmark MCP server using official SDK",
5
5
  "main": "index.js",
6
6
  "bin": {