From 124c00ba5cdd6753366530c1935bdb4eddb58ec7 Mon Sep 17 00:00:00 2001 From: Ivan Lutokhin Date: Tue, 16 Apr 2019 11:06:32 +0300 Subject: [PATCH 1/3] Add mailbox validator --- .../Validation/Error/IllegalMailbox.php | 28 ++ .../Validation/MailboxCheckValidation.php | 240 ++++++++++++++++++ EmailValidator/Warning/SocketWarning.php | 13 + .../Validation/MailboxCheckValidationTest.php | 40 +++ 4 files changed, 321 insertions(+) create mode 100644 EmailValidator/Validation/Error/IllegalMailbox.php create mode 100644 EmailValidator/Validation/MailboxCheckValidation.php create mode 100644 EmailValidator/Warning/SocketWarning.php create mode 100644 Tests/EmailValidator/Validation/MailboxCheckValidationTest.php diff --git a/EmailValidator/Validation/Error/IllegalMailbox.php b/EmailValidator/Validation/Error/IllegalMailbox.php new file mode 100644 index 0000000..8f0f33c --- /dev/null +++ b/EmailValidator/Validation/Error/IllegalMailbox.php @@ -0,0 +1,28 @@ +responseCode = $responseCode; + } +} diff --git a/EmailValidator/Validation/MailboxCheckValidation.php b/EmailValidator/Validation/MailboxCheckValidation.php new file mode 100644 index 0000000..d82786a --- /dev/null +++ b/EmailValidator/Validation/MailboxCheckValidation.php @@ -0,0 +1,240 @@ +error; + } + + /** + * @inheritDoc + */ + 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 + */ + public function isValid($email, EmailLexer $emailLexer) + { + $mxHosts = $this->getMXHosts($email); + + $isValid = false; + foreach ($mxHosts as $mxHost) { + if ( ($isValid = $this->checkMailbox($mxHost, $this->port, $this->timeout, $this->fromEmail, $email)) ) { + break; + } + } + + if ( ! $isValid ) { + $this->error = new IllegalMailbox($this->lastResponseCode); + } + + return $this->error === null; + } + + /** + * @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), '.') . '.'; + + $mxHosts = []; + $result = getmxrr($hostname, $mxHosts); + if ( ! $result ) { + $this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord(); + } + + return $mxHosts; + } + + /** + * @param string $hostname + * @param int $port + * @param int $timeout + * @param string $fromEmail + * @param string $toEmail + * @return bool + */ + protected function checkMailbox($hostname, $port, $timeout, $fromEmail, $toEmail) + { + $socket = @fsockopen($hostname, $port, $errno, $errstr, $timeout); + + if ( ! $socket ) { + $this->warnings[SocketWarning::CODE][] = new SocketWarning($hostname, $errno, $errstr); + + return false; + } + + if ( ! ($this->assertResponse($socket, 220) ) ) { + return false; + } + + fwrite($socket, "EHLO {$hostname}" . self::END_OF_LINE); + if ( ! ($this->assertResponse($socket, 250) ) ) { + return false; + } + + fwrite($socket, "MAIL FROM: <{$fromEmail}>" . self::END_OF_LINE); + if ( ! ($this->assertResponse($socket, 250) ) ) { + return false; + } + + fwrite($socket, "RCPT TO: <{$toEmail}>" . self::END_OF_LINE); + if ( ! ($this->assertResponse($socket, 250) ) ) { + return false; + } + + fwrite($socket, 'QUIT' . self::END_OF_LINE); + + fclose($socket); + + 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/EmailValidator/Warning/SocketWarning.php b/EmailValidator/Warning/SocketWarning.php new file mode 100644 index 0000000..05072ab --- /dev/null +++ b/EmailValidator/Warning/SocketWarning.php @@ -0,0 +1,13 @@ +message = "Error connecting to {$hostname} ({$errno}) ({$errstr})"; + } +} diff --git a/Tests/EmailValidator/Validation/MailboxCheckValidationTest.php b/Tests/EmailValidator/Validation/MailboxCheckValidationTest.php new file mode 100644 index 0000000..1c654cb --- /dev/null +++ b/Tests/EmailValidator/Validation/MailboxCheckValidationTest.php @@ -0,0 +1,40 @@ +assertTrue($validation->isValid('no-reply@gmail.com', new EmailLexer())); + } + + public function testInvalidMailbox() + { + $validation = new MailboxCheckValidation(); + $this->assertFalse($validation->isValid('invalid-mailbox@example.com', new EmailLexer())); + } + + public function testDNSWarnings() + { + $validation = new MailboxCheckValidation(); + $expectedWarnings = [NoDNSMXRecord::CODE => new NoDNSMXRecord()]; + $validation->isValid('example@invalid.example.com', new EmailLexer()); + $this->assertEquals($expectedWarnings, $validation->getWarnings()); + } + + public function testIllegalMailboxError() + { + $validation = new MailboxCheckValidation(); + $expectedError = new IllegalMailbox(550); + $validation->isValid('invalid-mailbox@gmail.com', new EmailLexer()); + $this->assertEquals($expectedError, $validation->getError()); + } +} From f48751b8815b663605751893070c1f8f4686d5a4 Mon Sep 17 00:00:00 2001 From: Ivan Lutokhin Date: Sun, 21 Apr 2019 12:22:02 +0300 Subject: [PATCH 2/3] Fix code style and tests --- EmailValidator/Helper/SmtpSocketHelper.php | 100 ++++++++++ .../Validation/Error/IllegalMailbox.php | 8 + .../Validation/MailboxCheckValidation.php | 177 ++++++------------ .../Validation/MailboxCheckValidationTest.php | 47 +++-- 4 files changed, 202 insertions(+), 130 deletions(-) create mode 100644 EmailValidator/Helper/SmtpSocketHelper.php 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()); } } From aeed5e473fe41316433239f819478f662a328d40 Mon Sep 17 00:00:00 2001 From: Ivan Lutokhin Date: Mon, 29 Apr 2019 11:33:46 +0300 Subject: [PATCH 3/3] * Fix tests php 5.5 --- EmailValidator/Validation/Error/IllegalMailbox.php | 13 +++++++++++++ .../Validation/MailboxCheckValidationTest.php | 8 ++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/EmailValidator/Validation/Error/IllegalMailbox.php b/EmailValidator/Validation/Error/IllegalMailbox.php index 51fa110..1b6646e 100644 --- a/EmailValidator/Validation/Error/IllegalMailbox.php +++ b/EmailValidator/Validation/Error/IllegalMailbox.php @@ -33,4 +33,17 @@ public function getResponseCode() { return $this->responseCode; } + + /** + * @return string + */ + public function __toString() + { + return sprintf( + "%s SMTP response code: %s. Internal code: %s.", + $this->message, + $this->responseCode, + $this->code + ); + } } diff --git a/Tests/EmailValidator/Validation/MailboxCheckValidationTest.php b/Tests/EmailValidator/Validation/MailboxCheckValidationTest.php index f7e0881..5ef87ce 100644 --- a/Tests/EmailValidator/Validation/MailboxCheckValidationTest.php +++ b/Tests/EmailValidator/Validation/MailboxCheckValidationTest.php @@ -13,7 +13,9 @@ class MailboxCheckValidationTest extends TestCase { public function testValidMailbox() { - $socketHelperMock = $this->createMock(SmtpSocketHelper::class); + $socketHelperMock = $this->getMockBuilder(SmtpSocketHelper::class) + ->disableOriginalConstructor() + ->getMock(); $socketHelperMock ->expects($this->any()) @@ -42,7 +44,9 @@ public function testDNSWarnings() public function testIllegalMailboxError() { - $socketHelperMock = $this->createMock(SmtpSocketHelper::class); + $socketHelperMock = $this->getMockBuilder(SmtpSocketHelper::class) + ->disableOriginalConstructor() + ->getMock(); $socketHelperMock ->expects($this->any())