@h-k-dev/angular-email-editor 0.0.0-stage → 0.0.2
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 +127 -2
- package/fesm2022/h-k-dev-angular-email-editor-actions.mjs +179 -0
- package/fesm2022/h-k-dev-angular-email-editor-actions.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs +383 -0
- package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs +1106 -0
- package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-address.mjs +489 -0
- package/fesm2022/h-k-dev-angular-email-editor-address.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-anchor.mjs +64 -0
- package/fesm2022/h-k-dev-angular-email-editor-anchor.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-attachment-chip.mjs +387 -0
- package/fesm2022/h-k-dev-angular-email-editor-attachment-chip.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-attachment-chips.mjs +80 -0
- package/fesm2022/h-k-dev-angular-email-editor-attachment-chips.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-chat-input.mjs +346 -0
- package/fesm2022/h-k-dev-angular-email-editor-chat-input.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-focus.mjs +78 -0
- package/fesm2022/h-k-dev-angular-email-editor-focus.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-image.mjs +132 -0
- package/fesm2022/h-k-dev-angular-email-editor-image.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-material.mjs +124 -0
- package/fesm2022/h-k-dev-angular-email-editor-material.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-metrics.mjs +243 -0
- package/fesm2022/h-k-dev-angular-email-editor-metrics.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-palette.mjs +87 -0
- package/fesm2022/h-k-dev-angular-email-editor-palette.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-proposal.mjs +307 -0
- package/fesm2022/h-k-dev-angular-email-editor-proposal.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor-suggestion-menu.mjs +392 -0
- package/fesm2022/h-k-dev-angular-email-editor-suggestion-menu.mjs.map +1 -0
- package/fesm2022/h-k-dev-angular-email-editor.mjs +18820 -0
- package/fesm2022/h-k-dev-angular-email-editor.mjs.map +1 -0
- package/package.json +133 -3
- package/styles/_scroll.scss +101 -0
- package/types/h-k-dev-angular-email-editor-actions.d.ts +147 -0
- package/types/h-k-dev-angular-email-editor-address-chip.d.ts +234 -0
- package/types/h-k-dev-angular-email-editor-address-input.d.ts +449 -0
- package/types/h-k-dev-angular-email-editor-address.d.ts +125 -0
- package/types/h-k-dev-angular-email-editor-anchor.d.ts +44 -0
- package/types/h-k-dev-angular-email-editor-attachment-chip.d.ts +271 -0
- package/types/h-k-dev-angular-email-editor-attachment-chips.d.ts +48 -0
- package/types/h-k-dev-angular-email-editor-chat-input.d.ts +101 -0
- package/types/h-k-dev-angular-email-editor-focus.d.ts +46 -0
- package/types/h-k-dev-angular-email-editor-image.d.ts +66 -0
- package/types/h-k-dev-angular-email-editor-material.d.ts +65 -0
- package/types/h-k-dev-angular-email-editor-metrics.d.ts +110 -0
- package/types/h-k-dev-angular-email-editor-palette.d.ts +51 -0
- package/types/h-k-dev-angular-email-editor-proposal.d.ts +170 -0
- package/types/h-k-dev-angular-email-editor-suggestion-menu.d.ts +177 -0
- package/types/h-k-dev-angular-email-editor.d.ts +3959 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
import { InjectionToken } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* **Our address rule** — the one place that decides what an email address
|
|
5
|
+
* is, how a mailbox is read and written, and how typed or pasted text splits
|
|
6
|
+
* into mailboxes. Everything else in the library asks through
|
|
7
|
+
* {@link AddressRules}; a host that wants another rule provides its own
|
|
8
|
+
* (`provideAddressRules`) and this file is simply not used.
|
|
9
|
+
*
|
|
10
|
+
* Kept as a living document: what it implements, and every place it
|
|
11
|
+
* deliberately does less or more than the RFCs, is listed here — change the
|
|
12
|
+
* list with the code.
|
|
13
|
+
*
|
|
14
|
+
* **Implements**
|
|
15
|
+
* - RFC 5322 §3.4 `mailbox`: `addr-spec` or `name-addr` — a phrase of atoms
|
|
16
|
+
* and quoted strings (quoted-pairs unescaped) in front of an `angle-addr`.
|
|
17
|
+
* - RFC 5322 §3.2.2 comments, nested, with quoted-pairs: skipped — and a
|
|
18
|
+
* comment after a bare address names it (`jane@x.io (Jane Doe)`), the
|
|
19
|
+
* de facto reading of the old form.
|
|
20
|
+
* - RFC 5322 §3.4 `group`: `Team: a@x.io, b@x.io;` yields its members; an
|
|
21
|
+
* empty group (`undisclosed-recipients:;`) yields nothing.
|
|
22
|
+
* - RFC 5322 §4.4 obsolete routes: `<@relay.io:jane@x.io>` is `jane@x.io`.
|
|
23
|
+
* - RFC 5322 §3.4.1 `addr-spec`: a dot-atom or quoted-string local part.
|
|
24
|
+
* - RFC 6532 / 6531: UTF-8 in local parts, names and domains (`jörg@müller.de`).
|
|
25
|
+
* - RFC 5321 §4.1.2 / 4.1.3 domains: LDH labels of 1–63 characters, no
|
|
26
|
+
* hyphen at either end; address literals `[192.0.2.1]` and `[IPv6:…]`.
|
|
27
|
+
* - RFC 5321 §4.5.3.1 lengths, in octets (UTF-8): local part ≤ 64,
|
|
28
|
+
* address ≤ 254 (the 256-octet path less its brackets, RFC 3696 errata).
|
|
29
|
+
* - RFC 2047 encoded words in names (`=?UTF-8?B?…?=`, `Q` too, any charset
|
|
30
|
+
* the platform's `TextDecoder` knows), adjacent words joined as §6.2 says.
|
|
31
|
+
*
|
|
32
|
+
* **Deliberately stricter than the RFCs** — the typo worth catching:
|
|
33
|
+
* - A domain needs a dot (`hans@gmail` is legal, and a typo every time).
|
|
34
|
+
* - A top-level label is not all digits (RFC 3696 §2) — `a@1.2.3.4` is an
|
|
35
|
+
* IP address missing its brackets.
|
|
36
|
+
* - General address literals (`[tag:content]`) are refused; only IPv4/IPv6.
|
|
37
|
+
*
|
|
38
|
+
* **Deliberately more lenient — people type these:**
|
|
39
|
+
* - `,` `;` and line breaks all separate (RFC 5322: only `,`).
|
|
40
|
+
* - A bare run is read as people type it: words in front of an address name
|
|
41
|
+
* it (`Ada Lovelace ada@x.io`), addresses only whitespace separates are
|
|
42
|
+
* several, words naming no address stay one token (one typo, not three).
|
|
43
|
+
* - A group needs whitespace, `;`, `,` or the end after its colon, so
|
|
44
|
+
* `hans:müller@x.de` stays one (invalid) address instead of silently
|
|
45
|
+
* becoming `müller@x.de` under a group called `hans`.
|
|
46
|
+
*
|
|
47
|
+
* **Placeholders** — not an RFC matter, a template one: an address holding
|
|
48
|
+
* a `{{…}}` token (`{{contact.email}}`, `"{{contact.name}}" <{{contact.email}}>`)
|
|
49
|
+
* is filled in later, so it is neither valid nor invalid yet
|
|
50
|
+
* (`isPlaceholder`), and a separator inside the braces is part of it
|
|
51
|
+
* (`{{join contacts ", "}}` is one entry). Only doubled braces count — a
|
|
52
|
+
* lone `{` is atext and stays an ordinary character.
|
|
53
|
+
*/
|
|
54
|
+
// ── characters ───────────────────────────────────────────────────────────
|
|
55
|
+
/** RFC 5322 `atext`, plus RFC 6532's UTF-8 beyond ASCII. */
|
|
56
|
+
const ATEXT = "A-Za-z0-9!#$%&'*+/=?^_`{|}~\\-\\u0080-\\u{10FFFF}";
|
|
57
|
+
const DOT_ATOM = new RegExp(`^[${ATEXT}]+(?:\\.[${ATEXT}]+)*$`, 'u');
|
|
58
|
+
/** RFC 5322 `quoted-string` as a whole local part: qtext, quoted-pairs, spaces. */
|
|
59
|
+
const QUOTED_LOCAL = /^"(?:[\x20\x21\x23-\x5B\x5D-\x7E\u0080-\u{10FFFF}]|\\[\x20-\x7E])*"$/u;
|
|
60
|
+
/** An RFC 5321 label, with RFC 6531 U-labels: letters and digits of any
|
|
61
|
+
script, hyphens inside. */
|
|
62
|
+
const LABEL = /^(?!-)[\p{L}\p{N}\p{M}-]{1,63}(?<!-)$/u;
|
|
63
|
+
/** Characters a display name must be quoted to carry: RFC 5322's specials. */
|
|
64
|
+
const SPECIALS = /[()<>[\]:;@\\,."]/;
|
|
65
|
+
const octets = (text) => new TextEncoder().encode(text).length;
|
|
66
|
+
// ── addr-spec ────────────────────────────────────────────────────────────
|
|
67
|
+
/** Whether a bare address — no name, no brackets — is well-formed by the
|
|
68
|
+
rule above. */
|
|
69
|
+
function isEmailAddress(address) {
|
|
70
|
+
const at = address.lastIndexOf('@');
|
|
71
|
+
if (at < 1 || at === address.length - 1)
|
|
72
|
+
return false;
|
|
73
|
+
const local = address.slice(0, at);
|
|
74
|
+
const domain = address.slice(at + 1);
|
|
75
|
+
if (octets(local) > 64 || octets(address) > 254)
|
|
76
|
+
return false;
|
|
77
|
+
if (!DOT_ATOM.test(local) && !QUOTED_LOCAL.test(local))
|
|
78
|
+
return false;
|
|
79
|
+
return isDomain(domain);
|
|
80
|
+
}
|
|
81
|
+
function isDomain(domain) {
|
|
82
|
+
if (domain.startsWith('['))
|
|
83
|
+
return isAddressLiteral(domain);
|
|
84
|
+
const labels = domain.split('.');
|
|
85
|
+
if (labels.length < 2 || !labels.every((label) => LABEL.test(label)))
|
|
86
|
+
return false;
|
|
87
|
+
return !/^\d+$/.test(labels[labels.length - 1]);
|
|
88
|
+
}
|
|
89
|
+
function isAddressLiteral(literal) {
|
|
90
|
+
const inner = /^\[(.*)\]$/.exec(literal)?.[1];
|
|
91
|
+
if (inner === undefined)
|
|
92
|
+
return false;
|
|
93
|
+
if (/^IPv6:/i.test(inner))
|
|
94
|
+
return isIPv6(inner.slice(5));
|
|
95
|
+
return isIPv4(inner);
|
|
96
|
+
}
|
|
97
|
+
function isIPv4(text) {
|
|
98
|
+
const parts = text.split('.');
|
|
99
|
+
return parts.length === 4 && parts.every((part) => /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/.test(part));
|
|
100
|
+
}
|
|
101
|
+
function isIPv6(text) {
|
|
102
|
+
// The platform's own parser is the reference; the shape check keeps a
|
|
103
|
+
// host name or a port from sneaking through it.
|
|
104
|
+
if (!/^[0-9A-Fa-f:.]+$/.test(text) || !text.includes(':'))
|
|
105
|
+
return false;
|
|
106
|
+
try {
|
|
107
|
+
return new URL(`http://[${text}]/`).hostname.length > 2;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const at_top = (state) => !state.quoted && !state.comment && !state.angled && !state.braced;
|
|
114
|
+
/** Steps the scan past one character; true when the character is structure
|
|
115
|
+
(a quote, a bracket, part of a comment or an escape), not content at the
|
|
116
|
+
top level. */
|
|
117
|
+
function step(state, char) {
|
|
118
|
+
const half = state.brace;
|
|
119
|
+
state.brace = false;
|
|
120
|
+
if (state.braced) {
|
|
121
|
+
if (char === '}' && half)
|
|
122
|
+
state.braced = false;
|
|
123
|
+
else
|
|
124
|
+
state.brace = char === '}';
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (char === '{' && !state.quoted && !state.comment) {
|
|
128
|
+
if (half)
|
|
129
|
+
state.braced = true;
|
|
130
|
+
else
|
|
131
|
+
state.brace = true;
|
|
132
|
+
return half;
|
|
133
|
+
}
|
|
134
|
+
if (state.escaped) {
|
|
135
|
+
state.escaped = false;
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if ((state.quoted || state.comment) && char === '\\') {
|
|
139
|
+
state.escaped = true;
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (state.quoted) {
|
|
143
|
+
if (char === '"')
|
|
144
|
+
state.quoted = false;
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (state.comment) {
|
|
148
|
+
if (char === '(')
|
|
149
|
+
state.comment++;
|
|
150
|
+
else if (char === ')')
|
|
151
|
+
state.comment--;
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (char === '"' && !state.angled) {
|
|
155
|
+
state.quoted = true;
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
if (char === '(' && !state.angled) {
|
|
159
|
+
state.comment = 1;
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
if (char === '<')
|
|
163
|
+
state.angled = true;
|
|
164
|
+
else if (char === '>')
|
|
165
|
+
state.angled = false;
|
|
166
|
+
else
|
|
167
|
+
return state.angled;
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
const fresh = () => ({
|
|
171
|
+
quoted: false,
|
|
172
|
+
comment: 0,
|
|
173
|
+
angled: false,
|
|
174
|
+
escaped: false,
|
|
175
|
+
braced: false,
|
|
176
|
+
brace: false,
|
|
177
|
+
});
|
|
178
|
+
/** Whether a separator typed at `caret` would separate — false inside a
|
|
179
|
+
quoted name, a comment or angle brackets, where a comma is part of the
|
|
180
|
+
mailbox: `"Lovelace, Ada"`. */
|
|
181
|
+
function separatesAt(raw, caret) {
|
|
182
|
+
const state = fresh();
|
|
183
|
+
for (const char of raw.slice(0, caret))
|
|
184
|
+
step(state, char);
|
|
185
|
+
return at_top(state) && !state.escaped;
|
|
186
|
+
}
|
|
187
|
+
// ── parse ────────────────────────────────────────────────────────────────
|
|
188
|
+
/**
|
|
189
|
+
* Splits a header-form mailbox into name and address: quoted strings
|
|
190
|
+
* unquoted and unescaped, encoded words decoded, comments skipped — or, after
|
|
191
|
+
* a bare address, taken as its name — and an obsolete route dropped.
|
|
192
|
+
* Anything that is no mailbox comes back as its trimmed text, as the address,
|
|
193
|
+
* for the rule to refuse.
|
|
194
|
+
*/
|
|
195
|
+
function parseMailbox(raw) {
|
|
196
|
+
const text = raw.trim();
|
|
197
|
+
const words = [];
|
|
198
|
+
const comments = [];
|
|
199
|
+
let address = null;
|
|
200
|
+
let bare = '';
|
|
201
|
+
let i = 0;
|
|
202
|
+
while (i < text.length) {
|
|
203
|
+
const char = text[i];
|
|
204
|
+
if (char === '"') {
|
|
205
|
+
const end = closing(text, i, '"');
|
|
206
|
+
if (end < 0)
|
|
207
|
+
return { address: text };
|
|
208
|
+
words.push(unescape(text.slice(i + 1, end)));
|
|
209
|
+
i = end + 1;
|
|
210
|
+
}
|
|
211
|
+
else if (char === '(') {
|
|
212
|
+
const end = closingComment(text, i);
|
|
213
|
+
if (end < 0)
|
|
214
|
+
return { address: text };
|
|
215
|
+
comments.push(decodeEncodedWords(unescape(text.slice(i + 1, end)).trim()));
|
|
216
|
+
i = end + 1;
|
|
217
|
+
}
|
|
218
|
+
else if (char === '<') {
|
|
219
|
+
const end = text.indexOf('>', i);
|
|
220
|
+
if (end < 0 || address !== null)
|
|
221
|
+
return { address: text };
|
|
222
|
+
address = text.slice(i + 1, end).trim().replace(/^@[^:]*:/, '');
|
|
223
|
+
i = end + 1;
|
|
224
|
+
}
|
|
225
|
+
else if (/\s/.test(char)) {
|
|
226
|
+
i++;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
let end = i;
|
|
230
|
+
while (end < text.length && !/[\s"(<]/.test(text[end]))
|
|
231
|
+
end++;
|
|
232
|
+
const atom = text.slice(i, end);
|
|
233
|
+
if (address === null)
|
|
234
|
+
words.push(atom);
|
|
235
|
+
else
|
|
236
|
+
return { address: text }; // text after the angle-addr
|
|
237
|
+
bare += atom;
|
|
238
|
+
i = end;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (address !== null) {
|
|
242
|
+
const name = phrase(words) || comments.join(' ');
|
|
243
|
+
return name ? { name, address } : { address };
|
|
244
|
+
}
|
|
245
|
+
// No angle brackets: an addr-spec, as one run of text.
|
|
246
|
+
if (words.length !== 1 || !bare)
|
|
247
|
+
return { address: text };
|
|
248
|
+
const name = comments.join(' ');
|
|
249
|
+
return name ? { name, address: bare } : { address: bare };
|
|
250
|
+
}
|
|
251
|
+
/** A phrase's words as a display name: one space between words, encoded
|
|
252
|
+
words decoded — and two encoded words only whitespace separates joined
|
|
253
|
+
with none (RFC 2047 §6.2). */
|
|
254
|
+
function phrase(words) {
|
|
255
|
+
return decodeEncodedWords(words.join(' ')).trim();
|
|
256
|
+
}
|
|
257
|
+
function closing(text, from, quote) {
|
|
258
|
+
for (let i = from + 1; i < text.length; i++) {
|
|
259
|
+
if (text[i] === '\\')
|
|
260
|
+
i++;
|
|
261
|
+
else if (text[i] === quote)
|
|
262
|
+
return i;
|
|
263
|
+
}
|
|
264
|
+
return -1;
|
|
265
|
+
}
|
|
266
|
+
function closingComment(text, from) {
|
|
267
|
+
let depth = 0;
|
|
268
|
+
for (let i = from; i < text.length; i++) {
|
|
269
|
+
if (text[i] === '\\')
|
|
270
|
+
i++;
|
|
271
|
+
else if (text[i] === '(')
|
|
272
|
+
depth++;
|
|
273
|
+
else if (text[i] === ')' && --depth === 0)
|
|
274
|
+
return i;
|
|
275
|
+
}
|
|
276
|
+
return -1;
|
|
277
|
+
}
|
|
278
|
+
const unescape = (text) => text.replace(/\\([\s\S])/g, '$1');
|
|
279
|
+
// ── format ───────────────────────────────────────────────────────────────
|
|
280
|
+
/** The header form of a mailbox — the name quoted (and escaped) when it has
|
|
281
|
+
to be. UTF-8 stays as it is (RFC 6532); a mailer encodes for the wire. */
|
|
282
|
+
function formatMailbox(mailbox) {
|
|
283
|
+
const name = mailbox.name?.trim();
|
|
284
|
+
if (!name)
|
|
285
|
+
return mailbox.address;
|
|
286
|
+
const quoted = SPECIALS.test(name) || /\s{2}/.test(name) ? `"${name.replace(/["\\]/g, '\\$&')}"` : name;
|
|
287
|
+
return `${quoted} <${mailbox.address}>`;
|
|
288
|
+
}
|
|
289
|
+
// ── split ────────────────────────────────────────────────────────────────
|
|
290
|
+
/** What separates the mailboxes of a typed run. */
|
|
291
|
+
const SEPARATORS = ',;\n\r';
|
|
292
|
+
/**
|
|
293
|
+
* Splits a typed, pasted or header run into mailboxes — see the leniencies
|
|
294
|
+
* above. A well-formed mailbox comes back in its canonical header form
|
|
295
|
+
* (`<a@x.io>` → `a@x.io`, `Dr. Ada <a@x.io>` → `"Dr. Ada" <a@x.io>`, a
|
|
296
|
+
* comment or an encoded word turned into the name); anything else comes back
|
|
297
|
+
* as typed, for the user to fix. Empty tokens are dropped; duplicates are
|
|
298
|
+
* the caller's.
|
|
299
|
+
*/
|
|
300
|
+
function splitAddresses(raw) {
|
|
301
|
+
const tokens = [];
|
|
302
|
+
const state = fresh();
|
|
303
|
+
let current = '';
|
|
304
|
+
for (let i = 0; i < raw.length; i++) {
|
|
305
|
+
const char = raw[i];
|
|
306
|
+
if (step(state, char)) {
|
|
307
|
+
current += char;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (SEPARATORS.includes(char)) {
|
|
311
|
+
tokens.push(current);
|
|
312
|
+
current = '';
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// A group's colon: what came before it is the group's name, not a
|
|
316
|
+
// mailbox — dropped; its members follow, up to the `;`.
|
|
317
|
+
if (char === ':' && !/[@<]/.test(current) && /^(?:[\s,;]|$)/.test(raw.slice(i + 1, i + 2))) {
|
|
318
|
+
current = '';
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
current += char;
|
|
322
|
+
}
|
|
323
|
+
tokens.push(current);
|
|
324
|
+
return tokens
|
|
325
|
+
.map((token) => token.trim())
|
|
326
|
+
.filter(Boolean)
|
|
327
|
+
.flatMap((token) => (/[<"(]/.test(token) ? [token] : bareRun(token)))
|
|
328
|
+
.map(canonical);
|
|
329
|
+
}
|
|
330
|
+
/** A bare run's mailboxes: each run of words names the address after it. */
|
|
331
|
+
function bareRun(token) {
|
|
332
|
+
const mailboxes = [];
|
|
333
|
+
let name = [];
|
|
334
|
+
for (const word of token.split(/\s+/)) {
|
|
335
|
+
if (!isEmailAddress(word)) {
|
|
336
|
+
name.push(word);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
mailboxes.push(formatMailbox({ name: name.join(' ') || undefined, address: word }));
|
|
340
|
+
name = [];
|
|
341
|
+
}
|
|
342
|
+
if (name.length)
|
|
343
|
+
mailboxes.push(name.join(' '));
|
|
344
|
+
return mailboxes;
|
|
345
|
+
}
|
|
346
|
+
/** A well-formed mailbox in its canonical header form; anything else as it is. */
|
|
347
|
+
function canonical(token) {
|
|
348
|
+
const mailbox = parseMailbox(token);
|
|
349
|
+
return isEmailAddress(mailbox.address) ? formatMailbox(mailbox) : token;
|
|
350
|
+
}
|
|
351
|
+
// ── identity ─────────────────────────────────────────────────────────────
|
|
352
|
+
/**
|
|
353
|
+
* What makes two entries one recipient. The domain is case-insensitive and
|
|
354
|
+
* compared in its ASCII form (`müller.de` is `xn--mller-kva.de`); the local
|
|
355
|
+
* part is lowercased too — RFC 5321 lets a server tell `Ada` from `ada`, no
|
|
356
|
+
* real one does, and one recipient twice is the mistake that matters. Both
|
|
357
|
+
* NFC-normalized, so a composed and a decomposed `ü` are one.
|
|
358
|
+
*/
|
|
359
|
+
function addressIdentity(address) {
|
|
360
|
+
const at = address.lastIndexOf('@');
|
|
361
|
+
if (at < 0)
|
|
362
|
+
return address.trim().normalize('NFC').toLowerCase();
|
|
363
|
+
const local = address.slice(0, at).normalize('NFC').toLowerCase();
|
|
364
|
+
return `${local}@${asciiDomain(address.slice(at + 1).normalize('NFC').toLowerCase())}`;
|
|
365
|
+
}
|
|
366
|
+
function asciiDomain(domain) {
|
|
367
|
+
if (domain.startsWith('[') || /^[\x00-\x7F]*$/.test(domain))
|
|
368
|
+
return domain;
|
|
369
|
+
try {
|
|
370
|
+
return new URL(`http://${domain}/`).hostname;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return domain;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// ── RFC 2047 ─────────────────────────────────────────────────────────────
|
|
377
|
+
const ENCODED_WORD = /=\?([^?\s]+)\?([BbQq])\?([^?\s]*)\?=/g;
|
|
378
|
+
/** Decodes the RFC 2047 encoded words in a header text; one that cannot be
|
|
379
|
+
decoded stays as it is. Two encoded words only whitespace separates are
|
|
380
|
+
joined with none. */
|
|
381
|
+
function decodeEncodedWords(text) {
|
|
382
|
+
return text
|
|
383
|
+
.replace(/(=\?[^?\s]+\?[BbQq]\?[^?\s]*\?=)\s+(?==\?[^?\s]+\?[BbQq]\?[^?\s]*\?=)/g, '$1')
|
|
384
|
+
.replace(ENCODED_WORD, (word, charset, encoding, data) => {
|
|
385
|
+
try {
|
|
386
|
+
const bytes = encoding.toUpperCase() === 'B'
|
|
387
|
+
? Uint8Array.from(atob(data), (c) => c.charCodeAt(0))
|
|
388
|
+
: quotedPrintable(data);
|
|
389
|
+
// RFC 2231 lets a charset carry a language: `UTF-8*de`.
|
|
390
|
+
return new TextDecoder(charset.split('*')[0], { fatal: true }).decode(bytes);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return word;
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function quotedPrintable(data) {
|
|
398
|
+
const bytes = [];
|
|
399
|
+
for (let i = 0; i < data.length; i++) {
|
|
400
|
+
const char = data[i];
|
|
401
|
+
if (char === '_')
|
|
402
|
+
bytes.push(0x20);
|
|
403
|
+
else if (char === '=' && /^[0-9A-Fa-f]{2}$/.test(data.slice(i + 1, i + 3))) {
|
|
404
|
+
bytes.push(parseInt(data.slice(i + 1, i + 3), 16));
|
|
405
|
+
i += 2;
|
|
406
|
+
}
|
|
407
|
+
else
|
|
408
|
+
bytes.push(...new TextEncoder().encode(char));
|
|
409
|
+
}
|
|
410
|
+
return Uint8Array.from(bytes);
|
|
411
|
+
}
|
|
412
|
+
// ── placeholders ─────────────────────────────────────────────────────────
|
|
413
|
+
/** Whether an address holds a `{{…}}` placeholder — one a template fills
|
|
414
|
+
in later, so neither valid nor invalid yet. */
|
|
415
|
+
function isAddressPlaceholder(address) {
|
|
416
|
+
return /\{\{[^{}]*\}\}/.test(address);
|
|
417
|
+
}
|
|
418
|
+
/** Our rule, as the contract every part of the library asks through. */
|
|
419
|
+
const defaultAddressRules = {
|
|
420
|
+
split: splitAddresses,
|
|
421
|
+
parse: parseMailbox,
|
|
422
|
+
format: formatMailbox,
|
|
423
|
+
isValid: isEmailAddress,
|
|
424
|
+
isPlaceholder: isAddressPlaceholder,
|
|
425
|
+
identity: addressIdentity,
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
/** The address rule in force — ours unless a host provides its own. */
|
|
429
|
+
const ADDRESS_RULES = new InjectionToken('ADDRESS_RULES', {
|
|
430
|
+
providedIn: 'root',
|
|
431
|
+
factory: () => defaultAddressRules,
|
|
432
|
+
});
|
|
433
|
+
/**
|
|
434
|
+
* Puts a host's address rule in force: the parts given replace ours, the
|
|
435
|
+
* rest stay ours. Keeping an existing validator is one line:
|
|
436
|
+
*
|
|
437
|
+
* ```ts
|
|
438
|
+
* providers: [provideAddressRules({ isValid: isValidEmail })]
|
|
439
|
+
* ```
|
|
440
|
+
*/
|
|
441
|
+
function provideAddressRules(rules) {
|
|
442
|
+
return { provide: ADDRESS_RULES, useValue: { ...defaultAddressRules, ...rules } };
|
|
443
|
+
}
|
|
444
|
+
/** Whether a header-form string is a mailbox whose address is well-formed. */
|
|
445
|
+
function isMailbox(raw, rules = defaultAddressRules) {
|
|
446
|
+
return rules.isValid(rules.parse(raw).address);
|
|
447
|
+
}
|
|
448
|
+
/** Whether a header-form entry's address is a placeholder — pending: a
|
|
449
|
+
template fills it in later, so it is no mistake to flag. */
|
|
450
|
+
function isPending(raw, rules = defaultAddressRules) {
|
|
451
|
+
return rules.isPlaceholder(rules.parse(raw).address);
|
|
452
|
+
}
|
|
453
|
+
/** What makes two header-form entries one recipient — the name in front of
|
|
454
|
+
the address makes no second one. For a list that holds each recipient
|
|
455
|
+
once. */
|
|
456
|
+
function addressKey(raw, rules = defaultAddressRules) {
|
|
457
|
+
return rules.identity(rules.parse(raw).address);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* A list of header-form entries as one header value — `"Müller, Hans"
|
|
461
|
+
* <h@x.de>, b@c.de` — for a backend or a template that keeps To, Cc and Bcc
|
|
462
|
+
* as text. The way back is the rule's own `split`: `rules.split(joined)` is
|
|
463
|
+
* the list again, entry for entry. A well-formed mailbox is written in its
|
|
464
|
+
* canonical form (a name with a comma quoted); anything else — a typo, a
|
|
465
|
+
* `{{placeholder}}` — goes in as it is, for the backend to resolve or refuse.
|
|
466
|
+
*
|
|
467
|
+
* ```ts
|
|
468
|
+
* to: joinAddresses(form.to().value()), // out to the backend
|
|
469
|
+
* form.to().value.set(splitAddresses(template.to)) // back in
|
|
470
|
+
* ```
|
|
471
|
+
*/
|
|
472
|
+
function joinAddresses(list, rules = defaultAddressRules) {
|
|
473
|
+
return list
|
|
474
|
+
.map((entry) => entry.trim())
|
|
475
|
+
.filter(Boolean)
|
|
476
|
+
.map((entry) => (isMailbox(entry, rules) ? rules.format(rules.parse(entry)) : entry))
|
|
477
|
+
.join(', ');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/*
|
|
481
|
+
* Public API Surface of @h-k-dev/angular-email-editor/address
|
|
482
|
+
*/
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Generated bundle index. Do not edit.
|
|
486
|
+
*/
|
|
487
|
+
|
|
488
|
+
export { ADDRESS_RULES, addressIdentity, addressKey, decodeEncodedWords, defaultAddressRules, formatMailbox, isAddressPlaceholder, isEmailAddress, isMailbox, isPending, joinAddresses, parseMailbox, provideAddressRules, separatesAt, splitAddresses };
|
|
489
|
+
//# sourceMappingURL=h-k-dev-angular-email-editor-address.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"h-k-dev-angular-email-editor-address.mjs","sources":["../../../projects/angular-email-editor/address/rules.ts","../../../projects/angular-email-editor/address/address.ts","../../../projects/angular-email-editor/address/public-api.ts","../../../projects/angular-email-editor/address/h-k-dev-angular-email-editor-address.ts"],"sourcesContent":["import type { AddressRules, Mailbox } from './address';\n\n/**\n * **Our address rule** — the one place that decides what an email address\n * is, how a mailbox is read and written, and how typed or pasted text splits\n * into mailboxes. Everything else in the library asks through\n * {@link AddressRules}; a host that wants another rule provides its own\n * (`provideAddressRules`) and this file is simply not used.\n *\n * Kept as a living document: what it implements, and every place it\n * deliberately does less or more than the RFCs, is listed here — change the\n * list with the code.\n *\n * **Implements**\n * - RFC 5322 §3.4 `mailbox`: `addr-spec` or `name-addr` — a phrase of atoms\n * and quoted strings (quoted-pairs unescaped) in front of an `angle-addr`.\n * - RFC 5322 §3.2.2 comments, nested, with quoted-pairs: skipped — and a\n * comment after a bare address names it (`jane@x.io (Jane Doe)`), the\n * de facto reading of the old form.\n * - RFC 5322 §3.4 `group`: `Team: a@x.io, b@x.io;` yields its members; an\n * empty group (`undisclosed-recipients:;`) yields nothing.\n * - RFC 5322 §4.4 obsolete routes: `<@relay.io:jane@x.io>` is `jane@x.io`.\n * - RFC 5322 §3.4.1 `addr-spec`: a dot-atom or quoted-string local part.\n * - RFC 6532 / 6531: UTF-8 in local parts, names and domains (`jörg@müller.de`).\n * - RFC 5321 §4.1.2 / 4.1.3 domains: LDH labels of 1–63 characters, no\n * hyphen at either end; address literals `[192.0.2.1]` and `[IPv6:…]`.\n * - RFC 5321 §4.5.3.1 lengths, in octets (UTF-8): local part ≤ 64,\n * address ≤ 254 (the 256-octet path less its brackets, RFC 3696 errata).\n * - RFC 2047 encoded words in names (`=?UTF-8?B?…?=`, `Q` too, any charset\n * the platform's `TextDecoder` knows), adjacent words joined as §6.2 says.\n *\n * **Deliberately stricter than the RFCs** — the typo worth catching:\n * - A domain needs a dot (`hans@gmail` is legal, and a typo every time).\n * - A top-level label is not all digits (RFC 3696 §2) — `a@1.2.3.4` is an\n * IP address missing its brackets.\n * - General address literals (`[tag:content]`) are refused; only IPv4/IPv6.\n *\n * **Deliberately more lenient — people type these:**\n * - `,` `;` and line breaks all separate (RFC 5322: only `,`).\n * - A bare run is read as people type it: words in front of an address name\n * it (`Ada Lovelace ada@x.io`), addresses only whitespace separates are\n * several, words naming no address stay one token (one typo, not three).\n * - A group needs whitespace, `;`, `,` or the end after its colon, so\n * `hans:müller@x.de` stays one (invalid) address instead of silently\n * becoming `müller@x.de` under a group called `hans`.\n *\n * **Placeholders** — not an RFC matter, a template one: an address holding\n * a `{{…}}` token (`{{contact.email}}`, `\"{{contact.name}}\" <{{contact.email}}>`)\n * is filled in later, so it is neither valid nor invalid yet\n * (`isPlaceholder`), and a separator inside the braces is part of it\n * (`{{join contacts \", \"}}` is one entry). Only doubled braces count — a\n * lone `{` is atext and stays an ordinary character.\n */\n\n// ── characters ───────────────────────────────────────────────────────────\n\n/** RFC 5322 `atext`, plus RFC 6532's UTF-8 beyond ASCII. */\nconst ATEXT = \"A-Za-z0-9!#$%&'*+/=?^_`{|}~\\\\-\\\\u0080-\\\\u{10FFFF}\";\nconst DOT_ATOM = new RegExp(`^[${ATEXT}]+(?:\\\\.[${ATEXT}]+)*$`, 'u');\n/** RFC 5322 `quoted-string` as a whole local part: qtext, quoted-pairs, spaces. */\nconst QUOTED_LOCAL = /^\"(?:[\\x20\\x21\\x23-\\x5B\\x5D-\\x7E\\u0080-\\u{10FFFF}]|\\\\[\\x20-\\x7E])*\"$/u;\n/** An RFC 5321 label, with RFC 6531 U-labels: letters and digits of any\n script, hyphens inside. */\nconst LABEL = /^(?!-)[\\p{L}\\p{N}\\p{M}-]{1,63}(?<!-)$/u;\n/** Characters a display name must be quoted to carry: RFC 5322's specials. */\nconst SPECIALS = /[()<>[\\]:;@\\\\,.\"]/;\n\nconst octets = (text: string): number => new TextEncoder().encode(text).length;\n\n// ── addr-spec ────────────────────────────────────────────────────────────\n\n/** Whether a bare address — no name, no brackets — is well-formed by the\n rule above. */\nexport function isEmailAddress(address: string): boolean {\n const at = address.lastIndexOf('@');\n if (at < 1 || at === address.length - 1) return false;\n const local = address.slice(0, at);\n const domain = address.slice(at + 1);\n if (octets(local) > 64 || octets(address) > 254) return false;\n if (!DOT_ATOM.test(local) && !QUOTED_LOCAL.test(local)) return false;\n return isDomain(domain);\n}\n\nfunction isDomain(domain: string): boolean {\n if (domain.startsWith('[')) return isAddressLiteral(domain);\n const labels = domain.split('.');\n if (labels.length < 2 || !labels.every((label) => LABEL.test(label))) return false;\n return !/^\\d+$/.test(labels[labels.length - 1]);\n}\n\nfunction isAddressLiteral(literal: string): boolean {\n const inner = /^\\[(.*)\\]$/.exec(literal)?.[1];\n if (inner === undefined) return false;\n if (/^IPv6:/i.test(inner)) return isIPv6(inner.slice(5));\n return isIPv4(inner);\n}\n\nfunction isIPv4(text: string): boolean {\n const parts = text.split('.');\n return parts.length === 4 && parts.every((part) => /^(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$/.test(part));\n}\n\nfunction isIPv6(text: string): boolean {\n // The platform's own parser is the reference; the shape check keeps a\n // host name or a port from sneaking through it.\n if (!/^[0-9A-Fa-f:.]+$/.test(text) || !text.includes(':')) return false;\n try {\n return new URL(`http://[${text}]/`).hostname.length > 2;\n } catch {\n return false;\n }\n}\n\n// ── the mailbox scanner ──────────────────────────────────────────────────\n\n/** Where the scan stands at one character: inside a quoted string, a\n comment (nested), angle brackets, or a `{{…}}` placeholder. */\ninterface ScanState {\n quoted: boolean;\n comment: number;\n angled: boolean;\n escaped: boolean;\n /** Inside `{{ … }}`. */\n braced: boolean;\n /** The character before was a `{` (outside) or a `}` (inside) — half\n of a doubled brace. */\n brace: boolean;\n}\n\nconst at_top = (state: ScanState) => !state.quoted && !state.comment && !state.angled && !state.braced;\n\n/** Steps the scan past one character; true when the character is structure\n (a quote, a bracket, part of a comment or an escape), not content at the\n top level. */\nfunction step(state: ScanState, char: string): boolean {\n const half = state.brace;\n state.brace = false;\n if (state.braced) {\n if (char === '}' && half) state.braced = false;\n else state.brace = char === '}';\n return true;\n }\n if (char === '{' && !state.quoted && !state.comment) {\n if (half) state.braced = true;\n else state.brace = true;\n return half;\n }\n if (state.escaped) {\n state.escaped = false;\n return true;\n }\n if ((state.quoted || state.comment) && char === '\\\\') {\n state.escaped = true;\n return true;\n }\n if (state.quoted) {\n if (char === '\"') state.quoted = false;\n return true;\n }\n if (state.comment) {\n if (char === '(') state.comment++;\n else if (char === ')') state.comment--;\n return true;\n }\n if (char === '\"' && !state.angled) {\n state.quoted = true;\n return true;\n }\n if (char === '(' && !state.angled) {\n state.comment = 1;\n return true;\n }\n if (char === '<') state.angled = true;\n else if (char === '>') state.angled = false;\n else return state.angled;\n return true;\n}\n\nconst fresh = (): ScanState => ({\n quoted: false,\n comment: 0,\n angled: false,\n escaped: false,\n braced: false,\n brace: false,\n});\n\n/** Whether a separator typed at `caret` would separate — false inside a\n quoted name, a comment or angle brackets, where a comma is part of the\n mailbox: `\"Lovelace, Ada\"`. */\nexport function separatesAt(raw: string, caret: number): boolean {\n const state = fresh();\n for (const char of raw.slice(0, caret)) step(state, char);\n return at_top(state) && !state.escaped;\n}\n\n// ── parse ────────────────────────────────────────────────────────────────\n\n/**\n * Splits a header-form mailbox into name and address: quoted strings\n * unquoted and unescaped, encoded words decoded, comments skipped — or, after\n * a bare address, taken as its name — and an obsolete route dropped.\n * Anything that is no mailbox comes back as its trimmed text, as the address,\n * for the rule to refuse.\n */\nexport function parseMailbox(raw: string): Mailbox {\n const text = raw.trim();\n const words: string[] = [];\n const comments: string[] = [];\n let address: string | null = null;\n let bare = '';\n let i = 0;\n while (i < text.length) {\n const char = text[i];\n if (char === '\"') {\n const end = closing(text, i, '\"');\n if (end < 0) return { address: text };\n words.push(unescape(text.slice(i + 1, end)));\n i = end + 1;\n } else if (char === '(') {\n const end = closingComment(text, i);\n if (end < 0) return { address: text };\n comments.push(decodeEncodedWords(unescape(text.slice(i + 1, end)).trim()));\n i = end + 1;\n } else if (char === '<') {\n const end = text.indexOf('>', i);\n if (end < 0 || address !== null) return { address: text };\n address = text.slice(i + 1, end).trim().replace(/^@[^:]*:/, '');\n i = end + 1;\n } else if (/\\s/.test(char)) {\n i++;\n } else {\n let end = i;\n while (end < text.length && !/[\\s\"(<]/.test(text[end])) end++;\n const atom = text.slice(i, end);\n if (address === null) words.push(atom);\n else return { address: text }; // text after the angle-addr\n bare += atom;\n i = end;\n }\n }\n if (address !== null) {\n const name = phrase(words) || comments.join(' ');\n return name ? { name, address } : { address };\n }\n // No angle brackets: an addr-spec, as one run of text.\n if (words.length !== 1 || !bare) return { address: text };\n const name = comments.join(' ');\n return name ? { name, address: bare } : { address: bare };\n}\n\n/** A phrase's words as a display name: one space between words, encoded\n words decoded — and two encoded words only whitespace separates joined\n with none (RFC 2047 §6.2). */\nfunction phrase(words: readonly string[]): string {\n return decodeEncodedWords(words.join(' ')).trim();\n}\n\nfunction closing(text: string, from: number, quote: string): number {\n for (let i = from + 1; i < text.length; i++) {\n if (text[i] === '\\\\') i++;\n else if (text[i] === quote) return i;\n }\n return -1;\n}\n\nfunction closingComment(text: string, from: number): number {\n let depth = 0;\n for (let i = from; i < text.length; i++) {\n if (text[i] === '\\\\') i++;\n else if (text[i] === '(') depth++;\n else if (text[i] === ')' && --depth === 0) return i;\n }\n return -1;\n}\n\nconst unescape = (text: string) => text.replace(/\\\\([\\s\\S])/g, '$1');\n\n// ── format ───────────────────────────────────────────────────────────────\n\n/** The header form of a mailbox — the name quoted (and escaped) when it has\n to be. UTF-8 stays as it is (RFC 6532); a mailer encodes for the wire. */\nexport function formatMailbox(mailbox: Mailbox): string {\n const name = mailbox.name?.trim();\n if (!name) return mailbox.address;\n const quoted = SPECIALS.test(name) || /\\s{2}/.test(name) ? `\"${name.replace(/[\"\\\\]/g, '\\\\$&')}\"` : name;\n return `${quoted} <${mailbox.address}>`;\n}\n\n// ── split ────────────────────────────────────────────────────────────────\n\n/** What separates the mailboxes of a typed run. */\nconst SEPARATORS = ',;\\n\\r';\n\n/**\n * Splits a typed, pasted or header run into mailboxes — see the leniencies\n * above. A well-formed mailbox comes back in its canonical header form\n * (`<a@x.io>` → `a@x.io`, `Dr. Ada <a@x.io>` → `\"Dr. Ada\" <a@x.io>`, a\n * comment or an encoded word turned into the name); anything else comes back\n * as typed, for the user to fix. Empty tokens are dropped; duplicates are\n * the caller's.\n */\nexport function splitAddresses(raw: string): string[] {\n const tokens: string[] = [];\n const state = fresh();\n let current = '';\n for (let i = 0; i < raw.length; i++) {\n const char = raw[i];\n if (step(state, char)) {\n current += char;\n continue;\n }\n if (SEPARATORS.includes(char)) {\n tokens.push(current);\n current = '';\n continue;\n }\n // A group's colon: what came before it is the group's name, not a\n // mailbox — dropped; its members follow, up to the `;`.\n if (char === ':' && !/[@<]/.test(current) && /^(?:[\\s,;]|$)/.test(raw.slice(i + 1, i + 2))) {\n current = '';\n continue;\n }\n current += char;\n }\n tokens.push(current);\n return tokens\n .map((token) => token.trim())\n .filter(Boolean)\n .flatMap((token) => (/[<\"(]/.test(token) ? [token] : bareRun(token)))\n .map(canonical);\n}\n\n/** A bare run's mailboxes: each run of words names the address after it. */\nfunction bareRun(token: string): string[] {\n const mailboxes: string[] = [];\n let name: string[] = [];\n for (const word of token.split(/\\s+/)) {\n if (!isEmailAddress(word)) {\n name.push(word);\n continue;\n }\n mailboxes.push(formatMailbox({ name: name.join(' ') || undefined, address: word }));\n name = [];\n }\n if (name.length) mailboxes.push(name.join(' '));\n return mailboxes;\n}\n\n/** A well-formed mailbox in its canonical header form; anything else as it is. */\nfunction canonical(token: string): string {\n const mailbox = parseMailbox(token);\n return isEmailAddress(mailbox.address) ? formatMailbox(mailbox) : token;\n}\n\n// ── identity ─────────────────────────────────────────────────────────────\n\n/**\n * What makes two entries one recipient. The domain is case-insensitive and\n * compared in its ASCII form (`müller.de` is `xn--mller-kva.de`); the local\n * part is lowercased too — RFC 5321 lets a server tell `Ada` from `ada`, no\n * real one does, and one recipient twice is the mistake that matters. Both\n * NFC-normalized, so a composed and a decomposed `ü` are one.\n */\nexport function addressIdentity(address: string): string {\n const at = address.lastIndexOf('@');\n if (at < 0) return address.trim().normalize('NFC').toLowerCase();\n const local = address.slice(0, at).normalize('NFC').toLowerCase();\n return `${local}@${asciiDomain(address.slice(at + 1).normalize('NFC').toLowerCase())}`;\n}\n\nfunction asciiDomain(domain: string): string {\n if (domain.startsWith('[') || /^[\\x00-\\x7F]*$/.test(domain)) return domain;\n try {\n return new URL(`http://${domain}/`).hostname;\n } catch {\n return domain;\n }\n}\n\n// ── RFC 2047 ─────────────────────────────────────────────────────────────\n\nconst ENCODED_WORD = /=\\?([^?\\s]+)\\?([BbQq])\\?([^?\\s]*)\\?=/g;\n\n/** Decodes the RFC 2047 encoded words in a header text; one that cannot be\n decoded stays as it is. Two encoded words only whitespace separates are\n joined with none. */\nexport function decodeEncodedWords(text: string): string {\n return text\n .replace(/(=\\?[^?\\s]+\\?[BbQq]\\?[^?\\s]*\\?=)\\s+(?==\\?[^?\\s]+\\?[BbQq]\\?[^?\\s]*\\?=)/g, '$1')\n .replace(ENCODED_WORD, (word, charset: string, encoding: string, data: string) => {\n try {\n const bytes =\n encoding.toUpperCase() === 'B'\n ? Uint8Array.from(atob(data), (c) => c.charCodeAt(0))\n : quotedPrintable(data);\n // RFC 2231 lets a charset carry a language: `UTF-8*de`.\n return new TextDecoder(charset.split('*')[0], { fatal: true }).decode(bytes);\n } catch {\n return word;\n }\n });\n}\n\nfunction quotedPrintable(data: string): Uint8Array {\n const bytes: number[] = [];\n for (let i = 0; i < data.length; i++) {\n const char = data[i];\n if (char === '_') bytes.push(0x20);\n else if (char === '=' && /^[0-9A-Fa-f]{2}$/.test(data.slice(i + 1, i + 3))) {\n bytes.push(parseInt(data.slice(i + 1, i + 3), 16));\n i += 2;\n } else bytes.push(...new TextEncoder().encode(char));\n }\n return Uint8Array.from(bytes);\n}\n\n// ── placeholders ─────────────────────────────────────────────────────────\n\n/** Whether an address holds a `{{…}}` placeholder — one a template fills\n in later, so neither valid nor invalid yet. */\nexport function isAddressPlaceholder(address: string): boolean {\n return /\\{\\{[^{}]*\\}\\}/.test(address);\n}\n\n/** Our rule, as the contract every part of the library asks through. */\nexport const defaultAddressRules: AddressRules = {\n split: splitAddresses,\n parse: parseMailbox,\n format: formatMailbox,\n isValid: isEmailAddress,\n isPlaceholder: isAddressPlaceholder,\n identity: addressIdentity,\n};\n","import { InjectionToken, Provider } from '@angular/core';\nimport { defaultAddressRules } from './rules';\n\n/**\n * A mailbox the way a header spells it: an address, and the display name a\n * client shows in front of it — `Ada Lovelace <ada@example.com>`,\n * `\"Lovelace, Ada\" <ada@example.com>`, or the bare `ada@example.com`.\n *\n * The chips hold that header form as one string, so a host's `string[]`\n * carries names without a second type and goes into a `To:` header as it\n * is.\n */\nexport interface Mailbox {\n /** The display name, if the mailbox has one; quotes already removed. */\n readonly name?: string;\n readonly address: string;\n}\n\n/**\n * The address rule — what an address is, how a mailbox is read and written,\n * how typed text splits into mailboxes. The library's own is `rules.ts`\n * ({@link defaultAddressRules}); a host with a rule of its own — a stricter\n * validator, an established parser, the rule its backend already enforces —\n * provides it with {@link provideAddressRules}, whole or in part, and every\n * address piece asks it instead: the input, the chip, the `addressList`\n * form rule, and (passed as an option) `replyEnvelope` and\n * `toInboundMessage`.\n */\nexport interface AddressRules {\n /** A typed, pasted or header run as mailboxes in header form. */\n split(raw: string): string[];\n /** A header-form mailbox as its name and address. */\n parse(raw: string): Mailbox;\n /** A mailbox in header form, the name quoted where it has to be. */\n format(mailbox: Mailbox): string;\n /** Whether a bare address — no name, no brackets — is well-formed. */\n isValid(address: string): boolean;\n /** Whether an address is a placeholder a template fills in later\n (`{{contact.email}}`) — neither valid nor invalid until then. A host\n that sends what is typed, unresolved, turns it off with\n `provideAddressRules({ isPlaceholder: () => false })`. */\n isPlaceholder(address: string): boolean;\n /** What makes two addresses one recipient: equal identities are. */\n identity(address: string): string;\n}\n\n/** The address rule in force — ours unless a host provides its own. */\nexport const ADDRESS_RULES = new InjectionToken<AddressRules>('ADDRESS_RULES', {\n providedIn: 'root',\n factory: () => defaultAddressRules,\n});\n\n/**\n * Puts a host's address rule in force: the parts given replace ours, the\n * rest stay ours. Keeping an existing validator is one line:\n *\n * ```ts\n * providers: [provideAddressRules({ isValid: isValidEmail })]\n * ```\n */\nexport function provideAddressRules(rules: Partial<AddressRules>): Provider {\n return { provide: ADDRESS_RULES, useValue: { ...defaultAddressRules, ...rules } };\n}\n\n/** Whether a header-form string is a mailbox whose address is well-formed. */\nexport function isMailbox(raw: string, rules: AddressRules = defaultAddressRules): boolean {\n return rules.isValid(rules.parse(raw).address);\n}\n\n/** Whether a header-form entry's address is a placeholder — pending: a\n template fills it in later, so it is no mistake to flag. */\nexport function isPending(raw: string, rules: AddressRules = defaultAddressRules): boolean {\n return rules.isPlaceholder(rules.parse(raw).address);\n}\n\n/** What makes two header-form entries one recipient — the name in front of\n the address makes no second one. For a list that holds each recipient\n once. */\nexport function addressKey(raw: string, rules: AddressRules = defaultAddressRules): string {\n return rules.identity(rules.parse(raw).address);\n}\n\n/**\n * A list of header-form entries as one header value — `\"Müller, Hans\"\n * <h@x.de>, b@c.de` — for a backend or a template that keeps To, Cc and Bcc\n * as text. The way back is the rule's own `split`: `rules.split(joined)` is\n * the list again, entry for entry. A well-formed mailbox is written in its\n * canonical form (a name with a comma quoted); anything else — a typo, a\n * `{{placeholder}}` — goes in as it is, for the backend to resolve or refuse.\n *\n * ```ts\n * to: joinAddresses(form.to().value()), // out to the backend\n * form.to().value.set(splitAddresses(template.to)) // back in\n * ```\n */\nexport function joinAddresses(list: readonly string[], rules: AddressRules = defaultAddressRules): string {\n return list\n .map((entry) => entry.trim())\n .filter(Boolean)\n .map((entry) => (isMailbox(entry, rules) ? rules.format(rules.parse(entry)) : entry))\n .join(', ');\n}\n","/*\n * Public API Surface of @h-k-dev/angular-email-editor/address\n */\n\nexport * from './address';\nexport * from './rules';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, inject, ElementRef, linkedSignal, Directive } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
const NOWHERE = { left: 0, top: 0, width: 0, height: 0 };
|
|
5
|
+
/**
|
|
6
|
+
* A real element kept over a box the editor reports — a selection, a caret,
|
|
7
|
+
* a block. The editor knows rectangles; everything that floats wants an
|
|
8
|
+
* *element* to hold on to: the CDK's `cdkOverlayOrigin`, Material's
|
|
9
|
+
* `matMenuTriggerFor`, Aria's menu trigger, CSS anchor positioning,
|
|
10
|
+
* Floating UI. This is that element, so each of them works as it is:
|
|
11
|
+
*
|
|
12
|
+
* <span [emailAnchor]="state().boundingBox" cdkOverlayOrigin #origin="cdkOverlayOrigin"></span>
|
|
13
|
+
* <ng-template cdkConnectedOverlay [cdkConnectedOverlayOrigin]="origin" …>
|
|
14
|
+
*
|
|
15
|
+
* Behaviour only: it places its element — fixed, at the box, out of the
|
|
16
|
+
* pointer's way — and renders nothing. While the box is `null` it stays
|
|
17
|
+
* where it last was, so what is anchored to it can close (and animate out)
|
|
18
|
+
* in place rather than jump to the corner.
|
|
19
|
+
*
|
|
20
|
+
* The box is in viewport coordinates, so the element is `position: fixed`;
|
|
21
|
+
* it must not sit inside a transformed ancestor, which would become its
|
|
22
|
+
* frame. It does not follow a scroll by itself: it is as current as the box
|
|
23
|
+
* it is given.
|
|
24
|
+
*/
|
|
25
|
+
class Anchor {
|
|
26
|
+
/** The box to stand over; `null` while there is none. */
|
|
27
|
+
rect = input(null, { ...(ngDevMode ? { debugName: "rect" } : /* istanbul ignore next */ {}), alias: 'emailAnchor' });
|
|
28
|
+
/** The element itself — for what takes an element rather than a template
|
|
29
|
+
reference (`#anchor="emailAnchor"`, then `anchor.element`). */
|
|
30
|
+
element = inject(ElementRef).nativeElement;
|
|
31
|
+
/** The box being stood over: the last one given, held through `null`. */
|
|
32
|
+
box = linkedSignal({ ...(ngDevMode ? { debugName: "box" } : /* istanbul ignore next */ {}), source: this.rect,
|
|
33
|
+
computation: (rect, previous) => rect
|
|
34
|
+
? { left: rect.left, top: rect.top, width: rect.width ?? 0, height: rect.height ?? 0 }
|
|
35
|
+
: (previous?.value ?? NOWHERE) });
|
|
36
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: Anchor, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
37
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.6", type: Anchor, isStandalone: true, selector: "[emailAnchor]", inputs: { rect: { classPropertyName: "rect", publicName: "emailAnchor", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style.position": "\"fixed\"", "style.pointer-events": "\"none\"", "style.left.px": "box().left", "style.top.px": "box().top", "style.width.px": "box().width", "style.height.px": "box().height" } }, exportAs: ["emailAnchor"], ngImport: i0 });
|
|
38
|
+
}
|
|
39
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: Anchor, decorators: [{
|
|
40
|
+
type: Directive,
|
|
41
|
+
args: [{
|
|
42
|
+
selector: '[emailAnchor]',
|
|
43
|
+
exportAs: 'emailAnchor',
|
|
44
|
+
host: {
|
|
45
|
+
'[style.position]': '"fixed"',
|
|
46
|
+
'[style.pointer-events]': '"none"',
|
|
47
|
+
'[style.left.px]': 'box().left',
|
|
48
|
+
'[style.top.px]': 'box().top',
|
|
49
|
+
'[style.width.px]': 'box().width',
|
|
50
|
+
'[style.height.px]': 'box().height',
|
|
51
|
+
},
|
|
52
|
+
}]
|
|
53
|
+
}], propDecorators: { rect: [{ type: i0.Input, args: [{ isSignal: true, alias: "emailAnchor", required: false }] }] } });
|
|
54
|
+
|
|
55
|
+
/*
|
|
56
|
+
* Public API Surface of @h-k-dev/angular-email-editor/anchor
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generated bundle index. Do not edit.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
export { Anchor };
|
|
64
|
+
//# sourceMappingURL=h-k-dev-angular-email-editor-anchor.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"h-k-dev-angular-email-editor-anchor.mjs","sources":["../../../projects/angular-email-editor/anchor/anchor.ts","../../../projects/angular-email-editor/anchor/public-api.ts","../../../projects/angular-email-editor/anchor/h-k-dev-angular-email-editor-anchor.ts"],"sourcesContent":["import { Directive, ElementRef, inject, input, linkedSignal } from '@angular/core';\n\n/** A box in viewport coordinates — a `DOMRect`, a menu state's\n `boundingBox`, a caret's `coordsAtPos`. A point when it has no size. */\nexport interface AnchorRect {\n left: number;\n top: number;\n width?: number;\n height?: number;\n}\n\nconst NOWHERE: Required<AnchorRect> = { left: 0, top: 0, width: 0, height: 0 };\n\n/**\n * A real element kept over a box the editor reports — a selection, a caret,\n * a block. The editor knows rectangles; everything that floats wants an\n * *element* to hold on to: the CDK's `cdkOverlayOrigin`, Material's\n * `matMenuTriggerFor`, Aria's menu trigger, CSS anchor positioning,\n * Floating UI. This is that element, so each of them works as it is:\n *\n * <span [emailAnchor]=\"state().boundingBox\" cdkOverlayOrigin #origin=\"cdkOverlayOrigin\"></span>\n * <ng-template cdkConnectedOverlay [cdkConnectedOverlayOrigin]=\"origin\" …>\n *\n * Behaviour only: it places its element — fixed, at the box, out of the\n * pointer's way — and renders nothing. While the box is `null` it stays\n * where it last was, so what is anchored to it can close (and animate out)\n * in place rather than jump to the corner.\n *\n * The box is in viewport coordinates, so the element is `position: fixed`;\n * it must not sit inside a transformed ancestor, which would become its\n * frame. It does not follow a scroll by itself: it is as current as the box\n * it is given.\n */\n@Directive({\n selector: '[emailAnchor]',\n exportAs: 'emailAnchor',\n host: {\n '[style.position]': '\"fixed\"',\n '[style.pointer-events]': '\"none\"',\n '[style.left.px]': 'box().left',\n '[style.top.px]': 'box().top',\n '[style.width.px]': 'box().width',\n '[style.height.px]': 'box().height',\n },\n})\nexport class Anchor {\n /** The box to stand over; `null` while there is none. */\n readonly rect = input<AnchorRect | null | undefined>(null, { alias: 'emailAnchor' });\n\n /** The element itself — for what takes an element rather than a template\n reference (`#anchor=\"emailAnchor\"`, then `anchor.element`). */\n readonly element = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n\n /** The box being stood over: the last one given, held through `null`. */\n protected readonly box = linkedSignal<AnchorRect | null | undefined, Required<AnchorRect>>({\n source: this.rect,\n computation: (rect, previous) =>\n rect\n ? { left: rect.left, top: rect.top, width: rect.width ?? 0, height: rect.height ?? 0 }\n : (previous?.value ?? NOWHERE),\n });\n}\n","/*\n * Public API Surface of @h-k-dev/angular-email-editor/anchor\n */\n\nexport * from './anchor';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;AAWA,MAAM,OAAO,GAAyB,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;AAE9E;;;;;;;;;;;;;;;;;;;AAmBG;MAaU,MAAM,CAAA;;IAER,IAAI,GAAG,KAAK,CAAgC,IAAI,4EAAI,KAAK,EAAE,aAAa,EAAA,CAAG;AAEpF;AACkE;AACzD,IAAA,OAAO,GAAG,MAAM,CAA0B,UAAU,CAAC,CAAC,aAAa;;AAGzD,IAAA,GAAG,GAAG,YAAY,CAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,KAAA,EAAA,8BAAA,EAAA,CAAA,EACnC,MAAM,EAAE,IAAI,CAAC,IAAI;QACjB,WAAW,EAAE,CAAC,IAAI,EAAE,QAAQ,KAC1B;AACE,cAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC;eACjF,QAAQ,EAAE,KAAK,IAAI,OAAO,CAAC,EAAA,CAClC;uGAfS,MAAM,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;2FAAN,MAAM,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,eAAA,EAAA,MAAA,EAAA,EAAA,IAAA,EAAA,EAAA,iBAAA,EAAA,MAAA,EAAA,UAAA,EAAA,aAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,gBAAA,EAAA,WAAA,EAAA,sBAAA,EAAA,UAAA,EAAA,eAAA,EAAA,YAAA,EAAA,cAAA,EAAA,WAAA,EAAA,gBAAA,EAAA,aAAA,EAAA,iBAAA,EAAA,cAAA,EAAA,EAAA,EAAA,QAAA,EAAA,CAAA,aAAA,CAAA,EAAA,QAAA,EAAA,EAAA,EAAA,CAAA;;2FAAN,MAAM,EAAA,UAAA,EAAA,CAAA;kBAZlB,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,eAAe;AACzB,oBAAA,QAAQ,EAAE,aAAa;AACvB,oBAAA,IAAI,EAAE;AACJ,wBAAA,kBAAkB,EAAE,SAAS;AAC7B,wBAAA,wBAAwB,EAAE,QAAQ;AAClC,wBAAA,iBAAiB,EAAE,YAAY;AAC/B,wBAAA,gBAAgB,EAAE,WAAW;AAC7B,wBAAA,kBAAkB,EAAE,aAAa;AACjC,wBAAA,mBAAmB,EAAE,cAAc;AACpC,qBAAA;AACF,iBAAA;;;AC5CD;;AAEG;;ACFH;;AAEG;;;;"}
|