From 8007879c7b6344189c638f119bfacc5ddb28e697 Mon Sep 17 00:00:00 2001
From: Jason Caldwell <jaswrks@o5p.me>
Date: Mon, 30 Dec 2024 01:21:47 -0500
Subject: [PATCH] Project update. [p][robotic]

---
 .browserslistrc                            |  2 +-
 .dockerignore                              |  2 +-
 .env.vault                                 |  8 +--
 .gitattributes                             |  2 +-
 .gitignore                                 |  2 +-
 .npmignore                                 |  2 +-
 .prettierignore                            |  2 +-
 .vscode/settings.json                      |  2 +-
 .vscodeignore                              |  2 +-
 dev/.files/vite/config.mjs                 |  2 +-
 dev/.files/vite/includes/rollup/config.mjs |  2 +-
 dev/.files/wrangler/settings.mjs           |  2 +-
 package-lock.json                          | 65 +++++++++++++++++-----
 package.json                               |  2 +-
 src/email.ts                               | 33 +++++++++--
 src/str.ts                                 | 30 +++++-----
 src/tests/email/index.ts                   | 34 +++++++++++
 tsconfig.json                              |  2 +-
 wrangler.toml                              |  4 +-
 19 files changed, 146 insertions(+), 54 deletions(-)

diff --git a/.browserslistrc b/.browserslistrc
index b91fdab6..c0817f14 100644
--- a/.browserslistrc
+++ b/.browserslistrc
@@ -17,7 +17,7 @@
 # Generated data.
 # <generated:start>
 
-# Last generated Dec 29, 2024 6:03 AM UTC.
+# Last generated Dec 30, 2024 6:09 AM UTC.
 
 [production]
 node >= 20.9.0
diff --git a/.dockerignore b/.dockerignore
index 1848d39c..c7e6eed7 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -17,7 +17,7 @@
 # Generated data.
 # <generated:start>
 
-# Last generated Dec 29, 2024 6:03 AM UTC.
+# Last generated Dec 30, 2024 6:09 AM UTC.
 
 # Locals
 
diff --git a/.env.vault b/.env.vault
index 5941a31f..a724d533 100644
--- a/.env.vault
+++ b/.env.vault
@@ -8,12 +8,12 @@ DOTENV_VAULT_MAIN="7GW98NfL4hM4N6HbfeDXCKreVxKvcVPupFRnZ8XJ+pomaQ=="
 DOTENV_VAULT_MAIN_VERSION=1
 
 # dev
-DOTENV_VAULT_DEV="TwxDnS+ApGYMAvvlyy1XL2ZHSF81VkHMf8Yj6YCMH4B3Qpg7jEOPFdVZt/I//Z4cRvmDYrr3+CZp15fx39KmzieZ/hgnf52hL3moq56IMuJQXlRej/pZFo4eV18akkxpltdVVa6qNb/qN8n5uH3dGrEwHZq8CC+zDDc4aMAEqrnqjRKOM04Y4NOCpuE5I+mqpkz+PuhmmknxgIU70eTa29La9LvaRPMTuAW6QXG5lH8izAGHHI0yylR3TtBzcZGz6A=="
-DOTENV_VAULT_DEV_VERSION=561
+DOTENV_VAULT_DEV="pi+QD4XHVkzTNq/0Fo7sBxeSYxxapR8x2EPN4+RTqu47dEAmcW38KN/YoR9CeBWbqKzhsnyWMBU960reqQzNJEcLPlpQ/U9YvBlMrDjFarx22qI95mt2UVZUuxAp1Yr6k+AYqQf+ikEt+DfsRrxC79v/AKVKEUk1yoHAs3eDqCU7emqwCsS802Mp4gKQ5r9kTxJZQauIT+mw6YT4e7YQbj2rXMwP9uGVTNohBPpm59I279lP9PlCmRvK1PbYyyyyMQ=="
+DOTENV_VAULT_DEV_VERSION=563
 
 # ci
-DOTENV_VAULT_CI="m73XqNmab2TEtFaq24HgWnbf01VOBtGEgpQLY8GQLzvjhDU+6UctelFDZ2OncOj33NKYJkgtVBx3frHi37rDiTjnmwDu/FD12PFxaRxG+AcJriAXDCHtlYJ5eTUtMgP8EdmRDe1NaTDYQiycRIaSMlGry7bJB5ZQyuVHvkTC7g=="
-DOTENV_VAULT_CI_VERSION=561
+DOTENV_VAULT_CI="bcZ5LMMuEyegFe6gOzBH8ygDoe/CcPzzd8QIApLkeYBKjU/a0t5DxXgstL0Ym3C/LEybJ7zU7Id1qLNkHuMadgQPVTCrqk/jsnoQIn6e7SUuOGCgTc+zsX9GVygGRTruux/rrlK2D5JAMLbskKs89wBppP/7kUU4ATWjxuek2A=="
+DOTENV_VAULT_CI_VERSION=563
 
 # stage
 DOTENV_VAULT_STAGE="aRP8su2YV4jZu3w1HZ/SLaots0IwJDFw75TCpvXEFeNp7tw="
