404 lines
12 KiB
PHP
404 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* Class to validate the email address
|
|
*
|
|
* @author CodexWorld.com <contact@codexworld.com>
|
|
* @copyright Copyright (c) 2018, CodexWorld.com
|
|
* @url https://www.codexworld.com
|
|
*/
|
|
class VerifyEmail {
|
|
|
|
protected $stream = false;
|
|
|
|
/**
|
|
* SMTP port number
|
|
* @var int
|
|
*/
|
|
protected $port = 25;
|
|
|
|
/**
|
|
* Email address for request
|
|
* @var string
|
|
*/
|
|
protected $from = 'root@localhost';
|
|
|
|
/**
|
|
* The connection timeout, in seconds.
|
|
* @var int
|
|
*/
|
|
protected $max_connection_timeout = 30;
|
|
|
|
/**
|
|
* Timeout value on stream, in seconds.
|
|
* @var int
|
|
*/
|
|
protected $stream_timeout = 5;
|
|
|
|
/**
|
|
* Wait timeout on stream, in seconds.
|
|
* * 0 - not wait
|
|
* @var int
|
|
*/
|
|
protected $stream_timeout_wait = 0;
|
|
|
|
/**
|
|
* Whether to throw exceptions for errors.
|
|
* @type boolean
|
|
* @access protected
|
|
*/
|
|
protected $exceptions = false;
|
|
|
|
/**
|
|
* The number of errors encountered.
|
|
* @type integer
|
|
* @access protected
|
|
*/
|
|
protected $error_count = 0;
|
|
|
|
/**
|
|
* class debug output mode.
|
|
* @type boolean
|
|
*/
|
|
public $Debug = false;
|
|
|
|
/**
|
|
* How to handle debug output.
|
|
* Options:
|
|
* * `echo` Output plain-text as-is, appropriate for CLI
|
|
* * `html` Output escaped, line breaks converted to `<br>`, appropriate for browser output
|
|
* * `log` Output to error log as configured in php.ini
|
|
* @type string
|
|
*/
|
|
public $Debugoutput = 'echo';
|
|
|
|
/**
|
|
* SMTP RFC standard line ending.
|
|
*/
|
|
const CRLF = "\r\n";
|
|
|
|
/**
|
|
* Holds the most recent error message.
|
|
* @type string
|
|
*/
|
|
public $ErrorInfo = '';
|
|
|
|
/**
|
|
* Constructor.
|
|
* @param boolean $exceptions Should we throw external exceptions?
|
|
*/
|
|
public function __construct($exceptions = false) {
|
|
$this->exceptions = (boolean) $exceptions;
|
|
}
|
|
|
|
/**
|
|
* Set email address for SMTP request
|
|
* @param string $email Email address
|
|
*/
|
|
public function setEmailFrom($email) {
|
|
if (!self::validate($email)) {
|
|
$this->set_error('Invalid address : ' . $email);
|
|
$this->edebug($this->ErrorInfo);
|
|
if ($this->exceptions) {
|
|
throw new verifyEmailException($this->ErrorInfo);
|
|
}
|
|
}
|
|
$this->from = $email;
|
|
}
|
|
|
|
/**
|
|
* Set connection timeout, in seconds.
|
|
* @param int $seconds
|
|
*/
|
|
public function setConnectionTimeout($seconds) {
|
|
if ($seconds > 0) {
|
|
$this->max_connection_timeout = (int) $seconds;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the timeout value on stream, expressed in the seconds
|
|
* @param int $seconds
|
|
*/
|
|
public function setStreamTimeout($seconds) {
|
|
if ($seconds > 0) {
|
|
$this->stream_timeout = (int) $seconds;
|
|
}
|
|
}
|
|
|
|
public function setStreamTimeoutWait($seconds) {
|
|
if ($seconds >= 0) {
|
|
$this->stream_timeout_wait = (int) $seconds;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate email address.
|
|
* @param string $email
|
|
* @return boolean True if valid.
|
|
*/
|
|
public static function validate($email) {
|
|
return (boolean) filter_var($email, FILTER_VALIDATE_EMAIL);
|
|
}
|
|
|
|
/**
|
|
* Get array of MX records for host. Sort by weight information.
|
|
* @param string $hostname The Internet host name.
|
|
* @return array Array of the MX records found.
|
|
*/
|
|
public function getMXrecords($hostname) {
|
|
$mxhosts = array();
|
|
$mxweights = array();
|
|
if (getmxrr($hostname, $mxhosts, $mxweights) === FALSE) {
|
|
$this->set_error('MX records not found or an error occurred');
|
|
$this->edebug($this->ErrorInfo);
|
|
} else {
|
|
array_multisort($mxweights, $mxhosts);
|
|
}
|
|
/**
|
|
* Add A-record as last chance (e.g. if no MX record is there).
|
|
* Thanks Nicht Lieb.
|
|
* @link http://www.faqs.org/rfcs/rfc2821.html RFC 2821 - Simple Mail Transfer Protocol
|
|
*/
|
|
if (empty($mxhosts)) {
|
|
$mxhosts[] = $hostname;
|
|
}
|
|
return $mxhosts;
|
|
}
|
|
|
|
/**
|
|
* Parses input string to array(0=>user, 1=>domain)
|
|
* @param string $email
|
|
* @param boolean $only_domain
|
|
* @return string|array
|
|
* @access private
|
|
*/
|
|
public static function parse_email($email, $only_domain = TRUE) {
|
|
sscanf($email, "%[^@]@%s", $user, $domain);
|
|
return ($only_domain) ? $domain : array($user, $domain);
|
|
}
|
|
|
|
/**
|
|
* Add an error message to the error container.
|
|
* @access protected
|
|
* @param string $msg
|
|
* @return void
|
|
*/
|
|
protected function set_error($msg) {
|
|
$this->error_count++;
|
|
$this->ErrorInfo = $msg;
|
|
}
|
|
|
|
/**
|
|
* Check if an error occurred.
|
|
* @access public
|
|
* @return boolean True if an error did occur.
|
|
*/
|
|
public function isError() {
|
|
return ($this->error_count > 0);
|
|
}
|
|
|
|
/**
|
|
* Output debugging info
|
|
* Only generates output if debug output is enabled
|
|
* @see verifyEmail::$Debugoutput
|
|
* @see verifyEmail::$Debug
|
|
* @param string $str
|
|
*/
|
|
protected function edebug($str) {
|
|
if (!$this->Debug) {
|
|
return;
|
|
}
|
|
switch ($this->Debugoutput) {
|
|
case 'log':
|
|
//Don't output, just log
|
|
error_log($str);
|
|
break;
|
|
case 'html':
|
|
//Cleans up output a bit for a better looking, HTML-safe output
|
|
echo htmlentities(
|
|
preg_replace('/[\r\n]+/', '', $str), ENT_QUOTES, 'UTF-8'
|
|
)
|
|
. "<br>\n";
|
|
break;
|
|
case 'echo':
|
|
default:
|
|
//Normalize line breaks
|
|
$str = preg_replace('/(\r\n|\r|\n)/ms', "\n", $str);
|
|
echo gmdate('Y-m-d H:i:s') . "\t" . str_replace(
|
|
"\n", "\n \t ", trim($str)
|
|
) . "\n";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate email
|
|
* @param string $email Email address
|
|
* @return boolean True if the valid email also exist
|
|
*/
|
|
public function check($email) {
|
|
$result = FALSE;
|
|
|
|
if (!self::validate($email)) {
|
|
$this->set_error("{$email} incorrect e-mail");
|
|
$this->edebug($this->ErrorInfo);
|
|
if ($this->exceptions) {
|
|
throw new verifyEmailException($this->ErrorInfo);
|
|
}
|
|
return FALSE;
|
|
}
|
|
$this->error_count = 0; // Reset errors
|
|
$this->stream = FALSE;
|
|
|
|
$mxs = $this->getMXrecords(self::parse_email($email));
|
|
$timeout = ceil($this->max_connection_timeout / count($mxs));
|
|
foreach ($mxs as $host) {
|
|
/**
|
|
* suppress error output from stream socket client...
|
|
* Thanks Michael.
|
|
*/
|
|
$this->stream = @stream_socket_client("tcp://" . $host . ":" . $this->port, $errno, $errstr, $timeout);
|
|
if ($this->stream === FALSE) {
|
|
if ($errno == 0) {
|
|
$this->set_error("Problem initializing the socket");
|
|
$this->edebug($this->ErrorInfo);
|
|
if ($this->exceptions) {
|
|
throw new verifyEmailException($this->ErrorInfo);
|
|
}
|
|
return FALSE;
|
|
} else {
|
|
$this->edebug($host . ":" . $errstr);
|
|
}
|
|
} else {
|
|
stream_set_timeout($this->stream, $this->stream_timeout);
|
|
stream_set_blocking($this->stream, 1);
|
|
|
|
if ($this->_streamCode($this->_streamResponse()) == '220') {
|
|
$this->edebug("Connection success {$host}");
|
|
break;
|
|
} else {
|
|
fclose($this->stream);
|
|
$this->stream = FALSE;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->stream === FALSE) {
|
|
$this->set_error("All connection fails");
|
|
$this->edebug($this->ErrorInfo);
|
|
if ($this->exceptions) {
|
|
throw new verifyEmailException($this->ErrorInfo);
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
$this->_streamQuery("HELO " . self::parse_email($this->from));
|
|
$this->_streamResponse();
|
|
$this->_streamQuery("MAIL FROM: <{$this->from}>");
|
|
$this->_streamResponse();
|
|
$this->_streamQuery("RCPT TO: <{$email}>");
|
|
$code = $this->_streamCode($this->_streamResponse());
|
|
$this->_streamResponse();
|
|
$this->_streamQuery("RSET");
|
|
$this->_streamResponse();
|
|
$code2 = $this->_streamCode($this->_streamResponse());
|
|
$this->_streamQuery("QUIT");
|
|
fclose($this->stream);
|
|
|
|
$code = !empty($code2)?$code2:$code;
|
|
switch ($code) {
|
|
case '250':
|
|
/**
|
|
* http://www.ietf.org/rfc/rfc0821.txt
|
|
* 250 Requested mail action okay, completed
|
|
* email address was accepted
|
|
*/
|
|
case '450':
|
|
case '451':
|
|
case '452':
|
|
/**
|
|
* http://www.ietf.org/rfc/rfc0821.txt
|
|
* 450 Requested action not taken: the remote mail server
|
|
* does not want to accept mail from your server for
|
|
* some reason (IP address, blacklisting, etc..)
|
|
* Thanks Nicht Lieb.
|
|
* 451 Requested action aborted: local error in processing
|
|
* 452 Requested action not taken: insufficient system storage
|
|
* email address was greylisted (or some temporary error occured on the MTA)
|
|
* i believe that e-mail exists
|
|
*/
|
|
return TRUE;
|
|
case '550':
|
|
return FALSE;
|
|
default :
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* writes the contents of string to the file stream pointed to by handle
|
|
* If an error occurs, returns FALSE.
|
|
* @access protected
|
|
* @param string $string The string that is to be written
|
|
* @return string Returns a result code, as an integer.
|
|
*/
|
|
protected function _streamQuery($query) {
|
|
$this->edebug($query);
|
|
return stream_socket_sendto($this->stream, $query . self::CRLF);
|
|
}
|
|
|
|
/**
|
|
* Reads all the line long the answer and analyze it.
|
|
* If an error occurs, returns FALSE
|
|
* @access protected
|
|
* @return string Response
|
|
*/
|
|
protected function _streamResponse($timed = 0) {
|
|
$reply = stream_get_line($this->stream, 1);
|
|
$status = stream_get_meta_data($this->stream);
|
|
|
|
if (!empty($status['timed_out'])) {
|
|
$this->edebug("Timed out while waiting for data! (timeout {$this->stream_timeout} seconds)");
|
|
}
|
|
|
|
if ($reply === FALSE && $status['timed_out'] && $timed < $this->stream_timeout_wait) {
|
|
return $this->_streamResponse($timed + $this->stream_timeout);
|
|
}
|
|
|
|
|
|
if ($reply !== FALSE && $status['unread_bytes'] > 0) {
|
|
$reply .= stream_get_line($this->stream, $status['unread_bytes'], self::CRLF);
|
|
}
|
|
$this->edebug($reply);
|
|
return $reply;
|
|
}
|
|
|
|
/**
|
|
* Get Response code from Response
|
|
* @param string $str
|
|
* @return string
|
|
*/
|
|
protected function _streamCode($str) {
|
|
preg_match('/^(?<code>[0-9]{3})(\s|-)(.*)$/ims', $str, $matches);
|
|
$code = isset($matches['code']) ? $matches['code'] : false;
|
|
return $code;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* verifyEmail exception handler
|
|
*/
|
|
class verifyEmailException extends Exception {
|
|
|
|
/**
|
|
* Prettify error message output
|
|
* @return string
|
|
*/
|
|
public function errorMessage() {
|
|
$errorMsg = $this->getMessage();
|
|
return $errorMsg;
|
|
}
|
|
|
|
}
|
|
|