email.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. 'use strict';
  2. const Util = require('util');
  3. const Domain = require('./domain');
  4. const Errors = require('./errors');
  5. const internals = {
  6. nonAsciiRx: /[^\x00-\x7f]/,
  7. encoder: new (Util.TextEncoder || TextEncoder)() // $lab:coverage:ignore$
  8. };
  9. exports.analyze = function (email, options) {
  10. return internals.email(email, options);
  11. };
  12. exports.isValid = function (email, options) {
  13. return !internals.email(email, options);
  14. };
  15. internals.email = function (email, options = {}) {
  16. if (typeof email !== 'string') {
  17. throw new Error('Invalid input: email must be a string');
  18. }
  19. if (!email) {
  20. return Errors.code('EMPTY_STRING');
  21. }
  22. // Unicode
  23. const ascii = !internals.nonAsciiRx.test(email);
  24. if (!ascii) {
  25. if (options.allowUnicode === false) { // Defaults to true
  26. return Errors.code('FORBIDDEN_UNICODE');
  27. }
  28. email = email.normalize('NFC');
  29. }
  30. // Basic structure
  31. const parts = email.split('@');
  32. if (parts.length !== 2) {
  33. return parts.length > 2 ? Errors.code('MULTIPLE_AT_CHAR') : Errors.code('MISSING_AT_CHAR');
  34. }
  35. const [local, domain] = parts;
  36. if (!local) {
  37. return Errors.code('EMPTY_LOCAL');
  38. }
  39. if (!options.ignoreLength) {
  40. if (email.length > 254) { // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.3
  41. return Errors.code('ADDRESS_TOO_LONG');
  42. }
  43. if (internals.encoder.encode(local).length > 64) { // http://tools.ietf.org/html/rfc5321#section-4.5.3.1.1
  44. return Errors.code('LOCAL_TOO_LONG');
  45. }
  46. }
  47. // Validate parts
  48. return internals.local(local, ascii) || Domain.analyze(domain, options);
  49. };
  50. internals.local = function (local, ascii) {
  51. const segments = local.split('.');
  52. for (const segment of segments) {
  53. if (!segment.length) {
  54. return Errors.code('EMPTY_LOCAL_SEGMENT');
  55. }
  56. if (ascii) {
  57. if (!internals.atextRx.test(segment)) {
  58. return Errors.code('INVALID_LOCAL_CHARS');
  59. }
  60. continue;
  61. }
  62. for (const char of segment) {
  63. if (internals.atextRx.test(char)) {
  64. continue;
  65. }
  66. const binary = internals.binary(char);
  67. if (!internals.atomRx.test(binary)) {
  68. return Errors.code('INVALID_LOCAL_CHARS');
  69. }
  70. }
  71. }
  72. };
  73. internals.binary = function (char) {
  74. return Array.from(internals.encoder.encode(char)).map((v) => String.fromCharCode(v)).join('');
  75. };
  76. /*
  77. From RFC 5321:
  78. Mailbox = Local-part "@" ( Domain / address-literal )
  79. Local-part = Dot-string / Quoted-string
  80. Dot-string = Atom *("." Atom)
  81. Atom = 1*atext
  82. atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
  83. Domain = sub-domain *("." sub-domain)
  84. sub-domain = Let-dig [Ldh-str]
  85. Let-dig = ALPHA / DIGIT
  86. Ldh-str = *( ALPHA / DIGIT / "-" ) Let-dig
  87. ALPHA = %x41-5A / %x61-7A ; a-z, A-Z
  88. DIGIT = %x30-39 ; 0-9
  89. From RFC 6531:
  90. sub-domain =/ U-label
  91. atext =/ UTF8-non-ascii
  92. UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
  93. UTF8-2 = %xC2-DF UTF8-tail
  94. UTF8-3 = %xE0 %xA0-BF UTF8-tail /
  95. %xE1-EC 2( UTF8-tail ) /
  96. %xED %x80-9F UTF8-tail /
  97. %xEE-EF 2( UTF8-tail )
  98. UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) /
  99. %xF1-F3 3( UTF8-tail ) /
  100. %xF4 %x80-8F 2( UTF8-tail )
  101. UTF8-tail = %x80-BF
  102. Note: The following are not supported:
  103. RFC 5321: address-literal, Quoted-string
  104. RFC 5322: obs-*, CFWS
  105. */
  106. internals.atextRx = /^[\w!#\$%&'\*\+\-/=\?\^`\{\|\}~]+$/; // _ included in \w
  107. internals.atomRx = new RegExp([
  108. // %xC2-DF UTF8-tail
  109. '(?:[\\xc2-\\xdf][\\x80-\\xbf])',
  110. // %xE0 %xA0-BF UTF8-tail %xE1-EC 2( UTF8-tail ) %xED %x80-9F UTF8-tail %xEE-EF 2( UTF8-tail )
  111. '(?:\\xe0[\\xa0-\\xbf][\\x80-\\xbf])|(?:[\\xe1-\\xec][\\x80-\\xbf]{2})|(?:\\xed[\\x80-\\x9f][\\x80-\\xbf])|(?:[\\xee-\\xef][\\x80-\\xbf]{2})',
  112. // %xF0 %x90-BF 2( UTF8-tail ) %xF1-F3 3( UTF8-tail ) %xF4 %x80-8F 2( UTF8-tail )
  113. '(?:\\xf0[\\x90-\\xbf][\\x80-\\xbf]{2})|(?:[\\xf1-\\xf3][\\x80-\\xbf]{3})|(?:\\xf4[\\x80-\\x8f][\\x80-\\xbf]{2})'
  114. ].join('|'));