ZF-8528: Patch to add SMTP pipelining support and suppression of RCPT exceptions to Zend_Mail_Protocol_Smtp


h2. Patch overview

The attached patch adds two capabilities to Zend_Mail_Protocol_Smtp:

(1) Option to enable SMTP pipelining (www.ietf.org/rfc/rfc2920.txt" rel="nofollow">rfc 2920). This greatly speeds up SMTP delivery on high-latency connections or when delivering many emails or recipients as there are less round-trips waiting for server responses. (To enable, use constructor $config option pipelining=true).

$transport = new Zend_Mail_Protocol_Smtp('smtp.domain.com', array('pipelining' => true));

(2) Option to suppress exceptions on RCPT commands. Useful if you want a message to proceed to send even if one or more recipients are rejected by the server when the RCPT command is issued (use constructor $config option throwRcptExceptions=false). Retrieve RCPT exceptions via getRcptExceptions() -- returns an array where the keys are the failed recipient address and the values are the stored exceptions -- use $exception->getMessage() to read each SMTP response.

$config = array('throwRcptExceptions' => false);
$transport = new Zend_Mail_Transport_Smtp('smtphost', $config));

$mail = new Zend_Mail;
$mail->addTo('invalid@domain.com', 'foo'); /* would normally throw exception */
$mail->addTo('valid@domain.com', 'foo');
// iterate through rcpt exceptions
foreach ($transport->getConnection()->getRcptExceptions() as $key => $exception) {
    echo sprintf('Failed to send to %s - server responded "%s"', $key, $exception->getMessage());

// get list of failed recipients
$exceptions       = $transport->getConnection()->getRcptExceptions();
$failedRecipients = array_keys($exceptions);

// get count of failed and successful recipients
$numFailed     = count($exceptions);
$numSuccessful = count($mail->getRecipients()) - $numFailed;

h2. Internals

SMTP pipelining allows batches of commands (as implemented here, MAIL FROM, RCPT TO, and RSET) to be sent without waiting for server response. This has been implemented in Zend_Mail_Protocol_Smtp by having the _expect() function queue expected server responses until a non-pipelining command (i.e. DATA) is issued, at which point all queued server responses are processed in sequence, evaluated against the expected responses.

-Unfortunately there is currently very thin unit tests for the SMTP components of Zend_Mail. As the parent class, Zend_Mail_Protocol_Abstract operates directly on sockets which is a necessity to facilitate TLS encryption features, I do not see an easy way to mock up a test adapter to use for testing. Any suggestions about how to do this to allow better testing of the new functionality would be appreciated.- {color:green}Came up with a way to mock the socket connection for unit testing; now awaiting commit of ZF-10741 and will then add unit tests to this patch{color}

The patch also adds internals to facilitate future improvements related to SMTP extensions. EHLO response (the list of server-supported SMTP extensions) is now parsed and can be queried with $this->_supports(). So, future enhancements could include parsing of ENHANCEDSTATUSCODES (www.ietf.org/rfc/rfc2034.txt" rel="nofollow">rfc 2034), SIZE (www.ietf.org/rfc/rfc1870.txt" rel="nofollow">rfc 1870), etc.


Recommend applying (ZF-8511) before this patch (ZF-8528) as ZF-8511 resolves an issue with Zend_Mail not clearing the receive buffer when throwing an exception. ZF-8528 needs this ability as it internally uses a try/catch block to continue processing after an RCPT error when throwRcptExceptions config option is set to false.

Satoru, could you please provide comment? This patch was for ZF 1.9.6 and it would be nice to get this applied before we're too far along if you think it is acceptable. Or, I'm happy to make changes if you feel they are needed. Would like to get this into ZF so I can stop patching my local version with every release. Thanks.

Sorry, I have been inactive since last April.

Linking as depending on ZF-10741. Will refactor this patch and add unit tests once the unit tests on ZF-10741 are committed