@upyo/core 0.5.3 → 0.5.4

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/dist/message.cjs CHANGED
@@ -22,26 +22,28 @@ const require_attachment = require('./attachment.cjs');
22
22
  * addresses and `File` objects for attachments.
23
23
  * @returns A new {@link Message} object with all properties normalized and
24
24
  * validated.
25
- * @throws {TypeError} When any email address string cannot be parsed or when
26
- * an attachment object is invalid.
25
+ * @throws {TypeError} When any email address string cannot be parsed, when an
26
+ * address or attachment carries a carriage return or line
27
+ * feed that could forge header fields, or when an
28
+ * attachment object is invalid.
27
29
  */
28
30
  function createMessage(constructor) {
29
- const sender = typeof constructor.from === "string" ? require_address.parseAddress(constructor.from) ?? throwTypeError(`Invalid sender address: ${JSON.stringify(constructor.from)}`) : constructor.from;
31
+ const sender = checkAddress("sender", constructor.from);
30
32
  return {
31
33
  sender,
32
- recipients: ensureArray(constructor.to).map((to) => typeof to === "string" ? require_address.parseAddress(to) ?? throwTypeError(`Invalid recipient address: ${JSON.stringify(to)}`) : to),
33
- ccRecipients: ensureArray(constructor.cc).map((cc) => typeof cc === "string" ? require_address.parseAddress(cc) ?? throwTypeError(`Invalid CC address: ${JSON.stringify(cc)}`) : cc),
34
- bccRecipients: ensureArray(constructor.bcc).map((bcc) => typeof bcc === "string" ? require_address.parseAddress(bcc) ?? throwTypeError(`Invalid BCC address: ${JSON.stringify(bcc)}`) : bcc),
35
- replyRecipients: ensureArray(constructor.replyTo).map((replyTo) => typeof replyTo === "string" ? require_address.parseAddress(replyTo) ?? throwTypeError(`Invalid reply-to address: ${JSON.stringify(replyTo)}`) : replyTo),
34
+ recipients: ensureArray(constructor.to).map((to) => checkAddress("recipient", to)),
35
+ ccRecipients: ensureArray(constructor.cc).map((cc) => checkAddress("CC", cc)),
36
+ bccRecipients: ensureArray(constructor.bcc).map((bcc) => checkAddress("BCC", bcc)),
37
+ replyRecipients: ensureArray(constructor.replyTo).map((replyTo) => checkAddress("reply-to", replyTo)),
36
38
  attachments: ensureArray(constructor.attachments).map((attachment) => {
37
- if (attachment instanceof File) return {
39
+ if (attachment instanceof File) return checkAttachment({
38
40
  inline: false,
39
41
  filename: attachment.name,
40
42
  content: attachment.arrayBuffer().then((b) => new Uint8Array(b)),
41
43
  contentType: attachment.type == null || attachment.type === "" ? "application/octet-stream" : attachment.type,
42
44
  contentId: `${crypto.randomUUID()}@${sender.address.replace(/^[^@]*@/, "")}`
43
- };
44
- else if (require_attachment.isAttachment(attachment)) return attachment;
45
+ });
46
+ else if (require_attachment.isAttachment(attachment)) return checkAttachment(attachment);
45
47
  else throwTypeError(`Invalid attachment: ${JSON.stringify(attachment)}`);
46
48
  }),
47
49
  subject: constructor.subject,
@@ -55,6 +57,45 @@ function createMessage(constructor) {
55
57
  function throwTypeError(message) {
56
58
  throw new TypeError(message);
57
59
  }
60
+ /**
61
+ * Normalizes an address and rejects one that could forge header fields.
62
+ *
63
+ * A transport that composes a message itself writes the address into a header
64
+ * field as given, so a carriage return or line feed in it would end that field
65
+ * and let the rest of the value appear as further fields. The string form has
66
+ * always been rejected by {@link parseAddress}; the object form used to pass
67
+ * through unchecked.
68
+ *
69
+ * @param label How the address is described in the error message.
70
+ * @param address The address, either an object or a string to parse.
71
+ * @returns The normalized address.
72
+ * @throws {TypeError} If the string cannot be parsed, or if the resulting
73
+ * address contains a carriage return or line feed.
74
+ */
75
+ function checkAddress(label, address) {
76
+ const parsed = typeof address === "string" ? require_address.parseAddress(address) ?? throwTypeError(`Invalid ${label} address: ${JSON.stringify(address)}`) : address;
77
+ if (/[\r\n]/.test(parsed.address)) throwTypeError(`Invalid ${label} address: ${JSON.stringify(parsed.address)}`);
78
+ return parsed;
79
+ }
80
+ /**
81
+ * Rejects an attachment whose content type or content ID could forge header
82
+ * fields.
83
+ *
84
+ * A transport that composes MIME itself writes both into the part headers as
85
+ * given, so a carriage return or line feed in either would end that field and
86
+ * let the rest of the value appear as further fields. An uploaded file's
87
+ * declared content type is routinely chosen by whoever uploaded it.
88
+ *
89
+ * @param attachment The attachment to check.
90
+ * @returns The attachment, unchanged.
91
+ * @throws {TypeError} If the content type or content ID contains a carriage
92
+ * return or line feed.
93
+ */
94
+ function checkAttachment(attachment) {
95
+ if (/[\r\n]/.test(attachment.contentType)) throwTypeError(`Invalid attachment content type: ${JSON.stringify(attachment.contentType)}`);
96
+ if (/[\r\n]/.test(attachment.contentId)) throwTypeError(`Invalid attachment content ID: ${JSON.stringify(attachment.contentId)}`);
97
+ return attachment;
98
+ }
58
99
  function ensureArray(value) {
59
100
  return Array.isArray(value) ? value : value == null ? [] : [value];
60
101
  }
@@ -239,8 +239,10 @@ interface MessageConstructor {
239
239
  * addresses and `File` objects for attachments.
240
240
  * @returns A new {@link Message} object with all properties normalized and
241
241
  * validated.
242
- * @throws {TypeError} When any email address string cannot be parsed or when
243
- * an attachment object is invalid.
242
+ * @throws {TypeError} When any email address string cannot be parsed, when an
243
+ * address or attachment carries a carriage return or line
244
+ * feed that could forge header fields, or when an
245
+ * attachment object is invalid.
244
246
  */
245
247
  declare function createMessage(constructor: MessageConstructor): Message;
246
248
  //#endregion
package/dist/message.d.ts CHANGED
@@ -239,8 +239,10 @@ interface MessageConstructor {
239
239
  * addresses and `File` objects for attachments.
240
240
  * @returns A new {@link Message} object with all properties normalized and
241
241
  * validated.
242
- * @throws {TypeError} When any email address string cannot be parsed or when
243
- * an attachment object is invalid.
242
+ * @throws {TypeError} When any email address string cannot be parsed, when an
243
+ * address or attachment carries a carriage return or line
244
+ * feed that could forge header fields, or when an
245
+ * attachment object is invalid.
244
246
  */
245
247
  declare function createMessage(constructor: MessageConstructor): Message;
246
248
  //#endregion
package/dist/message.js CHANGED
@@ -22,26 +22,28 @@ import { isAttachment } from "./attachment.js";
22
22
  * addresses and `File` objects for attachments.
23
23
  * @returns A new {@link Message} object with all properties normalized and
24
24
  * validated.
25
- * @throws {TypeError} When any email address string cannot be parsed or when
26
- * an attachment object is invalid.
25
+ * @throws {TypeError} When any email address string cannot be parsed, when an
26
+ * address or attachment carries a carriage return or line
27
+ * feed that could forge header fields, or when an
28
+ * attachment object is invalid.
27
29
  */
28
30
  function createMessage(constructor) {
29
- const sender = typeof constructor.from === "string" ? parseAddress(constructor.from) ?? throwTypeError(`Invalid sender address: ${JSON.stringify(constructor.from)}`) : constructor.from;
31
+ const sender = checkAddress("sender", constructor.from);
30
32
  return {
31
33
  sender,
32
- recipients: ensureArray(constructor.to).map((to) => typeof to === "string" ? parseAddress(to) ?? throwTypeError(`Invalid recipient address: ${JSON.stringify(to)}`) : to),
33
- ccRecipients: ensureArray(constructor.cc).map((cc) => typeof cc === "string" ? parseAddress(cc) ?? throwTypeError(`Invalid CC address: ${JSON.stringify(cc)}`) : cc),
34
- bccRecipients: ensureArray(constructor.bcc).map((bcc) => typeof bcc === "string" ? parseAddress(bcc) ?? throwTypeError(`Invalid BCC address: ${JSON.stringify(bcc)}`) : bcc),
35
- replyRecipients: ensureArray(constructor.replyTo).map((replyTo) => typeof replyTo === "string" ? parseAddress(replyTo) ?? throwTypeError(`Invalid reply-to address: ${JSON.stringify(replyTo)}`) : replyTo),
34
+ recipients: ensureArray(constructor.to).map((to) => checkAddress("recipient", to)),
35
+ ccRecipients: ensureArray(constructor.cc).map((cc) => checkAddress("CC", cc)),
36
+ bccRecipients: ensureArray(constructor.bcc).map((bcc) => checkAddress("BCC", bcc)),
37
+ replyRecipients: ensureArray(constructor.replyTo).map((replyTo) => checkAddress("reply-to", replyTo)),
36
38
  attachments: ensureArray(constructor.attachments).map((attachment) => {
37
- if (attachment instanceof File) return {
39
+ if (attachment instanceof File) return checkAttachment({
38
40
  inline: false,
39
41
  filename: attachment.name,
40
42
  content: attachment.arrayBuffer().then((b) => new Uint8Array(b)),
41
43
  contentType: attachment.type == null || attachment.type === "" ? "application/octet-stream" : attachment.type,
42
44
  contentId: `${crypto.randomUUID()}@${sender.address.replace(/^[^@]*@/, "")}`
43
- };
44
- else if (isAttachment(attachment)) return attachment;
45
+ });
46
+ else if (isAttachment(attachment)) return checkAttachment(attachment);
45
47
  else throwTypeError(`Invalid attachment: ${JSON.stringify(attachment)}`);
46
48
  }),
47
49
  subject: constructor.subject,
@@ -55,6 +57,45 @@ function createMessage(constructor) {
55
57
  function throwTypeError(message) {
56
58
  throw new TypeError(message);
57
59
  }
60
+ /**
61
+ * Normalizes an address and rejects one that could forge header fields.
62
+ *
63
+ * A transport that composes a message itself writes the address into a header
64
+ * field as given, so a carriage return or line feed in it would end that field
65
+ * and let the rest of the value appear as further fields. The string form has
66
+ * always been rejected by {@link parseAddress}; the object form used to pass
67
+ * through unchecked.
68
+ *
69
+ * @param label How the address is described in the error message.
70
+ * @param address The address, either an object or a string to parse.
71
+ * @returns The normalized address.
72
+ * @throws {TypeError} If the string cannot be parsed, or if the resulting
73
+ * address contains a carriage return or line feed.
74
+ */
75
+ function checkAddress(label, address) {
76
+ const parsed = typeof address === "string" ? parseAddress(address) ?? throwTypeError(`Invalid ${label} address: ${JSON.stringify(address)}`) : address;
77
+ if (/[\r\n]/.test(parsed.address)) throwTypeError(`Invalid ${label} address: ${JSON.stringify(parsed.address)}`);
78
+ return parsed;
79
+ }
80
+ /**
81
+ * Rejects an attachment whose content type or content ID could forge header
82
+ * fields.
83
+ *
84
+ * A transport that composes MIME itself writes both into the part headers as
85
+ * given, so a carriage return or line feed in either would end that field and
86
+ * let the rest of the value appear as further fields. An uploaded file's
87
+ * declared content type is routinely chosen by whoever uploaded it.
88
+ *
89
+ * @param attachment The attachment to check.
90
+ * @returns The attachment, unchanged.
91
+ * @throws {TypeError} If the content type or content ID contains a carriage
92
+ * return or line feed.
93
+ */
94
+ function checkAttachment(attachment) {
95
+ if (/[\r\n]/.test(attachment.contentType)) throwTypeError(`Invalid attachment content type: ${JSON.stringify(attachment.contentType)}`);
96
+ if (/[\r\n]/.test(attachment.contentId)) throwTypeError(`Invalid attachment content ID: ${JSON.stringify(attachment.contentId)}`);
97
+ return attachment;
98
+ }
58
99
  function ensureArray(value) {
59
100
  return Array.isArray(value) ? value : value == null ? [] : [value];
60
101
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/core",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Simple email sending library for Node.js, Deno, Bun, and edge functions",
5
5
  "keywords": [
6
6
  "email",