@upyo/core 0.6.0-dev.295 → 0.6.0-dev.296

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
@@ -71,6 +71,21 @@ const message = createMessage({
71
71
  });
72
72
  ~~~~
73
73
 
74
+ Internationalized mailbox addresses are accepted, including UTF-8 local parts
75
+ and Unicode domains:
76
+
77
+ ~~~~ typescript
78
+ const message = createMessage({
79
+ from: "josé@example.com",
80
+ to: "用户@例子.广告",
81
+ subject: "Hello",
82
+ content: { text: "Welcome!" },
83
+ });
84
+ ~~~~
85
+
86
+ Support for delivering these addresses depends on the selected transport. The
87
+ *@upyo/smtp* transport negotiates RFC 6531 SMTPUTF8 automatically.
88
+
74
89
  ### Adding attachments
75
90
 
76
91
  Attachments can be added using the standard [`File`] API:
package/dist/address.cjs CHANGED
@@ -82,11 +82,13 @@ function isValidEmail(email) {
82
82
  if (email.includes("\r") || email.includes("\n")) return false;
83
83
  let atIndex = -1;
84
84
  let inQuotes = false;
85
+ let escaped = false;
85
86
  for (let i = 0; i < email.length; i++) {
86
87
  const char = email[i];
87
- if (char === "\"" && (i === 0 || email[i - 1] !== "\\")) inQuotes = !inQuotes;
88
+ if (char === "\"" && !escaped) inQuotes = !inQuotes;
88
89
  else if (char === "@" && !inQuotes) if (atIndex === -1) atIndex = i;
89
90
  else return false;
91
+ escaped = char === "\\" && !escaped;
90
92
  }
91
93
  if (atIndex === -1) return false;
92
94
  const localPart = email.substring(0, atIndex);
@@ -94,24 +96,34 @@ function isValidEmail(email) {
94
96
  return isValidLocalPart(localPart) && isValidDomainPart(domainPart);
95
97
  }
96
98
  function isValidLocalPart(localPart) {
97
- if (!localPart || localPart.length === 0 || localPart.length > 64) return false;
99
+ if (!localPart || localPart.length === 0 || new TextEncoder().encode(localPart).byteLength > 64) return false;
98
100
  if (localPart.startsWith("\"") && localPart.endsWith("\"")) {
99
101
  const quotedContent = localPart.slice(1, -1);
100
- let isValid = true;
101
- for (let i = 0; i < quotedContent.length; i++) {
102
- const char = quotedContent[i];
103
- if (char === "\"" || char === "\r" || char === "\n") {
104
- if (i === 0 || quotedContent[i - 1] !== "\\") {
105
- isValid = false;
106
- break;
107
- }
102
+ for (let i = 0; i < quotedContent.length;) {
103
+ const codePoint = quotedContent.codePointAt(i);
104
+ if (codePoint == null || codePoint >= 55296 && codePoint <= 57343) return false;
105
+ if (codePoint > 127) {
106
+ i += codePoint > 65535 ? 2 : 1;
107
+ continue;
108
108
  }
109
+ if (codePoint === 92) {
110
+ const escapedCodePoint = quotedContent.codePointAt(i + 1);
111
+ if (escapedCodePoint == null || escapedCodePoint < 32 || escapedCodePoint > 126) return false;
112
+ i += 2;
113
+ continue;
114
+ }
115
+ if (codePoint < 32 || codePoint === 34 || codePoint === 92 || codePoint > 126) return false;
116
+ i++;
109
117
  }
110
- return isValid;
118
+ return true;
111
119
  }
112
120
  if (localPart.startsWith(".") || localPart.endsWith(".") || localPart.includes("..")) return false;
113
- const validLocalPartRegex = /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*$/;
114
- return validLocalPartRegex.test(localPart);
121
+ const asciiAtext = /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]$/;
122
+ return localPart.split(".").every((atom) => atom.length > 0 && Array.from(atom).every((character) => {
123
+ if (asciiAtext.test(character)) return true;
124
+ const codePoint = character.codePointAt(0);
125
+ return codePoint != null && codePoint > 127 && !(codePoint >= 55296 && codePoint <= 57343);
126
+ }));
115
127
  }
116
128
  function isValidDomainPart(domainPart) {
117
129
  if (!domainPart || domainPart.length === 0 || domainPart.length > 253) return false;
package/dist/address.js CHANGED
@@ -81,11 +81,13 @@ function isValidEmail(email) {
81
81
  if (email.includes("\r") || email.includes("\n")) return false;
82
82
  let atIndex = -1;
83
83
  let inQuotes = false;
84
+ let escaped = false;
84
85
  for (let i = 0; i < email.length; i++) {
85
86
  const char = email[i];
86
- if (char === "\"" && (i === 0 || email[i - 1] !== "\\")) inQuotes = !inQuotes;
87
+ if (char === "\"" && !escaped) inQuotes = !inQuotes;
87
88
  else if (char === "@" && !inQuotes) if (atIndex === -1) atIndex = i;
88
89
  else return false;
90
+ escaped = char === "\\" && !escaped;
89
91
  }
90
92
  if (atIndex === -1) return false;
91
93
  const localPart = email.substring(0, atIndex);
@@ -93,24 +95,34 @@ function isValidEmail(email) {
93
95
  return isValidLocalPart(localPart) && isValidDomainPart(domainPart);
94
96
  }
95
97
  function isValidLocalPart(localPart) {
96
- if (!localPart || localPart.length === 0 || localPart.length > 64) return false;
98
+ if (!localPart || localPart.length === 0 || new TextEncoder().encode(localPart).byteLength > 64) return false;
97
99
  if (localPart.startsWith("\"") && localPart.endsWith("\"")) {
98
100
  const quotedContent = localPart.slice(1, -1);
99
- let isValid = true;
100
- for (let i = 0; i < quotedContent.length; i++) {
101
- const char = quotedContent[i];
102
- if (char === "\"" || char === "\r" || char === "\n") {
103
- if (i === 0 || quotedContent[i - 1] !== "\\") {
104
- isValid = false;
105
- break;
106
- }
101
+ for (let i = 0; i < quotedContent.length;) {
102
+ const codePoint = quotedContent.codePointAt(i);
103
+ if (codePoint == null || codePoint >= 55296 && codePoint <= 57343) return false;
104
+ if (codePoint > 127) {
105
+ i += codePoint > 65535 ? 2 : 1;
106
+ continue;
107
107
  }
108
+ if (codePoint === 92) {
109
+ const escapedCodePoint = quotedContent.codePointAt(i + 1);
110
+ if (escapedCodePoint == null || escapedCodePoint < 32 || escapedCodePoint > 126) return false;
111
+ i += 2;
112
+ continue;
113
+ }
114
+ if (codePoint < 32 || codePoint === 34 || codePoint === 92 || codePoint > 126) return false;
115
+ i++;
108
116
  }
109
- return isValid;
117
+ return true;
110
118
  }
111
119
  if (localPart.startsWith(".") || localPart.endsWith(".") || localPart.includes("..")) return false;
112
- const validLocalPartRegex = /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*$/;
113
- return validLocalPartRegex.test(localPart);
120
+ const asciiAtext = /^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]$/;
121
+ return localPart.split(".").every((atom) => atom.length > 0 && Array.from(atom).every((character) => {
122
+ if (asciiAtext.test(character)) return true;
123
+ const codePoint = character.codePointAt(0);
124
+ return codePoint != null && codePoint > 127 && !(codePoint >= 55296 && codePoint <= 57343);
125
+ }));
114
126
  }
115
127
  function isValidDomainPart(domainPart) {
116
128
  if (!domainPart || domainPart.length === 0 || domainPart.length > 253) return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upyo/core",
3
- "version": "0.6.0-dev.295",
3
+ "version": "0.6.0-dev.296",
4
4
  "description": "Simple email sending library for Node.js, Deno, Bun, and edge functions",
5
5
  "keywords": [
6
6
  "email",