diff --git a/.gitattributes b/.gitattributes
index f670119d..60715fcb 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -17,7 +17,7 @@
 # Generated data.
 # <generated:start>
 
-# Last generated Dec 29, 2024 6:03 AM UTC.
+# Last generated Dec 30, 2024 6:09 AM UTC.
 
 # Default
 
diff --git a/.gitignore b/.gitignore
index 5da81192..1916673e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,7 +17,7 @@
 # Generated data.
 # <generated:start>
 
-# Last generated Dec 29, 2024 6:03 AM UTC.
+# Last generated Dec 30, 2024 6:09 AM UTC.
 
 # Locals
 
diff --git a/.npmignore b/.npmignore
index e857be34..a9287eeb 100644
--- a/.npmignore
+++ b/.npmignore
@@ -25,7 +25,7 @@
 # Generated data.
 # <generated:start>
 
-# Last generated Dec 29, 2024 6:03 AM UTC.
+# Last generated Dec 30, 2024 6:09 AM UTC.
 
 # Locals
 
diff --git a/.prettierignore b/.prettierignore
index 567a0fb0..0def5f45 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -17,7 +17,7 @@
 # Generated data.
 # <generated:start>
 
-# Last generated Dec 29, 2024 6:03 AM UTC.
+# Last generated Dec 30, 2024 6:09 AM UTC.
 
 # Packages
 
diff --git a/.vscode/settings.json b/.vscode/settings.json
index f510d6e9..8929cadf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,7 +7,7 @@
  * @note This entire file will be updated automatically.
  * @note Instead of editing here, please review `./settings.mjs`.
  *
