--- /dev/null
+<?php\r
+\r
+if (!defined("_ECRIRE_INC_VERSION")) return;\r
+\r
+/**\r
+ * @package isemail\r
+ * @author Dominic Sayers <dominic_sayers@hotmail.com>\r
+ * @copyright 2009 Dominic Sayers\r
+ * @license http://www.opensource.org/licenses/bsd-license.php BSD License\r
+ * @link http://www.dominicsayers.com/isemail\r
+ * @version 1.16 - Added optional diagnosis codes (amended all lines with a return statement)\r
+ */\r
+\r
+/*\r
+Copyright (c) 2008-2010, Dominic Sayers\r
+All rights reserved.\r
+\r
+Redistribution and use in source and binary forms, with or without modification,\r
+are permitted provided that the following conditions are met:\r
+\r
+ * Redistributions of source code must retain the above copyright notice, this\r
+ list of conditions and the following disclaimer.\r
+ * Redistributions in binary form must reproduce the above copyright notice,\r
+ this list of conditions and the following disclaimer in the documentation\r
+ and/or other materials provided with the distribution.\r
+ * Neither the name of Dominic Sayers nor the names of its contributors may be\r
+ used to endorse or promote products derived from this software without\r
+ specific prior written permission.\r
+\r
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND\r
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\r
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\r
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\r
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\r
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\r
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\r
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\r
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\r
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\r
+*/\r
+\r
+/*.\r
+ require_module 'standard';\r
+ require_module 'pcre';\r
+.*/\r
+/*.mixed.*/ function is_email (/*.string.*/ $email, $checkDNS = false, $diagnose = false) {\r
+ // Check that $email is a valid address. Read the following RFCs to understand the constraints:\r
+ // (http://tools.ietf.org/html/rfc5322)\r
+ // (http://tools.ietf.org/html/rfc3696)\r
+ // (http://tools.ietf.org/html/rfc5321)\r
+ // (http://tools.ietf.org/html/rfc4291#section-2.2)\r
+ // (http://tools.ietf.org/html/rfc1123#section-2.1)\r
+\r
+ if (!defined('ISEMAIL_VALID')) {\r
+ define('ISEMAIL_VALID' , 0);\r
+ define('ISEMAIL_TOOLONG' , 1);\r
+ define('ISEMAIL_NOAT' , 2);\r
+ define('ISEMAIL_NOLOCALPART' , 3);\r
+ define('ISEMAIL_NODOMAIN' , 4);\r
+ define('ISEMAIL_ZEROLENGTHELEMENT' , 5);\r
+ define('ISEMAIL_BADCOMMENT_START' , 6);\r
+ define('ISEMAIL_BADCOMMENT_END' , 7);\r
+ define('ISEMAIL_UNESCAPEDDELIM' , 8);\r
+ define('ISEMAIL_EMPTYELEMENT' , 9);\r
+ define('ISEMAIL_UNESCAPEDSPECIAL' , 10);\r
+ define('ISEMAIL_LOCALTOOLONG' , 11);\r
+ define('ISEMAIL_IPV4BADPREFIX' , 12);\r
+ define('ISEMAIL_IPV6BADPREFIXMIXED' , 13);\r
+ define('ISEMAIL_IPV6BADPREFIX' , 14);\r
+ define('ISEMAIL_IPV6GROUPCOUNT' , 15);\r
+ define('ISEMAIL_IPV6DOUBLEDOUBLECOLON' , 16);\r
+ define('ISEMAIL_IPV6BADCHAR' , 17);\r
+ define('ISEMAIL_IPV6TOOMANYGROUPS' , 18);\r
+ define('ISEMAIL_TLD' , 19);\r
+ define('ISEMAIL_DOMAINEMPTYELEMENT' , 20);\r
+ define('ISEMAIL_DOMAINELEMENTTOOLONG' , 21);\r
+ define('ISEMAIL_DOMAINBADCHAR' , 22);\r
+ define('ISEMAIL_DOMAINTOOLONG' , 23);\r
+ define('ISEMAIL_TLDNUMERIC' , 24);\r
+ define('ISEMAIL_DOMAINNOTFOUND' , 25);\r
+ define('ISEMAIL_NOTDEFINED' , 99);\r
+ }\r
+\r
+ // the upper limit on address lengths should normally be considered to be 256\r
+ // (http://www.rfc-editor.org/errata_search.php?rfc=3696)\r
+ // NB I think John Klensin is misreading RFC 5321 and the the limit should actually be 254\r
+ // However, I will stick to the published number until it is changed.\r
+ //\r
+ // The maximum total length of a reverse-path or forward-path is 256\r
+ // characters (including the punctuation and element separators)\r
+ // (http://tools.ietf.org/html/rfc5321#section-4.5.3.1.3)\r
+ $emailLength = strlen($email);\r
+ if ($emailLength > 256) return $diagnose ? ISEMAIL_TOOLONG : false; // Too long\r
+\r
+ // Contemporary email addresses consist of a "local part" separated from\r
+ // a "domain part" (a fully-qualified domain name) by an at-sign ("@").\r
+ // (http://tools.ietf.org/html/rfc3696#section-3)\r
+ $atIndex = strrpos($email,'@');\r
+\r
+ if ($atIndex === false) return $diagnose ? ISEMAIL_NOAT : false; // No at-sign\r
+ if ($atIndex === 0) return $diagnose ? ISEMAIL_NOLOCALPART : false; // No local part\r
+ if ($atIndex === $emailLength - 1) return $diagnose ? ISEMAIL_NODOMAIN : false; // No domain part\r
+// revision 1.14: Length test bug suggested by Andrew Campbell of Gloucester, MA\r
+ \r
+ // Sanitize comments\r
+ // - remove nested comments, quotes and dots in comments\r
+ // - remove parentheses and dots from quoted strings\r
+ $braceDepth = 0;\r
+ $inQuote = false;\r
+ $escapeThisChar = false;\r
+\r
+ for ($i = 0; $i < $emailLength; ++$i) {\r
+ $char = $email[$i];\r
+ $replaceChar = false;\r
+\r
+ if ($char === '\\') {\r
+ $escapeThisChar = !$escapeThisChar; // Escape the next character?\r
+ } else {\r
+ switch ($char) {\r
+ case '(':\r
+ if ($escapeThisChar) {\r
+ $replaceChar = true;\r
+ } else {\r
+ if ($inQuote) {\r
+ $replaceChar = true;\r
+ } else {\r
+ if ($braceDepth++ > 0) $replaceChar = true; // Increment brace depth\r
+ }\r
+ }\r
+\r
+ break;\r
+ case ')':\r
+ if ($escapeThisChar) {\r
+ $replaceChar = true;\r
+ } else {\r
+ if ($inQuote) {\r
+ $replaceChar = true;\r
+ } else {\r
+ if (--$braceDepth > 0) $replaceChar = true; // Decrement brace depth\r
+ if ($braceDepth < 0) $braceDepth = 0;\r
+ }\r
+ }\r
+\r
+ break;\r
+ case '"':\r
+ if ($escapeThisChar) {\r
+ $replaceChar = true;\r
+ } else {\r
+ if ($braceDepth === 0) {\r
+ $inQuote = !$inQuote; // Are we inside a quoted string?\r
+ } else {\r
+ $replaceChar = true;\r
+ }\r
+ }\r
+\r
+ break;\r
+ case '.': // Dots don't help us either\r
+ if ($escapeThisChar) {\r
+ $replaceChar = true;\r
+ } else {\r
+ if ($braceDepth > 0) $replaceChar = true;\r
+ }\r
+\r
+ break;\r
+ default:\r
+ }\r
+\r
+ $escapeThisChar = false;\r
+// if ($replaceChar) $email[$i] = 'x'; // Replace the offending character with something harmless\r
+// revision 1.12: Line above replaced because PHPLint doesn't like that syntax\r
+ if ($replaceChar) $email = (string) substr_replace($email, 'x', $i, 1); // Replace the offending character with something harmless\r
+ }\r
+ }\r
+\r
+ $localPart = substr($email, 0, $atIndex);\r
+ $domain = substr($email, $atIndex + 1);\r
+ $FWS = "(?:(?:(?:[ \\t]*(?:\\r\\n))?[ \\t]+)|(?:[ \\t]+(?:(?:\\r\\n)[ \\t]+)*))"; // Folding white space\r
+ // Let's check the local part for RFC compliance...\r
+ //\r
+ // local-part = dot-atom / quoted-string / obs-local-part\r
+ // obs-local-part = word *("." word)\r
+ // (http://tools.ietf.org/html/rfc5322#section-3.4.1)\r
+ //\r
+ // Problem: need to distinguish between "first.last" and "first"."last"\r
+ // (i.e. one element or two). And I suck at regexes.\r
+ $dotArray = /*. (array[int]string) .*/ preg_split('/\\.(?=(?:[^\\"]*\\"[^\\"]*\\")*(?![^\\"]*\\"))/m', $localPart);\r
+ $partLength = 0;\r
+\r
+ foreach ($dotArray as $element) {\r
+ // Remove any leading or trailing FWS\r
+ $element = preg_replace("/^$FWS|$FWS\$/", '', $element);\r
+ $elementLength = strlen($element);\r
+\r
+ if ($elementLength === 0) return $diagnose ? ISEMAIL_ZEROLENGTHELEMENT : false; // Can't have empty element (consecutive dots or dots at the start or end)\r
+// revision 1.15: Speed up the test and get rid of "unitialized string offset" notices from PHP\r
+\r
+ // We need to remove any valid comments (i.e. those at the start or end of the element)\r
+ if ($element[0] === '(') {\r
+ $indexBrace = strpos($element, ')');\r
+ if ($indexBrace !== false) {\r
+ if (preg_match('/(?<!\\\\)[\\(\\)]/', substr($element, 1, $indexBrace - 1)) > 0) {\r
+ return $diagnose ? ISEMAIL_BADCOMMENT_START : false; // Illegal characters in comment\r
+ }\r
+ $element = substr($element, $indexBrace + 1, $elementLength - $indexBrace - 1);\r
+ $elementLength = strlen($element);\r
+ }\r
+ }\r
+ \r
+ if ($element[$elementLength - 1] === ')') {\r
+ $indexBrace = strrpos($element, '(');\r
+ if ($indexBrace !== false) {\r
+ if (preg_match('/(?<!\\\\)(?:[\\(\\)])/', substr($element, $indexBrace + 1, $elementLength - $indexBrace - 2)) > 0) {\r
+ return $diagnose ? ISEMAIL_BADCOMMENT_END : false; // Illegal characters in comment\r
+ }\r
+ $element = substr($element, 0, $indexBrace);\r
+ $elementLength = strlen($element);\r
+ }\r
+ }\r
+\r
+ // Remove any leading or trailing FWS around the element (inside any comments)\r
+ $element = preg_replace("/^$FWS|$FWS\$/", '', $element);\r
+\r
+ // What's left counts towards the maximum length for this part\r
+ if ($partLength > 0) $partLength++; // for the dot\r
+ $partLength += strlen($element);\r
+\r
+ // Each dot-delimited component can be an atom or a quoted string\r
+ // (because of the obs-local-part provision)\r
+ if (preg_match('/^"(?:.)*"$/s', $element) > 0) {\r
+ // Quoted-string tests:\r
+ //\r
+ // Remove any FWS\r
+ $element = preg_replace("/(?<!\\\\)$FWS/", '', $element);\r
+ // My regex skillz aren't up to distinguishing between \" \\" \\\" \\\\" etc.\r
+ // So remove all \\ from the string first...\r
+ $element = preg_replace('/\\\\\\\\/', ' ', $element);\r
+ if (preg_match('/(?<!\\\\|^)["\\r\\n\\x00](?!$)|\\\\"$|""/', $element) > 0) return $diagnose ? ISEMAIL_UNESCAPEDDELIM : false; // ", CR, LF and NUL must be escaped, "" is too short\r
+ } else {\r
+ // Unquoted string tests:\r
+ //\r
+ // Period (".") may...appear, but may not be used to start or end the\r
+ // local part, nor may two or more consecutive periods appear.\r
+ // (http://tools.ietf.org/html/rfc3696#section-3)\r
+ //\r
+ // A zero-length element implies a period at the beginning or end of the\r
+ // local part, or two periods together. Either way it's not allowed.\r
+ if ($element === '') return $diagnose ? ISEMAIL_EMPTYELEMENT : false; // Dots in wrong place\r
+\r
+ // Any ASCII graphic (printing) character other than the\r
+ // at-sign ("@"), backslash, double quote, comma, or square brackets may\r
+ // appear without quoting. If any of that list of excluded characters\r
+ // are to appear, they must be quoted\r
+ // (http://tools.ietf.org/html/rfc3696#section-3)\r
+ //\r
+ // Any excluded characters? i.e. 0x00-0x20, (, ), <, >, [, ], :, ;, @, \, comma, period, "\r
+ if (preg_match('/[\\x00-\\x20\\(\\)<>\\[\\]:;@\\\\,\\."]/', $element) > 0) return $diagnose ? ISEMAIL_UNESCAPEDSPECIAL : false; // These characters must be in a quoted string\r
+ }\r
+ }\r
+\r
+ if ($partLength > 64) return $diagnose ? ISEMAIL_LOCALTOOLONG : false; // Local part must be 64 characters or less\r
+\r
+ // Now let's check the domain part...\r
+\r
+ // The domain name can also be replaced by an IP address in square brackets\r
+ // (http://tools.ietf.org/html/rfc3696#section-3)\r
+ // (http://tools.ietf.org/html/rfc5321#section-4.1.3)\r
+ // (http://tools.ietf.org/html/rfc4291#section-2.2)\r
+ if (preg_match('/^\\[(.)+]$/', $domain) === 1) {\r
+ // It's an address-literal\r
+ $addressLiteral = substr($domain, 1, strlen($domain) - 2);\r
+ $matchesIP = array();\r
+ \r
+ // Extract IPv4 part from the end of the address-literal (if there is one)\r
+ if (preg_match('/\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/', $addressLiteral, $matchesIP) > 0) {\r
+ $index = strrpos($addressLiteral, $matchesIP[0]);\r
+ \r
+ if ($index === 0) {\r
+ // Nothing there except a valid IPv4 address, so...\r
+ return $diagnose ? ISEMAIL_VALID : true;\r
+ } else {\r
+ // Assume it's an attempt at a mixed address (IPv6 + IPv4)\r
+ if ($addressLiteral[$index - 1] !== ':') return $diagnose ? ISEMAIL_IPV4BADPREFIX : false; // Character preceding IPv4 address must be ':'\r
+ if (substr($addressLiteral, 0, 5) !== 'IPv6:') return $diagnose ? ISEMAIL_IPV6BADPREFIXMIXED : false; // RFC5321 section 4.1.3\r
+\r
+ $IPv6 = substr($addressLiteral, 5, ($index ===7) ? 2 : $index - 6);\r
+ $groupMax = 6;\r
+ }\r
+ } else {\r
+ // It must be an attempt at pure IPv6\r
+ if (substr($addressLiteral, 0, 5) !== 'IPv6:') return $diagnose ? ISEMAIL_IPV6BADPREFIX : false; // RFC5321 section 4.1.3\r
+ $IPv6 = substr($addressLiteral, 5);\r
+ $groupMax = 8;\r
+ }\r
+\r
+ $groupCount = preg_match_all('/^[0-9a-fA-F]{0,4}|\\:[0-9a-fA-F]{0,4}|(.)/', $IPv6, $matchesIP);\r
+ $index = strpos($IPv6,'::');\r
+\r
+ if ($index === false) {\r
+ // We need exactly the right number of groups\r
+ if ($groupCount !== $groupMax) return $diagnose ? ISEMAIL_IPV6GROUPCOUNT : false; // RFC5321 section 4.1.3\r
+ } else {\r
+ if ($index !== strrpos($IPv6,'::')) return $diagnose ? ISEMAIL_IPV6DOUBLEDOUBLECOLON : false; // More than one '::'\r
+ $groupMax = ($index === 0 || $index === (strlen($IPv6) - 2)) ? $groupMax : $groupMax - 1;\r
+ if ($groupCount > $groupMax) return $diagnose ? ISEMAIL_IPV6TOOMANYGROUPS : false; // Too many IPv6 groups in address\r
+ }\r
+\r
+ // Check for unmatched characters\r
+ array_multisort($matchesIP[1], SORT_DESC);\r
+ if ($matchesIP[1][0] !== '') return $diagnose ? ISEMAIL_IPV6BADCHAR : false; // Illegal characters in address\r
+\r
+ // It's a valid IPv6 address, so...\r
+ return $diagnose ? ISEMAIL_VALID : true;\r
+ } else {\r
+ // It's a domain name...\r
+\r
+ // The syntax of a legal Internet host name was specified in RFC-952\r
+ // One aspect of host name syntax is hereby changed: the\r
+ // restriction on the first character is relaxed to allow either a\r
+ // letter or a digit.\r
+ // (http://tools.ietf.org/html/rfc1123#section-2.1)\r
+ //\r
+ // NB RFC 1123 updates RFC 1035, but this is not currently apparent from reading RFC 1035.\r
+ //\r
+ // Most common applications, including email and the Web, will generally not\r
+ // permit...escaped strings\r
+ // (http://tools.ietf.org/html/rfc3696#section-2)\r
+ //\r
+ // the better strategy has now become to make the "at least one period" test,\r
+ // to verify LDH conformance (including verification that the apparent TLD name\r
+ // is not all-numeric)\r
+ // (http://tools.ietf.org/html/rfc3696#section-2)\r
+ //\r
+ // Characters outside the set of alphabetic characters, digits, and hyphen MUST NOT appear in domain name\r
+ // labels for SMTP clients or servers\r
+ // (http://tools.ietf.org/html/rfc5321#section-4.1.2)\r
+ //\r
+ // RFC5321 precludes the use of a trailing dot in a domain name for SMTP purposes\r
+ // (http://tools.ietf.org/html/rfc5321#section-4.1.2)\r
+ $dotArray = /*. (array[int]string) .*/ preg_split('/\\.(?=(?:[^\\"]*\\"[^\\"]*\\")*(?![^\\"]*\\"))/m', $domain);\r
+ $partLength = 0;\r
+ $element = ''; // Since we use $element after the foreach loop let's make sure it has a value\r
+// revision 1.13: Line above added because PHPLint now checks for Definitely Assigned Variables\r
+\r
+ if (count($dotArray) === 1) return $diagnose ? ISEMAIL_TLD : false; // Mail host can't be a TLD (cite? What about localhost?)\r
+\r
+ foreach ($dotArray as $element) {\r
+ // Remove any leading or trailing FWS\r
+ $element = preg_replace("/^$FWS|$FWS\$/", '', $element);\r
+ $elementLength = strlen($element);\r
+ \r
+ // Each dot-delimited component must be of type atext\r
+ // A zero-length element implies a period at the beginning or end of the\r
+ // local part, or two periods together. Either way it's not allowed.\r
+ if ($elementLength === 0) return $diagnose ? ISEMAIL_DOMAINEMPTYELEMENT : false; // Dots in wrong place\r
+// revision 1.15: Speed up the test and get rid of "unitialized string offset" notices from PHP\r
+ \r
+ // Then we need to remove all valid comments (i.e. those at the start or end of the element\r
+ if ($element[0] === '(') {\r
+ $indexBrace = strpos($element, ')');\r
+ if ($indexBrace !== false) {\r
+ if (preg_match('/(?<!\\\\)[\\(\\)]/', substr($element, 1, $indexBrace - 1)) > 0) {\r
+ return $diagnose ? ISEMAIL_BADCOMMENTSTART : false; // Illegal characters in comment\r
+ }\r
+ $element = substr($element, $indexBrace + 1, $elementLength - $indexBrace - 1);\r
+ $elementLength = strlen($element);\r
+ }\r
+ }\r
+ \r
+ if ($element[$elementLength - 1] === ')') {\r
+ $indexBrace = strrpos($element, '(');\r
+ if ($indexBrace !== false) {\r
+ if (preg_match('/(?<!\\\\)(?:[\\(\\)])/', substr($element, $indexBrace + 1, $elementLength - $indexBrace - 2)) > 0)\r
+ return $diagnose ? ISEMAIL_BADCOMMENTEND : false; // Illegal characters in comment\r
+\r
+ $element = substr($element, 0, $indexBrace);\r
+ $elementLength = strlen($element);\r
+ }\r
+ } \r
+ \r
+ // Remove any leading or trailing FWS around the element (inside any comments)\r
+ $element = preg_replace("/^$FWS|$FWS\$/", '', $element);\r
+ \r
+ // What's left counts towards the maximum length for this part\r
+ if ($partLength > 0) $partLength++; // for the dot\r
+ $partLength += strlen($element);\r
+ \r
+ // The DNS defines domain name syntax very generally -- a\r
+ // string of labels each containing up to 63 8-bit octets,\r
+ // separated by dots, and with a maximum total of 255\r
+ // octets.\r
+ // (http://tools.ietf.org/html/rfc1123#section-6.1.3.5)\r
+ if ($elementLength > 63) return $diagnose ? ISEMAIL_DOMAINELEMENTTOOLONG : false; // Label must be 63 characters or less\r
+ \r
+ // Any ASCII graphic (printing) character other than the\r
+ // at-sign ("@"), backslash, double quote, comma, or square brackets may\r
+ // appear without quoting. If any of that list of excluded characters\r
+ // are to appear, they must be quoted\r
+ // (http://tools.ietf.org/html/rfc3696#section-3)\r
+ //\r
+ // If the hyphen is used, it is not permitted to appear at\r
+ // either the beginning or end of a label.\r
+ // (http://tools.ietf.org/html/rfc3696#section-2)\r
+ //\r
+ // Any excluded characters? i.e. 0x00-0x20, (, ), <, >, [, ], :, ;, @, \, comma, period, "\r
+ if (preg_match('/[\\x00-\\x20\\(\\)<>\\[\\]:;@\\\\,\\."]|^-|-$/', $element) > 0) {\r
+ return $diagnose ? ISEMAIL_DOMAINBADCHAR : false;\r
+ }\r
+ }\r
+\r
+ if ($partLength > 255) return $diagnose ? ISEMAIL_DOMAINTOOLONG : false; // Domain part must be 255 characters or less (http://tools.ietf.org/html/rfc1123#section-6.1.3.5)\r
+\r
+ if (preg_match('/^[0-9]+$/', $element) > 0) return $diagnose ? ISEMAIL_TLDNUMERIC : false; // TLD can't be all-numeric (http://www.apps.ietf.org/rfc/rfc3696.html#sec-2)\r
+\r
+ // Check DNS?\r
+ if ($checkDNS && function_exists('checkdnsrr')) {\r
+ if (!(checkdnsrr($domain, 'A') || checkdnsrr($domain, 'MX'))) {\r
+ return $diagnose ? ISEMAIL_DOMAINNOTFOUND : false; // Domain doesn't actually exist\r
+ }\r
+ }\r
+ }\r
+\r
+ // Eliminate all other factors, and the one which remains must be the truth.\r
+ // (Sherlock Holmes, The Sign of Four)\r
+ return $diagnose ? ISEMAIL_VALID : true;\r
+}\r
+?>\r