Skip to content

Commit

Permalink
Merge pull request #11838 from mfb/ses-smtp-support
Browse files Browse the repository at this point in the history
CiviMail: Fix logic for handling SMTP socket errors, temporary failures and permanent failures
  • Loading branch information
eileenmcnaughton authored Jul 13, 2018
2 parents a2b25ec + e02b6fc commit a866838
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 4 deletions.
42 changes: 38 additions & 4 deletions CRM/Mailing/BAO/MailingJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -676,10 +676,7 @@ public function deliverGroup(&$fields, &$mailing, &$mailer, &$job_date, &$attach
if (is_a($result, 'PEAR_Error') && !$mailing->sms_provider_id) {
// CRM-9191
$message = $result->getMessage();
if (
strpos($message, 'Failed to write to socket') !== FALSE ||
strpos($message, 'Failed to set sender') !== FALSE
) {
if ($this->isTemporaryError($message)) {
// lets log this message and code
$code = $result->getCode();
CRM_Core_Error::debug_log_message("SMTP Socket Error or failed to set sender error. Message: $message, Code: $code");
Expand Down Expand Up @@ -786,6 +783,43 @@ public function deliverGroup(&$fields, &$mailing, &$mailer, &$job_date, &$attach
return $result;
}

/**
* Determine if an SMTP error is temporary or permanent.
*
* @param string $message
* PEAR error message.
* @return bool
* TRUE - Temporary/retriable error
* FALSE - Permanent/non-retriable error
*/
protected function isTemporaryError($message) {
// SMTP response code is buried in the message.
$code = preg_match('/ \(code: (.+), response: /', $message, $matches) ? $matches[1] : '';

if (strpos($message, 'Failed to write to socket') !== FALSE) {
return TRUE;
}

// Register 5xx SMTP response code (permanent failure) as bounce.
if (isset($code{0}) && $code{0} === '5') {
return FALSE;
}

if (strpos($message, 'Failed to set sender') !== FALSE) {
return TRUE;
}

if (strpos($message, 'Failed to add recipient') !== FALSE) {
return TRUE;
}

if (strpos($message, 'Failed to send data') !== FALSE) {
return TRUE;
}

return FALSE;
}

/**
* Cancel a mailing.
*
Expand Down
67 changes: 67 additions & 0 deletions tests/phpunit/CRM/Mailing/BAO/MailingJobTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php
/*
+--------------------------------------------------------------------+
| CiviCRM version 5.4 |
+--------------------------------------------------------------------+
| Copyright CiviCRM LLC (c) 2004-2018 |
+--------------------------------------------------------------------+
| This file is a part of CiviCRM. |
| |
| CiviCRM is free software; you can copy, modify, and distribute it |
| under the terms of the GNU Affero General Public License |
| Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
| |
| CiviCRM is distributed in the hope that it will be useful, but |
| WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
| See the GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public |
| License and the CiviCRM Licensing Exception along |
| with this program; if not, contact CiviCRM LLC |
| at info[AT]civicrm[DOT]org. If you have questions about the |
| GNU Affero General Public License or the licensing of CiviCRM, |
| see the CiviCRM license FAQ at http://civicrm.org/licensing |
+--------------------------------------------------------------------+
*/

/**
* Class CRM_Mailing_BAO_MailingTest
* @group headless
*/
class CRM_Mailing_BAO_MailingJobTest extends CiviUnitTestCase {

/**
* Calls a protected method.
*/
public static function callMethod($obj, $name, $args) {
$class = new ReflectionClass($obj);
$method = $class->getMethod($name);
$method->setAccessible(TRUE);
return $method->invokeArgs($obj, $args);
}

/**
* Tests CRM_Mailing_BAO_MailingJob::isTemporaryError() method.
*/
public function testIsTemporaryError() {
$testcases[] = ['return' => TRUE, 'message' => 'Failed to set sender: test@example.org [SMTP: Invalid response code received from SMTP server while sending email. This is often caused by a misconfiguration in Outbound Email settings. Please verify the settings at Administer CiviCRM >> Global Settings >> Outbound Email (SMTP). (code: 421, response: Timeout waiting for data from client.)]'];
$testcases[] = ['return' => TRUE, 'message' => 'Failed to send data [SMTP: Invalid response code received from SMTP server while sending email. This is often caused by a misconfiguration in Outbound Email settings. Please verify the settings at Administer CiviCRM >> Global Settings >> Outbound Email (SMTP). (code: 454, response: Throttling failure: Maximum sending rate exceeded.)]'];
$testcases[] = ['return' => TRUE, 'message' => 'Failed to set sender: test@example.org [SMTP: Failed to write to socket: not connected (code: -1, response: )]'];
// @fixme: These errors also seem to be temporary, but are not yet handled as temporary.
$testcases[] = ['return' => FALSE, 'message' => 'Failed to connect to email.example.com:587 [SMTP: Failed to connect socket: Connection timed out (code: -1, response: )]'];
$testcases[] = ['return' => FALSE, 'message' => 'Failed to send data [SMTP: Invalid response code received from SMTP server while sending email. This is often caused by a misconfiguration in Outbound Email settings. Please verify the settings at Administer CiviCRM >> Global Settings >> Outbound Email (SMTP). (code: 554, response: Message rejected: Sending suspended for this account. For more information, please check the inbox of the email address associated with your AWS account.)]'];
$testcases[] = ['return' => FALSE, 'message' => 'authentication failure [SMTP: Invalid response code received from SMTP server while sending email. This is often caused by a misconfiguration in Outbound Email settings. Please verify the settings at Administer CiviCRM >> Global Settings >> Outbound Email (SMTP). (code: 454, response: Temporary authentication failure)]'];
$object = new CRM_Mailing_BAO_MailingJob();
foreach ($testcases as $testcase) {
$isTemporaryError = self::callMethod($object, 'isTemporaryError', [$testcase['message']]);
if ($testcase['return']) {
$this->assertTrue($isTemporaryError);
}
else {
$this->assertFalse($isTemporaryError);
}
}
}

}

0 comments on commit a866838

Please sign in to comment.