@mantiq/mail 0.5.23 → 0.6.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantiq/mail",
3
- "version": "0.5.23",
3
+ "version": "0.6.0",
4
4
  "description": "Transactional email — SMTP, Resend, SendGrid, Mailgun, Postmark, SES, markdown templates",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,6 +17,27 @@ export class SmtpTransport implements MailTransport {
17
17
  this.config = config
18
18
  }
19
19
 
20
+ /**
21
+ * Security: validate that a value contains no CR, LF, or null bytes
22
+ * to prevent SMTP header/command injection.
23
+ */
24
+ private assertNoInjection(value: string, field: string): void {
25
+ if (/[\r\n\0]/.test(value)) {
26
+ throw new MailError(
27
+ `SMTP injection detected in ${field}: value contains CR, LF, or null bytes`,
28
+ )
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Security: sanitize a subject line by stripping CR/LF characters
34
+ * to prevent SMTP header injection. Per RFC 2047, subjects should
35
+ * not contain bare newlines.
36
+ */
37
+ private sanitizeSubject(subject: string): string {
38
+ return subject.replace(/[\r\n\0]/g, '')
39
+ }
40
+
20
41
  async send(message: Message): Promise<{ id: string }> {
21
42
  const { host, port, username, password, encryption = 'none' } = this.config
22
43
 
@@ -133,13 +154,19 @@ export class SmtpTransport implements MailTransport {
133
154
  }
134
155
  }
135
156
 
157
+ // Security: validate all email addresses against SMTP injection
158
+ this.assertNoInjection(message.from.address, 'from address')
159
+ const allRecipients = [...message.to, ...message.cc, ...message.bcc]
160
+ for (const recipient of allRecipients) {
161
+ this.assertNoInjection(recipient.address, 'recipient address')
162
+ }
163
+
136
164
  // MAIL FROM
137
165
  socket.write(`MAIL FROM:<${message.from.address}>\r\n`)
138
166
  const mailFrom = await waitForResponse()
139
167
  this.assertCode(mailFrom, 250, 'MAIL FROM')
140
168
 
141
169
  // RCPT TO — all recipients
142
- const allRecipients = [...message.to, ...message.cc, ...message.bcc]
143
170
  for (const recipient of allRecipients) {
144
171
  socket.write(`RCPT TO:<${recipient.address}>\r\n`)
145
172
  const rcpt = await waitForResponse()
@@ -236,11 +263,14 @@ export class SmtpTransport implements MailTransport {
236
263
  lines.push(`Reply-To: ${Message.formatAddresses(message.replyTo)}`)
237
264
  }
238
265
 
239
- lines.push(`Subject: ${message.subject}`)
266
+ // Security: sanitize subject to prevent SMTP header injection
267
+ lines.push(`Subject: ${this.sanitizeSubject(message.subject)}`)
240
268
  lines.push(`MIME-Version: 1.0`)
241
269
 
242
- // Custom headers
270
+ // Custom headers — validate against SMTP header injection
243
271
  for (const [key, value] of Object.entries(message.headers)) {
272
+ this.assertNoInjection(key, 'custom header name')
273
+ this.assertNoInjection(value, `custom header '${key}'`)
244
274
  lines.push(`${key}: ${value}`)
245
275
  }
246
276
 
@@ -148,9 +148,19 @@ function sanitizeLinkUrl(url: string): string | null {
148
148
  return trimmed
149
149
  }
150
150
 
151
+ /**
152
+ * Security: escape HTML special characters to prevent XSS when
153
+ * user-provided text is interpolated into HTML output.
154
+ */
155
+ function escapeHtml(s: string): string {
156
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;')
157
+ }
158
+
151
159
  /** Process inline markdown: bold, italic, code, links */
152
160
  function inlineFormatting(text: string): string {
153
- return text
161
+ // Security: escape HTML in the raw text first to prevent XSS injection,
162
+ // then apply markdown formatting on the escaped output.
163
+ return escapeHtml(text)
154
164
  // Bold
155
165
  .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
156
166
  // Italic
@@ -164,6 +174,7 @@ function inlineFormatting(text: string): string {
164
174
  // Security: strip dangerous link, render as plain text only
165
175
  return label
166
176
  }
177
+ // Security: label is already HTML-escaped from the escapeHtml() call above
167
178
  return `<a href="${safeUrl}" class="email-link" style="color:#10b981;text-decoration:underline;">${label}</a>`
168
179
  })
169
180
  }