Browse Source

Merge pull request #3014 from ndm2/smtp-extensibility-response-access

SMTP transport - Extensibility and response access
Mark Story 12 years ago
parent
commit
dea6709d89

+ 133 - 17
lib/Cake/Network/Email/SmtpTransport.php

@@ -47,6 +47,42 @@ class SmtpTransport extends AbstractTransport {
 	protected $_content;
 
 /**
+ * The response of the last sent SMTP command.
+ *
+ * @var array
+ */
+	protected $_lastResponse = array();
+
+/**
+ * Returns the response of the last sent SMTP command.
+ *
+ * A response consists of one or more lines containing a response
+ * code and an optional response message text:
+ * {{{
+ * array(
+ *     array(
+ *         'code' => '250',
+ *         'message' => 'mail.example.com'
+ *     ),
+ *     array(
+ *         'code' => '250',
+ *         'message' => 'PIPELINING'
+ *     ),
+ *     array(
+ *         'code' => '250',
+ *         'message' => '8BITMIME'
+ *     ),
+ *     // etc...
+ * )
+ * }}}
+ *
+ * @return array
+ */
+	public function getLastResponse() {
+		return $this->_lastResponse;
+	}
+
+/**
  * Send mail
  *
  * @param CakeEmail $email CakeEmail
@@ -89,6 +125,25 @@ class SmtpTransport extends AbstractTransport {
 	}
 
 /**
+ * Parses and stores the reponse lines in `'code' => 'message'` format.
+ *
+ * @param array $responseLines
+ * @return void
+ */
+	protected function _bufferResponseLines(array $responseLines) {
+		$response = array();
+		foreach ($responseLines as $responseLine) {
+			if (preg_match('/^(\d{3})(?:[ -]+(.*))?$/', $responseLine, $match)) {
+				$response[] = array(
+					'code' => $match[1],
+					'message' => isset($match[2]) ? $match[2] : null
+				);
+			}
+		}
+		$this->_lastResponse = array_merge($this->_lastResponse, $response);
+	}
+
+/**
  * Connect to SMTP Server
  *
  * @return void
@@ -153,38 +208,65 @@ class SmtpTransport extends AbstractTransport {
 	}
 
 /**
- * Send emails
+ * Prepares the `MAIL FROM` SMTP command.
  *
- * @return void
- * @throws SocketException
+ * @param string $email The email address to send with the command.
+ * @return string
  */
-	protected function _sendRcpt() {
+	protected function _prepareFromCmd($email) {
+		return 'MAIL FROM:<' . $email . '>';
+	}
+
+/**
+ * Prepares the `RCPT TO` SMTP command.
+ *
+ * @param string $email The email address to send with the command.
+ * @return string
+ */
+	protected function _prepareRcptCmd($email) {
+		return 'RCPT TO:<' . $email . '>';
+	}
+
+/**
+ * Prepares the `from` email address.
+ *
+ * @return array
+ */
+	protected function _prepareFromAddress() {
 		$from = $this->_cakeEmail->returnPath();
 		if (empty($from)) {
 			$from = $this->_cakeEmail->from();
 		}
-		$this->_smtpSend('MAIL FROM:<' . key($from) . '>');
+		return $from;
+	}
 
+/**
+ * Prepares the recipient email addresses.
+ *
+ * @return array
+ */
+	protected function _prepareRecipientAddresses() {
 		$to = $this->_cakeEmail->to();
 		$cc = $this->_cakeEmail->cc();
 		$bcc = $this->_cakeEmail->bcc();
-		$emails = array_merge(array_keys($to), array_keys($cc), array_keys($bcc));
-		foreach ($emails as $email) {
-			$this->_smtpSend('RCPT TO:<' . $email . '>');
-		}
+		return array_merge(array_keys($to), array_keys($cc), array_keys($bcc));
 	}
 
 /**
- * Send Data
+ * Prepares the message headers.
  *
- * @return void
- * @throws SocketException
+ * @return array
  */
-	protected function _sendData() {
-		$this->_smtpSend('DATA', '354');
+	protected function _prepareMessageHeaders() {
+		return $this->_cakeEmail->getHeaders(array('from', 'sender', 'replyTo', 'readReceipt', 'to', 'cc', 'subject'));
+	}
 
-		$headers = $this->_cakeEmail->getHeaders(array('from', 'sender', 'replyTo', 'readReceipt', 'to', 'cc', 'subject'));
-		$headers = $this->_headersToString($headers);
+/**
+ * Prepares the message body.
+ *
+ * @return string
+ */
+	protected function _prepareMessage() {
 		$lines = $this->_cakeEmail->message();
 		$messages = array();
 		foreach ($lines as $line) {
@@ -194,7 +276,37 @@ class SmtpTransport extends AbstractTransport {
 				$messages[] = $line;
 			}
 		}
-		$message = implode("\r\n", $messages);
+		return implode("\r\n", $messages);
+	}
+
+/**
+ * Send emails
+ *
+ * @return void
+ * @throws SocketException
+ */
+	protected function _sendRcpt() {
+		$from = $this->_prepareFromAddress();
+		$this->_smtpSend($this->_prepareFromCmd(key($from)));
+
+		$emails = $this->_prepareRecipientAddresses();
+		foreach ($emails as $email) {
+			$this->_smtpSend($this->_prepareRcptCmd($email));
+		}
+	}
+
+/**
+ * Send Data
+ *
+ * @return void
+ * @throws SocketException
+ */
+	protected function _sendData() {
+		$this->_smtpSend('DATA', '354');
+
+		$headers = $this->_headersToString($this->_prepareMessageHeaders());
+		$message = $this->_prepareMessage();
+
 		$this->_smtpSend($headers . "\r\n\r\n" . $message . "\r\n\r\n\r\n.");
 		$this->_content = array('headers' => $headers, 'message' => $message);
 	}
@@ -229,6 +341,8 @@ class SmtpTransport extends AbstractTransport {
  * @throws SocketException
  */
 	protected function _smtpSend($data, $checkCode = '250') {
+		$this->_lastResponse = array();
+
 		if ($data !== null) {
 			$this->_socket->write($data . "\r\n");
 		}
@@ -244,6 +358,8 @@ class SmtpTransport extends AbstractTransport {
 			$responseLines = explode("\r\n", rtrim($response, "\r\n"));
 			$response = end($responseLines);
 
+			$this->_bufferResponseLines($responseLines);
+
 			if (preg_match('/^(' . $checkCode . ')(.)/', $response, $code)) {
 				if ($code[2] === '-') {
 					continue;

+ 89 - 1
lib/Cake/Test/Case/Network/Email/SmtpTransportTest.php

@@ -63,7 +63,7 @@ class SmtpTestTransport extends SmtpTransport {
  */
 	public function __call($method, $args) {
 		$method = '_' . $method;
-		return $this->$method();
+		return call_user_func_array(array($this, $method), $args);
 	}
 
 }
@@ -362,4 +362,92 @@ class SmtpTransportTest extends CakeTestCase {
 		$this->assertEquals($expected, $result);
 	}
 
+/**
+ * testGetLastResponse method
+ *
+ * @return void
+ */
+	public function testGetLastResponse() {
+		$this->assertEmpty($this->SmtpTransport->getLastResponse());
+
+		$this->socket->expects($this->any())->method('connect')->will($this->returnValue(true));
+		$this->socket->expects($this->at(0))->method('read')->will($this->returnValue(false));
+		$this->socket->expects($this->at(1))->method('read')->will($this->returnValue("220 Welcome message\r\n"));
+		$this->socket->expects($this->at(2))->method('write')->with("EHLO localhost\r\n");
+		$this->socket->expects($this->at(3))->method('read')->will($this->returnValue(false));
+		$this->socket->expects($this->at(4))->method('read')->will($this->returnValue("250-PIPELINING\r\n"));
+		$this->socket->expects($this->at(5))->method('read')->will($this->returnValue("250-SIZE 102400000\r\n"));
+		$this->socket->expects($this->at(6))->method('read')->will($this->returnValue("250-VRFY\r\n"));
+		$this->socket->expects($this->at(7))->method('read')->will($this->returnValue("250-ETRN\r\n"));
+		$this->socket->expects($this->at(8))->method('read')->will($this->returnValue("250-STARTTLS\r\n"));
+		$this->socket->expects($this->at(9))->method('read')->will($this->returnValue("250-AUTH PLAIN LOGIN\r\n"));
+		$this->socket->expects($this->at(10))->method('read')->will($this->returnValue("250-AUTH=PLAIN LOGIN\r\n"));
+		$this->socket->expects($this->at(11))->method('read')->will($this->returnValue("250-ENHANCEDSTATUSCODES\r\n"));
+		$this->socket->expects($this->at(12))->method('read')->will($this->returnValue("250-8BITMIME\r\n"));
+		$this->socket->expects($this->at(13))->method('read')->will($this->returnValue("250 DSN\r\n"));
+		$this->SmtpTransport->connect();
+
+		$expected = array(
+			array('code' => '250', 'message' => 'PIPELINING'),
+			array('code' => '250', 'message' => 'SIZE 102400000'),
+			array('code' => '250', 'message' => 'VRFY'),
+			array('code' => '250', 'message' => 'ETRN'),
+			array('code' => '250', 'message' => 'STARTTLS'),
+			array('code' => '250', 'message' => 'AUTH PLAIN LOGIN'),
+			array('code' => '250', 'message' => 'AUTH=PLAIN LOGIN'),
+			array('code' => '250', 'message' => 'ENHANCEDSTATUSCODES'),
+			array('code' => '250', 'message' => '8BITMIME'),
+			array('code' => '250', 'message' => 'DSN')
+		);
+		$result = $this->SmtpTransport->getLastResponse();
+		$this->assertEquals($expected, $result);
+
+		$email = new CakeEmail();
+		$email->from('noreply@cakephp.org', 'CakePHP Test');
+		$email->to('cake@cakephp.org', 'CakePHP');
+
+		$this->socket->expects($this->at(0))->method('write')->with("MAIL FROM:<noreply@cakephp.org>\r\n");
+		$this->socket->expects($this->at(1))->method('read')->will($this->returnValue(false));
+		$this->socket->expects($this->at(2))->method('read')->will($this->returnValue("250 OK\r\n"));
+		$this->socket->expects($this->at(3))->method('write')->with("RCPT TO:<cake@cakephp.org>\r\n");
+		$this->socket->expects($this->at(4))->method('read')->will($this->returnValue(false));
+		$this->socket->expects($this->at(5))->method('read')->will($this->returnValue("250 OK\r\n"));
+
+		$this->SmtpTransport->setCakeEmail($email);
+		$this->SmtpTransport->sendRcpt();
+
+		$expected = array(
+			array('code' => '250', 'message' => 'OK'),
+		);
+		$result = $this->SmtpTransport->getLastResponse();
+		$this->assertEquals($expected, $result);
+	}
+
+/**
+ * testBufferResponseLines method
+ *
+ * @return void
+ */
+	public function testBufferResponseLines() {
+		$reponseLines = array(
+			'123',
+			"456\tFOO",
+			'FOOBAR',
+			'250-PIPELINING',
+			'250-ENHANCEDSTATUSCODES',
+			'250-8BITMIME',
+			'250 DSN',
+		);
+		$this->SmtpTransport->bufferResponseLines($reponseLines);
+
+		$expected = array(
+			array('code' => '123', 'message' => null),
+			array('code' => '250', 'message' => 'PIPELINING'),
+			array('code' => '250', 'message' => 'ENHANCEDSTATUSCODES'),
+			array('code' => '250', 'message' => '8BITMIME'),
+			array('code' => '250', 'message' => 'DSN')
+		);
+		$result = $this->SmtpTransport->getLastResponse();
+		$this->assertEquals($expected, $result);
+	}
 }