-
-
Notifications
You must be signed in to change notification settings - Fork 214
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
124c00b
commit f48751b
Showing
4 changed files
with
202 additions
and
130 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
<?php | ||
|
||
namespace Egulias\EmailValidator\Helper; | ||
|
||
class SmtpSocketHelper | ||
{ | ||
/** | ||
* @var int | ||
*/ | ||
private $port; | ||
|
||
/** | ||
* @var int | ||
*/ | ||
private $timeout; | ||
|
||
/** | ||
* @var resource | ||
*/ | ||
private $handle; | ||
|
||
public function __construct($port = 25, $timeout = 15) | ||
{ | ||
$this->port = $port; | ||
$this->timeout = $timeout; | ||
} | ||
|
||
/** | ||
* Checks is resource | ||
* | ||
* @return bool | ||
*/ | ||
public function isResource() | ||
{ | ||
return is_resource($this->handle); | ||
} | ||
|
||
/** | ||
* Opens resource | ||
* | ||
* @param string $hostname | ||
* @param int $errno | ||
* @param string $errstr | ||
*/ | ||
public function open($hostname, &$errno, &$errstr) | ||
{ | ||
$this->handle = @fsockopen($hostname, $this->port, $errno, $errstr, $this->timeout); | ||
} | ||
|
||
/** | ||
* Writes message | ||
* | ||
* @param string $message | ||
* | ||
* @return bool|int | ||
*/ | ||
public function write($message) | ||
{ | ||
if (!$this->isResource()) { | ||
return false; | ||
} | ||
|
||
return @fwrite($this->handle, $message); | ||
} | ||
|
||
/** | ||
* Get last response code | ||
* | ||
* @return int | ||
*/ | ||
public function getResponseCode() | ||
{ | ||
if (!$this->isResource()) { | ||
return -1; | ||
} | ||
|
||
$data = ''; | ||
while (substr($data, 3, 1) !== ' ') { | ||
if (!($data = @fgets($this->handle, 256))) { | ||
return -1; | ||
} | ||
} | ||
|
||
return intval(substr($data, 0, 3)); | ||
} | ||
|
||
/** | ||
* Closes resource | ||
*/ | ||
public function close() | ||
{ | ||
if (!$this->isResource()) { | ||
return; | ||
} | ||
|
||
@fclose($this->handle); | ||
|
||
$this->handle = null; | ||
} | ||
} |
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
|
||
use Egulias\EmailValidator\EmailLexer; | ||
use Egulias\EmailValidator\Exception\InvalidEmail; | ||
use Egulias\EmailValidator\Helper\SmtpSocketHelper; | ||
use Egulias\EmailValidator\Validation\Error\IllegalMailbox; | ||
use Egulias\EmailValidator\Warning\NoDNSMXRecord; | ||
use Egulias\EmailValidator\Warning\SocketWarning; | ||
|
@@ -24,33 +25,34 @@ class MailboxCheckValidation implements EmailValidation | |
private $warnings = []; | ||
|
||
/** | ||
* @var int | ||
* @var SmtpSocketHelper | ||
*/ | ||
private $lastResponseCode; | ||
private $socketHelper; | ||
|
||
/** | ||
* @var int | ||
* @var string | ||
*/ | ||
private $port = 25; | ||
private $fromEmail; | ||
|
||
/** | ||
* @var int | ||
*/ | ||
private $timeout = 10; | ||
|
||
/** | ||
* @var string | ||
*/ | ||
private $fromEmail = '[email protected]'; | ||
private $lastResponseCode; | ||
|
||
/** | ||
* MailboxCheckValidation constructor. | ||
* | ||
* @param SmtpSocketHelper $socketHelper | ||
* @param string $fromEmail | ||
*/ | ||
public function __construct() | ||
public function __construct(SmtpSocketHelper $socketHelper, $fromEmail) | ||
{ | ||
if (!extension_loaded('intl')) { | ||
throw new \LogicException(sprintf('The %s class requires the Intl extension.', __CLASS__)); | ||
} | ||
|
||
$this->socketHelper = $socketHelper; | ||
$this->fromEmail = $fromEmail; | ||
} | ||
|
||
/** | ||
|
@@ -69,62 +71,6 @@ public function getWarnings() | |
return $this->warnings; | ||
} | ||
|
||
/** | ||
* @return int | ||
*/ | ||
public function getLastResponseCode() | ||
{ | ||
return $this->lastResponseCode; | ||
} | ||
|
||
/** | ||
* @return int | ||
*/ | ||
public function getPort() | ||
{ | ||
return $this->port; | ||
} | ||
|
||
/** | ||
* @param int $port | ||
*/ | ||
public function setPort($port) | ||
{ | ||
$this->port = $port; | ||
} | ||
|
||
/** | ||
* @return int | ||
*/ | ||
public function getTimeout() | ||
{ | ||
return $this->timeout; | ||
} | ||
|
||
/** | ||
* @param int $timeout | ||
*/ | ||
public function setTimeout($timeout) | ||
{ | ||
$this->timeout = $timeout; | ||
} | ||
|
||
/** | ||
* @return string | ||
*/ | ||
public function getFromEmail() | ||
{ | ||
return $this->fromEmail; | ||
} | ||
|
||
/** | ||
* @param string $fromEmail | ||
*/ | ||
public function setFromEmail($fromEmail) | ||
{ | ||
$this->fromEmail = $fromEmail; | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
|
@@ -134,107 +80,102 @@ public function isValid($email, EmailLexer $emailLexer) | |
|
||
$isValid = false; | ||
foreach ($mxHosts as $mxHost) { | ||
if ( ($isValid = $this->checkMailbox($mxHost, $this->port, $this->timeout, $this->fromEmail, $email)) ) { | ||
if ($this->checkMailboxExists($mxHost, $email)) { | ||
$isValid = true; | ||
break; | ||
} | ||
} | ||
|
||
if ( ! $isValid ) { | ||
if (!$isValid) { | ||
$this->error = new IllegalMailbox($this->lastResponseCode); | ||
} | ||
|
||
return $this->error === null; | ||
} | ||
|
||
/** | ||
* Gets MX Hosts from email | ||
* | ||
* @param string $email | ||
* | ||
* @return array | ||
*/ | ||
protected function getMXHosts($email) | ||
{ | ||
$variant = defined('INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : INTL_IDNA_VARIANT_2003; | ||
|
||
$hostname = $email; | ||
if ( false !== ($lastAtPos = strrpos($email, '@')) ) { | ||
$hostname = substr($email, $lastAtPos + 1); | ||
} | ||
$hostname = rtrim(idn_to_ascii($hostname, IDNA_DEFAULT, $variant), '.') . '.'; | ||
$hostname = $this->extractHostname($email); | ||
|
||
$mxHosts = []; | ||
$result = getmxrr($hostname, $mxHosts); | ||
if ( ! $result ) { | ||
if (!$result) { | ||
$this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord(); | ||
} | ||
|
||
return $mxHosts; | ||
} | ||
|
||
/** | ||
* Extracts hostname from email | ||
* | ||
* @param string $email | ||
* | ||
* @return string | ||
*/ | ||
private function extractHostname($email) | ||
{ | ||
$variant = defined('INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : INTL_IDNA_VARIANT_2003; | ||
|
||
$lastAtPos = strrpos($email, '@'); | ||
if ((bool) $lastAtPos) { | ||
$hostname = substr($email, $lastAtPos + 1); | ||
return rtrim(idn_to_ascii($hostname, IDNA_DEFAULT, $variant), '.') . '.'; | ||
} | ||
|
||
return rtrim(idn_to_ascii($email, IDNA_DEFAULT, $variant), '.') . '.'; | ||
} | ||
|
||
/** | ||
* Checks mailbox | ||
* | ||
* @param string $hostname | ||
* @param int $port | ||
* @param int $timeout | ||
* @param string $fromEmail | ||
* @param string $toEmail | ||
* @param string $email | ||
* @return bool | ||
*/ | ||
protected function checkMailbox($hostname, $port, $timeout, $fromEmail, $toEmail) | ||
private function checkMailboxExists($hostname, $email) | ||
{ | ||
$socket = @fsockopen($hostname, $port, $errno, $errstr, $timeout); | ||
$this->socketHelper->open($hostname, $errno, $errstr); | ||
|
||
if ( ! $socket ) { | ||
if (!$this->socketHelper->isResource()) { | ||
$this->warnings[SocketWarning::CODE][] = new SocketWarning($hostname, $errno, $errstr); | ||
|
||
return false; | ||
} | ||
|
||
if ( ! ($this->assertResponse($socket, 220) ) ) { | ||
$this->lastResponseCode = $this->socketHelper->getResponseCode(); | ||
if ($this->lastResponseCode !== 220) { | ||
return false; | ||
} | ||
|
||
fwrite($socket, "EHLO {$hostname}" . self::END_OF_LINE); | ||
if ( ! ($this->assertResponse($socket, 250) ) ) { | ||
$this->socketHelper->write("EHLO {$hostname}" . self::END_OF_LINE); | ||
$this->lastResponseCode = $this->socketHelper->getResponseCode(); | ||
if ($this->lastResponseCode !== 250) { | ||
return false; | ||
} | ||
|
||
fwrite($socket, "MAIL FROM: <{$fromEmail}>" . self::END_OF_LINE); | ||
if ( ! ($this->assertResponse($socket, 250) ) ) { | ||
$this->socketHelper->write("MAIL FROM: <{$this->fromEmail}>" . self::END_OF_LINE); | ||
$this->lastResponseCode = $this->socketHelper->getResponseCode(); | ||
if ($this->lastResponseCode !== 250) { | ||
return false; | ||
} | ||
|
||
fwrite($socket, "RCPT TO: <{$toEmail}>" . self::END_OF_LINE); | ||
if ( ! ($this->assertResponse($socket, 250) ) ) { | ||
$this->socketHelper->write("RCPT TO: <{$email}>" . self::END_OF_LINE); | ||
$this->lastResponseCode = $this->socketHelper->getResponseCode(); | ||
if ($this->lastResponseCode !== 250) { | ||
return false; | ||
} | ||
|
||
fwrite($socket, 'QUIT' . self::END_OF_LINE); | ||
$this->socketHelper->write('QUIT' . self::END_OF_LINE); | ||
|
||
fclose($socket); | ||
$this->socketHelper->close(); | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* @param resource $socket | ||
* @param int $expectedCode | ||
* | ||
* @return bool | ||
*/ | ||
private function assertResponse($socket, $expectedCode) | ||
{ | ||
if ( ! is_resource($socket) ) { | ||
return false; | ||
} | ||
|
||
$data = ''; | ||
while (substr($data, 3, 1) !== ' ') { | ||
if ( ! ( $data = @fgets($socket, 256) ) ) { | ||
$this->lastResponseCode = -1; | ||
|
||
return false; | ||
} | ||
} | ||
|
||
return ($this->lastResponseCode = intval( substr($data, 0, 3) )) === $expectedCode; | ||
} | ||
} |
Oops, something went wrong.