254) { return false; } // An @ can only appear in the local part inside quotes, and never in // the domain, so the last one is the separator $atIndex = strrpos($email, '@'); if ($atIndex === false) { return false; } // RFC 5321, section 4.5.3.1.1 if ($atIndex > 64) { return false; } $localPart = is_valid_email_code_points(substr($email, 0, $atIndex)); $domain = is_valid_email_code_points(substr($email, $atIndex + 1)); if (! is_valid_email_local_part($localPart)) { return false; } if (count($domain) && $domain[0] == 91) { if ($domain[count($domain) - 1] != 93) { return false; } return is_valid_email_address_literal(substr($email, $atIndex + 2, - 1)); } return is_valid_email_domain($domain); } /* Splits UTF-8 into Unicode code points. Returns false for anything that is * not valid UTF-8: stray continuation bytes, overlong forms, surrogates, or * values past U+10FFFF. */ function is_valid_email_code_points($text) { $points = array(); $length = strlen($text); for ($i = 0; $i < $length; $i ++) { $byte = ord($text[$i]); if ($byte < 0x80) { $points[] = $byte; continue; } if ($byte >= 0xC2 && $byte <= 0xDF) { $more = 1; $code = $byte & 0x1F; $smallest = 0x80; } elseif ($byte >= 0xE0 && $byte <= 0xEF) { $more = 2; $code = $byte & 0x0F; $smallest = 0x800; } elseif ($byte >= 0xF0 && $byte <= 0xF4) { $more = 3; $code = $byte & 0x07; $smallest = 0x10000; } else { return false; } if ($i + $more >= $length) { return false; } for ($j = 1; $j <= $more; $j ++) { $next = ord($text[$i + $j]); if (($next & 0xC0) != 0x80) { return false; } $code = ($code << 6) | ($next & 0x3F); } if ($code < $smallest || $code > 0x10FFFF || ($code >= 0xD800 && $code <= 0xDFFF)) { return false; } $points[] = $code; $i += $more; } return $points; } function is_valid_email_is_letter($code) { return ($code >= 65 && $code <= 90) || ($code >= 97 && $code <= 122); } function is_valid_email_is_digit($code) { return $code >= 48 && $code <= 57; } /* atext from RFC 5322, section 3.2.3, which RFC 5321 builds its atoms from. * RFC 6531 adds every character outside of ASCII. */ function is_valid_email_is_atext($code) { if ($code >= 0x80) { return true; } return is_valid_email_is_letter($code) || is_valid_email_is_digit($code) || strpos("!#$%&'*+-/=?^_`{|}~", chr($code)) !== false; } /* Local-part from RFC 5321, section 4.1.2: a Dot-string, which is atoms * separated by periods, or one Quoted-string around the whole thing. * RFC 5322 also accepts an obsolete form that quotes each piece on its own, * like "first".second, but SMTP does not, so neither does this. The * 64-octet limit was checked before this is called. */ function is_valid_email_local_part($points) { $count = count($points); if ($count == 0) { return false; } $last = $count - 1; if ($points[0] == 34) { // The quote that opens the string has to be the one that closes it, // so it must be the last character and the only unescaped quote // after the first if ($last == 0 || $points[$last] != 34) { return false; } for ($i = 1; $i < $last; $i ++) { $code = $points[$i]; if ($code == 92) { // quoted-pairSMTP: a backslash, then printable ASCII or a // space. RFC 6531 leaves this alone, so a character outside // of ASCII can not be escaped. $i ++; if ($i == $last || $points[$i] < 32 || $points[$i] > 126) { return false; } } elseif ($code == 34 || $code < 32 || ($code > 126 && $code < 0x80)) { // qtextSMTP is printable ASCII and the space, except for the // quote and the backslash, plus anything outside of ASCII // under RFC 6531. Tabs are not in it. return false; } } return true; } if ($points[0] == 46 || $points[$last] == 46) { return false; } for ($i = 0; $i <= $last; $i ++) { $code = $points[$i]; if ($code == 46) { if ($i < $last && $points[$i + 1] == 46) { return false; } } elseif (! is_valid_email_is_atext($code)) { return false; } } return true; } /* Domain from RFC 5321, section 4.1.2: labels of letters, digits, and * hyphens, separated by periods, where no label starts or ends with a * hyphen. Two hyphens in a row are fine, which internationalized domains * rely on. RFC 6531 also allows U-labels, the Unicode form of an * internationalized label. Which Unicode characters a U-label may use is a * long table in RFC 5892; that is not checked here. */ function is_valid_email_domain($points) { $labels = array(); $label = array(); foreach ($points as $code) { if ($code == 46) { $labels[] = $label; $label = array(); } else { $label[] = $code; } } $labels[] = $label; // The periods between the labels $length = count($labels) - 1; foreach ($labels as $label) { $count = count($label); // An empty label means a period at either end or two together if ($count == 0 || $label[0] == 45 || $label[$count - 1] == 45) { return false; } $ascii = true; foreach ($label as $code) { if ($code >= 0x80) { $ascii = false; } elseif (! is_valid_email_is_letter($code) && ! is_valid_email_is_digit($code) && $code != 45) { return false; } } if ($ascii) { // RFC 1035, section 2.3.4 if ($count > 63) { return false; } $length += $count; } else { // RFC 5891, section 4.2.3.1. The same shape is allowed in an // ASCII label, since SMTP does not run IDNA on those. if ($count > 3 && $label[2] == 45 && $label[3] == 45) { return false; } // RFC 5890, section 2.3.2.1: the A-label has to fit $aLabel = 'xn--' . is_valid_email_punycode($label); if (strlen($aLabel) > 63) { return false; } $length += strlen($aLabel); } } // RFC 1034, section 3.1: 255 octets, which includes a length byte for // every label and one for the root. That leaves 253 as text. if ($length > 253) { return false; } // A top-level domain is never all digits (RFC 3696, section 2), and every // one that exists is either all letters or an xn-- label $tld = $labels[count($labels) - 1]; foreach ($tld as $code) { if ($code >= 0x80) { return true; } } $text = ''; foreach ($tld as $code) { $text .= chr($code); } if (strlen($text) > 4 && strtolower(substr($text, 0, 4)) == 'xn--') { // Letters, digits, and hyphens were already checked return true; } foreach ($tld as $code) { if (! is_valid_email_is_letter($code)) { return false; } } return true; } /* address-literal from RFC 5321, section 4.1.3. Brackets hold an IP * address, not a name: [192.0.2.1], or [IPv6:2001:db8::1]. Other tags are * possible in the grammar, but a tag has to be registered with IANA, and * IPv6 is the only one that is. */ function is_valid_email_address_literal($text) { $length = strlen($text); for ($i = 0; $i < $length; $i ++) { $code = ord($text[$i]); if ($code < 33 || $code > 126) { return false; } } // Strings in ABNF are not case-sensitive (RFC 5234, section 2.3) if (strtolower(substr($text, 0, 5)) == 'ipv6:') { return is_valid_email_ipv6(substr($text, 5)); } return is_valid_email_ipv4($text); } /* IPv4-address-literal from RFC 5321, section 4.1.3: four numbers from 0 to * 255. Each is one to three digits, so leading zeros are allowed. */ function is_valid_email_ipv4($text) { $parts = explode('.', $text); if (count($parts) != 4) { return false; } foreach ($parts as $part) { $length = strlen($part); if ($length < 1 || $length > 3) { return false; } for ($i = 0; $i < $length; $i ++) { if (! is_valid_email_is_digit(ord($part[$i]))) { return false; } } if ((int) $part > 255) { return false; } } return true; } /* IPv6-addr from RFC 5321, section 4.1.3. Groups are one to four hex * digits. Without "::" there are exactly eight of them. "::" stands for at * least two groups of zeros, so it leaves room for six more. An IPv4 * address can take the place of the last two groups, which is what the * IPv6v4 forms of the grammar say. */ function is_valid_email_ipv6($text) { if (strpos($text, '.') !== false) { $lastColon = strrpos($text, ':'); if ($lastColon === false || ! is_valid_email_ipv4(substr($text, $lastColon + 1))) { return false; } $text = substr($text, 0, $lastColon + 1) . '0:0'; } $halves = explode('::', $text); if (count($halves) > 2) { return false; } $groups = array(); foreach ($halves as $half) { if ($half !== '') { $groups = array_merge($groups, explode(':', $half)); } } if (count($halves) == 1 ? count($groups) != 8 : count($groups) > 6) { return false; } foreach ($groups as $group) { $length = strlen($group); if ($length < 1 || $length > 4) { return false; } for ($i = 0; $i < $length; $i ++) { if (strpos('0123456789abcdefABCDEF', $group[$i]) === false) { return false; } } } return true; } /* Punycode from RFC 3492. It turns a Unicode label into the ASCII that * follows "xn--" in the A-label DNS actually stores, which has to fit in * 63 octets. */ function is_valid_email_punycode($points) { $output = ''; foreach ($points as $code) { if ($code < 0x80) { $output .= chr($code); } } $basic = strlen($output); $handled = $basic; if ($basic) { $output .= '-'; } $n = 128; $delta = 0; $bias = 72; while ($handled < count($points)) { $next = PHP_INT_MAX; foreach ($points as $code) { if ($code >= $n && $code < $next) { $next = $code; } } $delta += ($next - $n) * ($handled + 1); $n = $next; foreach ($points as $code) { if ($code < $n) { $delta ++; } if ($code == $n) { $q = $delta; for ($k = 36; true; $k += 36) { if ($k <= $bias) { $t = 1; } elseif ($k >= $bias + 26) { $t = 26; } else { $t = $k - $bias; } if ($q < $t) { break; } $output .= is_valid_email_punycode_digit($t + ($q - $t) % (36 - $t)); $q = intdiv($q - $t, 36 - $t); } $output .= is_valid_email_punycode_digit($q); $bias = is_valid_email_punycode_adapt($delta, $handled + 1, $handled == $basic); $delta = 0; $handled ++; } } $delta ++; $n ++; } return $output; } function is_valid_email_punycode_digit($value) { return chr($value < 26 ? $value + 97 : $value + 22); } function is_valid_email_punycode_adapt($delta, $count, $first) { $delta = $first ? intdiv($delta, 700) : intdiv($delta, 2); $delta += intdiv($delta, $count); $steps = 0; while ($delta > 455) { $delta = intdiv($delta, 35); $steps += 36; } return $steps + intdiv(36 * $delta, $delta + 38); }