formio/node_modules/isemail/lib/index.js   F
last analyzed

Complexity

Total Complexity 215
Complexity/F 14.33

Size

Lines of Code 1333
Function Count 15

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 0
wmc 215
nc 0
mnd 15
bc 123
fnc 15
dl 0
loc 1333
rs 0.6314
bpm 8.2
cpm 14.3333
noi 1
c 0
b 0
f 0

12 Functions

Rating   Name   Duplication   Size   Complexity  
A internals.specials 0 4 1
A internals.c0Controls 0 4 1
A internals.specials.constructor 0 15 2
F internals.validate 0 1085 191
A internals.validate.diagnoses.constructor 0 11 2
A internals.validDomain 0 16 4
A internals.c1Controls.constructor 0 16 2
A internals.normalize 0 11 3
A internals.nulNormalize 0 10 1
A internals.c0Controls.constructor 0 16 2
A internals.c1Controls 0 4 1
A internals.checkIpV6 0 4 1

How to fix   Complexity   

Complexity

Complex classes like formio/node_modules/isemail/lib/index.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
'use strict';
2
3
// Load modules
4
5
const Punycode = require('punycode');
6
7
// Declare internals
8
9
const internals = {
10
    hasOwn: Object.prototype.hasOwnProperty,
11
    indexOf: Array.prototype.indexOf,
12
    defaultThreshold: 16,
13
    maxIPv6Groups: 8,
14
15
    categories: {
16
        valid: 1,
17
        dnsWarn: 7,
18
        rfc5321: 15,
19
        cfws: 31,
20
        deprecated: 63,
21
        rfc5322: 127,
22
        error: 255
23
    },
24
25
    diagnoses: {
26
27
        // Address is valid
28
29
        valid: 0,
30
31
        // Address is valid for SMTP but has unusual elements
32
33
        rfc5321TLD: 9,
34
        rfc5321TLDNumeric: 10,
35
        rfc5321QuotedString: 11,
36
        rfc5321AddressLiteral: 12,
37
38
        // Address is valid for message, but must be modified for envelope
39
40
        cfwsComment: 17,
41
        cfwsFWS: 18,
42
43
        // Address contains deprecated elements, but may still be valid in some contexts
44
45
        deprecatedLocalPart: 33,
46
        deprecatedFWS: 34,
47
        deprecatedQTEXT: 35,
48
        deprecatedQP: 36,
49
        deprecatedComment: 37,
50
        deprecatedCTEXT: 38,
51
        deprecatedIPv6: 39,
52
        deprecatedCFWSNearAt: 49,
53
54
        // Address is only valid according to broad definition in RFC 5322, but is otherwise invalid
55
56
        rfc5322Domain: 65,
57
        rfc5322TooLong: 66,
58
        rfc5322LocalTooLong: 67,
59
        rfc5322DomainTooLong: 68,
60
        rfc5322LabelTooLong: 69,
61
        rfc5322DomainLiteral: 70,
62
        rfc5322DomainLiteralOBSDText: 71,
63
        rfc5322IPv6GroupCount: 72,
64
        rfc5322IPv62x2xColon: 73,
65
        rfc5322IPv6BadCharacter: 74,
66
        rfc5322IPv6MaxGroups: 75,
67
        rfc5322IPv6ColonStart: 76,
68
        rfc5322IPv6ColonEnd: 77,
69
70
        // Address is invalid for any purpose
71
72
        errExpectingDTEXT: 129,
73
        errNoLocalPart: 130,
74
        errNoDomain: 131,
75
        errConsecutiveDots: 132,
76
        errATEXTAfterCFWS: 133,
77
        errATEXTAfterQS: 134,
78
        errATEXTAfterDomainLiteral: 135,
79
        errExpectingQPair: 136,
80
        errExpectingATEXT: 137,
81
        errExpectingQTEXT: 138,
82
        errExpectingCTEXT: 139,
83
        errBackslashEnd: 140,
84
        errDotStart: 141,
85
        errDotEnd: 142,
86
        errDomainHyphenStart: 143,
87
        errDomainHyphenEnd: 144,
88
        errUnclosedQuotedString: 145,
89
        errUnclosedComment: 146,
90
        errUnclosedDomainLiteral: 147,
91
        errFWSCRLFx2: 148,
92
        errFWSCRLFEnd: 149,
93
        errCRNoLF: 150,
94
        errUnknownTLD: 160,
95
        errDomainTooShort: 161
96
    },
97
98
    components: {
99
        localpart: 0,
100
        domain: 1,
101
        literal: 2,
102
        contextComment: 3,
103
        contextFWS: 4,
104
        contextQuotedString: 5,
105
        contextQuotedPair: 6
106
    }
107
};
108
109
110
internals.specials = function () {
111
112
    const specials = '()<>[]:;@\\,."';        // US-ASCII visible characters not valid for atext (http://tools.ietf.org/html/rfc5322#section-3.2.3)
113
    const lookup = new Array(0x100);
114
    lookup.fill(false);
115
116
    for (let i = 0; i < specials.length; ++i) {
117
        lookup[specials.codePointAt(i)] = true;
118
    }
119
120
    return function (code) {
121
122
        return lookup[code];
123
    };
124
}();
125
126
internals.c0Controls = function () {
127
128
    const lookup = new Array(0x100);
129
    lookup.fill(false);
130
131
    // add C0 control characters
132
133
    for (let i = 0; i < 33; ++i) {
134
        lookup[i] = true;
135
    }
136
137
    return function (code) {
138
139
        return lookup[code];
140
    };
141
}();
142
143
internals.c1Controls = function () {
144
145
    const lookup = new Array(0x100);
146
    lookup.fill(false);
147
148
    // add C1 control characters
149
150
    for (let i = 127; i < 160; ++i) {
151
        lookup[i] = true;
152
    }
153
154
    return function (code) {
155
156
        return lookup[code];
157
    };
158
}();
159
160
internals.regex = {
161
    ipV4: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
162
    ipV6: /^[a-fA-F\d]{0,4}$/
163
};
164
165
// $lab:coverage:off$
166
internals.nulNormalize = function (email) {
167
168
    let emailPieces = email.split('\u0000');
169
    emailPieces = emailPieces.map((string) => {
170
171
        return string.normalize('NFC');
172
    });
173
174
    return emailPieces.join('\u0000');
175
};
176
// $lab:coverage:on$
177
178
179
internals.checkIpV6 = function (items) {
180
181
    return items.every((value) => internals.regex.ipV6.test(value));
182
};
183
184
185
internals.validDomain = function (tldAtom, options) {
186
187
    if (options.tldBlacklist) {
188
        if (Array.isArray(options.tldBlacklist)) {
189
            return internals.indexOf.call(options.tldBlacklist, tldAtom) === -1;
190
        }
191
192
        return !internals.hasOwn.call(options.tldBlacklist, tldAtom);
193
    }
194
195
    if (Array.isArray(options.tldWhitelist)) {
196
        return internals.indexOf.call(options.tldWhitelist, tldAtom) !== -1;
197
    }
198
199
    return internals.hasOwn.call(options.tldWhitelist, tldAtom);
200
};
201
202
203
/**
204
 * Check that an email address conforms to RFCs 5321, 5322, 6530 and others
205
 *
206
 * We distinguish clearly between a Mailbox as defined by RFC 5321 and an
207
 * addr-spec as defined by RFC 5322. Depending on the context, either can be
208
 * regarded as a valid email address. The RFC 5321 Mailbox specification is
209
 * more restrictive (comments, white space and obsolete forms are not allowed).
210
 *
211
 * @param {string} email The email address to check. See README for specifics.
212
 * @param {Object} options The (optional) options:
213
 *   {*} errorLevel Determines the boundary between valid and invalid
214
 *     addresses.
215
 *   {*} tldBlacklist The set of domains to consider invalid.
216
 *   {*} tldWhitelist The set of domains to consider valid.
217
 *   {*} minDomainAtoms The minimum number of domain atoms which must be present
218
 *     for the address to be valid.
219
 * @param {function(number|boolean)} callback The (optional) callback handler.
220
 * @return {*}
221
 */
