nodemailer 10.1.4 → 10.1.5
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/CHANGELOG.md +8 -0
- package/dist/cjs/addressparser/index.js +220 -85
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/esm/addressparser/index.js +220 -85
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [10.1.5](https://github.com/nodemailer/nodemailer/compare/v10.1.4...v10.1.5) (2026-10-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **addressparser:** handle an address list without copying it per address ([6bd09b3](https://github.com/nodemailer/nodemailer/commit/6bd09b395cb7ffd1b0ae95f960f3985ba9c25afb))
|
|
9
|
+
* **addressparser:** tokenize into offsets instead of an object per token ([3fd3f50](https://github.com/nodemailer/nodemailer/commit/3fd3f50ce86671884896ef1eaddafff5e137f820))
|
|
10
|
+
|
|
3
11
|
## [10.1.4](https://github.com/nodemailer/nodemailer/compare/v10.1.3...v10.1.4) (2026-10-18)
|
|
4
12
|
|
|
5
13
|
|
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/* eslint-disable no-bitwise -- the kind of a token is a set of bit flags packed into one slot of a typed array */
|
|
4
|
+
/** The token is an operator, a text run otherwise */
|
|
5
|
+
const TOKEN_OPERATOR = 1;
|
|
6
|
+
/** The next character continues the same run without whitespace in between */
|
|
7
|
+
const TOKEN_NO_BREAK = 2;
|
|
8
|
+
/** The text run holds characters its value leaves out or changes, so it is no plain slice */
|
|
9
|
+
const TOKEN_DIRTY = 4;
|
|
10
|
+
/** A backslash escapes the next character in the text run, inside a quoted string or a comment */
|
|
11
|
+
const TOKEN_ESCAPES = 8;
|
|
12
|
+
/** The text run opens on a character an escaping backslash in front of it has escaped */
|
|
13
|
+
const TOKEN_STARTS_ESCAPED = 16;
|
|
14
|
+
/**
|
|
15
|
+
* The tokens of an address field, kept as offsets into it.
|
|
16
|
+
*
|
|
17
|
+
* Every token used to be an object of its own, an operator for every ',' and a value string
|
|
18
|
+
* for every run of text, and a list of one letter names, 'a,a,a,...', took some 65 times its
|
|
19
|
+
* size in tokens before any address was read. A token is now three numbers in one typed
|
|
20
|
+
* array, its kind and the range of the field it covers, and its value is sliced off the field
|
|
21
|
+
* when it is read. The few text runs whose value differs from their slice, as they hold an
|
|
22
|
+
* escaping backslash, a line break or a control character, are rebuilt once and kept aside.
|
|
23
|
+
*/
|
|
24
|
+
class TokenList {
|
|
25
|
+
constructor(str) {
|
|
26
|
+
this.str = str;
|
|
27
|
+
this.slots = new Int32Array(48);
|
|
28
|
+
this.length = 0;
|
|
29
|
+
this.cleaned = new Map();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Appends a token
|
|
33
|
+
*
|
|
34
|
+
* @param kind Kind and flags of the token
|
|
35
|
+
* @param start Offset of its first character
|
|
36
|
+
* @param end Offset just past its last character
|
|
37
|
+
* @return Index of the token
|
|
38
|
+
*/
|
|
39
|
+
push(kind, start, end) {
|
|
40
|
+
if ((this.length + 1) * 3 > this.slots.length) {
|
|
41
|
+
const grown = new Int32Array(this.slots.length * 2);
|
|
42
|
+
grown.set(this.slots);
|
|
43
|
+
this.slots = grown;
|
|
44
|
+
}
|
|
45
|
+
const at = this.length * 3;
|
|
46
|
+
this.slots[at] = kind;
|
|
47
|
+
this.slots[at + 1] = start;
|
|
48
|
+
this.slots[at + 2] = end;
|
|
49
|
+
return this.length++;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The character of an operator token, an empty string for a text run
|
|
53
|
+
*/
|
|
54
|
+
operatorAt(index) {
|
|
55
|
+
const at = index * 3;
|
|
56
|
+
return this.slots[at] & TOKEN_OPERATOR ? this.str.charAt(this.slots[at + 1]) : '';
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether a token is an operator
|
|
60
|
+
*/
|
|
61
|
+
isOperator(index) {
|
|
62
|
+
return (this.slots[index * 3] & TOKEN_OPERATOR) !== 0;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether the next character continues the run of a token without whitespace in between
|
|
66
|
+
*/
|
|
67
|
+
noBreak(index) {
|
|
68
|
+
return (this.slots[index * 3] & TOKEN_NO_BREAK) !== 0;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Value of a token
|
|
72
|
+
*/
|
|
73
|
+
value(index) {
|
|
74
|
+
const cleaned = this.cleaned.get(index);
|
|
75
|
+
if (cleaned !== undefined) {
|
|
76
|
+
return cleaned;
|
|
77
|
+
}
|
|
78
|
+
return this.str.slice(this.slots[index * 3 + 1], this.slots[index * 3 + 2]);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
3
81
|
/**
|
|
4
82
|
* A domain literal that ends an address. dtext includes '@' (RFC 5322 3.4.1), so the '@'
|
|
5
83
|
* in front of the literal is the one that splits the domain off, not the last one: reading
|
|
@@ -456,11 +534,13 @@ function _stripAddressComments(address) {
|
|
|
456
534
|
/**
|
|
457
535
|
* Converts tokens for a single address into an address object
|
|
458
536
|
*
|
|
459
|
-
* @param tokens Tokens
|
|
537
|
+
* @param tokens Tokens of the whole address list
|
|
538
|
+
* @param from Index of the first token of this address
|
|
539
|
+
* @param to Index just past the last token of this address
|
|
460
540
|
* @param depth Current recursion depth for nested group protection
|
|
461
541
|
* @return Address object
|
|
462
542
|
*/
|
|
463
|
-
function _handleAddress(tokens, depth) {
|
|
543
|
+
function _handleAddress(tokens, from, to, depth) {
|
|
464
544
|
let isGroup = false;
|
|
465
545
|
let state = 'text';
|
|
466
546
|
const addresses = [];
|
|
@@ -483,11 +563,13 @@ function _handleAddress(tokens, depth) {
|
|
|
483
563
|
// character is carried along instead of re-read.
|
|
484
564
|
const lastChars = { address: '', comment: '', group: '', text: '' };
|
|
485
565
|
// Filter out <addresses>, (comments) and regular text
|
|
486
|
-
for (let i =
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
566
|
+
for (let i = from; i < to; i++) {
|
|
567
|
+
const value = tokens.value(i);
|
|
568
|
+
// the operator in front of this token, if there is one, and whether it runs on into it
|
|
569
|
+
const prevOperator = i > from ? tokens.operatorAt(i - 1) : '';
|
|
570
|
+
const prevNoBreak = i > from && tokens.noBreak(i - 1);
|
|
571
|
+
if (tokens.isOperator(i)) {
|
|
572
|
+
switch (value) {
|
|
491
573
|
case '<':
|
|
492
574
|
state = 'address';
|
|
493
575
|
insideQuotes = false;
|
|
@@ -511,7 +593,7 @@ function _handleAddress(tokens, depth) {
|
|
|
511
593
|
break;
|
|
512
594
|
}
|
|
513
595
|
}
|
|
514
|
-
else
|
|
596
|
+
else {
|
|
515
597
|
// An empty quoted string is dropped by the tokenizer, leaving no text token of its
|
|
516
598
|
// own for textWasQuoted to be recorded on, so the pair of quote operators right in
|
|
517
599
|
// front of this token is all that is left of it and the run it opens carries the
|
|
@@ -519,44 +601,34 @@ function _handleAddress(tokens, depth) {
|
|
|
519
601
|
// the quotes never go back on, and the value is no longer an addr-spec a trailing
|
|
520
602
|
// comment can be peeled off of. It only ever opens a run: a run that already holds
|
|
521
603
|
// material collected outside the quotes is not a quoted string, whatever follows it
|
|
522
|
-
const
|
|
523
|
-
const opensAfterEmptyQuotedString = prevToken?.type === 'operator' &&
|
|
524
|
-
prevToken.value === '"' &&
|
|
525
|
-
!!prevToken.noBreak &&
|
|
526
|
-
prevPrevToken?.type === 'operator' &&
|
|
527
|
-
prevPrevToken.value === '"';
|
|
604
|
+
const opensAfterEmptyQuotedString = prevOperator === '"' && prevNoBreak && i > from + 1 && tokens.operatorAt(i - 2) === '"';
|
|
528
605
|
// A comment is folding whitespace. It may sit inside an addr-spec, on either side
|
|
529
606
|
// of the '@', but it cannot join two atoms into one: gluing across it would read
|
|
530
607
|
// 'user@example.com(x)evil.com' as the single domain 'example.comevil.com' and
|
|
531
608
|
// deliver to a domain the sender never named.
|
|
532
609
|
const parts = data[state];
|
|
533
|
-
const joins =
|
|
534
|
-
prevToken.noBreak &&
|
|
535
|
-
parts.length &&
|
|
536
|
-
(prevToken.value !== ')' || lastChars[state] === '@' || token.value.charAt(0) === '@');
|
|
610
|
+
const joins = prevNoBreak && parts.length && (prevOperator !== ')' || lastChars[state] === '@' || value.charAt(0) === '@');
|
|
537
611
|
if (joins) {
|
|
538
612
|
const last = data[state].length - 1;
|
|
539
613
|
if (state === 'text' && !insideQuotes) {
|
|
540
|
-
if (lastTextPureQuoted &&
|
|
614
|
+
if (lastTextPureQuoted && value.charAt(0) === '@') {
|
|
541
615
|
data.quotedLocalAt[last] = data.text[last].length;
|
|
542
616
|
}
|
|
543
617
|
lastTextPureQuoted = false;
|
|
544
618
|
}
|
|
545
|
-
data[state][last] +=
|
|
546
|
-
|
|
547
|
-
lastChars[state] = token.value.charAt(token.value.length - 1);
|
|
548
|
-
}
|
|
619
|
+
data[state][last] += value;
|
|
620
|
+
lastChars[state] = value.charAt(value.length - 1);
|
|
549
621
|
if (state === 'text' && insideQuotes) {
|
|
550
622
|
data.textWasQuoted[last] = true;
|
|
551
623
|
}
|
|
552
624
|
}
|
|
553
625
|
else {
|
|
554
|
-
data[state].push(
|
|
555
|
-
lastChars[state] =
|
|
626
|
+
data[state].push(value);
|
|
627
|
+
lastChars[state] = value.charAt(value.length - 1);
|
|
556
628
|
if (state === 'text') {
|
|
557
629
|
data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
|
558
630
|
// an empty quoted string leaves no text of its own, so the '@' opens the run
|
|
559
|
-
data.quotedLocalAt.push(opensAfterEmptyQuotedString &&
|
|
631
|
+
data.quotedLocalAt.push(opensAfterEmptyQuotedString && value.charAt(0) === '@' ? 0 : -1);
|
|
560
632
|
lastTextPureQuoted = insideQuotes;
|
|
561
633
|
}
|
|
562
634
|
}
|
|
@@ -719,6 +791,7 @@ function _handleAddress(tokens, depth) {
|
|
|
719
791
|
}
|
|
720
792
|
return addresses;
|
|
721
793
|
}
|
|
794
|
+
/* eslint-disable no-bitwise -- see TokenList */
|
|
722
795
|
/**
|
|
723
796
|
* Creates a Tokenizer object for tokenizing address field strings
|
|
724
797
|
*
|
|
@@ -730,7 +803,7 @@ class Tokenizer {
|
|
|
730
803
|
this.str = (str || '').toString();
|
|
731
804
|
this.operatorCurrent = '';
|
|
732
805
|
this.operatorExpecting = '';
|
|
733
|
-
this.node =
|
|
806
|
+
this.node = -1;
|
|
734
807
|
this.escaped = false;
|
|
735
808
|
this.inDomainLiteral = false;
|
|
736
809
|
this.afterAt = false;
|
|
@@ -738,7 +811,7 @@ class Tokenizer {
|
|
|
738
811
|
this.innerComment = 0;
|
|
739
812
|
this.innerLiteral = false;
|
|
740
813
|
this.innerEscaped = false;
|
|
741
|
-
this.list =
|
|
814
|
+
this.list = new TokenList(this.str);
|
|
742
815
|
/**
|
|
743
816
|
* Operator tokens and which tokens are expected to end the sequence
|
|
744
817
|
*/
|
|
@@ -763,27 +836,96 @@ class Tokenizer {
|
|
|
763
836
|
* @return An array of operator|text tokens
|
|
764
837
|
*/
|
|
765
838
|
tokenize() {
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
this.checkChar(chr, nextChr);
|
|
839
|
+
const str = this.str;
|
|
840
|
+
const len = str.length;
|
|
841
|
+
for (let i = 0; i < len; i++) {
|
|
842
|
+
this.checkChar(str.charAt(i), i);
|
|
771
843
|
}
|
|
772
|
-
this.
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
844
|
+
this._closeText(len);
|
|
845
|
+
// Compacted in place: a text run is trimmed to its value and one left empty is
|
|
846
|
+
// dropped, and the value of a run that is no plain slice is rebuilt and kept aside
|
|
847
|
+
const list = this.list;
|
|
848
|
+
const slots = list.slots;
|
|
849
|
+
let kept = 0;
|
|
850
|
+
for (let i = 0, count = list.length; i < count; i++) {
|
|
851
|
+
const kind = slots[i * 3];
|
|
852
|
+
let start = slots[i * 3 + 1];
|
|
853
|
+
let end = slots[i * 3 + 2];
|
|
854
|
+
if (kind & TOKEN_DIRTY) {
|
|
855
|
+
const value = this._cleanValue(start, end, kind).trim();
|
|
856
|
+
if (!value) {
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
list.cleaned.set(kept, value);
|
|
776
860
|
}
|
|
777
|
-
|
|
861
|
+
else if (!(kind & TOKEN_OPERATOR)) {
|
|
862
|
+
while (start < end && _isSpaceCode(str.charCodeAt(start))) {
|
|
863
|
+
start++;
|
|
864
|
+
}
|
|
865
|
+
while (end > start && _isSpaceCode(str.charCodeAt(end - 1))) {
|
|
866
|
+
end--;
|
|
867
|
+
}
|
|
868
|
+
if (start === end) {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
slots[kept * 3] = kind;
|
|
873
|
+
slots[kept * 3 + 1] = start;
|
|
874
|
+
slots[kept * 3 + 2] = end;
|
|
875
|
+
kept++;
|
|
876
|
+
}
|
|
877
|
+
list.length = kept;
|
|
778
878
|
return list;
|
|
779
879
|
}
|
|
880
|
+
/**
|
|
881
|
+
* Ends the text run being collected, if there is one
|
|
882
|
+
*
|
|
883
|
+
* @param end Offset just past its last character
|
|
884
|
+
*/
|
|
885
|
+
_closeText(end) {
|
|
886
|
+
if (this.node >= 0) {
|
|
887
|
+
this.list.slots[this.node * 3 + 2] = end;
|
|
888
|
+
this.node = -1;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Rebuilds the value of a text run that is no plain slice of the field, applying the
|
|
893
|
+
* rules checkChar collects characters by: an escaping backslash is dropped, a line break
|
|
894
|
+
* reads as a space and any other control character is left out
|
|
895
|
+
*
|
|
896
|
+
* @param start Offset of the first character of the run
|
|
897
|
+
* @param end Offset just past its last character
|
|
898
|
+
* @param kind Kind and flags of the run
|
|
899
|
+
* @return Value of the run
|
|
900
|
+
*/
|
|
901
|
+
_cleanValue(start, end, kind) {
|
|
902
|
+
const str = this.str;
|
|
903
|
+
const escapes = (kind & TOKEN_ESCAPES) !== 0;
|
|
904
|
+
let escaped = (kind & TOKEN_STARTS_ESCAPED) !== 0;
|
|
905
|
+
let value = '';
|
|
906
|
+
for (let i = start; i < end; i++) {
|
|
907
|
+
const chr = str.charAt(i);
|
|
908
|
+
if (escapes && !escaped && chr === '\\') {
|
|
909
|
+
escaped = true;
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
escaped = false;
|
|
913
|
+
if (chr === '\n') {
|
|
914
|
+
value += ' ';
|
|
915
|
+
}
|
|
916
|
+
else if (chr.charCodeAt(0) >= 0x21 || chr === ' ' || chr === '\t') {
|
|
917
|
+
value += chr;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return value;
|
|
921
|
+
}
|
|
780
922
|
/**
|
|
781
923
|
* Checks if a character is an operator or text and acts accordingly
|
|
782
924
|
*
|
|
783
925
|
* @param chr Character from the address field
|
|
784
|
-
* @param
|
|
926
|
+
* @param i Offset of the character in the field
|
|
785
927
|
*/
|
|
786
|
-
checkChar(chr,
|
|
928
|
+
checkChar(chr, i) {
|
|
787
929
|
// Track RFC 5322 domain-literals ("[" *dtext "]"). The ":" of an IPv6
|
|
788
930
|
// address-literal (user@[IPv6:2001:db8::1]) is dtext and must not be treated as
|
|
789
931
|
// the group delimiter while inside the brackets. That is the only operator the
|
|
@@ -830,15 +972,9 @@ class Tokenizer {
|
|
|
830
972
|
// ignore next condition blocks
|
|
831
973
|
}
|
|
832
974
|
else if (chr === this.operatorExpecting && !nested) {
|
|
833
|
-
this.
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
};
|
|
837
|
-
if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
|
|
838
|
-
this.node.noBreak = true;
|
|
839
|
-
}
|
|
840
|
-
this.list.push(this.node);
|
|
841
|
-
this.node = null;
|
|
975
|
+
const nextChr = this.str.charAt(i + 1);
|
|
976
|
+
this._closeText(i);
|
|
977
|
+
this.list.push(nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr) ? TOKEN_OPERATOR | TOKEN_NO_BREAK : TOKEN_OPERATOR, i, i + 1);
|
|
842
978
|
if (chr !== ')') {
|
|
843
979
|
// a comment is folding whitespace, it does not part the "@" from the "["
|
|
844
980
|
this.afterAt = false;
|
|
@@ -849,12 +985,8 @@ class Tokenizer {
|
|
|
849
985
|
return;
|
|
850
986
|
}
|
|
851
987
|
else if (!this.operatorExpecting && !(this.inDomainLiteral && chr === ':') && chr in this.operators) {
|
|
852
|
-
this.
|
|
853
|
-
|
|
854
|
-
value: chr
|
|
855
|
-
};
|
|
856
|
-
this.list.push(this.node);
|
|
857
|
-
this.node = null;
|
|
988
|
+
this._closeText(i);
|
|
989
|
+
this.list.push(TOKEN_OPERATOR, i, i + 1);
|
|
858
990
|
if (chr !== '(') {
|
|
859
991
|
this.afterAt = false;
|
|
860
992
|
}
|
|
@@ -869,29 +1001,34 @@ class Tokenizer {
|
|
|
869
1001
|
// only in quoted strings let an escaped parenthesis close the comment, which
|
|
870
1002
|
// released the rest of it, an address included, into the header
|
|
871
1003
|
this.escaped = true;
|
|
1004
|
+
if (this.node >= 0) {
|
|
1005
|
+
// the backslash is left out of the value
|
|
1006
|
+
this.list.slots[this.node * 3] |= TOKEN_DIRTY;
|
|
1007
|
+
}
|
|
872
1008
|
return;
|
|
873
1009
|
}
|
|
874
|
-
if (
|
|
875
|
-
this.node =
|
|
876
|
-
|
|
877
|
-
value: ''
|
|
878
|
-
};
|
|
879
|
-
this.list.push(this.node);
|
|
1010
|
+
if (this.node < 0) {
|
|
1011
|
+
this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) |
|
|
1012
|
+
(this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
|
880
1013
|
}
|
|
881
1014
|
if (chr === '\n') {
|
|
882
1015
|
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
|
883
1016
|
// go together anyway and there already is a WS for \n. Lone \r means something is fishy.
|
|
884
1017
|
chr = ' ';
|
|
1018
|
+
this.list.slots[this.node * 3] |= TOKEN_DIRTY;
|
|
885
1019
|
}
|
|
886
|
-
if (chr.charCodeAt(0) >= 0x21 ||
|
|
887
|
-
//
|
|
888
|
-
this.node.value += chr;
|
|
1020
|
+
if (chr.charCodeAt(0) >= 0x21 || chr === ' ' || chr === '\t') {
|
|
1021
|
+
// the character is part of the value, which is read off the field later
|
|
889
1022
|
// text inside a quoted string or a comment is not part of an addr-spec. Tracked
|
|
890
1023
|
// as it goes rather than read back off the value (see lastChars in _handleAddress)
|
|
891
1024
|
if (!this.operatorExpecting && chr !== ' ' && chr !== '\t') {
|
|
892
1025
|
this.afterAt = chr === '@';
|
|
893
1026
|
}
|
|
894
1027
|
}
|
|
1028
|
+
else {
|
|
1029
|
+
// skip command bytes
|
|
1030
|
+
this.list.slots[this.node * 3] |= TOKEN_DIRTY;
|
|
1031
|
+
}
|
|
895
1032
|
this.escaped = false;
|
|
896
1033
|
}
|
|
897
1034
|
/**
|
|
@@ -967,6 +1104,7 @@ class Tokenizer {
|
|
|
967
1104
|
this.innerEscaped = false;
|
|
968
1105
|
}
|
|
969
1106
|
}
|
|
1107
|
+
/* eslint-enable no-bitwise */
|
|
970
1108
|
/**
|
|
971
1109
|
* Maximum recursion depth for parsing nested groups.
|
|
972
1110
|
* RFC 5322 doesn't allow nested groups, so this is a safeguard against
|
|
@@ -993,31 +1131,28 @@ function _parseAddressList(str, depth) {
|
|
|
993
1131
|
}
|
|
994
1132
|
const tokenizer = new Tokenizer(str);
|
|
995
1133
|
const tokens = tokenizer.tokenize();
|
|
996
|
-
const addresses = [];
|
|
997
|
-
let address = [];
|
|
998
1134
|
const parsedAddresses = [];
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1135
|
+
// Every address is handed over as the range of the token list it covers, as soon as the
|
|
1136
|
+
// separator after it is seen. Copying the tokens of each address into an array of its own,
|
|
1137
|
+
// and collecting those before handling any, held a second structure as large as the token
|
|
1138
|
+
// list for the whole parse, and an array that receives a single push is allocated room for
|
|
1139
|
+
// seventeen: a list of one letter names took some 80 times its size in that alone
|
|
1140
|
+
let from = 0;
|
|
1141
|
+
for (let i = 0, len = tokens.length; i <= len; i++) {
|
|
1142
|
+
const operator = i < len ? tokens.operatorAt(i) : '';
|
|
1143
|
+
if (i < len && operator !== ',' && operator !== ';') {
|
|
1144
|
+
continue;
|
|
1005
1145
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1146
|
+
if (i > from) {
|
|
1147
|
+
const handled = _handleAddress(tokens, from, i, depth);
|
|
1148
|
+
// Appended in place. Rebuilding the accumulator with concat() would copy every
|
|
1149
|
+
// entry collected so far on each address, making a flat list cost O(n^2).
|
|
1150
|
+
for (let j = 0; j < handled.length; j++) {
|
|
1151
|
+
parsedAddresses.push(handled[j]);
|
|
1152
|
+
}
|
|
1008
1153
|
}
|
|
1009
|
-
|
|
1010
|
-
if (address.length) {
|
|
1011
|
-
addresses.push(address);
|
|
1154
|
+
from = i + 1;
|
|
1012
1155
|
}
|
|
1013
|
-
addresses.forEach(addr => {
|
|
1014
|
-
const handled = _handleAddress(addr, depth);
|
|
1015
|
-
// Appended in place. Rebuilding the accumulator with concat() would copy every
|
|
1016
|
-
// entry collected so far on each address, making a flat list cost O(n^2).
|
|
1017
|
-
for (let i = 0; i < handled.length; i++) {
|
|
1018
|
-
parsedAddresses.push(handled[i]);
|
|
1019
|
-
}
|
|
1020
|
-
});
|
|
1021
1156
|
// Merge fragments produced when unquoted display names contain commas.
|
|
1022
1157
|
// "Joe Foo, PhD <joe@example.com>" is split on the comma into
|
|
1023
1158
|
// [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}].
|
package/dist/cjs/package-info.js
CHANGED
|
@@ -1,3 +1,81 @@
|
|
|
1
|
+
/* eslint-disable no-bitwise -- the kind of a token is a set of bit flags packed into one slot of a typed array */
|
|
2
|
+
/** The token is an operator, a text run otherwise */
|
|
3
|
+
const TOKEN_OPERATOR = 1;
|
|
4
|
+
/** The next character continues the same run without whitespace in between */
|
|
5
|
+
const TOKEN_NO_BREAK = 2;
|
|
6
|
+
/** The text run holds characters its value leaves out or changes, so it is no plain slice */
|
|
7
|
+
const TOKEN_DIRTY = 4;
|
|
8
|
+
/** A backslash escapes the next character in the text run, inside a quoted string or a comment */
|
|
9
|
+
const TOKEN_ESCAPES = 8;
|
|
10
|
+
/** The text run opens on a character an escaping backslash in front of it has escaped */
|
|
11
|
+
const TOKEN_STARTS_ESCAPED = 16;
|
|
12
|
+
/**
|
|
13
|
+
* The tokens of an address field, kept as offsets into it.
|
|
14
|
+
*
|
|
15
|
+
* Every token used to be an object of its own, an operator for every ',' and a value string
|
|
16
|
+
* for every run of text, and a list of one letter names, 'a,a,a,...', took some 65 times its
|
|
17
|
+
* size in tokens before any address was read. A token is now three numbers in one typed
|
|
18
|
+
* array, its kind and the range of the field it covers, and its value is sliced off the field
|
|
19
|
+
* when it is read. The few text runs whose value differs from their slice, as they hold an
|
|
20
|
+
* escaping backslash, a line break or a control character, are rebuilt once and kept aside.
|
|
21
|
+
*/
|
|
22
|
+
class TokenList {
|
|
23
|
+
constructor(str) {
|
|
24
|
+
this.str = str;
|
|
25
|
+
this.slots = new Int32Array(48);
|
|
26
|
+
this.length = 0;
|
|
27
|
+
this.cleaned = new Map();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Appends a token
|
|
31
|
+
*
|
|
32
|
+
* @param kind Kind and flags of the token
|
|
33
|
+
* @param start Offset of its first character
|
|
34
|
+
* @param end Offset just past its last character
|
|
35
|
+
* @return Index of the token
|
|
36
|
+
*/
|
|
37
|
+
push(kind, start, end) {
|
|
38
|
+
if ((this.length + 1) * 3 > this.slots.length) {
|
|
39
|
+
const grown = new Int32Array(this.slots.length * 2);
|
|
40
|
+
grown.set(this.slots);
|
|
41
|
+
this.slots = grown;
|
|
42
|
+
}
|
|
43
|
+
const at = this.length * 3;
|
|
44
|
+
this.slots[at] = kind;
|
|
45
|
+
this.slots[at + 1] = start;
|
|
46
|
+
this.slots[at + 2] = end;
|
|
47
|
+
return this.length++;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The character of an operator token, an empty string for a text run
|
|
51
|
+
*/
|
|
52
|
+
operatorAt(index) {
|
|
53
|
+
const at = index * 3;
|
|
54
|
+
return this.slots[at] & TOKEN_OPERATOR ? this.str.charAt(this.slots[at + 1]) : '';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether a token is an operator
|
|
58
|
+
*/
|
|
59
|
+
isOperator(index) {
|
|
60
|
+
return (this.slots[index * 3] & TOKEN_OPERATOR) !== 0;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether the next character continues the run of a token without whitespace in between
|
|
64
|
+
*/
|
|
65
|
+
noBreak(index) {
|
|
66
|
+
return (this.slots[index * 3] & TOKEN_NO_BREAK) !== 0;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Value of a token
|
|
70
|
+
*/
|
|
71
|
+
value(index) {
|
|
72
|
+
const cleaned = this.cleaned.get(index);
|
|
73
|
+
if (cleaned !== undefined) {
|
|
74
|
+
return cleaned;
|
|
75
|
+
}
|
|
76
|
+
return this.str.slice(this.slots[index * 3 + 1], this.slots[index * 3 + 2]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
1
79
|
/**
|
|
2
80
|
* A domain literal that ends an address. dtext includes '@' (RFC 5322 3.4.1), so the '@'
|
|
3
81
|
* in front of the literal is the one that splits the domain off, not the last one: reading
|
|
@@ -454,11 +532,13 @@ function _stripAddressComments(address) {
|
|
|
454
532
|
/**
|
|
455
533
|
* Converts tokens for a single address into an address object
|
|
456
534
|
*
|
|
457
|
-
* @param tokens Tokens
|
|
535
|
+
* @param tokens Tokens of the whole address list
|
|
536
|
+
* @param from Index of the first token of this address
|
|
537
|
+
* @param to Index just past the last token of this address
|
|
458
538
|
* @param depth Current recursion depth for nested group protection
|
|
459
539
|
* @return Address object
|
|
460
540
|
*/
|
|
461
|
-
function _handleAddress(tokens, depth) {
|
|
541
|
+
function _handleAddress(tokens, from, to, depth) {
|
|
462
542
|
let isGroup = false;
|
|
463
543
|
let state = 'text';
|
|
464
544
|
const addresses = [];
|
|
@@ -481,11 +561,13 @@ function _handleAddress(tokens, depth) {
|
|
|
481
561
|
// character is carried along instead of re-read.
|
|
482
562
|
const lastChars = { address: '', comment: '', group: '', text: '' };
|
|
483
563
|
// Filter out <addresses>, (comments) and regular text
|
|
484
|
-
for (let i =
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
564
|
+
for (let i = from; i < to; i++) {
|
|
565
|
+
const value = tokens.value(i);
|
|
566
|
+
// the operator in front of this token, if there is one, and whether it runs on into it
|
|
567
|
+
const prevOperator = i > from ? tokens.operatorAt(i - 1) : '';
|
|
568
|
+
const prevNoBreak = i > from && tokens.noBreak(i - 1);
|
|
569
|
+
if (tokens.isOperator(i)) {
|
|
570
|
+
switch (value) {
|
|
489
571
|
case '<':
|
|
490
572
|
state = 'address';
|
|
491
573
|
insideQuotes = false;
|
|
@@ -509,7 +591,7 @@ function _handleAddress(tokens, depth) {
|
|
|
509
591
|
break;
|
|
510
592
|
}
|
|
511
593
|
}
|
|
512
|
-
else
|
|
594
|
+
else {
|
|
513
595
|
// An empty quoted string is dropped by the tokenizer, leaving no text token of its
|
|
514
596
|
// own for textWasQuoted to be recorded on, so the pair of quote operators right in
|
|
515
597
|
// front of this token is all that is left of it and the run it opens carries the
|
|
@@ -517,44 +599,34 @@ function _handleAddress(tokens, depth) {
|
|
|
517
599
|
// the quotes never go back on, and the value is no longer an addr-spec a trailing
|
|
518
600
|
// comment can be peeled off of. It only ever opens a run: a run that already holds
|
|
519
601
|
// material collected outside the quotes is not a quoted string, whatever follows it
|
|
520
|
-
const
|
|
521
|
-
const opensAfterEmptyQuotedString = prevToken?.type === 'operator' &&
|
|
522
|
-
prevToken.value === '"' &&
|
|
523
|
-
!!prevToken.noBreak &&
|
|
524
|
-
prevPrevToken?.type === 'operator' &&
|
|
525
|
-
prevPrevToken.value === '"';
|
|
602
|
+
const opensAfterEmptyQuotedString = prevOperator === '"' && prevNoBreak && i > from + 1 && tokens.operatorAt(i - 2) === '"';
|
|
526
603
|
// A comment is folding whitespace. It may sit inside an addr-spec, on either side
|
|
527
604
|
// of the '@', but it cannot join two atoms into one: gluing across it would read
|
|
528
605
|
// 'user@example.com(x)evil.com' as the single domain 'example.comevil.com' and
|
|
529
606
|
// deliver to a domain the sender never named.
|
|
530
607
|
const parts = data[state];
|
|
531
|
-
const joins =
|
|
532
|
-
prevToken.noBreak &&
|
|
533
|
-
parts.length &&
|
|
534
|
-
(prevToken.value !== ')' || lastChars[state] === '@' || token.value.charAt(0) === '@');
|
|
608
|
+
const joins = prevNoBreak && parts.length && (prevOperator !== ')' || lastChars[state] === '@' || value.charAt(0) === '@');
|
|
535
609
|
if (joins) {
|
|
536
610
|
const last = data[state].length - 1;
|
|
537
611
|
if (state === 'text' && !insideQuotes) {
|
|
538
|
-
if (lastTextPureQuoted &&
|
|
612
|
+
if (lastTextPureQuoted && value.charAt(0) === '@') {
|
|
539
613
|
data.quotedLocalAt[last] = data.text[last].length;
|
|
540
614
|
}
|
|
541
615
|
lastTextPureQuoted = false;
|
|
542
616
|
}
|
|
543
|
-
data[state][last] +=
|
|
544
|
-
|
|
545
|
-
lastChars[state] = token.value.charAt(token.value.length - 1);
|
|
546
|
-
}
|
|
617
|
+
data[state][last] += value;
|
|
618
|
+
lastChars[state] = value.charAt(value.length - 1);
|
|
547
619
|
if (state === 'text' && insideQuotes) {
|
|
548
620
|
data.textWasQuoted[last] = true;
|
|
549
621
|
}
|
|
550
622
|
}
|
|
551
623
|
else {
|
|
552
|
-
data[state].push(
|
|
553
|
-
lastChars[state] =
|
|
624
|
+
data[state].push(value);
|
|
625
|
+
lastChars[state] = value.charAt(value.length - 1);
|
|
554
626
|
if (state === 'text') {
|
|
555
627
|
data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
|
556
628
|
// an empty quoted string leaves no text of its own, so the '@' opens the run
|
|
557
|
-
data.quotedLocalAt.push(opensAfterEmptyQuotedString &&
|
|
629
|
+
data.quotedLocalAt.push(opensAfterEmptyQuotedString && value.charAt(0) === '@' ? 0 : -1);
|
|
558
630
|
lastTextPureQuoted = insideQuotes;
|
|
559
631
|
}
|
|
560
632
|
}
|
|
@@ -717,6 +789,7 @@ function _handleAddress(tokens, depth) {
|
|
|
717
789
|
}
|
|
718
790
|
return addresses;
|
|
719
791
|
}
|
|
792
|
+
/* eslint-disable no-bitwise -- see TokenList */
|
|
720
793
|
/**
|
|
721
794
|
* Creates a Tokenizer object for tokenizing address field strings
|
|
722
795
|
*
|
|
@@ -728,7 +801,7 @@ class Tokenizer {
|
|
|
728
801
|
this.str = (str || '').toString();
|
|
729
802
|
this.operatorCurrent = '';
|
|
730
803
|
this.operatorExpecting = '';
|
|
731
|
-
this.node =
|
|
804
|
+
this.node = -1;
|
|
732
805
|
this.escaped = false;
|
|
733
806
|
this.inDomainLiteral = false;
|
|
734
807
|
this.afterAt = false;
|
|
@@ -736,7 +809,7 @@ class Tokenizer {
|
|
|
736
809
|
this.innerComment = 0;
|
|
737
810
|
this.innerLiteral = false;
|
|
738
811
|
this.innerEscaped = false;
|
|
739
|
-
this.list =
|
|
812
|
+
this.list = new TokenList(this.str);
|
|
740
813
|
/**
|
|
741
814
|
* Operator tokens and which tokens are expected to end the sequence
|
|
742
815
|
*/
|
|
@@ -761,27 +834,96 @@ class Tokenizer {
|
|
|
761
834
|
* @return An array of operator|text tokens
|
|
762
835
|
*/
|
|
763
836
|
tokenize() {
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
this.checkChar(chr, nextChr);
|
|
837
|
+
const str = this.str;
|
|
838
|
+
const len = str.length;
|
|
839
|
+
for (let i = 0; i < len; i++) {
|
|
840
|
+
this.checkChar(str.charAt(i), i);
|
|
769
841
|
}
|
|
770
|
-
this.
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
842
|
+
this._closeText(len);
|
|
843
|
+
// Compacted in place: a text run is trimmed to its value and one left empty is
|
|
844
|
+
// dropped, and the value of a run that is no plain slice is rebuilt and kept aside
|
|
845
|
+
const list = this.list;
|
|
846
|
+
const slots = list.slots;
|
|
847
|
+
let kept = 0;
|
|
848
|
+
for (let i = 0, count = list.length; i < count; i++) {
|
|
849
|
+
const kind = slots[i * 3];
|
|
850
|
+
let start = slots[i * 3 + 1];
|
|
851
|
+
let end = slots[i * 3 + 2];
|
|
852
|
+
if (kind & TOKEN_DIRTY) {
|
|
853
|
+
const value = this._cleanValue(start, end, kind).trim();
|
|
854
|
+
if (!value) {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
list.cleaned.set(kept, value);
|
|
774
858
|
}
|
|
775
|
-
|
|
859
|
+
else if (!(kind & TOKEN_OPERATOR)) {
|
|
860
|
+
while (start < end && _isSpaceCode(str.charCodeAt(start))) {
|
|
861
|
+
start++;
|
|
862
|
+
}
|
|
863
|
+
while (end > start && _isSpaceCode(str.charCodeAt(end - 1))) {
|
|
864
|
+
end--;
|
|
865
|
+
}
|
|
866
|
+
if (start === end) {
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
slots[kept * 3] = kind;
|
|
871
|
+
slots[kept * 3 + 1] = start;
|
|
872
|
+
slots[kept * 3 + 2] = end;
|
|
873
|
+
kept++;
|
|
874
|
+
}
|
|
875
|
+
list.length = kept;
|
|
776
876
|
return list;
|
|
777
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Ends the text run being collected, if there is one
|
|
880
|
+
*
|
|
881
|
+
* @param end Offset just past its last character
|
|
882
|
+
*/
|
|
883
|
+
_closeText(end) {
|
|
884
|
+
if (this.node >= 0) {
|
|
885
|
+
this.list.slots[this.node * 3 + 2] = end;
|
|
886
|
+
this.node = -1;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Rebuilds the value of a text run that is no plain slice of the field, applying the
|
|
891
|
+
* rules checkChar collects characters by: an escaping backslash is dropped, a line break
|
|
892
|
+
* reads as a space and any other control character is left out
|
|
893
|
+
*
|
|
894
|
+
* @param start Offset of the first character of the run
|
|
895
|
+
* @param end Offset just past its last character
|
|
896
|
+
* @param kind Kind and flags of the run
|
|
897
|
+
* @return Value of the run
|
|
898
|
+
*/
|
|
899
|
+
_cleanValue(start, end, kind) {
|
|
900
|
+
const str = this.str;
|
|
901
|
+
const escapes = (kind & TOKEN_ESCAPES) !== 0;
|
|
902
|
+
let escaped = (kind & TOKEN_STARTS_ESCAPED) !== 0;
|
|
903
|
+
let value = '';
|
|
904
|
+
for (let i = start; i < end; i++) {
|
|
905
|
+
const chr = str.charAt(i);
|
|
906
|
+
if (escapes && !escaped && chr === '\\') {
|
|
907
|
+
escaped = true;
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
escaped = false;
|
|
911
|
+
if (chr === '\n') {
|
|
912
|
+
value += ' ';
|
|
913
|
+
}
|
|
914
|
+
else if (chr.charCodeAt(0) >= 0x21 || chr === ' ' || chr === '\t') {
|
|
915
|
+
value += chr;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return value;
|
|
919
|
+
}
|
|
778
920
|
/**
|
|
779
921
|
* Checks if a character is an operator or text and acts accordingly
|
|
780
922
|
*
|
|
781
923
|
* @param chr Character from the address field
|
|
782
|
-
* @param
|
|
924
|
+
* @param i Offset of the character in the field
|
|
783
925
|
*/
|
|
784
|
-
checkChar(chr,
|
|
926
|
+
checkChar(chr, i) {
|
|
785
927
|
// Track RFC 5322 domain-literals ("[" *dtext "]"). The ":" of an IPv6
|
|
786
928
|
// address-literal (user@[IPv6:2001:db8::1]) is dtext and must not be treated as
|
|
787
929
|
// the group delimiter while inside the brackets. That is the only operator the
|
|
@@ -828,15 +970,9 @@ class Tokenizer {
|
|
|
828
970
|
// ignore next condition blocks
|
|
829
971
|
}
|
|
830
972
|
else if (chr === this.operatorExpecting && !nested) {
|
|
831
|
-
this.
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
};
|
|
835
|
-
if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
|
|
836
|
-
this.node.noBreak = true;
|
|
837
|
-
}
|
|
838
|
-
this.list.push(this.node);
|
|
839
|
-
this.node = null;
|
|
973
|
+
const nextChr = this.str.charAt(i + 1);
|
|
974
|
+
this._closeText(i);
|
|
975
|
+
this.list.push(nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr) ? TOKEN_OPERATOR | TOKEN_NO_BREAK : TOKEN_OPERATOR, i, i + 1);
|
|
840
976
|
if (chr !== ')') {
|
|
841
977
|
// a comment is folding whitespace, it does not part the "@" from the "["
|
|
842
978
|
this.afterAt = false;
|
|
@@ -847,12 +983,8 @@ class Tokenizer {
|
|
|
847
983
|
return;
|
|
848
984
|
}
|
|
849
985
|
else if (!this.operatorExpecting && !(this.inDomainLiteral && chr === ':') && chr in this.operators) {
|
|
850
|
-
this.
|
|
851
|
-
|
|
852
|
-
value: chr
|
|
853
|
-
};
|
|
854
|
-
this.list.push(this.node);
|
|
855
|
-
this.node = null;
|
|
986
|
+
this._closeText(i);
|
|
987
|
+
this.list.push(TOKEN_OPERATOR, i, i + 1);
|
|
856
988
|
if (chr !== '(') {
|
|
857
989
|
this.afterAt = false;
|
|
858
990
|
}
|
|
@@ -867,29 +999,34 @@ class Tokenizer {
|
|
|
867
999
|
// only in quoted strings let an escaped parenthesis close the comment, which
|
|
868
1000
|
// released the rest of it, an address included, into the header
|
|
869
1001
|
this.escaped = true;
|
|
1002
|
+
if (this.node >= 0) {
|
|
1003
|
+
// the backslash is left out of the value
|
|
1004
|
+
this.list.slots[this.node * 3] |= TOKEN_DIRTY;
|
|
1005
|
+
}
|
|
870
1006
|
return;
|
|
871
1007
|
}
|
|
872
|
-
if (
|
|
873
|
-
this.node =
|
|
874
|
-
|
|
875
|
-
value: ''
|
|
876
|
-
};
|
|
877
|
-
this.list.push(this.node);
|
|
1008
|
+
if (this.node < 0) {
|
|
1009
|
+
this.node = this.list.push((this.operatorExpecting === '"' || this.operatorExpecting === ')' ? TOKEN_ESCAPES : 0) |
|
|
1010
|
+
(this.escaped ? TOKEN_STARTS_ESCAPED : 0), i, i);
|
|
878
1011
|
}
|
|
879
1012
|
if (chr === '\n') {
|
|
880
1013
|
// Convert newlines to spaces. Carriage return is ignored as \r and \n usually
|
|
881
1014
|
// go together anyway and there already is a WS for \n. Lone \r means something is fishy.
|
|
882
1015
|
chr = ' ';
|
|
1016
|
+
this.list.slots[this.node * 3] |= TOKEN_DIRTY;
|
|
883
1017
|
}
|
|
884
|
-
if (chr.charCodeAt(0) >= 0x21 ||
|
|
885
|
-
//
|
|
886
|
-
this.node.value += chr;
|
|
1018
|
+
if (chr.charCodeAt(0) >= 0x21 || chr === ' ' || chr === '\t') {
|
|
1019
|
+
// the character is part of the value, which is read off the field later
|
|
887
1020
|
// text inside a quoted string or a comment is not part of an addr-spec. Tracked
|
|
888
1021
|
// as it goes rather than read back off the value (see lastChars in _handleAddress)
|
|
889
1022
|
if (!this.operatorExpecting && chr !== ' ' && chr !== '\t') {
|
|
890
1023
|
this.afterAt = chr === '@';
|
|
891
1024
|
}
|
|
892
1025
|
}
|
|
1026
|
+
else {
|
|
1027
|
+
// skip command bytes
|
|
1028
|
+
this.list.slots[this.node * 3] |= TOKEN_DIRTY;
|
|
1029
|
+
}
|
|
893
1030
|
this.escaped = false;
|
|
894
1031
|
}
|
|
895
1032
|
/**
|
|
@@ -965,6 +1102,7 @@ class Tokenizer {
|
|
|
965
1102
|
this.innerEscaped = false;
|
|
966
1103
|
}
|
|
967
1104
|
}
|
|
1105
|
+
/* eslint-enable no-bitwise */
|
|
968
1106
|
/**
|
|
969
1107
|
* Maximum recursion depth for parsing nested groups.
|
|
970
1108
|
* RFC 5322 doesn't allow nested groups, so this is a safeguard against
|
|
@@ -991,31 +1129,28 @@ function _parseAddressList(str, depth) {
|
|
|
991
1129
|
}
|
|
992
1130
|
const tokenizer = new Tokenizer(str);
|
|
993
1131
|
const tokens = tokenizer.tokenize();
|
|
994
|
-
const addresses = [];
|
|
995
|
-
let address = [];
|
|
996
1132
|
const parsedAddresses = [];
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1133
|
+
// Every address is handed over as the range of the token list it covers, as soon as the
|
|
1134
|
+
// separator after it is seen. Copying the tokens of each address into an array of its own,
|
|
1135
|
+
// and collecting those before handling any, held a second structure as large as the token
|
|
1136
|
+
// list for the whole parse, and an array that receives a single push is allocated room for
|
|
1137
|
+
// seventeen: a list of one letter names took some 80 times its size in that alone
|
|
1138
|
+
let from = 0;
|
|
1139
|
+
for (let i = 0, len = tokens.length; i <= len; i++) {
|
|
1140
|
+
const operator = i < len ? tokens.operatorAt(i) : '';
|
|
1141
|
+
if (i < len && operator !== ',' && operator !== ';') {
|
|
1142
|
+
continue;
|
|
1003
1143
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1144
|
+
if (i > from) {
|
|
1145
|
+
const handled = _handleAddress(tokens, from, i, depth);
|
|
1146
|
+
// Appended in place. Rebuilding the accumulator with concat() would copy every
|
|
1147
|
+
// entry collected so far on each address, making a flat list cost O(n^2).
|
|
1148
|
+
for (let j = 0; j < handled.length; j++) {
|
|
1149
|
+
parsedAddresses.push(handled[j]);
|
|
1150
|
+
}
|
|
1006
1151
|
}
|
|
1007
|
-
|
|
1008
|
-
if (address.length) {
|
|
1009
|
-
addresses.push(address);
|
|
1152
|
+
from = i + 1;
|
|
1010
1153
|
}
|
|
1011
|
-
addresses.forEach(addr => {
|
|
1012
|
-
const handled = _handleAddress(addr, depth);
|
|
1013
|
-
// Appended in place. Rebuilding the accumulator with concat() would copy every
|
|
1014
|
-
// entry collected so far on each address, making a flat list cost O(n^2).
|
|
1015
|
-
for (let i = 0; i < handled.length; i++) {
|
|
1016
|
-
parsedAddresses.push(handled[i]);
|
|
1017
|
-
}
|
|
1018
|
-
});
|
|
1019
1154
|
// Merge fragments produced when unquoted display names contain commas.
|
|
1020
1155
|
// "Joe Foo, PhD <joe@example.com>" is split on the comma into
|
|
1021
1156
|
// [{name:"Joe Foo", address:""}, {name:"PhD", address:"joe@example.com"}].
|
package/dist/esm/package-info.js
CHANGED