--- tests/Zend/Mail/AllTests.php (revision 23439) +++ tests/Zend/Mail/AllTests.php (working copy) @@ -39,6 +39,7 @@ require_once 'Zend/Mail/InterfaceTest.php'; require_once 'Zend/Mail/MessageTest.php'; require_once 'Zend/Mail/SmtpOfflineTest.php'; +require_once 'Zend/Mail/SmtpProtocolTest.php'; require_once 'Zend/Mail/SmtpTest.php'; require_once 'Zend/Mail/FileTransportTest.php'; @@ -80,6 +81,7 @@ $suite->addTestSuite('Zend_Mail_MaildirWritableTest'); } $suite->addTestSuite('Zend_Mail_SmtpOfflineTest'); + $suite->addTestSuite('Zend_Mail_SmtpProtocolTest'); if (defined('TESTS_ZEND_MAIL_SMTP_ENABLED') && constant('TESTS_ZEND_MAIL_SMTP_ENABLED') == true) { $suite->addTestSuite('Zend_Mail_SmtpTest'); } Index: tests/Zend/Mail/SmtpProtocolTest.php =================================================================== --- tests/Zend/Mail/SmtpProtocolTest.php (revision 0) +++ tests/Zend/Mail/SmtpProtocolTest.php (revision 0) @@ -0,0 +1,374 @@ +_protocol = new ProtocolMock(); + } + + public function testEhlo() + { + $this->_connectAndEhlo(); // expects 250 response + + $this->assertEquals(array( + '220 example.com ESMTP welcome', + 'EHLO 127.0.0.1', + '250 Hello 127.0.0.1, go ahead' + ), $this->_protocol->dialog); + } + + /** + * @depends testEhlo + * @expectedException Zend_Mail_Protocol_Exception + */ + public function testHeloIsOnlyAllowedOncePerSession() + { + $this->_connectAndEhlo(); // do it once + $this->_protocol->helo(); // do it again + } + + /** + * @depends testEhlo + */ + public function testEhloFallsBackToHelo() + { + $this->_protocol->responseBuffer = array( + '220 example.com ESMTP welcome', + '500 Unrecognized', /* 500 or 502 error on unrecognized EHLO */ + '250 Hello 127.0.0.1, go ahead' + ); + + $this->_protocol->connect(); + $this->_protocol->helo(); + + $this->assertEquals(array( + '220 example.com ESMTP welcome', + 'EHLO 127.0.0.1', // tries EHLO + '500 Unrecognized', // .. which fails + 'HELO 127.0.0.1', // continues to HELO + '250 Hello 127.0.0.1, go ahead' // success + ), $this->_protocol->dialog); + } + + /** + * @depends testEhlo + */ + public function testMail() + { + $p = $this->_protocol; + $expectedDialog = $this->_connectAndEhlo(); + + $expectedDialog[] = 'MAIL FROM:'; + $expectedDialog[] = $p->responseBuffer[] = '250 Sender accepted'; + $this->_protocol->mail('from@example.com'); + + $this->assertEquals($expectedDialog, $this->_protocol->dialog); + } + + /** + * @depends testMail + */ + public function testRcptExpects250() + { + $p = $this->_protocol; + $expectedDialog = $this->_connectAndEhlo(); + + $expectedDialog[] = 'MAIL FROM:'; + $expectedDialog[] = $p->responseBuffer[] = '250 Sender accepted'; + $this->_protocol->mail('from@example.com'); + + $expectedDialog[] = 'RCPT TO:'; + $expectedDialog[] = $p->responseBuffer[] = '250 Recipient OK'; + $this->_protocol->rcpt('to@example.com'); + + $this->assertEquals($expectedDialog, $this->_protocol->dialog); + } + + /** + * @depends testMail + */ + public function testRcptExpects251() + { + $p = $this->_protocol; + $expectedDialog = $this->_connectAndEhlo(); + + $expectedDialog[] = 'MAIL FROM:'; + $expectedDialog[] = $p->responseBuffer[] = '250 Sender accepted'; + $this->_protocol->mail('from@example.com'); + + $expectedDialog[] = 'RCPT TO:'; + $expectedDialog[] = $p->responseBuffer[] = '251 Recipient OK'; + $this->_protocol->rcpt('to@example.com'); + + $this->assertEquals($expectedDialog, $this->_protocol->dialog); + } + + + /** + * @depends testRcptExpects250 + */ + public function testData() + { + $p = $this->_protocol; + $expectedDialog = $this->_connectAndEhlo(); + + $expectedDialog[] = 'MAIL FROM:'; + $expectedDialog[] = $p->responseBuffer[] = '250 Sender accepted'; + $this->_protocol->mail('from@example.com'); + + $expectedDialog[] = 'RCPT TO:'; + $expectedDialog[] = $p->responseBuffer[] = '250 Recipient OK'; + $this->_protocol->rcpt('to@example.com'); + + $expectedDialog[] = 'DATA'; + $expectedDialog[] = $p->responseBuffer[] = '354 Go ahead'; + $expectedDialog[] = 'foo'; + $expectedDialog[] = '.'; // end of data marker + $expectedDialog[] = $p->responseBuffer[] = '250 Accepted'; + $this->_protocol->data('foo'); + + $this->assertEquals($expectedDialog, $this->_protocol->dialog); + } + + /** + * @depends testEhlo + */ + public function testRset() + { + $expectedDialog = $this->_connectAndEhlo(); + + $this->_protocol->responseBuffer = array('250 OK'); + $expectedDialog[] = 'RSET'; + $expectedDialog[] = '250 OK'; + + $this->_protocol->rset(); + + $this->assertEquals($expectedDialog, $this->_protocol->dialog); + } + + /** + * @depends testEhlo + * @group ZF-1377 + */ + public function testRsetExpects220() + { + $expectedDialog = $this->_connectAndEhlo(); + + // Microsoft ESMTP server responds to RSET with 220 rather than 250 + $this->_protocol->responseBuffer = array('220 OK'); + $expectedDialog[] = 'RSET'; + $expectedDialog[] = '220 OK'; + + $this->_protocol->rset(); + + $this->assertEquals($expectedDialog, $this->_protocol->dialog); + } + + /** + * @depends testEhlo + */ + public function testQuit() + { + $p = $this->_protocol; + $expectedDialog = $this->_connectAndEhlo(); + + $expectedDialog[] = 'QUIT'; + $expectedDialog[] = $p->responseBuffer[] = '221 goodbye'; + + $this->_protocol->quit(); + + $this->assertEquals($expectedDialog, $this->_protocol->dialog); + } + + /** + * @depends testMail + * @group ZF-8511 + */ + public function testMultilineResponsesAreNotTruncated() + { + $this->_connectAndEhlo(); + + $this->_protocol->responseBuffer[] = '550-line one'; + $this->_protocol->responseBuffer[] = '550 line two'; + + try { + $this->_protocol->mail('from@example.com'); + $this->fail('Expected exception on 550 response'); + } catch (Zend_Mail_Protocol_Exception $e) { + $this->assertEquals('line one line two', $e->getMessage()); + } + } + + /** + * @depends testMail + * @group ZF-10249 + */ + public function testExceptionCodeIsSmtpStatusCode() + { + $p = $this->_protocol; + $this->_connectAndEhlo(); + + $p->responseBuffer[] = '550 failure'; + + try { + $this->_protocol->mail('from@example.com'); + $this->fail('Expected exception on 550 response'); + } catch (Zend_Mail_Protocol_Exception $e) { + $this->assertEquals(550, $e->getCode()); + } + } + + /** + * @depends testMail + * @expectedException Zend_Mail_Protocol_Exception + */ + public function testRcptThrowsExceptionOnUnexpectedResponse() + { + $p = $this->_protocol; + $expectedDialog = $this->_connectAndEhlo(); + + $expectedDialog[] = 'MAIL FROM:'; + $expectedDialog[] = $p->responseBuffer[] = '250 Sender accepted'; + $this->_protocol->mail('from@example.com'); + + $expectedDialog[] = 'RCPT TO:'; + $expectedDialog[] = $p->responseBuffer[] = '500 error'; + $this->_protocol->rcpt('to@example.com'); + } + + + public function testMailBeforeHeloThrowsException() + { + try { + $this->_protocol->mail('from@example.com'); + $this->fail('mail() before helo() should throw exception'); + } catch (Zend_Mail_Protocol_Exception $e) { + $this->assertEquals('A valid session has not been started', $e->getMessage()); + } + } + + /** + * @depends testEhlo + */ + public function testRcptBeforeMailThrowsException() + { + $this->_connectAndEhlo(); + + try { + $this->_protocol->rcpt('to@example.com'); + $this->fail('rcpt() before mail() should throw exception'); + } catch (Zend_Mail_Protocol_Exception $e) { + $this->assertEquals('No sender reverse path has been supplied', $e->getMessage()); + } + } + + /** + * @depends testEhlo + * @expectedException Zend_Mail_Protocol_Exception + */ + public function testDataBeforeRcptThrowsException() + { + $this->_connectAndEhlo(); + + $this->_protocol->data('foo'); + } + + /** + * Performs the initial EHLO dialog + */ + protected function _connectAndEhlo() + { + $this->_protocol->responseBuffer = array( + '220 example.com ESMTP welcome', + '250 Hello 127.0.0.1, go ahead' + ); + + $this->_protocol->connect(); + $this->_protocol->helo(); + return $this->_protocol->dialog; + } +} + + +class ProtocolMock extends Zend_Mail_Protocol_Smtp +{ + public $dialog = array(); + public $responseBuffer = array(); + + /** + * Override connect function to use local file for testing + * + * @param string $remote + */ + protected function _connect($remote) + { + $this->_socket = tmpfile(); + } + + protected function _send($request) + { + $this->dialog[] = $request; + } + + protected function _receive($timeout = null) + { + $line = array_shift($this->responseBuffer); + $this->dialog[] = $line; + return $line; + } +} \ No newline at end of file Index: library/Zend/Mail/Protocol/Abstract.php =================================================================== --- library/Zend/Mail/Protocol/Abstract.php (revision 23443) +++ library/Zend/Mail/Protocol/Abstract.php (working copy) @@ -277,7 +277,7 @@ throw new Zend_Mail_Protocol_Exception($errorStr); } - if (($result = stream_set_timeout($this->_socket, self::TIMEOUT_CONNECTION)) === false) { + if (($result = $this->_setStreamTimeout(self::TIMEOUT_CONNECTION)) === false) { /** * @see Zend_Mail_Protocol_Exception */ @@ -357,7 +357,7 @@ // Adapters may wish to supply per-commend timeouts according to appropriate RFC if ($timeout !== null) { - stream_set_timeout($this->_socket, $timeout); + $this->_setStreamTimeout($timeout); } // Retrieve response @@ -428,9 +428,20 @@ * @see Zend_Mail_Protocol_Exception */ require_once 'Zend/Mail/Protocol/Exception.php'; - throw new Zend_Mail_Protocol_Exception($errMsg); + throw new Zend_Mail_Protocol_Exception($errMsg, $cmd); } return $msg; } + + /** + * Set stream timeout + * + * @param integer $timeout + * @return boolean + */ + protected function _setStreamTimeout($timeout) + { + return stream_set_timeout($this->_socket, $timeout); + } } Index: library/Zend/Mail/Protocol/Smtp.php =================================================================== --- library/Zend/Mail/Protocol/Smtp.php (revision 23443) +++ library/Zend/Mail/Protocol/Smtp.php (working copy) @@ -327,7 +327,7 @@ /** - * Issues the RSET command end validates answer + * Issues the RSET command and validates answer * * Can be used to restore a clean smtp communication state when a transaction has been cancelled or commencing a new transaction. * @@ -346,7 +346,7 @@ /** - * Issues the NOOP command end validates answer + * Issues the NOOP command and validates answer * * Not used by Zend_Mail, could be used to keep a connection alive or check if it is still open. * @@ -360,7 +360,7 @@ /** - * Issues the VRFY command end validates answer + * Issues the VRFY command and validates answer * * Not used by Zend_Mail. *