@schemavaults/send-email 0.4.0 → 0.4.1

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/README.md CHANGED
@@ -121,9 +121,9 @@ const attachment = createEmailAttachment({
121
121
  // => { filename: "logo.png", content: "iVBORw0KGgo...", contentType: "image/png", contentId: "logo" }
122
122
  ```
123
123
 
124
- Setting `contentId` marks the attachment as inline so the HTML body can embed it via a `cid:` URL; pass the bare identifier without angle brackets. `contentType` is optional -- when omitted, the mail-server's transport derives it from the filename's extension.
124
+ Setting `contentId` marks the attachment as inline so the HTML body can embed it via a `cid:` URL; pass the bare identifier without angle brackets, in printable ASCII. `contentType` is optional -- when omitted, the mail-server's transport derives it from the filename's extension. Non-ASCII names belong in `filename`, not in a `contentType` parameter.
125
125
 
126
- Limits enforced client-side (and exported as `MAX_ATTACHMENTS_PER_EMAIL` / `MAX_TOTAL_ATTACHMENT_BYTES`): at most 20 attachments per email, totalling at most 25 MiB once decoded. Filenames may not contain path separators or control characters. The mail-server, its hosting platform, and the receiving mail systems may enforce lower limits of their own.
126
+ Limits enforced client-side (and exported as `MAX_ATTACHMENTS_PER_EMAIL` / `MAX_TOTAL_ATTACHMENT_BYTES`): at most 20 attachments per email, totalling at most 25 MiB once decoded. Filenames may not contain path separators, control characters, or bidirectional formatting characters (which can disguise `invoice\u202Efdp.exe` as `invoiceexe.pdf`). `contentType` and `contentId` must be printable ASCII, and `subject` must not contain line breaks or other control characters, so no value can break out of its MIME header. The mail-server, its hosting platform, and the receiving mail systems may enforce lower limits of their own.
127
127
 
128
128
  From the CLI, `--attach <path>` is repeatable on both `send` and `send-to-mailing-list`, and may be combined with `--body-file` (the files are appended to any `attachments` already in the JSON):
129
129
 
@@ -286,15 +286,15 @@ type Message =
286
286
  | { text: string; html: string }; // both required
287
287
 
288
288
  type EmailAttachment = {
289
- filename: string; // 1-255 chars; no path separators or control characters
289
+ filename: string; // 1-255 chars; no path separators, control or bidi formatting characters
290
290
  content: string; // base64-encoded bytes (standard alphabet, padded)
291
- contentType?: string; // MIME type; derived from the filename when omitted
292
- contentId?: string; // marks the attachment inline; reference as cid:<contentId>
291
+ contentType?: string; // MIME type, printable ASCII; derived from the filename when omitted
292
+ contentId?: string; // printable ASCII; marks the attachment inline; reference as cid:<contentId>
293
293
  };
294
294
 
295
295
  type SendEmailRequestBody = {
296
296
  to: string | string[]; // email address(es) or mailing list UUID
297
- subject: string;
297
+ subject: string; // no line breaks or control characters (tabs allowed)
298
298
  message: Message;
299
299
  from?: string; // defaults to mail-server's configured sender
300
300
  replyTo?: string;
package/dist/cli.js CHANGED
@@ -18021,7 +18021,27 @@ function base64DecodedByteLength(base643) {
18021
18021
  function hasControlCharacters(value) {
18022
18022
  for (let i = 0; i < value.length; i++) {
18023
18023
  const code = value.charCodeAt(i);
18024
- if (code < 32 || code === 127) return true;
18024
+ if (code < 32 || code >= 127 && code <= 159) return true;
18025
+ }
18026
+ return false;
18027
+ }
18028
+ var BIDI_FORMATTING = /* @__PURE__ */ new Set([
18029
+ 1564,
18030
+ 8206,
18031
+ 8207,
18032
+ 8234,
18033
+ 8235,
18034
+ 8236,
18035
+ 8237,
18036
+ 8238,
18037
+ 8294,
18038
+ 8295,
18039
+ 8296,
18040
+ 8297
18041
+ ]);
18042
+ function hasBidiFormatting(value) {
18043
+ for (let i = 0; i < value.length; i++) {
18044
+ if (BIDI_FORMATTING.has(value.charCodeAt(i))) return true;
18025
18045
  }
18026
18046
  return false;
18027
18047
  }
@@ -18031,18 +18051,22 @@ var attachmentFilenameSchema = external_exports.string().min(1, "Attachment file
18031
18051
  message: "Attachment filename must not contain path separators!"
18032
18052
  }).refine((name) => !hasControlCharacters(name), {
18033
18053
  message: "Attachment filename must not contain control characters!"
18054
+ }).refine((name) => !hasBidiFormatting(name), {
18055
+ message: "Attachment filename must not contain bidirectional formatting characters!"
18034
18056
  });
18035
18057
  var attachmentContentSchema = external_exports.base64("Attachment content must be base64-encoded!").min(1, "Attachment content must not be empty!");
18036
18058
  var MIME_TOKEN = "[A-Za-z0-9!#$&^_.+-]+";
18059
+ var MIME_PARAM_VALUE = '(?:"[\\x20\\x21\\x23-\\x7E]*"|[\\x21\\x23-\\x3A\\x3C-\\x7E]+)';
18037
18060
  var attachmentContentTypeSchema = external_exports.string().min(1, "Attachment content type must be non-empty!").max(255, "Attachment content type must be at most 255 characters!").regex(
18061
+ // `[ \t]*`, not `\s*`: `\s` matches CR/LF.
18038
18062
  new RegExp(
18039
- `^${MIME_TOKEN}\\/${MIME_TOKEN}(?:\\s*;\\s*${MIME_TOKEN}=(?:"[^"]*"|[^\\s;"]+))*$`
18063
+ `^${MIME_TOKEN}\\/${MIME_TOKEN}(?:[ \\t]*;[ \\t]*${MIME_TOKEN}=${MIME_PARAM_VALUE})*$`
18040
18064
  ),
18041
18065
  "Attachment content type must be a MIME type such as 'application/pdf'!"
18042
18066
  );
18043
18067
  var attachmentContentIdSchema = external_exports.string().min(1, "Attachment content ID must be non-empty!").max(255, "Attachment content ID must be at most 255 characters!").regex(
18044
- /^[^\s<>]+$/,
18045
- "Attachment content ID must not contain whitespace or angle brackets!"
18068
+ /^[\x21-\x3B\x3D\x3F-\x7E]+$/,
18069
+ "Attachment content ID must be printable ASCII without spaces or angle brackets!"
18046
18070
  );
18047
18071
  var emailAttachmentSchema = external_exports.object({
18048
18072
  filename: attachmentFilenameSchema,
@@ -18084,6 +18108,13 @@ var sendRawEmailOptions = external_exports.object({
18084
18108
  html: true
18085
18109
  }).strict();
18086
18110
  var MAX_RECIPIENTS = 50;
18111
+ function hasHeaderUnsafeCharacters(value) {
18112
+ for (let i = 0; i < value.length; i++) {
18113
+ const code = value.charCodeAt(i);
18114
+ if (code < 32 && code !== 9 || code === 127) return true;
18115
+ }
18116
+ return false;
18117
+ }
18087
18118
  function createRecipientSchema(allow_mailing_list_ids_as_recipients = false) {
18088
18119
  if (allow_mailing_list_ids_as_recipients) {
18089
18120
  return external_exports.union([
@@ -18107,7 +18138,9 @@ function createSendEmailRequestBodySchema(allow_mailing_list_ids_as_recipients =
18107
18138
  return external_exports.object({
18108
18139
  to: createRecipientSchema(allow_mailing_list_ids_as_recipients),
18109
18140
  from: external_exports.string().email().optional(),
18110
- subject: external_exports.string().nonempty(),
18141
+ subject: external_exports.string().nonempty().refine((subject) => !hasHeaderUnsafeCharacters(subject), {
18142
+ message: "Subject must not contain line breaks or control characters!"
18143
+ }),
18111
18144
  message: external_exports.union([sendEmailTemplateOptions, sendRawEmailOptions]),
18112
18145
  replyTo: external_exports.string().email().optional(),
18113
18146
  cc: external_exports.union([
@@ -22,6 +22,16 @@ const sendRawEmailOptions = z
22
22
  })
23
23
  .strict();
24
24
  const MAX_RECIPIENTS = 50;
25
+ // C0 controls except HTAB, plus DEL: a line break in a header value can
26
+ // start a new header (e.g. `Bcc:`).
27
+ function hasHeaderUnsafeCharacters(value) {
28
+ for (let i = 0; i < value.length; i++) {
29
+ const code = value.charCodeAt(i);
30
+ if ((code < 0x20 && code !== 0x09) || code === 0x7f)
31
+ return true;
32
+ }
33
+ return false;
34
+ }
25
35
  export function createRecipientSchema(allow_mailing_list_ids_as_recipients = false) {
26
36
  if (allow_mailing_list_ids_as_recipients) {
27
37
  return z.union([
@@ -47,7 +57,12 @@ export function createSendEmailRequestBodySchema(allow_mailing_list_ids_as_recip
47
57
  .object({
48
58
  to: createRecipientSchema(allow_mailing_list_ids_as_recipients),
49
59
  from: z.string().email().optional(),
50
- subject: z.string().nonempty(),
60
+ subject: z
61
+ .string()
62
+ .nonempty()
63
+ .refine((subject) => !hasHeaderUnsafeCharacters(subject), {
64
+ message: "Subject must not contain line breaks or control characters!",
65
+ }),
51
66
  message: z.union([sendEmailTemplateOptions, sendRawEmailOptions]),
52
67
  replyTo: z.string().email().optional(),
53
68
  cc: z
@@ -1 +1 @@
1
- {"version":3,"file":"send-email-request-body-schema.js","sourceRoot":"","sources":["../src/send-email-request-body-schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"send-email-request-body-schema.js","sourceRoot":"","sources":["../src/send-email-request-body-schema.ts"],"names":[],"mappings":"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"}
@@ -18,7 +18,8 @@ export declare const MAX_TOTAL_ATTACHMENT_BYTES: number;
18
18
  export declare function base64DecodedByteLength(base64: string): number;
19
19
  /**
20
20
  * Name the recipient's mail client shows for the attached file. Unicode is
21
- * allowed; path separators and control characters are not.
21
+ * allowed; path separators, control characters and bidirectional formatting
22
+ * characters are not.
22
23
  */
23
24
  export declare const attachmentFilenameSchema: z.ZodString;
24
25
  /**
@@ -29,14 +30,16 @@ export declare const attachmentFilenameSchema: z.ZodString;
29
30
  export declare const attachmentContentSchema: z.ZodBase64;
30
31
  /**
31
32
  * MIME type of the attachment, e.g. "application/pdf" or
32
- * "text/csv; charset=utf-8". When omitted, the mail-server's transport
33
- * derives it from the filename's extension.
33
+ * "text/csv; charset=utf-8". Printable ASCII only. When omitted, the
34
+ * mail-server's transport derives it from the filename's extension.
34
35
  */
35
36
  export declare const attachmentContentTypeSchema: z.ZodString;
36
37
  /**
37
38
  * Content-ID for inline attachments. Setting it marks the attachment as
38
39
  * inline so the HTML body can reference it as `<img src="cid:<contentId>">`.
39
- * Pass the bare identifier -- without the angle brackets.
40
+ * Pass the bare identifier -- without the angle brackets. Content-ID is an
41
+ * ASCII header; anything else is emitted as an encoded-word that no longer
42
+ * matches the HTML's `cid:` reference.
40
43
  */
41
44
  export declare const attachmentContentIdSchema: z.ZodString;
42
45
  /**
@@ -25,17 +25,34 @@ export function base64DecodedByteLength(base64) {
25
25
  padding = 1;
26
26
  return Math.floor((base64.length * 3) / 4) - padding;
27
27
  }
28
+ // C0, DEL and C1 controls.
28
29
  function hasControlCharacters(value) {
29
30
  for (let i = 0; i < value.length; i++) {
30
31
  const code = value.charCodeAt(i);
31
- if (code < 0x20 || code === 0x7f)
32
+ if (code < 0x20 || (code >= 0x7f && code <= 0x9f))
33
+ return true;
34
+ }
35
+ return false;
36
+ }
37
+ // Bidirectional formatting characters (ALM, LRM, RLM, LRE through RLO, LRI
38
+ // through PDI) let a filename like "invoice<U+202E>fdp.exe" display as
39
+ // "invoiceexe.pdf". Listed as code points because the characters are
40
+ // invisible in source.
41
+ const BIDI_FORMATTING = new Set([
42
+ 0x061c, 0x200e, 0x200f, 0x202a, 0x202b, 0x202c, 0x202d, 0x202e, 0x2066,
43
+ 0x2067, 0x2068, 0x2069,
44
+ ]);
45
+ function hasBidiFormatting(value) {
46
+ for (let i = 0; i < value.length; i++) {
47
+ if (BIDI_FORMATTING.has(value.charCodeAt(i)))
32
48
  return true;
33
49
  }
34
50
  return false;
35
51
  }
36
52
  /**
37
53
  * Name the recipient's mail client shows for the attached file. Unicode is
38
- * allowed; path separators and control characters are not.
54
+ * allowed; path separators, control characters and bidirectional formatting
55
+ * characters are not.
39
56
  */
40
57
  export const attachmentFilenameSchema = z
41
58
  .string()
@@ -49,6 +66,9 @@ export const attachmentFilenameSchema = z
49
66
  })
50
67
  .refine((name) => !hasControlCharacters(name), {
51
68
  message: "Attachment filename must not contain control characters!",
69
+ })
70
+ .refine((name) => !hasBidiFormatting(name), {
71
+ message: "Attachment filename must not contain bidirectional formatting characters!",
52
72
  });
53
73
  /**
54
74
  * The attached file's bytes, base64-encoded (standard alphabet, padded, no
@@ -59,26 +79,34 @@ export const attachmentContentSchema = z
59
79
  .base64("Attachment content must be base64-encoded!")
60
80
  .min(1, "Attachment content must not be empty!");
61
81
  const MIME_TOKEN = "[A-Za-z0-9!#$&^_.+-]+";
82
+ // Printable ASCII only, so no value can carry CR/LF or other controls out of
83
+ // its header. Quoted: anything printable but `"`. Unquoted: printable minus
84
+ // space, `"` and `;`.
85
+ const MIME_PARAM_VALUE = '(?:"[\\x20\\x21\\x23-\\x7E]*"|[\\x21\\x23-\\x3A\\x3C-\\x7E]+)';
62
86
  /**
63
87
  * MIME type of the attachment, e.g. "application/pdf" or
64
- * "text/csv; charset=utf-8". When omitted, the mail-server's transport
65
- * derives it from the filename's extension.
88
+ * "text/csv; charset=utf-8". Printable ASCII only. When omitted, the
89
+ * mail-server's transport derives it from the filename's extension.
66
90
  */
