diff options
Diffstat (limited to 'modules/validatemail/validateEmail.php')
-rw-r--r-- | modules/validatemail/validateEmail.php | 638 |
1 files changed, 638 insertions, 0 deletions
diff --git a/modules/validatemail/validateEmail.php b/modules/validatemail/validateEmail.php new file mode 100644 index 00000000..e8cc037a --- /dev/null +++ b/modules/validatemail/validateEmail.php @@ -0,0 +1,638 @@ +<?php +/* +validateEmail.php +version 2.0 +by Clay Loveless <clay@killersoft.com> + + +Originally +By: Jon S. Stevens jon@clearink.com +Copyright 1998 Jon S. Stevens, Clear Ink +This code has all the normal disclaimers. +It is free for any use, just keep the credits intact. + +Enhancements and modifications: + + By: Shane Y. Gibson shane@tuna.org +Organization: The Unix Network Archives (http://www.tuna.org/) + Date: November 16th, 1998 + Changes: - Added **all** comments, as original code lacked them. + - Added some return codes to include a bit more description + for useability. + + By: berber +Organization: webdev.berber.co.il + Date: April 10th, 1999 + Changes: - The script now handles all kinds of domains (not only @xxx.yyy) as before. + - Added a debugging mode which also works as a verbose mode. + + By: Frank Vogel vogel@simec.com +Organization: Simec Corp. (http://www.simec.com) + Date: June 13th, 2000 + Changes: - Check for MX records for each qualification step of the domain name + - Use nobody@$SERVER_NAME as MAIL FROM: argument + Disclaimers: I disclaim nothing...nor do I claim anything...but + it would be nice if you included this disclaimer... + + + NOTE: berber and Frank Vogel made some of the same changes regarding + domain name checking to seperate versions of Shane Gibson's validateEmail variant. + Their changes have been merged into version 2.0. + + + By: Clay Loveless <clay@killersoft.com> +Organization: KillerSoft < http://www.killersoft.com/ > + Date: March 12th, 2002 + Changes: - Added 'Preferences' section, enabling several variables to be easily set + - Changed "nobody@$SERVER_NAME" for MAIL FROM: argument to be + "$from@$serverName" - set via Preferences section + - Signifcantly enhanced berber's 'debug' mode. It has become 'Verbose' mode + to ease debugging. + - Made 'Verbose' mode a function argument. Call validateEmail($email,1) to enable. + - Added environment detection - 'Verbose' output is adaptable to command-line + execution and embedded web execution. + - Added $socketTimeout Preferences variable for controlling how long we'll wait + during fsockopen() to any given host. + - Added $waitTimeout Preferences variable to control how long we'll wait for + a server we've successfully connected with to actually respond with an SMTP greeting. + Note -- this is a complete replacement of the previous "wait" method of simply + increasing a counter, which proved extremely inadequate in testing on sluggish hosts. + - Added $mxcutoff Preferences variable to control how many MX hosts we're willing to + talk to before calling it quits. (So we're not required to hear "no" from 14 + hotmail.com mail servers if we don't want to.) + - Added routine to check SMTP server greeting line for ESTMP, and respond accordingly + with EHLO. + - Added routines to listen for multi-line output from servers. + - Fixed all commands ending in "\n" to end in "\r\n" as specified by accurate SMTP + communication. THIS FIXES THE "HANG" PROBLEM EXPERIENCED WITH MANY MAIL SERVERS, + INCLUDING AOL.COM. (See Disclaimers about AOL.com connections, though ...) + - Added support for Jeffrey E.F. Friedl's definitive email format regex, translated + from perl into PHP. Will reject email addresses with invalid formatting before + opening any server connections. + - Changed initial "listening" routine to listen for one of two SMTP greeting responses + (220 or 421) instead of just listening for anything. validateEmail is now well-behaved + if a 421 "temporary rejection" code is received. + - Assorted optimizations -- using explode() instead of split(), preg_match() + instead of ereg(), etc. + - Improved error reporting on failures. + - Fixed typos in comments. : ) + - Modified comments where Shane Gibson's were no longer needed or accurate (due to changes). + Added the comments for features that didn't exist in Shane's version. + - Incremented version number. + + Disclaimers: - All additions and modifications Copyright 2002 KillerSoft.com. + - Program is free for any use as long as these notes & credits remain intact. + - Yes, I know there is no foolproof way to validate an e-mail address. But this is better than + nothing. + - Yes, I know that fewer and fewer mail servers are supporting the type of connection + and validation that this script performs. There are still a hell of a lot more of them + that DO support it than those that DON'T. Yes, this may change over time. + - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + - By using this code you agree to indemnify Clay Loveless, KillerSoft, and Crawlspace, Inc. + from any liability that might arise from its use. + - Use at your own risk. This may not work for you. It may produce results other than what you'd expect, + or even prefer. + - AOL.COM Disclaimer: As of this release, mail servers operated by AOL.com (netscape.com, + aol.com, cs.com, anything with aoltw.net, just to name a few) return "250" (recipient OK) codes for + _any_ address you throw at them. Bounces for invalid recipients are handled and sent out + through alternate means. So -- this script won't help you in validating AOL.com (and affiliated) + e-mail addresses. BUT ... at least it won't choke/hang on them either, as previous versions + of this script would. + + - Please send bugs, comments or suggestions to info@killersoft.com! +*/ + +/* This function takes in an email address (say 'shane@tuna.org') +* and tests to see if it's a valid email address. +* +* An array with the results is passed back to the caller. +* +* Possible result codes for the array items are: +* +* Item 0: [true|false] true for valid email address +* false for NON-valid email address +* +* Item 1: [SMTP Code] if a valid MX mail server found, then +* fill this array in with failed SMTP +* reply codes +* IF no MX mail server found or connected to, +* errors will be explained in this response. +* +* Possible Internal error messages: +* Invalid email address (bad domain name) [ default message from the old days ] +* fsockopen error $errno: $errstr +* 554 No MX records found for $domain +* 554 No DNS reverse record found for $domain +* +* (554 Response code borrowed from ESMTP's "Transaction failed" response) +* +* Item 2: [true|false] true for valid mail server found for +* host/domain +* false if no valid mail server found +* +* Item 3: [MX server] if a valid MX host was found and +* connected to then fill in this item +* with the MX server hostname +* +* EXAMPLE CODE for use is available at: +* http://www.killersoft.com/contrib/ +*/ + +function validateEmail ( $email, $verbose=0 ) { + global $SERVER_NAME; + + // DEFINE PREFERENCES + + // Passed along with the HELO/EHLO statement. + // Leave blank to use $SERVER_NAME. + // Note that most modern MTAs will ignore (but require) whatever you say here ... + // the server will determine your domain via other means. + if (conf('mail_domain')){ + $serverName = conf('mail_domain'); + } else { + $serverName = "domain.tld"; + } + // MAIL FROM -- who's asking? + // Good values: nobody, postmaster, info, buckwheat, gumby + $from = "info"; + + // fsockopen() timeout - in seconds + $socketTimeout = 15; + + // waitTimeout - how long we'll wait for a server to respond after + // a successful connection. In seconds. + // Recommended to keep this above 35 seconds - some servers are really slow. + $waitTimeout = 50; + + // MX Server cutoff + // Some hosts (like hotmail.com) have MANY MX hosts -- 12 or more. + // Set this to a number where you'd like to say "I get the picture" + // ... so you don't wind up having to hit EVERY MX host. + $mxcutoff = 15; + + // END OF PREFERENCES + + /////////////////////////////////////////////////////////////////////////////// + // DO NOT EDIT BELOW THIS LINE + /////////////////////////////////////////////////////////////////////////////// + + + // Default initiation statement + $send = "HELO"; + + // Let's give good commands + $CRLF = "\r\n"; + + // Make a few adjustments for verbose mode + if ( $verbose ) { + + // Version + $version = "validateEmail 2.0 - http://killersoft.com/contrib/"; + + // Start stopwatch + list ( $msecStart, $secStart ) = explode ( " ", microtime() ); + + // Adjust verbose output format + // for php.cgi or webserver interface + $sapi_type = php_sapi_name(); + if ( $sapi_type == "cgi" ) { + // format < > + $leftCarrot = "<"; + $rightCarrot = ">"; + // set type of "new line" + $vNL = "echo \"\n\";"; + // verbose Flush Only + $vFlush = ""; + // output for debugging + eval("echo \"Internal: $version - running as ".AddSlashes($sapi_type)."\"; $vNL"); + } else { + // format < > + $leftCarrot = "<"; + $rightCarrot = ">"; + // set type of "new line" ... flush output for web browsing + echo "<pre>"; + $vNL = "echo \"\n\"; flush();"; + // verbose Flush Only + $vFlush = "flush();"; + // output for debugging + eval("echo \"Internal: $version - running as ".AddSlashes($sapi_type)."\"; $vNL"); + } + } + + // How we'll identify ourselves in SMTP HELO/EHLO argument + if ( $serverName == "" ) $serverName = "$SERVER_NAME"; + if ( $serverName == "" ) $serverName = "localhost"; + + // Initialize return values with default + $return[0] = false; + $return[1] = "Invalid email address (bad domain name)"; + $return[2] = false; + $return[3] = ""; + + // make sure that we're dealing with a valid email address format + $isValid = true; // just in case validateEmailFormat is not available + if ( function_exists('validateEmailFormat') ) $isValid = validateEmailFormat ( $email ); + + // abort if necessary + if ( !$isValid ) { + if ( $verbose ) eval("echo \"Internal: $email format is invalid! Quitting ...\"; $vNL"); + return $return; + + } else { + if ( $verbose ) eval("echo \"Internal: $email is a valid RFC 822 formatted address\"; $vNL"); + + // assign our user part and domain parts respectively to seperate + // variables + list ( $user, $domain ) = explode ( "@", $email ); + if ( $verbose ) { + eval("echo \"Internal: user ..... $user\"; $vNL"); + eval("echo \"Internal: domain ... $domain\"; $vNL"); + } + + // split up the domain into sub-parts + $arr = explode ( ".", $domain ); + + // figure out how many parts there are in the host/domain name portion + $count = count ( $arr ); + + // flag to indicate success + $bSuccess = false; + + // we try this for each qualification step of domain name + // (from full qualified to TopLevel) + for ( $i = 0; $i < $count - 1 && !$bSuccess; $i = $i + 1 ) { + + // create the domain name + $domain = ""; + for ( $j = $i; $j < $count; $j = $j + 1 ) { + $domain = $domain . $arr[$j]; + if ( $j < $count - 1 ) + // tack on the last dot + $domain = $domain . "."; + } + if ( $verbose ) eval("echo \"Internal: checking DNS for $domain ... \"; $vNL"); + + // check that an MX record exists for Top-Level domain + // If it exists, start our email address checking + if ( checkdnsrr ( $domain, "MX" ) ) { + + // Okay -- we've got a valid DNS reverse record. + if ( $verbose ) eval("echo \"Internal: ... Check DNS RR OK!\"; $vNL"); + // Test that MX record for host exists, + // then fill 'mxhosts' and 'weight' arrays with correct info + if ( getmxrr ( $domain, $mxhosts, $weight ) ) { + + // Now we've got MX records + if ( $verbose ) { + eval("echo \"Internal: MX LOOKUP RESULTS:\"; $vNL"); + for ( $i = 0; $i < count ( $mxhosts ); $i++) { + eval("echo \" $mxhosts[$i]\"; $vNL"); + } + } + // sift through the 'mxhosts', connecting to each one + // ONLY until we get a good match + $mxcount = count( $mxhosts ); + // determine our MX host cutoff + $mxstop = ($mxcount > $mxcutoff) ? $mxcutoff : $mxcount; + for ( $i = 0; $i < $mxstop ; $i++ ) { + + // open socket on port 25 to mxhost, setting + // returned socket pointer to $sp + if( $verbose ) eval("echo \"Internal: attempting to open $mxhosts[$i] ...\"; $vNL"); + $sp = fsockopen ( $mxhosts[$i], 25, $errno, $errstr, $socketTimeout); + + // Greeting Code default + // Sets default greeting code to 421, just in case we + // don't ever hear ANYTHING from this host. + // If we hear nothing, we'll want to skip it. + $greetCode = "421"; + + // if $sp connection is good, let's rock on + if ( $sp ) { + if ( $verbose ) { + eval("echo \"* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\"; $vNL"); + eval("echo \"Internal: socket open to $mxhosts[$i]\"; $vNL"); + } + // work variables + $waitMarker = 0; + $msec = 0; // milisec count + $tsec = 0; // tensec count + $out = ""; + + // set our created socket for $sp to + // non-blocking mode so that our fgets() + // calls will return with a quickness + set_socket_blocking ( $sp, false ); + + // as long as our 'out' variable does not begin + // with a valid SMTP greeting (220 or 421), + // keep looping (do) until we get something + do { + + // prepare for clean debug output if necessary + // (puts a line break after the waitMarkers) + if ( $verbose && $msec > 0 ) { + $elapsed = $tsec + ($msec/4); + $clean = "echo \"($elapsed seconds)\"; $vNL"; + } + // output of the stream assigned to + // 'out' variable + $out = fgets ( $sp, 2500 ); + + // Check for multi-line output (###-) + if ( preg_match ( "/^2..-/", $out ) ) { + $end = false; + while ( !$end ) { + // keep listening + $line = fgets ( $sp, 2500 ); + $out .= $line; + if ( preg_match ( "/^2.. /", $line ) ) { + // the last line of output shouldn't + // have a dash after the response code + $end = true; + } + } + } + + if ( $verbose && $out != "" ) eval("$clean echo \"Server: ".AddSlashes($out)."\"; $vNL"); + + // if we get a "220" code (service ready), + // we're ready to rock on + if ( substr ( $out, 0, 3 ) == "220" ) { + if ( $verbose ) eval("echo \"Internal: service ready on $mxhosts[$i] ... moving on\"; $vNL"); + $return[2] = true; + $return[3] = "$mxhosts[$i]"; + // determine if we should speak in terms of HELO or EHLO + if ( preg_match ( "/ESMTP/", $out ) ) { + $send = "HELO"; + } else { + $send = "HELO"; + } + + // Set Greeting Code + $greetCode = "220"; + + } + + // else if ... + // Perhaps we've gotten a 421 Temporarily Refused error + else if ( substr ( $out, 0, 3 ) == "421" ) { + + //if ( $verbose ) echo " ... moving on\n"; + if ( $verbose ) eval("echo \"Internal: $mxhosts[$i] temporarily rejected connection. (421 response)\"; $vNL"); + $return[2] = false; + // Set Greeting Code + $greetCode = "421"; + break; // get out of this loop + + } + + // increase our waitTimeout counters + // if we still haven't heard anything ... + // Note that the time looping isn't an exact science + // with usleep or the Windows hack ... but + // it's in the ballpark. Close enough. + if ( $out == "" && $msec < $waitTimeout ) { + + // wait for a quarter of a second + if ( $verbose ) { + if ( $msec == 0 ) { + eval("echo \"Internal: Waiting: one '.' ~ 0.25 seconds of waiting\"; $vNL"); + } + eval("echo \".\"; $vFlush"); + $waitMarker++; + if ( $waitMarker == 40 ) { + // ten seconds + $tsec += 10; + eval("echo \" ($tsec seconds)\"; $vNL"); + $waitMarker = 0; + } + } + $msec = $msec + 0.25; + usleep(250000); + + } elseif ( $msec == $waitTimeout ) { + + // let's get out of here. Toooo sloooooww ... + if ( $verbose ) eval("$clean echo \"Internal: !! we've waited $waitTimeout seconds !!\nbreaking ...\"; $vNL"); + break; + + } + + + // end of 'do' loop + } while ( substr ( $out, 0, 3 ) != "220" ); + + // Make sure we got a "220" greetCode + // before we start shoveling requests + // at this server. + if ( $greetCode == "220" ) { + + // reset our file pointer to blocking mode, + // so we can wait for communication to finish + // before moving on ... + set_socket_blocking ( $sp, true ); + + // talk to the MX mail server, attempt to validate + // ourself. Use "HELO" or "EHLO", as determined above + fputs ( $sp, "$send $serverName"."$CRLF" ); + if ( $verbose ) eval("echo \"Client: $send $serverName\"; $vNL"); + + // get the mail server's reply, check it + // + $originalOutput = fgets ( $sp, 2500 ); + // Check for multi-line positive output + if ( preg_match ( "/^...-/", $originalOutput ) ) { + $end = false; + while ( !$end ) { + // keep listening + $line = fgets ( $sp, 2500 ); + $originalOutput .= $line; + if ( preg_match ( "/^... /", $line ) ) { + // the last line of output shouldn't + // have a dash after the response code + $end = true; + } + } + } + if ( $verbose ) eval("echo \"Server: ".AddSlashes($originalOutput)."\"; $vNL"); + + + // if there's a HELP option, let's see it + if ( $verbose ) { + if( preg_match( "/250.HELP/m", $originalOutput ) && $verbose == true ) { + + eval("echo \"Internal: VERBOSE-MODE ONLY: Getting the HELP output\"; $vNL"); + // Get the output of the HELP command + fputs ( $sp, "HELP"."$CRLF" ); + if ( $verbose ) eval("echo \"Client: HELP\"; $vNL"); + // Get output again + $output = fgets ( $sp, 2500 ); + // Check for multi-line positive output + if ( preg_match ( "/^...-/", $output ) ) { + $end = false; + while ( !$end ) { + // keep listening + $line = fgets ( $sp, 2500 ); + $output .= $line; + if ( preg_match ( "/^... /", $line ) ) { + // the last line of output shouldn't + // have a dash after the response code + $end = true; + } + } + } + if ( $verbose ) eval("echo \"Server: ".AddSlashes($output)."\"; $vNL"); + + } + } + + // Give the MAIL FROM: header to the server + fputs ( $sp, "MAIL FROM: <$from" . "@" . "$serverName" . ">"."$CRLF"); + if ( $verbose ) eval("echo \"Client: MAIL FROM: $leftCarrot"."$from" . "@" . "$serverName" . "$rightCarrot\"; $vNL"); + + // Get output again + $output = fgets ( $sp, 2500 ); + // Check for multi-line positive output + if ( preg_match ( "/^...-/", $output ) ) { + $end = false; + while ( !$end ) { + // keep listening + $line = fgets ( $sp, 2500 ); + $output .= $line; + if ( preg_match ( "/^... /", $line ) ) { + // the last line of output shouldn't + // have a dash after the response code + $end = true; + } + } + } + if ( $verbose ) eval("echo \"Server: ".AddSlashes($output)."\"; $vNL"); + + // Give the RCPT TO: header for the email address we're testing + fputs ( $sp, "RCPT TO: <$email>"."$CRLF" ); + if ( $verbose ) eval("echo \"Client: RCPT TO: $leftCarrot"."$email"."$rightCarrot\"; $vNL"); + + // Get output again + // This will be the one we check for validity + $output = fgets ( $sp, 2500 ); + // Check for multi-line positive output + if ( preg_match ( "/^...-/", $output ) ) { + $end = false; + while ( !$end ) { + // keep listening + $line = fgets ( $sp, 2500 ); + $output .= $line; + if ( preg_match ( "/^... /", $line ) ) { + // the last line of output shouldn't + // have a dash after the response code + $end = true; + } + } + } + if ( $verbose ) eval("echo \"Server: ".AddSlashes($output)."\"; $vNL"); + + // test the last reply code from the mail server + // for the 250 (okay) response + if ( substr ( $output, 0, 3 ) == "250" ) { + + // set our true/false(ness) + // array item for testing + $return[0] = true; + $return[1] = $output; + if ( $verbose ) eval("echo \"Internal: Check for 250 ... Recipient OK\"; $vNL"); + + } else { + + // we didn't get a 250 + // may be a bogus address + if ( $verbose ) eval("echo \"Internal: Check for 250 ... Response did not begin with 250!\"; $vNL"); + // fill in 2nd array item with mail server's + // reply for user to test if they want + $return[0] = false; + $return[1] = $output; + + } + + // tell the mail server we're done + fputs ( $sp, "QUIT"."$CRLF" ); + if ( $verbose ) { + eval("echo \"Client: QUIT\"; $vNL"); + eval("echo \"* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\"; $vNL $vNL"); + } + + // close the socket/file pointer + fclose ( $sp ); + + // If we got a good response back on RCPT TO, + // break here + // Otherwise, keep trying MX servers until we + // get a good response or run out of MX servers + // to try. + if ( $return[0] == true ) { + if ( $verbose ) { + eval("echo \"Internal: Recipient is OK - thanks, $mxhosts[$i]!\"; $vNL"); + eval("echo \"Internal: Stop checking MX hosts ...\"; $vNL"); + } + $bSuccess = true; + break; + } + + } else { + + // greetCode wasn't "220" + // we better skip this one and move on + if ( $verbose ) eval("echo \"Internal: SKIPPING $mxhosts[$i] -- never got 220 welcome\"; $vNL"); + // close out this connection + fclose ( $sp ); + + } // end of greetCode check + + } else { + // $sp socket pointer was false -- couldn't open it + if ( $verbose ) { + eval("echo \"* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\"; $vNL"); + eval("echo \"Internal: could not open socket to $mxhosts[$i]!\"; $vNL"); + eval("echo \"fsockopen error $errno: $errstr\"; $vNL"); + eval("echo \"* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\"; $vNL $vNL"); + } + $return[0] = false; + $return[1] = "fsockopen error $errno: $errstr"; + } // end of $sp check + + } // end for $mxhosts + + } // getmxrr test + else { + // getmxrr failed + if ( $verbose ) eval("echo \"Internal: No MX reverse records found for $domain\"; $vNL"); + $return[0] = false; + $return[1] = "554 No MX records found for $domain"; + } // end getmxrr test + + } // continue checkdnsrr test + else { + if ( $verbose ) eval("echo \"Internal: No DNS Reverse Record available!\"; $vNL"); + $return[0] = false; + $return[1] = "554 No DNS reverse record found for $domain"; + } // end checkdnsrr test + + } // end walking through each domain possibility + + } // end isValid + + // output elapsed time if Verbose + if ( $verbose ) { + list ( $msecStop, $secStop ) = explode ( " ", microtime() ); + $elapsedTime = (double)($secStop + $msecStop) - ($secStart + $msecStart); + $elapsedTime = number_format($elapsedTime,3); + eval("echo \"Internal: VERBOSE-MODE execution time: $elapsedTime seconds (silent mode somewhat faster)\"; $vNL"); + if ( $sapi_type != "cgi" ) echo "</pre>"; + } + + // return the array for the user to test against + return $return; + +} // END validateEmail-2.0 +?> |