Subversion Repositories javautils

Rev

Blame | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3.         #
  4.         # RFC3696 Email Parser
  5.         #
  6.         # By Cal Henderson <cal@iamcal.com>
  7.         #
  8.         # This code is dual licensed:
  9.         # CC Attribution-ShareAlike 2.5 - http://creativecommons.org/licenses/by-sa/2.5/
  10.         # GPLv3 - http://www.gnu.org/copyleft/gpl.html
  11.         #
  12.         # $Revision: 5039 $
  13.         #
  14.  
  15.         ##################################################################################
  16.  
  17.         function is_rfc3696_valid_email_address($email){
  18.  
  19.  
  20.                 ####################################################################################
  21.                 #
  22.                 # NO-WS-CTL       =       %d1-8 /         ; US-ASCII control characters
  23.                 #                         %d11 /          ;  that do not include the
  24.                 #                         %d12 /          ;  carriage return, line feed,
  25.                 #                         %d14-31 /       ;  and white space characters
  26.                 #                         %d127
  27.                 # ALPHA          =  %x41-5A / %x61-7A   ; A-Z / a-z
  28.                 # DIGIT          =  %x30-39
  29.  
  30.                 $no_ws_ctl      = "[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]";
  31.                 $alpha          = "[\\x41-\\x5a\\x61-\\x7a]";
  32.                 $digit          = "[\\x30-\\x39]";
  33.                 $cr             = "\\x0d";
  34.                 $lf             = "\\x0a";
  35.                 $crlf           = "(?:$cr$lf)";
  36.  
  37.  
  38.                 ####################################################################################
  39.                 #
  40.                 # obs-char        =       %d0-9 / %d11 /          ; %d0-127 except CR and
  41.                 #                         %d12 / %d14-127         ;  LF
  42.                 # obs-text        =       *LF *CR *(obs-char *LF *CR)
  43.                 # text            =       %d1-9 /         ; Characters excluding CR and LF
  44.                 #                         %d11 /
  45.                 #                         %d12 /
  46.                 #                         %d14-127 /
  47.                 #                         obs-text
  48.                 # obs-qp          =       "\" (%d0-127)
  49.                 # quoted-pair     =       ("\" text) / obs-qp
  50.  
  51.                 $obs_char       = "[\\x00-\\x09\\x0b\\x0c\\x0e-\\x7f]";
  52.                 $obs_text       = "(?:$lf*$cr*(?:$obs_char$lf*$cr*)*)";
  53.                 $text           = "(?:[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f]|$obs_text)";
  54.  
  55.                 #
  56.                 # there's an issue with the definition of 'text', since 'obs_text' can
  57.                 # be blank and that allows qp's with no character after the slash. we're
  58.                 # treating that as bad, so this just checks we have at least one
  59.                 # (non-CRLF) character
  60.                 #
  61.  
  62.                 $text           = "(?:$lf*$cr*$obs_char$lf*$cr*)";
  63.                 $obs_qp         = "(?:\\x5c[\\x00-\\x7f])";
  64.                 $quoted_pair    = "(?:\\x5c$text|$obs_qp)";
  65.  
  66.  
  67.                 ####################################################################################
  68.                 #
  69.                 # obs-FWS         =       1*WSP *(CRLF 1*WSP)
  70.                 # FWS             =       ([*WSP CRLF] 1*WSP) /   ; Folding white space
  71.                 #                         obs-FWS
  72.                 # ctext           =       NO-WS-CTL /     ; Non white space controls
  73.                 #                         %d33-39 /       ; The rest of the US-ASCII
  74.                 #                         %d42-91 /       ;  characters not including "(",
  75.                 #                         %d93-126        ;  ")", or "\"
  76.                 # ccontent        =       ctext / quoted-pair / comment
  77.                 # comment         =       "(" *([FWS] ccontent) [FWS] ")"
  78.                 # CFWS            =       *([FWS] comment) (([FWS] comment) / FWS)
  79.  
  80.                 #
  81.                 # note: we translate ccontent only partially to avoid an infinite loop
  82.                 # instead, we'll recursively strip *nested* comments before processing
  83.                 # the input. that will leave 'plain old comments' to be matched during
  84.                 # the main parse.
  85.                 #
  86.  
  87.                 $wsp            = "[\\x20\\x09]";
  88.                 $obs_fws        = "(?:$wsp+(?:$crlf$wsp+)*)";
  89.                 $fws            = "(?:(?:(?:$wsp*$crlf)?$wsp+)|$obs_fws)";
  90.                 $ctext          = "(?:$no_ws_ctl|[\\x21-\\x27\\x2A-\\x5b\\x5d-\\x7e])";
  91.                 $ccontent       = "(?:$ctext|$quoted_pair)";
  92.                 $comment        = "(?:\\x28(?:$fws?$ccontent)*$fws?\\x29)";
  93.                 $cfws           = "(?:(?:$fws?$comment)*(?:$fws?$comment|$fws))";
  94.  
  95.  
  96.                 #
  97.                 # these are the rules for removing *nested* comments. we'll just detect
  98.                 # outer comment and replace it with an empty comment, and recurse until
  99.                 # we stop.
  100.                 #
  101.  
  102.                 $outer_ccontent_dull    = "(?:$fws?$ctext|$quoted_pair)";
  103.                 $outer_ccontent_nest    = "(?:$fws?$comment)";
  104.                 $outer_comment          = "(?:\\x28$outer_ccontent_dull*(?:$outer_ccontent_nest$outer_ccontent_dull*)+$fws?\\x29)";
  105.  
  106.  
  107.                 ####################################################################################
  108.                 #
  109.                 # atext           =       ALPHA / DIGIT / ; Any character except controls,
  110.                 #                         "!" / "#" /     ;  SP, and specials.
  111.                 #                         "$" / "%" /     ;  Used for atoms
  112.                 #                         "&" / "'" /
  113.                 #                         "*" / "+" /
  114.                 #                         "-" / "/" /
  115.                 #                         "=" / "?" /
  116.                 #                         "^" / "_" /
  117.                 #                         "`" / "{" /
  118.                 #                         "|" / "}" /
  119.                 #                         "~"
  120.                 # atom            =       [CFWS] 1*atext [CFWS]
  121.  
  122.                 $atext          = "(?:$alpha|$digit|[\\x21\\x23-\\x27\\x2a\\x2b\\x2d\\x2f\\x3d\\x3f\\x5e\\x5f\\x60\\x7b-\\x7e])";
  123.                 $atom           = "(?:$cfws?(?:$atext)+$cfws?)";
  124.  
  125.  
  126.                 ####################################################################################
  127.                 #
  128.                 # qtext           =       NO-WS-CTL /     ; Non white space controls
  129.                 #                         %d33 /          ; The rest of the US-ASCII
  130.                 #                         %d35-91 /       ;  characters not including "\"
  131.                 #                         %d93-126        ;  or the quote character
  132.                 # qcontent        =       qtext / quoted-pair
  133.                 # quoted-string   =       [CFWS]
  134.                 #                         DQUOTE *([FWS] qcontent) [FWS] DQUOTE
  135.                 #                         [CFWS]
  136.                 # word            =       atom / quoted-string
  137.  
  138.                 $qtext          = "(?:$no_ws_ctl|[\\x21\\x23-\\x5b\\x5d-\\x7e])";
  139.                 $qcontent       = "(?:$qtext|$quoted_pair)";
  140.                 $quoted_string  = "(?:$cfws?\\x22(?:$fws?$qcontent)*$fws?\\x22$cfws?)";
  141.  
  142.                 #
  143.                 # changed the '*' to a '+' to require that quoted strings are not empty
  144.                 #
  145.  
  146.                 $quoted_string  = "(?:$cfws?\\x22(?:$fws?$qcontent)+$fws?\\x22$cfws?)";
  147.                 $word           = "(?:$atom|$quoted_string)";
  148.  
  149.  
  150.                 ####################################################################################
  151.                 #
  152.                 # obs-local-part  =       word *("." word)
  153.                 # obs-domain      =       atom *("." atom)
  154.  
  155.                 $obs_local_part = "(?:$word(?:\\x2e$word)*)";
  156.                 $obs_domain     = "(?:$atom(?:\\x2e$atom)*)";
  157.  
  158.  
  159.                 ####################################################################################
  160.                 #
  161.                 # dot-atom-text   =       1*atext *("." 1*atext)
  162.                 # dot-atom        =       [CFWS] dot-atom-text [CFWS]
  163.  
  164.                 $dot_atom_text  = "(?:$atext+(?:\\x2e$atext+)*)";
  165.                 $dot_atom       = "(?:$cfws?$dot_atom_text$cfws?)";
  166.  
  167.  
  168.                 ####################################################################################
  169.                 #
  170.                 # domain-literal  =       [CFWS] "[" *([FWS] dcontent) [FWS] "]" [CFWS]
  171.                 # dcontent        =       dtext / quoted-pair
  172.                 # dtext           =       NO-WS-CTL /     ; Non white space controls
  173.                 #
  174.                 #                         %d33-90 /       ; The rest of the US-ASCII
  175.                 #                         %d94-126        ;  characters not including "[",
  176.                 #                                         ;  "]", or "\"
  177.  
  178.                 $dtext          = "(?:$no_ws_ctl|[\\x21-\\x5a\\x5e-\\x7e])";
  179.                 $dcontent       = "(?:$dtext|$quoted_pair)";
  180.                 $domain_literal = "(?:$cfws?\\x5b(?:$fws?$dcontent)*$fws?\\x5d$cfws?)";
  181.  
  182.  
  183.                 ####################################################################################
  184.                 #
  185.                 # local-part      =       dot-atom / quoted-string / obs-local-part
  186.                 # domain          =       dot-atom / domain-literal / obs-domain
  187.                 # addr-spec       =       local-part "@" domain
  188.  
  189.                 $local_part     = "(($dot_atom)|($quoted_string)|($obs_local_part))";
  190.                 $domain         = "(($dot_atom)|($domain_literal)|($obs_domain))";
  191.                 $addr_spec      = "$local_part\\x40$domain";
  192.  
  193.  
  194.  
  195.                 #
  196.                 # see http://www.dominicsayers.com/isemail/ for details, but this should probably be 254
  197.                 #
  198.  
  199.                 if (strlen($email) > 256) return 0;
  200.  
  201.  
  202.                 #
  203.                 # we need to strip nested comments first - we replace them with a simple comment
  204.                 #
  205.  
  206.                 $email = rfc3696_strip_comments($outer_comment, $email, "(x)");
  207.  
  208.  
  209.                 #
  210.                 # now match what's left
  211.                 #
  212.  
  213.                 if (!preg_match("!^$addr_spec$!", $email, $m)){
  214.  
  215.                         return 0;
  216.                 }
  217.  
  218.                 $bits = array(
  219.                         'local'                 => isset($m[1]) ? $m[1] : '',
  220.                         'local-atom'            => isset($m[2]) ? $m[2] : '',
  221.                         'local-quoted'          => isset($m[3]) ? $m[3] : '',
  222.                         'local-obs'             => isset($m[4]) ? $m[4] : '',
  223.                         'domain'                => isset($m[5]) ? $m[5] : '',
  224.                         'domain-atom'           => isset($m[6]) ? $m[6] : '',
  225.                         'domain-literal'        => isset($m[7]) ? $m[7] : '',
  226.                         'domain-obs'            => isset($m[8]) ? $m[8] : '',
  227.                 );
  228.  
  229.  
  230.                 #
  231.                 # we need to now strip comments from $bits[local] and $bits[domain],
  232.                 # since we know they're i the right place and we want them out of the
  233.                 # way for checking IPs, label sizes, etc
  234.                 #
  235.  
  236.                 $bits['local']  = rfc3696_strip_comments($comment, $bits['local']);
  237.                 $bits['domain'] = rfc3696_strip_comments($comment, $bits['domain']);
  238.  
  239.  
  240.                 #
  241.                 # length limits on segments
  242.                 #
  243.  
  244.                 if (strlen($bits['local']) > 64) return 0;
  245.                 if (strlen($bits['domain']) > 255) return 0;
  246.  
  247.  
  248.                 #
  249.                 # restrictuions on domain-literals from RFC2821 section 4.1.3
  250.                 #
  251.  
  252.                 if (strlen($bits['domain-literal'])){
  253.  
  254.                         $Snum                   = "(\d{1,3})";
  255.                         $IPv4_address_literal   = "$Snum\.$Snum\.$Snum\.$Snum";
  256.  
  257.                         $IPv6_hex               = "(?:[0-9a-fA-F]{1,4})";
  258.  
  259.                         $IPv6_full              = "IPv6\:$IPv6_hex(:?\:$IPv6_hex){7}";
  260.  
  261.                         $IPv6_comp_part         = "(?:$IPv6_hex(?:\:$IPv6_hex){0,5})?";
  262.                         $IPv6_comp              = "IPv6\:($IPv6_comp_part\:\:$IPv6_comp_part)";
  263.  
  264.                         $IPv6v4_full            = "IPv6\:$IPv6_hex(?:\:$IPv6_hex){5}\:$IPv4_address_literal";
  265.  
  266.                         $IPv6v4_comp_part       = "$IPv6_hex(?:\:$IPv6_hex){0,3}";
  267.                         $IPv6v4_comp            = "IPv6\:((?:$IPv6v4_comp_part)?\:\:(?:$IPv6v4_comp_part\:)?)$IPv4_address_literal";
  268.  
  269.  
  270.                         #
  271.                         # IPv4 is simple
  272.                         #
  273.  
  274.                         if (preg_match("!^\[$IPv4_address_literal\]$!", $bits['domain'], $m)){
  275.  
  276.                                 if (intval($m[1]) > 255) return 0;
  277.                                 if (intval($m[2]) > 255) return 0;
  278.                                 if (intval($m[3]) > 255) return 0;
  279.                                 if (intval($m[4]) > 255) return 0;
  280.  
  281.                         }else{
  282.  
  283.                                 #
  284.                                 # this should be IPv6 - a bunch of tests are needed here :)
  285.                                 #
  286.  
  287.                                 while (1){
  288.  
  289.                                         if (preg_match("!^\[$IPv6_full\]$!", $bits['domain'])){
  290.                                                 break;
  291.                                         }
  292.  
  293.                                         if (preg_match("!^\[$IPv6_comp\]$!", $bits['domain'], $m)){
  294.                                                 list($a, $b) = explode('::', $m[1]);
  295.                                                 $folded = (strlen($a) && strlen($b)) ? "$a:$b" : "$a$b";
  296.                                                 $groups = explode(':', $folded);
  297.                                                 if (count($groups) > 6) return 0;
  298.                                                 break;
  299.                                         }
  300.  
  301.                                         if (preg_match("!^\[$IPv6v4_full\]$!", $bits['domain'], $m)){
  302.  
  303.                                                 if (intval($m[1]) > 255) return 0;
  304.                                                 if (intval($m[2]) > 255) return 0;
  305.                                                 if (intval($m[3]) > 255) return 0;
  306.                                                 if (intval($m[4]) > 255) return 0;
  307.                                                 break;
  308.                                         }
  309.  
  310.                                         if (preg_match("!^\[$IPv6v4_comp\]$!", $bits['domain'], $m)){
  311.                                                 list($a, $b) = explode('::', $m[1]);
  312.                                                 $b = substr($b, 0, -1); # remove the trailing colon before the IPv4 address
  313.                                                 $folded = (strlen($a) && strlen($b)) ? "$a:$b" : "$a$b";
  314.                                                 $groups = explode(':', $folded);
  315.                                                 if (count($groups) > 4) return 0;
  316.                                                 break;
  317.                                         }
  318.  
  319.                                         return 0;
  320.                                 }
  321.                         }                      
  322.                 }else{
  323.  
  324.                         #
  325.                         # the domain is either dot-atom or obs-domain - either way, it's
  326.                         # made up of simple labels and we split on dots
  327.                         #
  328.  
  329.                         $labels = explode('.', $bits['domain']);
  330.  
  331.  
  332.                         #
  333.                         # this is allowed by both dot-atom and obs-domain, but is un-routeable on the
  334.                         # public internet, so we'll fail it (e.g. user@localhost)
  335.                         #
  336.  
  337.                         if (count($labels) == 1) return 0;
  338.  
  339.  
  340.                         #
  341.                         # checks on each label
  342.                         #
  343.  
  344.                         foreach ($labels as $label){
  345.  
  346.                                 if (strlen($label) > 63) return 0;
  347.                                 if (substr($label, 0, 1) == '-') return 0;
  348.                                 if (substr($label, -1) == '-') return 0;
  349.                         }
  350.  
  351.  
  352.                         #
  353.                         # last label can't be all numeric
  354.                         #
  355.  
  356.                         if (preg_match('!^[0-9]+$!', array_pop($labels))) return 0;
  357.                 }
  358.  
  359.  
  360.                 return 1;
  361.         }
  362.  
  363.         ##################################################################################
  364.  
  365.         function rfc3696_strip_comments($comment, $email, $replace=''){
  366.  
  367.                 while (1){
  368.                         $new = preg_replace("!$comment!", $replace, $email);
  369.                         if (strlen($new) == strlen($email)){
  370.                                 return $email;
  371.                         }
  372.                         $email = $new;
  373.                 }
  374.         }
  375.  
  376.         ##################################################################################
  377. ?>