diff --git a/EmailValidator/Helper/SmtpSocketHelper.php b/EmailValidator/Helper/SmtpSocketHelper.php new file mode 100644 index 0000000..9e0c2de --- /dev/null +++ b/EmailValidator/Helper/SmtpSocketHelper.php @@ -0,0 +1,100 @@ +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; + } +} \ No newline at end of file diff --git a/EmailValidator/Validation/Error/IllegalMailbox.php b/EmailValidator/Validation/Error/IllegalMailbox.php index 8f0f33c..51fa110 100644 --- a/EmailValidator/Validation/Error/IllegalMailbox.php +++ b/EmailValidator/Validation/Error/IllegalMailbox.php @@ -25,4 +25,12 @@ public function __construct($responseCode) $this->responseCode = $responseCode; } + + /** + * @return int + */ + public function getResponseCode() + { + return $this->responseCode; + } } diff --git a/EmailValidator/Validation/MailboxCheckValidation.php b/EmailValidator/Validation/MailboxCheckValidation.php index d82786a..97e3f71 100644 --- a/EmailValidator/Validation/MailboxCheckValidation.php +++ b/EmailValidator/Validation/MailboxCheckValidation.php @@ -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 = 'test-mailbox@validation.email'; + 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,12 +80,13 @@ 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); } @@ -147,23 +94,18 @@ public function isValid($email, EmailLexer $emailLexer) } /** + * 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(); } @@ -171,70 +113,69 @@ protected function getMXHosts($email) } /** + * 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; - } } \ No newline at end of file diff --git a/Tests/EmailValidator/Validation/MailboxCheckValidationTest.php b/Tests/EmailValidator/Validation/MailboxCheckValidationTest.php index 1c654cb..f7e0881 100644 --- a/Tests/EmailValidator/Validation/MailboxCheckValidationTest.php +++ b/Tests/EmailValidator/Validation/MailboxCheckValidationTest.php @@ -3,6 +3,7 @@ namespace Egulias\Tests\EmailValidator\Validation; use Egulias\EmailValidator\EmailLexer; +use Egulias\EmailValidator\Helper\SmtpSocketHelper; use Egulias\EmailValidator\Validation\Error\IllegalMailbox; use Egulias\EmailValidator\Validation\MailboxCheckValidation; use Egulias\EmailValidator\Warning\NoDNSMXRecord; @@ -12,19 +13,28 @@ class MailboxCheckValidationTest extends TestCase { public function testValidMailbox() { - $validation = new MailboxCheckValidation(); - $this->assertTrue($validation->isValid('no-reply@gmail.com', new EmailLexer())); - } + $socketHelperMock = $this->createMock(SmtpSocketHelper::class); - public function testInvalidMailbox() - { - $validation = new MailboxCheckValidation(); - $this->assertFalse($validation->isValid('invalid-mailbox@example.com', new EmailLexer())); + $socketHelperMock + ->expects($this->any()) + ->method('isResource') + ->willReturn(true) + ; + + $socketHelperMock + ->expects($this->any()) + ->method('getResponseCode') + ->willReturnOnConsecutiveCalls(220, 250, 250, 250) + ; + + $validation = new MailboxCheckValidation($socketHelperMock, 'test@validation.email'); + + $this->assertTrue($validation->isValid('success@validation.email', new EmailLexer())); } public function testDNSWarnings() { - $validation = new MailboxCheckValidation(); + $validation = new MailboxCheckValidation(new SmtpSocketHelper(), 'test@validation.email'); $expectedWarnings = [NoDNSMXRecord::CODE => new NoDNSMXRecord()]; $validation->isValid('example@invalid.example.com', new EmailLexer()); $this->assertEquals($expectedWarnings, $validation->getWarnings()); @@ -32,9 +42,22 @@ public function testDNSWarnings() public function testIllegalMailboxError() { - $validation = new MailboxCheckValidation(); - $expectedError = new IllegalMailbox(550); - $validation->isValid('invalid-mailbox@gmail.com', new EmailLexer()); - $this->assertEquals($expectedError, $validation->getError()); + $socketHelperMock = $this->createMock(SmtpSocketHelper::class); + + $socketHelperMock + ->expects($this->any()) + ->method('isResource') + ->willReturn(true) + ; + + $socketHelperMock + ->expects($this->any()) + ->method('getResponseCode') + ->willReturnOnConsecutiveCalls(220, 250, 250, 550) + ; + + $validation = new MailboxCheckValidation($socketHelperMock, 'test@validation.email'); + $validation->isValid('failure@validation.email', new EmailLexer()); + $this->assertEquals(new IllegalMailbox(550), $validation->getError()); } }