Created
February 26, 2020 07:26
-
-
Save ArrayIterator/689a4fac96c35513457cd3b948fdd5a1 to your computer and use it in GitHub Desktop.
Verify Email Existences Via Port 25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace ArrayIterator\Email; | |
/** | |
* Class ArrayEmailVerifier | |
* @package ArrayIterator\Email | |
*/ | |
class ArrayEmailVerifier | |
{ | |
protected $debugCLI = false; | |
protected $error; | |
const TYPE_COMMAND = 'COMMAND'; | |
const TYPE_RESPONSE = 'RESPONSE'; | |
const ERROR_CONNECT = 60; | |
const ERROR_UKNOWN = 30; | |
const EMAIL_EXIST = 250; | |
const EMAIL_INVALID = 550; | |
/** | |
* ArrayEmailVerify constructor. | |
* @param bool $debugCLI | |
*/ | |
public function __construct(bool $debugCLI = true) | |
{ | |
$this->debugCLI = $debugCLI; | |
} | |
/** | |
* @param string $data | |
* @param string $type | |
*/ | |
protected function debug(string $data, string $type) | |
{ | |
if (!$this->debugCLI) { | |
return; | |
} | |
$ex = explode('.', (string) microtime(true)); | |
$ex = isset($ex[1]) ? $ex[1] : '0'; | |
while (strlen($ex) < 6) { | |
$ex .= '0'; | |
} | |
$date = gmdate('Y-m-d H:i:s.').$ex; | |
$data = trim(str_replace(["\r"], '', $data)); | |
printf( | |
"[%s][%s] %s\n", | |
$date, | |
strtoupper($type), | |
trim($data) | |
); | |
} | |
/** | |
* @param resource $stream | |
* @return array | |
*/ | |
protected function getLine($stream) : array | |
{ | |
$data = ''; | |
$timeout = 5; | |
$limit = 10; | |
stream_set_timeout($stream, $timeout); | |
$endTime = time() + $limit; | |
$selR = [$stream]; | |
$selW = null; | |
$break = false; | |
while (is_resource($stream) && !feof($stream)) { | |
//Must pass vars in here as params are by reference | |
if (!stream_select($selR, $selW, $selW, $limit)) { | |
break; | |
} | |
//Deliberate noise suppression - errors are handled afterwards | |
$str = @fgets($stream, 512); | |
$data .= $str; | |
// If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled), | |
// or 4th character is a space or a line break char, we are done reading, break the loop. | |
// String array access is a significant micro-optimisation over strlen | |
if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") { | |
break; | |
} | |
// Timed-out? Log and break | |
$info = stream_get_meta_data($stream); | |
if ($info['timed_out']) { | |
break; | |
} | |
// Now check if reads took too long | |
if ($endTime && time() > $endTime) { | |
break; | |
} | |
} | |
if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $data, $matches)) { | |
$code = (int) $matches[1]; | |
$code_ex = (count($matches) > 2 ? $matches[2] : null); | |
// Cut off error code from each response line | |
$detail = preg_replace( | |
"/{$code}[ -]" . | |
($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m', | |
'', | |
$data | |
); | |
} else { | |
// Fall back to simple parsing if regex fails | |
$code = (int) substr($data, 0, 3); | |
$code_ex = null; | |
$detail = substr($data, 4); | |
} | |
return [ | |
'code' => $code, | |
'code_ex' => $code_ex, | |
'data' => $data, | |
]; | |
} | |
/** | |
* @return mixed | |
*/ | |
public function getError() | |
{ | |
return $this->error; | |
} | |
private function sendCommand($stream, $command, array &$history) | |
{ | |
$this->debug( | |
sprintf('SENDING %s', $command), | |
self::TYPE_COMMAND | |
); | |
$command = rtrim($command); | |
fwrite($stream, "{$command}\r\n"); | |
$line = $this->getLine($stream); | |
$count = 3; | |
while ($count-- > 0 && ! $line['data']) { | |
$line = $this->getLine($stream); | |
} | |
$this->debug( | |
$line['data'], | |
self::TYPE_RESPONSE | |
); | |
$history[] = $line['data']; | |
return $line; | |
} | |
public function verify(string $email, string $host) | |
{ | |
$response = [ | |
'status' => self::ERROR_UKNOWN, | |
'exist' => null, | |
'email' => $email, | |
'host' => $host, | |
'reply' => [], | |
'history' => [] | |
]; | |
$line =& $response['reply']; | |
$history =& $response['history']; | |
$smtpHost = "{$host}:25"; | |
$this->debug( | |
sprintf('CONNECTING %s', $smtpHost), | |
'CONNECTING' | |
); | |
$stream = @stream_socket_client($smtpHost, $errno, $errMsg, 5, STREAM_CLIENT_CONNECT); | |
if (!$stream) { | |
$this->error = $errMsg; | |
$this->debug( | |
sprintf('CONNECTING FAILED : %s', $errMsg), | |
self::TYPE_RESPONSE | |
); | |
return self::ERROR_CONNECT; | |
} | |
$line = $this->getLine($stream); | |
$count = 3; | |
while ($count-- > 0 && ! $line['data']) { | |
$line = $this->getLine($stream); | |
} | |
if (!$line['data']) { | |
$this->debug( | |
sprintf('COULD NOT GET RESPONSE FROM SERVER : %s', $smtpHost), | |
self::TYPE_RESPONSE | |
); | |
$response['status'] = self::ERROR_UKNOWN; | |
return $response; | |
} | |
$history[] = $line['data']; | |
$this->debug($line['data'], self::TYPE_RESPONSE); | |
$line = $this->sendCommand($stream, "EHLO $host", $history); | |
$history[] = $line['data']; | |
if ($line['code'] !== 250) { | |
$response['status'] = $line['code']; | |
$this->sendCommand($stream, "HELO $host", $history); | |
} | |
if ($line['code'] !== 250) { | |
$response['status'] = $line['code']; | |
$this->sendCommand($stream, "QUIT", $history); | |
return $response; | |
} | |
$line = $this->sendCommand($stream, "MAIL FROM: <$email>", $history); | |
if ($line['code'] !== 250) { | |
$response['status'] = $line['code']; | |
$this->sendCommand($stream, "QUIT", $history); | |
return $response; | |
} | |
$line = $this->sendCommand($stream, "RCPT TO: <$email>", $history); | |
$code = $line['code']; | |
if ($code === 500 && $line['code_ex'] && preg_match('~Reject|Spam|5\.7~i', $line['code_ex'])) { | |
$code = 450; | |
} | |
$response['exist'] = in_array($code, [250, 251, 450, 451, 452, 541]); | |
$response['status'] = $code; | |
$this->sendCommand($stream, "QUIT", $history); | |
return $response; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* usage: php cli.php [email protected] | |
*/ | |
use ArrayIterator\Email\ArrayEmailVerifier; | |
if (PHP_SAPI !== 'cli') { | |
printf('Application only run on CLI Mode, currently %s', PHP_SAPI); | |
exit(); | |
} | |
$array = new ArrayEmailVerifier(true); | |
$email = $argv[1]??null; | |
if (!$email) { | |
exit("PLEASE ENTER EMAIL ADDRESS\n"); | |
} | |
$oldEmail = $email; | |
$email = filter_var($email, FILTER_VALIDATE_EMAIL); | |
if (!$email) { | |
exit(sprintf("EMAIL %s is not valid\n", $oldEmail)); | |
} | |
$domain = explode('@', $email)[1]?? null; | |
if (!$domain || !preg_match('~\.[^.]+$~', $domain)) { | |
exit(sprintf("Domain for email %s is not valid\n", $email)); | |
} | |
$limit = (int) ini_get('max_execution_time'); | |
set_time_limit(5); | |
@getmxrr($domain, $mx, $weight); | |
set_time_limit($limit); | |
if (empty($mx)) { | |
printf("Could not get mx for email %s\n", $email); | |
exit(); | |
} | |
$mx = array_combine($mx, $weight); | |
asort($mx); | |
$line = null; | |
foreach ($mx as $host => $weight) { | |
$data = $array->verify($email, $host); | |
if ($data) { | |
$line = $data; | |
break; | |
} | |
} | |
if (!$line) { | |
printf('Could not get data from email %s', $oldEmail); | |
exit; | |
} | |
print_r($line); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment