|
| 1 | +# frozen_string_literal: true |
| 2 | +module JSONSchemer |
| 3 | + module Format |
| 4 | + module Email |
| 5 | + # https://datatracker.ietf.org/doc/html/rfc6531#section-3.3 |
| 6 | + # I think this is the same as "UTF8-non-ascii"? (https://datatracker.ietf.org/doc/html/rfc6532#section-3.1) |
| 7 | + UTF8_NON_ASCII = '[^[:ascii:]]' |
| 8 | + # https://datatracker.ietf.org/doc/html/rfc5321#section-4.1.2 |
| 9 | + A_TEXT = "([\\w!#$%&'*+\\-/=?\\^`{|}~]|#{UTF8_NON_ASCII})" # atext = ALPHA / DIGIT / ; Printable US-ASCII |
| 10 | + # "!" / "#" / ; characters not including |
| 11 | + # "$" / "%" / ; specials. Used for atoms. |
| 12 | + # "&" / "'" / |
| 13 | + # "*" / "+" / |
| 14 | + # "-" / "/" / |
| 15 | + # "=" / "?" / |
| 16 | + # "^" / "_" / |
| 17 | + # "`" / "{" / |
| 18 | + # "|" / "}" / |
| 19 | + # "~" |
| 20 | + Q_TEXT_SMTP = "([\\x20-\\x21\\x23-\\x5B\\x5D-\\x7E]|#{UTF8_NON_ASCII})" # qtextSMTP = %d32-33 / %d35-91 / %d93-126 |
| 21 | + # ; i.e., within a quoted string, any |
| 22 | + # ; ASCII graphic or space is permitted |
| 23 | + # ; without blackslash-quoting except |
| 24 | + # ; double-quote and the backslash itself. |
| 25 | + QUOTED_PAIR_SMTP = '\x5C[\x20-\x7E]' # quoted-pairSMTP = %d92 %d32-126 |
| 26 | + # ; i.e., backslash followed by any ASCII |
| 27 | + # ; graphic (including itself) or SPace |
| 28 | + Q_CONTENT_SMTP = "#{Q_TEXT_SMTP}|#{QUOTED_PAIR_SMTP}" # QcontentSMTP = qtextSMTP / quoted-pairSMTP |
| 29 | + QUOTED_STRING = "\"(#{Q_CONTENT_SMTP})*\"" # Quoted-string = DQUOTE *QcontentSMTP DQUOTE |
| 30 | + ATOM = "#{A_TEXT}+" # Atom = 1*atext |
| 31 | + DOT_STRING = "#{ATOM}(\\.#{ATOM})*" # Dot-string = Atom *("." Atom) |
| 32 | + LOCAL_PART = "#{DOT_STRING}|#{QUOTED_STRING}" # Local-part = Dot-string / Quoted-string |
| 33 | + # ; MAY be case-sensitive |
| 34 | + # IPv4-address-literal = Snum 3("." Snum) |
| 35 | + # using `valid_id?` to check ip addresses because it's complicated. # IPv6-address-literal = "IPv6:" IPv6-addr |
| 36 | + ADDRESS_LITERAL = '\[(IPv6:(?<ipv6>[\h:]+)|(?<ipv4>[\d.]+))\]' # address-literal = "[" ( IPv4-address-literal / |
| 37 | + # IPv6-address-literal / |
| 38 | + # General-address-literal ) "]" |
| 39 | + # ; See Section 4.1.3 |
| 40 | + # using `valid_hostname?` to check domain because it's complicated |
| 41 | + MAILBOX = "(#{LOCAL_PART})@(#{ADDRESS_LITERAL}|(?<domain>.+))" # Mailbox = Local-part "@" ( Domain / address-literal ) |
| 42 | + EMAIL_REGEX = /\A#{MAILBOX}\z/ |
| 43 | + |
| 44 | + def valid_email?(data) |
| 45 | + return false unless match = EMAIL_REGEX.match(data) |
| 46 | + if ipv4 = match.named_captures.fetch('ipv4') |
| 47 | + valid_ip?(ipv4, Socket::AF_INET) |
| 48 | + elsif ipv6 = match.named_captures.fetch('ipv6') |
| 49 | + valid_ip?(ipv6, Socket::AF_INET6) |
| 50 | + else |
| 51 | + valid_hostname?(match.named_captures.fetch('domain')) |
| 52 | + end |
| 53 | + end |
| 54 | + end |
| 55 | + end |
| 56 | +end |
0 commit comments