- * Last generated using `./settings.mjs` Dec 29, 2024 6:03 AM UTC.
+ * Last generated using `./settings.mjs` Dec 30, 2024 6:09 AM UTC.
  */
 {
     "editor.formatOnType": false,
diff --git a/.vscodeignore b/.vscodeignore
index dbbf1bfc..93c005a3 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -17,7 +17,7 @@
 # Generated data.
 # <generated:start>
 
-# Last generated Dec 29, 2024 6:03 AM UTC.
+# Last generated Dec 30, 2024 6:09 AM UTC.
 
 # Locals
 
diff --git a/dev/.files/vite/config.mjs b/dev/.files/vite/config.mjs
index 475a3e5a..d452e80f 100644
--- a/dev/.files/vite/config.mjs
+++ b/dev/.files/vite/config.mjs
@@ -323,7 +323,7 @@ export default async ({ mode, command, isSsrBuild: isSSRBuild }) => {
         optimizeDeps: {
             force: true, // Don’t use cache for optimized deps; recreate.
             esbuildOptions: {
-                external: ['cloudflare:sockets'],
+                external: ['cloudflare:email', 'cloudflare:sockets'],
                 plugins: [await viteMDXESBuildConfig({ projDir })],
             },
             // Preact is required by prefresh plugin; {@see https://o5p.me/WmuefH}.
diff --git a/dev/.files/vite/includes/rollup/config.mjs b/dev/.files/vite/includes/rollup/config.mjs
index 36a47126..eea14394 100644
--- a/dev/.files/vite/includes/rollup/config.mjs
+++ b/dev/.files/vite/includes/rollup/config.mjs
@@ -41,9 +41,9 @@ export default async ({ projDir, srcDir, distDir, a16sDir, appType, appEntries,
             }, // {@see https://o5p.me/7YF2NU}.
         },
         external: [
+            /^(?:node:|cloudflare:).*$/iu,
             ...(['lib'].includes(appType) ? [/^(?![./~#]|file:|data:|virtual:).*$/iu] : []),
             ...peerDepKeys.map((pkgName) => new RegExp('^' + $str.escRegExp(pkgName) + '(?:$|[/?])', 'u')),
-            /^(?:cloudflare:).*$/iu,
         ],
         output: {
             interop: 'auto', // Matches TypeScript configuration.
diff --git a/dev/.files/wrangler/settings.mjs b/dev/.files/wrangler/settings.mjs
index a1323a58..32777c72 100755
--- a/dev/.files/wrangler/settings.mjs
+++ b/dev/.files/wrangler/settings.mjs
@@ -41,7 +41,7 @@ export default async () => {
         defaultAccountId: brandAccountId,
         defaultLogpush: brandSupportsLogpush,
 
-        compatibilityDate: '2024-03-02',
+        compatibilityDate: '2024-09-23',
         compatibilityFlags: [],
 
         defaultLocalIP: '0.0.0.0',
diff --git a/package-lock.json b/package-lock.json
index 0dc17706..b6559300 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
     "name": "@clevercanyon/utilities",
-    "version": "1.0.966",
+    "version": "1.0.967",
     "lockfileVersion": 3,
     "requires": true,
     "packages": {
         "": {
             "name": "@clevercanyon/utilities",
-            "version": "1.0.966",
+            "version": "1.0.967",
             "cpu": [
                 "x64",
                 "arm64"
@@ -847,9 +847,9 @@
             }
         },
         "node_modules/@clevercanyon/utilities": {
-            "version": "1.0.966",
-            "resolved": "https://registry.npmjs.org/@clevercanyon/utilities/-/utilities-1.0.966.tgz",
-            "integrity": "sha512-4Iu4buOtR8g96XQt9kxubQbRf0AqCwui6HcX3VHYnri1w2ii+ZvAiyi5kBVuti57CQD/sy86wJ/6GquZIz9tcg==",
+            "version": "1.0.967",
+            "resolved": "https://registry.npmjs.org/@clevercanyon/utilities/-/utilities-1.0.967.tgz",
+            "integrity": "sha512-T+m/ZvcyiaoOjqhw+I06ueWdfgY83U0NtxPCGt6vFgcQ+DHbsw6494GIXkLtC1bYqYSkzKWfVbH5u8Yt5FlqdQ==",
             "cpu": [
                 "x64",
                 "arm64"
@@ -934,9 +934,9 @@
             }
         },
         "node_modules/@clevercanyon/utilities.cfw": {
-            "version": "1.0.325",
-            "resolved": "https://registry.npmjs.org/@clevercanyon/utilities.cfw/-/utilities.cfw-1.0.325.tgz",
-            "integrity": "sha512-wPMVodWzM4FqtC4Wu1o5pGPHmyaNy8f/YJa2fxqb2NOrYt23dsGkj36KoRAfV1f9locw65VQ6+UJ9BiDcglcXw==",
+            "version": "1.0.326",
+            "resolved": "https://registry.npmjs.org/@clevercanyon/utilities.cfw/-/utilities.cfw-1.0.326.tgz",
+            "integrity": "sha512-AmLUo0ZI+WwSezTEbrq9EE8bygMlfxfMO8FnnPNhlJcEVMEN95NFzKN1PzcQnsVHdWVUIS1Y5h8bx4gTvGCOhg==",
             "cpu": [
                 "x64",
                 "arm64"
@@ -955,7 +955,7 @@
                 "url": "https://github.com/sponsors/clevercanyon"
             },
             "peerDependencies": {
-                "@clevercanyon/utilities": "^1.0.966",
+                "@clevercanyon/utilities": "^1.0.967",
                 "@cloudflare/ai": "1.0.53",
                 "@upstash/ratelimit": "1.0.0",
                 "@upstash/redis": "1.28.0"
@@ -9700,9 +9700,9 @@
             }
         },
         "node_modules/es-abstract": {
-            "version": "1.23.7",
-            "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.7.tgz",
-            "integrity": "sha512-OygGC8kIcDhXX+6yAZRGLqwi2CmEXCbLQixeGUgYeR+Qwlppqmo7DIDr8XibtEBZp+fJcoYpoatp5qwLMEdcqQ==",
+            "version": "1.23.8",
+            "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.8.tgz",
+            "integrity": "sha512-lfab8IzDn6EpI1ibZakcgS6WsfEBiB+43cuJo+wgylx1xKXf+Sp+YR3vFuQwC/u3sxYwV8Cxe3B0DpVUu/WiJQ==",
             "dev": true,
             "dependencies": {
                 "array-buffer-byte-length": "^1.0.2",
@@ -9740,8 +9740,10 @@
                 "object-inspect": "^1.13.3",
                 "object-keys": "^1.1.1",
                 "object.assign": "^4.1.7",
+                "own-keys": "^1.0.0",
                 "regexp.prototype.flags": "^1.5.3",
                 "safe-array-concat": "^1.1.3",
+                "safe-push-apply": "^1.0.0",
                 "safe-regex-test": "^1.1.0",
                 "string.prototype.trim": "^1.2.10",
                 "string.prototype.trimend": "^1.0.9",
@@ -20996,6 +20998,23 @@
                 "node": ">=0.10.0"
             }
         },
+        "node_modules/own-keys": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+            "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+            "dev": true,
+            "dependencies": {
+                "get-intrinsic": "^1.2.6",
+                "object-keys": "^1.1.1",
+                "safe-push-apply": "^1.0.0"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
         "node_modules/p-defer": {
             "version": "1.0.0",
             "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
@@ -27068,6 +27087,22 @@
                 }
             ]
         },
+        "node_modules/safe-push-apply": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+            "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+            "dev": true,
+            "dependencies": {
+                "es-errors": "^1.3.0",
+                "isarray": "^2.0.5"
+            },
+            "engines": {
+                "node": ">= 0.4"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/ljharb"
+            }
+        },
         "node_modules/safe-regex-test": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
@@ -28827,9 +28862,9 @@
             "dev": true
         },
         "node_modules/tinyexec": {
-            "version": "0.3.1",
-            "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz",
-            "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==",
+            "version": "0.3.2",
+            "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+            "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
             "dev": true
         },
         "node_modules/tinypool": {
diff --git a/package.json b/package.json
index 07a5d7d7..a99d0f24 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
     "publishConfig": {
         "access": "public"
     },
-    "version": "1.0.967",
+    "version": "1.0.968",
     "license": "GPL-3.0-or-later",
     "name": "@clevercanyon/utilities",
     "description": "Utilities for JavaScript apps running in any environment.",
diff --git a/src/email.ts b/src/email.ts
index 27047042..a72b7fc6 100644
--- a/src/email.ts
+++ b/src/email.ts
@@ -6,6 +6,11 @@ import '#@initialize.ts';
 
 import { $str } from '#index.ts';
 
+/**
+ * Defines types.
+ */
+export type Addr = { name: string; email: string };
+
 /**
  * Gets email from an addr.
  *
@@ -14,12 +19,28 @@ import { $str } from '#index.ts';
  *
  * @param   str String to consider.
  *
- * @returns     Email from an addr; else empty string.
+ * @returns     Email address; else empty string.
  */
 export const fromAddr = (str: string): string => {
-    if (!str) return '';
-    if ($str.isEmail(str)) return str;
+    return parseAddr(str)?.email || '';
+};
+
+/**
+ * Parses an addr.
+ *
+ * - `username@hostname`.
+ * - `"Name" <username@hostname>`.
+ *
+ * @param   str String to consider.
+ *
+ * @returns     Addr parts; else undefined.
+ */
+export const parseAddr = (str: string): Addr | undefined => {
+    if (!str) return; // Empty string.
 
+    if ($str.isEmail(str) /* Email only. */) {
+        return { name: '', email: str };
+    }
     const parts = str.split(/(?<=")\s(?=<)/u);
     if (
         2 === parts.length &&
@@ -34,7 +55,9 @@ export const fromAddr = (str: string): string => {
         '>' === parts[1][parts[1].length - 1] && // Closing bracket.
         $str.isEmail(parts[1].slice(1, -1)) // `<email>` validation.
     ) {
-        return parts[1].slice(1, -1).toLowerCase();
+        return {
+            name: parts[0].slice(1, -1),
+            email: parts[1].slice(1, -1).toLowerCase(),
+        };
     }
-    return ''; // Not an addr.
 };
diff --git a/src/str.ts b/src/str.ts
index 4038ad64..d23a9131 100644
--- a/src/str.ts
+++ b/src/str.ts
@@ -964,6 +964,20 @@ export const escFTSQuery = (str: string, options?: EscFTSQueryOptions): string =
  * Email utilities.
  */
 
+/**
+ * Tests if a string is an addr.
+ *
+ * - `username@hostname`.
+ * - `"Name" <username@hostname>`.
+ *
+ * @param   str String to consider.
+ *
+ * @returns     True if string is an addr.
+ */
+export const isAddr = (str: string): boolean => {
+    return $email.parseAddr(str) ? true : false;
+};
+
 /**
  * Tests if a string is an email address.
  *
@@ -983,25 +997,11 @@ export const isEmail = (str: string): boolean => {
         parts[1].length > 255 || // Hostname.
         parts[1].split('.').some((part) => part.length > 63)
     )
-        return false; // Not an email.
+        return false; // Not an email address.
 
     return emailRegExp.test(str);
 };
 
-/**
- * Tests if a string is an addr.
- *
- * - `username@hostname`.
- * - `"Name" <username@hostname>`.
- *
- * @param   str String to consider.
- *
- * @returns     True if string is an addr.
- */
-export const isAddr = (str: string): boolean => {
-    return $email.fromAddr(str) ? true : false;
-};
-
 /* ---
  * IP utilities.
  */
diff --git a/src/tests/email/index.ts b/src/tests/email/index.ts
index 72f9ba9e..53eefd11 100644
--- a/src/tests/email/index.ts
+++ b/src/tests/email/index.ts
@@ -40,4 +40,38 @@ describe('$email', async () => {
         expect($email.fromAddr('"X" <x@>')).toBe('');
         expect($email.fromAddr('"X" <@x>')).toBe('');
     });
+    test('.parseAddr()', async () => {
+        expect($email.parseAddr('x@x')).toStrictEqual({ name: '', email: 'x@x' });
+        expect($email.parseAddr('x+x@x')).toStrictEqual({ name: '', email: 'x+x@x' });
+
+        expect($email.parseAddr('x@localhost')).toStrictEqual({ name: '', email: 'x@localhost' });
+        expect($email.parseAddr('x+x@localhost')).toStrictEqual({ name: '', email: 'x+x@localhost' });
+
+        expect($email.parseAddr('x@hop.gdn')).toStrictEqual({ name: '', email: 'x@hop.gdn' });
+        expect($email.parseAddr('x+x@hop.gdn')).toStrictEqual({ name: '', email: 'x+x@hop.gdn' });
+
+        expect($email.parseAddr('"X" <x@x>')).toStrictEqual({ name: 'X', email: 'x@x' });
+        expect($email.parseAddr('"X" <x+x@x>')).toStrictEqual({ name: 'X', email: 'x+x@x' });
+
+        expect($email.parseAddr('"X" <x@localhost>')).toStrictEqual({ name: 'X', email: 'x@localhost' });
+        expect($email.parseAddr('"X" <x+x@localhost>')).toStrictEqual({ name: 'X', email: 'x+x@localhost' });
+
+        expect($email.parseAddr('"X" <x@hop.gdn>')).toStrictEqual({ name: 'X', email: 'x@hop.gdn' });
+        expect($email.parseAddr('"X" <x+x@hop.gdn>')).toStrictEqual({ name: 'X', email: 'x+x@hop.gdn' });
+
+        expect($email.parseAddr('"X" <x@x>')).toStrictEqual({ name: 'X', email: 'x@x' });
+        expect($email.parseAddr('"X X" <x@x>')).toStrictEqual({ name: 'X X', email: 'x@x' });
+
+        expect($email.parseAddr('x@x,x')).toBe(undefined);
+        expect($email.parseAddr('x,x@x')).toBe(undefined);
+        expect($email.parseAddr('<x@hop.gdn>')).toBe(undefined);
+        expect($email.parseAddr('x @hop.gdn')).toBe(undefined);
+
+        expect($email.parseAddr('X x@x')).toBe(undefined);
+        expect($email.parseAddr('"X" x@x')).toBe(undefined);
+        expect($email.parseAddr('"" <x@x>')).toBe(undefined);
+        expect($email.parseAddr('"X"  <x@x>')).toBe(undefined);
+        expect($email.parseAddr('"X" <x@>')).toBe(undefined);
+        expect($email.parseAddr('"X" <@x>')).toBe(undefined);
+    });
 });
diff --git a/tsconfig.json b/tsconfig.json
index 98dea4e5..3b3ded70 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,7 +7,7 @@
  * @note This entire file will be updated automatically.
  * @note Instead of editing here, please review `./tsconfig.mjs`.
  *
- * Last generated using `./tsconfig.mjs` Dec 29, 2024 6:03 AM UTC.
+ * Last generated using `./tsconfig.mjs` Dec 30, 2024 6:09 AM UTC.
  */
 {
     "include": ["./src/**/*", "./dev-types.d.ts"],
diff --git a/wrangler.toml b/wrangler.toml
index 6f952930..829fac75 100644
--- a/wrangler.toml
+++ b/wrangler.toml
@@ -7,9 +7,9 @@
 # @note This entire file will be updated automatically.
 # @note Instead of editing here, please review `./wrangler.mjs`.
 #
-# Last generated using `./wrangler.mjs` Dec 29, 2024 6:03 AM UTC.
+# Last generated using `./wrangler.mjs` Dec 30, 2024 6:09 AM UTC.
 ##
 
 send_metrics = false
-compatibility_date = "2024-03-02"
+compatibility_date = "2024-09-23"
 compatibility_flags = [ ]