222
223
exports.validate = internals.validate = function (email, options, callback) {
224
225
    options = options || {};
226
    email = internals.normalize(email);
227
228
    if (typeof options === 'function') {
229
        callback = options;
230
        options = {};
231
    }
232
233
    if (typeof callback !== 'function') {
234
        callback = null;
235
    }
236
237
    let diagnose;
238
    let threshold;
239
240
    if (typeof options.errorLevel === 'number') {
241
        diagnose = true;
242
        threshold = options.errorLevel;
243
    }
244
    else {
245
        diagnose = !!options.errorLevel;
246
        threshold = internals.diagnoses.valid;
247
    }
248
249
    if (options.tldWhitelist) {
250
        if (typeof options.tldWhitelist === 'string') {
251
            options.tldWhitelist = [options.tldWhitelist];
252
        }
253
        else if (typeof options.tldWhitelist !== 'object') {
254
            throw new TypeError('expected array or object tldWhitelist');
255
        }
256
    }
257
258
    if (options.tldBlacklist) {
259
        if (typeof options.tldBlacklist === 'string') {
260
            options.tldBlacklist = [options.tldBlacklist];
261
        }
262
        else if (typeof options.tldBlacklist !== 'object') {
263
            throw new TypeError('expected array or object tldBlacklist');
264
        }
265
    }
266
267
    if (options.minDomainAtoms && (options.minDomainAtoms !== ((+options.minDomainAtoms) | 0) || options.minDomainAtoms < 0)) {
268
        throw new TypeError('expected positive integer minDomainAtoms');
269
    }
270
271
    let maxResult = internals.diagnoses.valid;
272
    const updateResult = (value) => {
273
274
        if (value > maxResult) {
275
            maxResult = value;
276
        }
277
    };
278
279
    const context = {
280
        now: internals.components.localpart,
281
        prev: internals.components.localpart,
282
        stack: [internals.components.localpart]
283
    };
284
285
    let prevToken = '';
286
287
    const parseData = {
288
        local: '',
289
        domain: ''
290
    };
291
    const atomData = {
292
        locals: [''],
293
        domains: ['']
294
    };
295
296
    let elementCount = 0;
297
    let elementLength = 0;
298
    let crlfCount = 0;
299
    let charCode;
300
301
    let hyphenFlag = false;
302
    let assertEnd = false;
303
304
    const emailLength = email.length;
305
306
    let token;                                      // Token is used outside the loop, must declare similarly
307
    for (let i = 0; i < emailLength; i += token.length) {
308
        // Utilize codepoints to account for Unicode surrogate pairs
309
        token = String.fromCodePoint(email.codePointAt(i));
310
311
        switch (context.now) {
312
            // Local-part
313
            case internals.components.localpart:
314
                // http://tools.ietf.org/html/rfc5322#section-3.4.1
315
                //   local-part      =   dot-atom / quoted-string / obs-local-part
316
                //
317
                //   dot-atom        =   [CFWS] dot-atom-text [CFWS]
318
                //
319
                //   dot-atom-text   =   1*atext *("." 1*atext)
320
                //
321
                //   quoted-string   =   [CFWS]
322
                //                       DQUOTE *([FWS] qcontent) [FWS] DQUOTE
323
                //                       [CFWS]
324
                //
325
                //   obs-local-part  =   word *("." word)
326
                //
327
                //   word            =   atom / quoted-string
328
                //
329
                //   atom            =   [CFWS] 1*atext [CFWS]
330
                switch (token) {
331
                    // Comment
332
                    case '(':
333
                        if (elementLength === 0) {
334
                            // Comments are OK at the beginning of an element
335
                            updateResult(elementCount === 0 ? internals.diagnoses.cfwsComment : internals.diagnoses.deprecatedComment);
336
                        }
337
                        else {
338
                            updateResult(internals.diagnoses.cfwsComment);
339
                            // Cannot start a comment in an element, should be end
340
                            assertEnd = true;
341
                        }
342
343
                        context.stack.push(context.now);
344
                        context.now = internals.components.contextComment;
345
                        break;
346
347
                        // Next dot-atom element
348
                    case '.':
349
                        if (elementLength === 0) {
350
                            // Another dot, already?
351
                            updateResult(elementCount === 0 ? internals.diagnoses.errDotStart : internals.diagnoses.errConsecutiveDots);
352
                        }
353
                        else {
354
                            // The entire local-part can be a quoted string for RFC 5321; if one atom is quoted it's an RFC 5322 obsolete form
355
                            if (assertEnd) {
356
                                updateResult(internals.diagnoses.deprecatedLocalPart);
357
                            }
358
359
                            // CFWS & quoted strings are OK again now we're at the beginning of an element (although they are obsolete forms)
360
                            assertEnd = false;
361
                            elementLength = 0;
362
                            ++elementCount;
363
                            parseData.local += token;
364
                            atomData.locals[elementCount] = '';
365
                        }
366
367
                        break;
368
369
                        // Quoted string
370
                    case '"':
371
                        if (elementLength === 0) {
372
                            // The entire local-part can be a quoted string for RFC 5321; if one atom is quoted it's an RFC 5322 obsolete form
373
                            updateResult(elementCount === 0 ? internals.diagnoses.rfc5321QuotedString : internals.diagnoses.deprecatedLocalPart);
374
375
                            parseData.local += token;
376
                            atomData.locals[elementCount] += token;
377
                            elementLength += Buffer.byteLength(token, 'utf8');
378
379
                            // Quoted string must be the entire element
380
                            assertEnd = true;
381
                            context.stack.push(context.now);
382
                            context.now = internals.components.contextQuotedString;
383
                        }
384
                        else {
385
                            updateResult(internals.diagnoses.errExpectingATEXT);
386
                        }
387
388
                        break;
389
390
                        // Folding white space
391
                    case '\r':
392
                        if (emailLength === ++i || email[i] !== '\n') {
393
                            // Fatal error
394
                            updateResult(internals.diagnoses.errCRNoLF);
395
                            break;
396
                        }
397
398
                        // Fallthrough
399
400
                    case ' ':
401
                    case '\t':
402
                        if (elementLength === 0) {
403
                            updateResult(elementCount === 0 ? internals.diagnoses.cfwsFWS : internals.diagnoses.deprecatedFWS);
404
                        }
405
                        else {
406
                            // We can't start FWS in the middle of an element, better be end
407
                            assertEnd = true;
408
                        }
409
410
                        context.stack.push(context.now);
411
                        context.now = internals.components.contextFWS;
412
                        prevToken = token;
413
                        break;
414
415
                    case '@':
416
                        // At this point we should have a valid local-part
417
                        // $lab:coverage:off$
418
                        if (context.stack.length !== 1) {
419
                            throw new Error('unexpected item on context stack');
420
                        }
421
                        // $lab:coverage:on$
422
423
                        if (parseData.local.length === 0) {
424
                            // Fatal error
425
                            updateResult(internals.diagnoses.errNoLocalPart);
426
                        }
427
                        else if (elementLength === 0) {
428
                            // Fatal error
429
                            updateResult(internals.diagnoses.errDotEnd);
430
                        }
431
                            // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.1 the maximum total length of a user name or other local-part is 64
432
                            //    octets
433
                        else if (Buffer.byteLength(parseData.local, 'utf8') > 64) {
434
                            updateResult(internals.diagnoses.rfc5322LocalTooLong);
435
                        }
436
                            // http://tools.ietf.org/html/rfc5322#section-3.4.1 comments and folding white space SHOULD NOT be used around "@" in the
437
                            //    addr-spec
438
                            //
439
                            // http://tools.ietf.org/html/rfc2119
440
                            // 4. SHOULD NOT this phrase, or the phrase "NOT RECOMMENDED" mean that there may exist valid reasons in particular
441
                            //    circumstances when the particular behavior is acceptable or even useful, but the full implications should be understood
442
                            //    and the case carefully weighed before implementing any behavior described with this label.
443
                        else if (context.prev === internals.components.contextComment || context.prev === internals.components.contextFWS) {
444
                            updateResult(internals.diagnoses.deprecatedCFWSNearAt);
445
                        }
446
447
                        // Clear everything down for the domain parsing
448
                        context.now = internals.components.domain;
449
                        context.stack[0] = internals.components.domain;
450
                        elementCount = 0;
451
                        elementLength = 0;
452
                        assertEnd = false; // CFWS can only appear at the end of the element
453
                        break;
454
455
                        // ATEXT
456
                    default:
457
                        // http://tools.ietf.org/html/rfc5322#section-3.2.3
458
                        //    atext = ALPHA / DIGIT / ; Printable US-ASCII
459
                        //            "!" / "#" /     ;  characters not including
460
                        //            "$" / "%" /     ;  specials.  Used for atoms.
461
                        //            "&" / "'" /
462
                        //            "*" / "+" /
463
                        //            "-" / "/" /
464
                        //            "=" / "?" /
465
                        //            "^" / "_" /
466
                        //            "`" / "{" /
467
                        //            "|" / "}" /
468
                        //            "~"
469
                        if (assertEnd) {
470
                            // We have encountered atext where it is no longer valid
471
                            switch (context.prev) {
472
                                case internals.components.contextComment:
473
                                case internals.components.contextFWS:
474
                                    updateResult(internals.diagnoses.errATEXTAfterCFWS);
475
                                    break;
476
477
                                case internals.components.contextQuotedString:
478
                                    updateResult(internals.diagnoses.errATEXTAfterQS);
479
                                    break;
480
481
                                    // $lab:coverage:off$
482
                                default:
483
                                    throw new Error('more atext found where none is allowed, but unrecognized prev context: ' + context.prev);
484
                                    // $lab:coverage:on$
485
                            }
486
                        }
487
                        else {
488
                            context.prev = context.now;
489
                            charCode = token.codePointAt(0);
490
491
                            // Especially if charCode == 10
492
                            if (internals.specials(charCode) || internals.c0Controls(charCode) || internals.c1Controls(charCode)) {
493
494
                                // Fatal error
495
                                updateResult(internals.diagnoses.errExpectingATEXT);
496
                            }
497
498
                            parseData.local += token;
499
                            atomData.locals[elementCount] += token;
500
                            elementLength += Buffer.byteLength(token, 'utf8');
501
                        }
502
                }
503
504
                break;
505
506
            case internals.components.domain:
507
                // http://tools.ietf.org/html/rfc5322#section-3.4.1
508
                //   domain          =   dot-atom / domain-literal / obs-domain
509
                //
510
                //   dot-atom        =   [CFWS] dot-atom-text [CFWS]
511
                //
512
                //   dot-atom-text   =   1*atext *("." 1*atext)
513
                //
514
                //   domain-literal  =   [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
515
                //
516
                //   dtext           =   %d33-90 /          ; Printable US-ASCII
517
                //                       %d94-126 /         ;  characters not including
518
                //                       obs-dtext          ;  "[", "]", or "\"
519
                //
520
                //   obs-domain      =   atom *("." atom)
521
                //
522
                //   atom            =   [CFWS] 1*atext [CFWS]
523
524
                // http://tools.ietf.org/html/rfc5321#section-4.1.2
525
                //   Mailbox        = Local-part "@" ( Domain / address-literal )
526
                //
527
                //   Domain         = sub-domain *("." sub-domain)
528
                //
529
                //   address-literal  = "[" ( IPv4-address-literal /
530
                //                    IPv6-address-literal /
531
                //                    General-address-literal ) "]"
532
                //                    ; See Section 4.1.3
533
534
                // http://tools.ietf.org/html/rfc5322#section-3.4.1
535
                //      Note: A liberal syntax for the domain portion of addr-spec is
536
                //      given here.  However, the domain portion contains addressing
537
                //      information specified by and used in other protocols (e.g.,
538
                //      [RFC1034], [RFC1035], [RFC1123], [RFC5321]).  It is therefore
539
                //      incumbent upon implementations to conform to the syntax of
540
                //      addresses for the context in which they are used.
541
                //
542
                // is_email() author's note: it's not clear how to interpret this in
543
                // he context of a general email address validator. The conclusion I
544
                // have reached is this: "addressing information" must comply with
545
                // RFC 5321 (and in turn RFC 1035), anything that is "semantically
546
                // invisible" must comply only with RFC 5322.
547
                switch (token) {
548
                    // Comment
549
                    case '(':
550
                        if (elementLength === 0) {
551
                            // Comments at the start of the domain are deprecated in the text, comments at the start of a subdomain are obs-domain
552
                            // http://tools.ietf.org/html/rfc5322#section-3.4.1
553
                            updateResult(elementCount === 0 ? internals.diagnoses.deprecatedCFWSNearAt : internals.diagnoses.deprecatedComment);
554
                        }
555
                        else {
556
                            // We can't start a comment mid-element, better be at the end
557
                            assertEnd = true;
558
                            updateResult(internals.diagnoses.cfwsComment);
559
                        }
560
561
                        context.stack.push(context.now);
562
                        context.now = internals.components.contextComment;
563
                        break;
564
565
                        // Next dot-atom element
566
                    case '.':
567
                        const punycodeLength = Punycode.encode(atomData.domains[elementCount]).length;
568
                        if (elementLength === 0) {
569
                            // Another dot, already? Fatal error.
570
                            updateResult(elementCount === 0 ? internals.diagnoses.errDotStart : internals.diagnoses.errConsecutiveDots);
571
                        }
572
                        else if (hyphenFlag) {
573
                            // Previous subdomain ended in a hyphen. Fatal error.
574
                            updateResult(internals.diagnoses.errDomainHyphenEnd);
575
                        }
576
                        else if (punycodeLength > 63) {
577
                            // RFC 5890 specifies that domain labels that are encoded using the Punycode algorithm
578
                            // must adhere to the <= 63 octet requirement.
579
                            // This includes string prefixes from the Punycode algorithm.
580
                            //
581
                            // https://tools.ietf.org/html/rfc5890#section-2.3.2.1
582
                            // labels          63 octets or less
583
584
                            updateResult(internals.diagnoses.rfc5322LabelTooLong);
585
                        }
586
587
                        // CFWS is OK again now we're at the beginning of an element (although
588
                        // it may be obsolete CFWS)
589
                        assertEnd = false;
590
                        elementLength = 0;
591
                        ++elementCount;
592
                        atomData.domains[elementCount] = '';
593
                        parseData.domain += token;
594
595
                        break;
596
597
                        // Domain literal
598
                    case '[':
599
                        if (parseData.domain.length === 0) {
600
                            // Domain literal must be the only component
601
                            assertEnd = true;
602
                            elementLength += Buffer.byteLength(token, 'utf8');
603
                            context.stack.push(context.now);
604
                            context.now = internals.components.literal;
605
                            parseData.domain += token;
606
                            atomData.domains[elementCount] += token;
607
                            parseData.literal = '';
608
                        }
609
                        else {
610
                            // Fatal error
611
                            updateResult(internals.diagnoses.errExpectingATEXT);
612
                        }
613
614
                        break;
615
616
                        // Folding white space
617
                    case '\r':
618
                        if (emailLength === ++i || email[i] !== '\n') {
619
                            // Fatal error
620
                            updateResult(internals.diagnoses.errCRNoLF);
621
                            break;
622
                        }
623
624
                        // Fallthrough
625
626
                    case ' ':
627
                    case '\t':
628
                        if (elementLength === 0) {
629
                            updateResult(elementCount === 0 ? internals.diagnoses.deprecatedCFWSNearAt : internals.diagnoses.deprecatedFWS);
630
                        }
631
                        else {
632
                            // We can't start FWS in the middle of an element, so this better be the end
633
                            updateResult(internals.diagnoses.cfwsFWS);
634
                            assertEnd = true;
635
                        }
636
637
                        context.stack.push(context.now);
638
                        context.now = internals.components.contextFWS;
639
                        prevToken = token;
640
                        break;
641
642
                        // This must be ATEXT
643
                    default:
644
                        // RFC 5322 allows any atext...
645
                        // http://tools.ietf.org/html/rfc5322#section-3.2.3
646
                        //    atext = ALPHA / DIGIT / ; Printable US-ASCII
647
                        //            "!" / "#" /     ;  characters not including
648
                        //            "$" / "%" /     ;  specials.  Used for atoms.
649
                        //            "&" / "'" /
650
                        //            "*" / "+" /
651
                        //            "-" / "/" /
652
                        //            "=" / "?" /
653
                        //            "^" / "_" /
654
                        //            "`" / "{" /
655
                        //            "|" / "}" /
656
                        //            "~"
657
658
                        // But RFC 5321 only allows letter-digit-hyphen to comply with DNS rules
659
                        //   (RFCs 1034 & 1123)
660
                        // http://tools.ietf.org/html/rfc5321#section-4.1.2
661
                        //   sub-domain     = Let-dig [Ldh-str]
662
                        //
663
                        //   Let-dig        = ALPHA / DIGIT
664
                        //
665
                        //   Ldh-str        = *( ALPHA / DIGIT / "-" ) Let-dig
666
                        //
667
                        if (assertEnd) {
668
                            // We have encountered ATEXT where it is no longer valid
669
                            switch (context.prev) {
670
                                case internals.components.contextComment:
671
                                case internals.components.contextFWS:
672
                                    updateResult(internals.diagnoses.errATEXTAfterCFWS);
673
                                    break;
674
675
                                case internals.components.literal:
676
                                    updateResult(internals.diagnoses.errATEXTAfterDomainLiteral);
677
                                    break;
678
679
                                    // $lab:coverage:off$
680
                                default:
681
                                    throw new Error('more atext found where none is allowed, but unrecognized prev context: ' + context.prev);
682
                                    // $lab:coverage:on$
683
                            }
684
                        }
685
686
                        charCode = token.codePointAt(0);
687
                        // Assume this token isn't a hyphen unless we discover it is
688
                        hyphenFlag = false;
689
690
                        if (internals.specials(charCode) || internals.c0Controls(charCode) || internals.c1Controls(charCode)) {
691
                            // Fatal error
692
                            updateResult(internals.diagnoses.errExpectingATEXT);
693
                        }
694
                        else if (token === '-') {
695
                            if (elementLength === 0) {
696
                                // Hyphens cannot be at the beginning of a subdomain, fatal error
697
                                updateResult(internals.diagnoses.errDomainHyphenStart);
698
                            }
699
700
                            hyphenFlag = true;
701
                        }
702
                            // Check if it's a neither a number nor a latin/unicode letter
703
                        else if (charCode < 48 || (charCode > 122 && charCode < 192) || (charCode > 57 && charCode < 65) || (charCode > 90 && charCode < 97)) {
704
                            // This is not an RFC 5321 subdomain, but still OK by RFC 5322
705
                            updateResult(internals.diagnoses.rfc5322Domain);
706
                        }
707
708
                        parseData.domain += token;
709
                        atomData.domains[elementCount] += token;
710
                        elementLength += Buffer.byteLength(token, 'utf8');
711
                }
712
713
                break;
714
715
                // Domain literal
716
            case internals.components.literal:
717
                // http://tools.ietf.org/html/rfc5322#section-3.4.1
718
                //   domain-literal  =   [CFWS] "[" *([FWS] dtext) [FWS] "]" [CFWS]
719
                //
720
                //   dtext           =   %d33-90 /          ; Printable US-ASCII
721
                //                       %d94-126 /         ;  characters not including
722
                //                       obs-dtext          ;  "[", "]", or "\"
723
                //
724
                //   obs-dtext       =   obs-NO-WS-CTL / quoted-pair
725
                switch (token) {
726
                    // End of domain literal
727
                    case ']':
728
                        if (maxResult < internals.categories.deprecated) {
729
                            // Could be a valid RFC 5321 address literal, so let's check
730
731
                            // http://tools.ietf.org/html/rfc5321#section-4.1.2
732
                            //   address-literal  = "[" ( IPv4-address-literal /
733
                            //                    IPv6-address-literal /
734
                            //                    General-address-literal ) "]"
735
                            //                    ; See Section 4.1.3
736
                            //
737
                            // http://tools.ietf.org/html/rfc5321#section-4.1.3
738
                            //   IPv4-address-literal  = Snum 3("."  Snum)
739
                            //
740
                            //   IPv6-address-literal  = "IPv6:" IPv6-addr
741
                            //
742
                            //   General-address-literal  = Standardized-tag ":" 1*dcontent
743
                            //
744
                            //   Standardized-tag  = Ldh-str
745
                            //                     ; Standardized-tag MUST be specified in a
746
                            //                     ; Standards-Track RFC and registered with IANA
747
                            //
748
                            //   dcontent      = %d33-90 / ; Printable US-ASCII
749
                            //                 %d94-126 ; excl. "[", "\", "]"
750
                            //
751
                            //   Snum          = 1*3DIGIT
752
                            //                 ; representing a decimal integer
753
                            //                 ; value in the range 0 through 255
754
                            //
755
                            //   IPv6-addr     = IPv6-full / IPv6-comp / IPv6v4-full / IPv6v4-comp
756
                            //
757
                            //   IPv6-hex      = 1*4HEXDIG
758
                            //
759
                            //   IPv6-full     = IPv6-hex 7(":" IPv6-hex)
760
                            //
761
                            //   IPv6-comp     = [IPv6-hex *5(":" IPv6-hex)] "::"
762
                            //                 [IPv6-hex *5(":" IPv6-hex)]
763
                            //                 ; The "::" represents at least 2 16-bit groups of
764
                            //                 ; zeros.  No more than 6 groups in addition to the
765
                            //                 ; "::" may be present.
766
                            //
767
                            //   IPv6v4-full   = IPv6-hex 5(":" IPv6-hex) ":" IPv4-address-literal
768
                            //
769
                            //   IPv6v4-comp   = [IPv6-hex *3(":" IPv6-hex)] "::"
770
                            //                 [IPv6-hex *3(":" IPv6-hex) ":"]
771
                            //                 IPv4-address-literal
772
                            //                 ; The "::" represents at least 2 16-bit groups of
773
                            //                 ; zeros.  No more than 4 groups in addition to the
774
                            //                 ; "::" and IPv4-address-literal may be present.
775
776
                            let index = -1;
777
                            let addressLiteral = parseData.literal;
778
                            const matchesIP = internals.regex.ipV4.exec(addressLiteral);
779
780
                            // Maybe extract IPv4 part from the end of the address-literal
781
                            if (matchesIP) {
782
                                index = matchesIP.index;
783
                                if (index !== 0) {
784
                                    // Convert IPv4 part to IPv6 format for futher testing
785
                                    addressLiteral = addressLiteral.slice(0, index) + '0:0';
786
                                }
787
                            }
788
789
                            if (index === 0) {
790
                                // Nothing there except a valid IPv4 address, so...
791
                                updateResult(internals.diagnoses.rfc5321AddressLiteral);
792
                            }
793
                            else if (addressLiteral.slice(0, 5).toLowerCase() !== 'ipv6:') {
794
                                updateResult(internals.diagnoses.rfc5322DomainLiteral);
795
                            }
796
                            else {
797
                                const match = addressLiteral.slice(5);
798
                                let maxGroups = internals.maxIPv6Groups;
799
                                const groups = match.split(':');
800
                                index = match.indexOf('::');
801
802
                                if (!~index) {
803
                                    // Need exactly the right number of groups
804
                                    if (groups.length !== maxGroups) {
805
                                        updateResult(internals.diagnoses.rfc5322IPv6GroupCount);
806
                                    }
807
                                }
808
                                else if (index !== match.lastIndexOf('::')) {
809
                                    updateResult(internals.diagnoses.rfc5322IPv62x2xColon);
810
                                }
811
                                else {
812
                                    if (index === 0 || index === match.length - 2) {
813
                                        // RFC 4291 allows :: at the start or end of an address with 7 other groups in addition
814
                                        ++maxGroups;
815
                                    }
816
817
                                    if (groups.length > maxGroups) {
818
                                        updateResult(internals.diagnoses.rfc5322IPv6MaxGroups);
819
                                    }
820
                                    else if (groups.length === maxGroups) {
821
                                        // Eliding a single "::"
822
                                        updateResult(internals.diagnoses.deprecatedIPv6);
823
                                    }
824
                                }
825
826
                                // IPv6 testing strategy
827
                                if (match[0] === ':' && match[1] !== ':') {
828
                                    updateResult(internals.diagnoses.rfc5322IPv6ColonStart);
829
                                }
830
                                else if (match[match.length - 1] === ':' && match[match.length - 2] !== ':') {
831
                                    updateResult(internals.diagnoses.rfc5322IPv6ColonEnd);
832
                                }
833
                                else if (internals.checkIpV6(groups)) {
834
                                    updateResult(internals.diagnoses.rfc5321AddressLiteral);
835
                                }
836
                                else {
837
                                    updateResult(internals.diagnoses.rfc5322IPv6BadCharacter);
838
                                }
839
                            }
840
                        }
841
                        else {
842
                            updateResult(internals.diagnoses.rfc5322DomainLiteral);
843
                        }
844
845
                        parseData.domain += token;
846
                        atomData.domains[elementCount] += token;
847
                        elementLength += Buffer.byteLength(token, 'utf8');
848
                        context.prev = context.now;
849
                        context.now = context.stack.pop();
850
                        break;
851
852
                    case '\\':
853
                        updateResult(internals.diagnoses.rfc5322DomainLiteralOBSDText);
854
                        context.stack.push(context.now);
855
                        context.now = internals.components.contextQuotedPair;
856
                        break;
857
858
                        // Folding white space
859
                    case '\r':
860
                        if (emailLength === ++i || email[i] !== '\n') {
861
                            updateResult(internals.diagnoses.errCRNoLF);
862
                            break;
863
                        }
864
865
                        // Fallthrough
866
867
                    case ' ':
868
                    case '\t':
869
                        updateResult(internals.diagnoses.cfwsFWS);
870
871
                        context.stack.push(context.now);
872
                        context.now = internals.components.contextFWS;
873
                        prevToken = token;
874
                        break;
875
876
                        // DTEXT
877
                    default:
878
                        // http://tools.ietf.org/html/rfc5322#section-3.4.1
879
                        //   dtext         =   %d33-90 /  ; Printable US-ASCII
880
                        //                     %d94-126 / ;  characters not including
881
                        //                     obs-dtext  ;  "[", "]", or "\"
882
                        //
883
                        //   obs-dtext     =   obs-NO-WS-CTL / quoted-pair
884
                        //
885
                        //   obs-NO-WS-CTL =   %d1-8 /    ; US-ASCII control
886
                        //                     %d11 /     ;  characters that do not
887
                        //                     %d12 /     ;  include the carriage
888
                        //                     %d14-31 /  ;  return, line feed, and
889
                        //                     %d127      ;  white space characters
890
                        charCode = token.codePointAt(0);
891
892
                        // '\r', '\n', ' ', and '\t' have already been parsed above
893
                        if ((charCode !== 127 && internals.c1Controls(charCode)) || charCode === 0 || token === '[') {
894
                            // Fatal error
895
                            updateResult(internals.diagnoses.errExpectingDTEXT);
896
                            break;
897
                        }
898
                        else if (internals.c0Controls(charCode) || charCode === 127) {
899
                            updateResult(internals.diagnoses.rfc5322DomainLiteralOBSDText);
900
                        }
901
902
                        parseData.literal += token;
903
                        parseData.domain += token;
904
                        atomData.domains[elementCount] += token;
905
                        elementLength += Buffer.byteLength(token, 'utf8');
906
                }
907
908
                break;
909
910
                // Quoted string
911
            case internals.components.contextQuotedString:
912
                // http://tools.ietf.org/html/rfc5322#section-3.2.4
913
                //   quoted-string = [CFWS]
914
                //                   DQUOTE *([FWS] qcontent) [FWS] DQUOTE
915
                //                   [CFWS]
916
                //
917
                //   qcontent      = qtext / quoted-pair
918
                switch (token) {
919
                    // Quoted pair
920
                    case '\\':
921
                        context.stack.push(context.now);
922
                        context.now = internals.components.contextQuotedPair;
923
                        break;
924
925
                        // Folding white space. Spaces are allowed as regular characters inside a quoted string - it's only FWS if we include '\t' or '\r\n'
926
                    case '\r':
927
                        if (emailLength === ++i || email[i] !== '\n') {
928
                            // Fatal error
929
                            updateResult(internals.diagnoses.errCRNoLF);
930
                            break;
931
                        }
932
933
                        // Fallthrough
934
935
                    case '\t':
936
                        // http://tools.ietf.org/html/rfc5322#section-3.2.2
937
                        //   Runs of FWS, comment, or CFWS that occur between lexical tokens in
938
                        //   a structured header field are semantically interpreted as a single
939
                        //   space character.
940
941
                        // http://tools.ietf.org/html/rfc5322#section-3.2.4
942
                        //   the CRLF in any FWS/CFWS that appears within the quoted-string [is]
943
                        //   semantically "invisible" and therefore not part of the
944
                        //   quoted-string
945
946
                        parseData.local += ' ';
947
                        atomData.locals[elementCount] += ' ';
948
                        elementLength += Buffer.byteLength(token, 'utf8');
949
950
                        updateResult(internals.diagnoses.cfwsFWS);
951
                        context.stack.push(context.now);
952
                        context.now = internals.components.contextFWS;
953
                        prevToken = token;
954
                        break;
955
956
                        // End of quoted string
957
                    case '"':
958
                        parseData.local += token;
959
                        atomData.locals[elementCount] += token;
960
                        elementLength += Buffer.byteLength(token, 'utf8');
961
                        context.prev = context.now;
962
                        context.now = context.stack.pop();
963
                        break;
964
965
                        // QTEXT
966
                    default:
967
                        // http://tools.ietf.org/html/rfc5322#section-3.2.4
968
                        //   qtext          =   %d33 /             ; Printable US-ASCII
969
                        //                      %d35-91 /          ;  characters not including
970
                        //                      %d93-126 /         ;  "\" or the quote character
971
                        //                      obs-qtext
972
                        //
973
                        //   obs-qtext      =   obs-NO-WS-CTL
974
                        //
975
                        //   obs-NO-WS-CTL  =   %d1-8 /            ; US-ASCII control
976
                        //                      %d11 /             ;  characters that do not
977
                        //                      %d12 /             ;  include the carriage
978
                        //                      %d14-31 /          ;  return, line feed, and
979
                        //                      %d127              ;  white space characters
980
                        charCode = token.codePointAt(0);
981
982
                        if ((charCode !== 127 && internals.c1Controls(charCode)) || charCode === 0 || charCode === 10) {
983
                            updateResult(internals.diagnoses.errExpectingQTEXT);
984
                        }
985
                        else if (internals.c0Controls(charCode) || charCode === 127) {
986
                            updateResult(internals.diagnoses.deprecatedQTEXT);
987
                        }
988
989
                        parseData.local += token;
990
                        atomData.locals[elementCount] += token;
991
                        elementLength += Buffer.byteLength(token, 'utf8');
992
                }
993
994
                // http://tools.ietf.org/html/rfc5322#section-3.4.1
995
                //   If the string can be represented as a dot-atom (that is, it contains
996
                //   no characters other than atext characters or "." surrounded by atext
997
                //   characters), then the dot-atom form SHOULD be used and the quoted-
998
                //   string form SHOULD NOT be used.
999
1000
                break;
1001
                // Quoted pair
1002
            case internals.components.contextQuotedPair:
1003
                // http://tools.ietf.org/html/rfc5322#section-3.2.1
1004
                //   quoted-pair     =   ("\" (VCHAR / WSP)) / obs-qp
1005
                //
1006
                //   VCHAR           =  %d33-126   ; visible (printing) characters
1007
                //   WSP             =  SP / HTAB  ; white space
1008
                //
1009
                //   obs-qp          =   "\" (%d0 / obs-NO-WS-CTL / LF / CR)
1010
                //
1011
                //   obs-NO-WS-CTL   =   %d1-8 /   ; US-ASCII control
1012
                //                       %d11 /    ;  characters that do not
1013
                //                       %d12 /    ;  include the carriage
1014
                //                       %d14-31 / ;  return, line feed, and
1015
                //                       %d127     ;  white space characters
1016
                //
1017
                // i.e. obs-qp       =  "\" (%d0-8, %d10-31 / %d127)
1018
                charCode = token.codePointAt(0);
1019
1020
                if (charCode !== 127 &&  internals.c1Controls(charCode)) {
1021
                    // Fatal error
1022
                    updateResult(internals.diagnoses.errExpectingQPair);
1023
                }
1024
                else if ((charCode < 31 && charCode !== 9) || charCode === 127) {
1025
                    // ' ' and '\t' are allowed
1026
                    updateResult(internals.diagnoses.deprecatedQP);
1027
                }
1028
1029
                // At this point we know where this qpair occurred so we could check to see if the character actually needed to be quoted at all.
1030
                // http://tools.ietf.org/html/rfc5321#section-4.1.2
1031
                //   the sending system SHOULD transmit the form that uses the minimum quoting possible.
1032
1033
                context.prev = context.now;
1034
                // End of qpair
1035
                context.now = context.stack.pop();
1036
                const escapeToken = '\\' + token;
1037
1038
                switch (context.now) {
1039
                    case internals.components.contextComment:
1040
                        break;
1041
1042
                    case internals.components.contextQuotedString:
1043
                        parseData.local += escapeToken;
1044
                        atomData.locals[elementCount] += escapeToken;
1045
1046
                        // The maximum sizes specified by RFC 5321 are octet counts, so we must include the backslash
1047
                        elementLength += 2;
1048
                        break;
1049
1050
                    case internals.components.literal:
1051
                        parseData.domain += escapeToken;
1052
                        atomData.domains[elementCount] += escapeToken;
1053
1054
                        // The maximum sizes specified by RFC 5321 are octet counts, so we must include the backslash
1055
                        elementLength += 2;
1056
                        break;
1057
1058
                        // $lab:coverage:off$
1059
                    default:
1060
                        throw new Error('quoted pair logic invoked in an invalid context: ' + context.now);
1061
                        // $lab:coverage:on$
1062
                }
1063
                break;
1064
1065
                // Comment
1066
            case internals.components.contextComment:
1067
                // http://tools.ietf.org/html/rfc5322#section-3.2.2
1068
                //   comment  = "(" *([FWS] ccontent) [FWS] ")"
1069
                //
1070
                //   ccontent = ctext / quoted-pair / comment
1071
                switch (token) {
1072
                    // Nested comment
1073
                    case '(':
1074
                        // Nested comments are ok
1075
                        context.stack.push(context.now);
1076
                        context.now = internals.components.contextComment;
1077
                        break;
1078
1079
                        // End of comment
1080
                    case ')':
1081
                        context.prev = context.now;
1082
                        context.now = context.stack.pop();
1083
                        break;
1084
1085
                        // Quoted pair
1086
                    case '\\':
1087
                        context.stack.push(context.now);
1088
                        context.now = internals.components.contextQuotedPair;
1089
                        break;
1090
1091
                        // Folding white space
1092
                    case '\r':
1093
                        if (emailLength === ++i || email[i] !== '\n') {
1094
                            // Fatal error
1095
                            updateResult(internals.diagnoses.errCRNoLF);
1096
                            break;
1097
                        }
1098
1099
                        // Fallthrough
1100
1101
                    case ' ':
1102
                    case '\t':
1103
                        updateResult(internals.diagnoses.cfwsFWS);
1104
1105
                        context.stack.push(context.now);
1106
                        context.now = internals.components.contextFWS;
1107
                        prevToken = token;
1108
                        break;
1109
1110
                        // CTEXT
1111
                    default:
1112
                        // http://tools.ietf.org/html/rfc5322#section-3.2.3
1113
                        //   ctext         = %d33-39 /  ; Printable US-ASCII
1114
                        //                   %d42-91 /  ;  characters not including
1115
                        //                   %d93-126 / ;  "(", ")", or "\"
1116
                        //                   obs-ctext
1117
                        //
1118
                        //   obs-ctext     = obs-NO-WS-CTL
1119
                        //
1120
                        //   obs-NO-WS-CTL = %d1-8 /    ; US-ASCII control
1121
                        //                   %d11 /     ;  characters that do not
1122
                        //                   %d12 /     ;  include the carriage
1123
                        //                   %d14-31 /  ;  return, line feed, and
1124
                        //                   %d127      ;  white space characters
1125
                        charCode = token.codePointAt(0);
1126
1127
                        if (charCode === 0 || charCode === 10 || (charCode !== 127 && internals.c1Controls(charCode))) {
1128
                            // Fatal error
1129
                            updateResult(internals.diagnoses.errExpectingCTEXT);
1130
                            break;
1131
                        }
1132
                        else if (internals.c0Controls(charCode) || charCode === 127) {
1133
                            updateResult(internals.diagnoses.deprecatedCTEXT);
1134
                        }
1135
                }
1136
1137
                break;
1138
1139
                // Folding white space
1140
            case internals.components.contextFWS:
1141
                // http://tools.ietf.org/html/rfc5322#section-3.2.2
1142
                //   FWS     =   ([*WSP CRLF] 1*WSP) /  obs-FWS
1143
                //                                   ; Folding white space
1144
1145
                // But note the erratum:
1146
                // http://www.rfc-editor.org/errata_search.php?rfc=5322&eid=1908:
1147
                //   In the obsolete syntax, any amount of folding white space MAY be
1148
                //   inserted where the obs-FWS rule is allowed.  This creates the
1149
                //   possibility of having two consecutive "folds" in a line, and
1150
                //   therefore the possibility that a line which makes up a folded header
1151
                //   field could be composed entirely of white space.
1152
                //
1153
                //   obs-FWS =   1*([CRLF] WSP)
1154
1155
                if (prevToken === '\r') {
1156
                    if (token === '\r') {
1157
                        // Fatal error
1158
                        updateResult(internals.diagnoses.errFWSCRLFx2);
1159
                        break;
1160
                    }
1161
1162
                    if (++crlfCount > 1) {
1163
                        // Multiple folds => obsolete FWS
1164
                        updateResult(internals.diagnoses.deprecatedFWS);
1165
                    }
1166
                    else {
1167
                        crlfCount = 1;
1168
                    }
1169
                }
1170
1171
                switch (token) {
1172
                    case '\r':
1173
                        if (emailLength === ++i || email[i] !== '\n') {
1174
                            // Fatal error
1175
                            updateResult(internals.diagnoses.errCRNoLF);
1176
                        }
1177
1178
                        break;
1179
1180
                    case ' ':
1181
                    case '\t':
1182
                        break;
1183
1184
                    default:
1185
                        if (prevToken === '\r') {
1186
                            // Fatal error
1187
                            updateResult(internals.diagnoses.errFWSCRLFEnd);
1188
                        }
1189
1190
                        crlfCount = 0;
1191
1192
                        // End of FWS
1193
                        context.prev = context.now;
1194
                        context.now = context.stack.pop();
1195
1196
                        // Look at this token again in the parent context
1197
                        --i;
1198
                }
1199
1200
                prevToken = token;
1201
                break;
1202
1203
                // Unexpected context
1204
                // $lab:coverage:off$
1205
            default:
1206
                throw new Error('unknown context: ' + context.now);
1207
                // $lab:coverage:on$
1208
        } // Primary state machine
1209
1210
        if (maxResult > internals.categories.rfc5322) {
1211
            // Fatal error, no point continuing
1212
            break;
1213
        }
1214
    } // Token loop
1215
1216
    // Check for errors
1217
    if (maxResult < internals.categories.rfc5322) {
1218
        const punycodeLength = Punycode.encode(parseData.domain).length;
1219
        // Fatal errors
1220
        if (context.now === internals.components.contextQuotedString) {
1221
            updateResult(internals.diagnoses.errUnclosedQuotedString);
1222
        }
1223
        else if (context.now === internals.components.contextQuotedPair) {
1224
            updateResult(internals.diagnoses.errBackslashEnd);
1225
        }
1226
        else if (context.now === internals.components.contextComment) {
1227
            updateResult(internals.diagnoses.errUnclosedComment);
1228
        }
1229
        else if (context.now === internals.components.literal) {
1230
            updateResult(internals.diagnoses.errUnclosedDomainLiteral);
1231
        }
1232
        else if (token === '\r') {
0 ignored issues
show
Bug introduced by
The variable token seems to not be initialized for all possible execution paths.
Loading history...
1233
            updateResult(internals.diagnoses.errFWSCRLFEnd);
1234
        }
1235
        else if (parseData.domain.length === 0) {
1236
            updateResult(internals.diagnoses.errNoDomain);
1237
        }
1238
        else if (elementLength === 0) {
1239
            updateResult(internals.diagnoses.errDotEnd);
1240
        }
1241
        else if (hyphenFlag) {
1242
            updateResult(internals.diagnoses.errDomainHyphenEnd);
1243
        }
1244
1245
            // Other errors
1246
        else if (punycodeLength > 255) {
1247
            // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.2
1248
            //   The maximum total length of a domain name or number is 255 octets.
1249
            updateResult(internals.diagnoses.rfc5322DomainTooLong);
1250
        }
1251
        else if (Buffer.byteLength(parseData.local, 'utf8') + punycodeLength + /* '@' */ 1 > 254) {
1252
            // http://tools.ietf.org/html/rfc5321#section-4.1.2
1253
            //   Forward-path   = Path
1254
            //
1255
            //   Path           = "<" [ A-d-l ":" ] Mailbox ">"
1256
            //
1257
            // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.3
1258
            //   The maximum total length of a reverse-path or forward-path is 256 octets (including the punctuation and element separators).
1259
            //
1260
            // Thus, even without (obsolete) routing information, the Mailbox can only be 254 characters long. This is confirmed by this verified
1261
            // erratum to RFC 3696:
1262
            //
1263
            // http://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690
1264
            //   However, there is a restriction in RFC 2821 on the length of an address in MAIL and RCPT commands of 254 characters.  Since
1265
            //   addresses that do not fit in those fields are not normally useful, the upper limit on address lengths should normally be considered
1266
            //   to be 254.
1267
            updateResult(internals.diagnoses.rfc5322TooLong);
1268
        }
1269
        else if (elementLength > 63) {
1270
            // http://tools.ietf.org/html/rfc1035#section-2.3.4
1271
            // labels   63 octets or less
1272
            updateResult(internals.diagnoses.rfc5322LabelTooLong);
1273
        }
1274
        else if (options.minDomainAtoms && atomData.domains.length < options.minDomainAtoms) {
1275
            updateResult(internals.diagnoses.errDomainTooShort);
1276
        }
1277
        else if (options.tldWhitelist || options.tldBlacklist) {
1278
            const tldAtom = atomData.domains[elementCount];
1279
1280
            if (!internals.validDomain(tldAtom, options)) {
1281
                updateResult(internals.diagnoses.errUnknownTLD);
1282
            }
1283
        }
1284
    } // Check for errors
1285
1286
    // Finish
1287
    if (maxResult < internals.categories.dnsWarn) {
1288
        // Per RFC 5321, domain atoms are limited to letter-digit-hyphen, so we only need to check code <= 57 to check for a digit
1289
        const code = atomData.domains[elementCount].codePointAt(0);
1290
1291
        if (code <= 57) {
1292
            updateResult(internals.diagnoses.rfc5321TLDNumeric);
1293
        }
1294
    }
1295
1296
    if (maxResult < threshold) {
1297
        maxResult = internals.diagnoses.valid;
1298
    }
1299
1300
    const finishResult = diagnose ? maxResult : maxResult < internals.defaultThreshold;
1301
1302
    if (callback) {
1303
        callback(finishResult);
1304
    }
1305
1306
    return finishResult;
1307
};
1308
1309
1310
exports.diagnoses = internals.validate.diagnoses = (function () {
1311
1312
    const diag = {};
1313
    const keys = Object.keys(internals.diagnoses);
1314
    for (let i = 0; i < keys.length; ++i) {
1315
        const key = keys[i];
1316
        diag[key] = internals.diagnoses[key];
1317
    }
1318
1319
    return diag;
1320
})();
1321
1322
1323
exports.normalize = internals.normalize = function (email) {
1324
1325
    // $lab:coverage:off$
1326
    if (process.version[1] === '4' && email.indexOf('\u0000') >= 0) {
1327
        return internals.nulNormalize(email);
1328
    }
1329
    // $lab:coverage:on$
1330
1331
1332
    return email.normalize('NFC');
1333
};
1334