67
91
  export const attachmentContentTypeSchema = z
68
92
  .string()
69
93
  .min(1, "Attachment content type must be non-empty!")
70
94
  .max(255, "Attachment content type must be at most 255 characters!")
71
- .regex(new RegExp(`^${MIME_TOKEN}\\/${MIME_TOKEN}(?:\\s*;\\s*${MIME_TOKEN}=(?:"[^"]*"|[^\\s;"]+))*$`), "Attachment content type must be a MIME type such as 'application/pdf'!");
95
+ .regex(
96
+ // `[ \t]*`, not `\s*`: `\s` matches CR/LF.
97
+ new RegExp(`^${MIME_TOKEN}\\/${MIME_TOKEN}(?:[ \\t]*;[ \\t]*${MIME_TOKEN}=${MIME_PARAM_VALUE})*$`), "Attachment content type must be a MIME type such as 'application/pdf'!");
72
98
  /**
73
99
  * Content-ID for inline attachments. Setting it marks the attachment as
74
100
  * inline so the HTML body can reference it as `<img src="cid:<contentId>">`.
75
- * Pass the bare identifier -- without the angle brackets.
101
+ * Pass the bare identifier -- without the angle brackets. Content-ID is an
102
+ * ASCII header; anything else is emitted as an encoded-word that no longer
103
+ * matches the HTML's `cid:` reference.
76
104
  */
77
105
  export const attachmentContentIdSchema = z
78
106
  .string()
79
107
  .min(1, "Attachment content ID must be non-empty!")
80
108
  .max(255, "Attachment content ID must be at most 255 characters!")
81
- .regex(/^[^\s<>]+$/, "Attachment content ID must not contain whitespace or angle brackets!");
109
+ .regex(/^[\x21-\x3B\x3D\x3F-\x7E]+$/, "Attachment content ID must be printable ASCII without spaces or angle brackets!");
82
110
  /**
83
111
  * A single file attached to an email, in the shape accepted by the
84
112
  * `@schemavaults/mail-server` `/api/send` route.
@@ -1 +1 @@
1
- {"version":3,"file":"email-attachment-schema.js","sourceRoot":"","sources":["../../src/validators/email-attachment-schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"email-attachment-schema.js","sourceRoot":"","sources":["../../src/validators/email-attachment-schema.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@schemavaults/send-email",
3
3
  "description": "Schema defining the shape of the request body to send an email via @schemavaults/mail-server",
4
- "version": "0.4.0",
4
+ "version": "0.4.1",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
7
7
  "repository": {