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 new file mode 100644 index 0000000..1b6646e --- /dev/null +++ b/EmailValidator/Validation/Error/IllegalMailbox.php @@ -0,0 +1,49 @@ +responseCode = $responseCode; + } + + /** + * @return int + */ + 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/EmailValidator/Validation/MailboxCheckValidation.php b/EmailValidator/Validation/MailboxCheckValidation.php new file mode 100644 index 0000000..97e3f71 --- /dev/null +++ b/EmailValidator/Validation/MailboxCheckValidation.php @@ -0,0 +1,181 @@ +socketHelper = $socketHelper; + $this->fromEmail = $fromEmail; + } + + /** + * @inheritDoc + */ + public function getError() + { + return $this->error; + } + + /** + * @inheritDoc + */ + public function getWarnings() + { + return $this->warnings; + } + + /** + * @inheritDoc + */ + public function isValid($email, EmailLexer $emailLexer) + { + $mxHosts = $this->getMXHosts($email); + + $isValid = false; + foreach ($mxHosts as $mxHost) { + if ($this->checkMailboxExists($mxHost, $email)) { + $isValid = true; + break; + } + } + + 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) + { + $hostname = $this->extractHostname($email); + + $result = getmxrr($hostname, $mxHosts); + 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 string $email + * @return bool + */ + private function checkMailboxExists($hostname, $email) + { + $this->socketHelper->open($hostname, $errno, $errstr); + + if (!$this->socketHelper->isResource()) { + $this->warnings[SocketWarning::CODE][] = new SocketWarning($hostname, $errno, $errstr); + + return false; + } + + $this->lastResponseCode = $this->socketHelper->getResponseCode(); + if ($this->lastResponseCode !== 220) { + return false; + } + + $this->socketHelper->write("EHLO {$hostname}" . self::END_OF_LINE); + $this->lastResponseCode = $this->socketHelper->getResponseCode(); + if ($this->lastResponseCode !== 250) { + return false; + } + + $this->socketHelper->write("MAIL FROM: <{$this->fromEmail}>" . self::END_OF_LINE); + $this->lastResponseCode = $this->socketHelper->getResponseCode(); + if ($this->lastResponseCode !== 250) { + return false; + } + + $this->socketHelper->write("RCPT TO: <{$email}>" . self::END_OF_LINE); + $this->lastResponseCode = $this->socketHelper->getResponseCode(); + if ($this->lastResponseCode !== 250) { + return false; + } + + $this->socketHelper->write('QUIT' . self::END_OF_LINE); + + $this->socketHelper->close(); + + return true; + } +} \ 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..5ef87ce --- /dev/null +++ b/Tests/EmailValidator/Validation/MailboxCheckValidationTest.php @@ -0,0 +1,67 @@ +getMockBuilder(SmtpSocketHelper::class) + ->disableOriginalConstructor() + ->getMock(); + + $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(new SmtpSocketHelper(), 'test@validation.email'); + $expectedWarnings = [NoDNSMXRecord::CODE => new NoDNSMXRecord()]; + $validation->isValid('example@invalid.example.com', new EmailLexer()); + $this->assertEquals($expectedWarnings, $validation->getWarnings()); + } + + public function testIllegalMailboxError() + { + $socketHelperMock = $this->getMockBuilder(SmtpSocketHelper::class) + ->disableOriginalConstructor() + ->getMock(); + + $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()); + } +}