diff --git a/CRM/Admin/Form/Setting/Smtp.php b/CRM/Admin/Form/Setting/Smtp.php index 87d30761cfa4..a3590b3ca2e6 100644 --- a/CRM/Admin/Form/Setting/Smtp.php +++ b/CRM/Admin/Form/Setting/Smtp.php @@ -165,7 +165,7 @@ public function postProcess() { 'Subject' => $subject, ]; - $mailer = Mail::factory($mailerName, $params); + $mailer = CRM_Utils_Mail::_createMailer($mailerName, $params); $errorScope = CRM_Core_TemporaryErrorScope::ignoreException(); $result = $mailer->send($toEmail, $headers, $message); diff --git a/CRM/Utils/Mail.php b/CRM/Utils/Mail.php index b9aa6ef617e2..696ed860d272 100644 --- a/CRM/Utils/Mail.php +++ b/CRM/Utils/Mail.php @@ -124,6 +124,17 @@ public static function _createMailer($driver, $params) { else { $mailer = Mail::factory($driver, $params); } + + // Previously, CiviCRM bundled patches to change the behavior of 3 specific drivers. Use wrapper/filters to avoid patching. + $mailer = new CRM_Utils_Mail_FilteredPearMailer($driver, $params, $mailer); + if (in_array($driver, ['smtp', 'mail', 'sendmail'])) { + $mailer->addFilter('2000_log', ['CRM_Utils_Mail_Logger', 'filter']); + $mailer->addFilter('2100_validate', function ($mailer, &$recipients, &$headers, &$body) { + if (!is_array($headers)) { + return PEAR::raiseError('$headers must be an array'); + } + }); + } CRM_Utils_Hook::alterMailer($mailer, $driver, $params); return $mailer; } @@ -268,7 +279,10 @@ public static function send(&$params) { // * All other mailers require that all be recipients be listed in the $to array AND that // the Bcc must not be present in $header as otherwise it will be shown to all recipients // ref: https://pear.php.net/bugs/bug.php?id=8047, full thread and answer [2011-04-19 20:48 UTC] - if (get_class($mailer) != "Mail_mail") { + // TODO: Refactor this quirk-handler as another filter in FilteredPearMailer. But that would merit review of impact on universe. + $driver = ($mailer instanceof CRM_Utils_Mail_FilteredPearMailer) ? $mailer->getDriver() : NULL; + $isPhpMail = (get_class($mailer) === "Mail_mail" || $driver === 'mail'); + if (!$isPhpMail) { // get emails from headers, since these are // combination of name and email addresses. if (!empty($headers['Cc'])) { @@ -326,34 +340,10 @@ public static function errorMessage($mailer, $result) { * @param $to * @param $headers * @param $message + * @deprecated */ public static function logger(&$to, &$headers, &$message) { - if (is_array($to)) { - $toString = implode(', ', $to); - $fileName = $to[0]; - } - else { - $toString = $fileName = $to; - } - $content = "To: " . $toString . "\n"; - foreach ($headers as $key => $val) { - $content .= "$key: $val\n"; - } - $content .= "\n" . $message . "\n"; - - if (is_numeric(CIVICRM_MAIL_LOG)) { - $config = CRM_Core_Config::singleton(); - // create the directory if not there - $dirName = $config->configAndLogDir . 'mail' . DIRECTORY_SEPARATOR; - CRM_Utils_File::createDir($dirName); - $fileName = md5(uniqid(CRM_Utils_String::munge($fileName))) . '.txt'; - file_put_contents($dirName . $fileName, - $content - ); - } - else { - file_put_contents(CIVICRM_MAIL_LOG, $content, FILE_APPEND); - } + CRM_Utils_Mail_Logger::log($to, $headers, $message); } /** diff --git a/CRM/Utils/Mail/FilteredPearMailer.php b/CRM/Utils/Mail/FilteredPearMailer.php new file mode 100644 index 000000000000..1c11e23d16db --- /dev/null +++ b/CRM/Utils/Mail/FilteredPearMailer.php @@ -0,0 +1,112 @@ +_driver = $driver; + $this->_params = $params; + $this->_delegate = $mailer; + } + + public function send($recipients, $headers, $body) { + $filterArgs = [$this, &$recipients, &$headers, &$body]; + foreach ($this->_filters as $filter) { + $result = call_user_func_array($filter, $filterArgs); + if ($result !== NULL) { + return $result; + } + } + + return $this->_delegate->send($recipients, $headers, $body); + } + + /** + * @param string $id + * Unique ID for this filter. Filters are sorted by ID. + * Suggestion: '{nnnn}_{name}', where '{nnnn}' is a number. + * Filters are sorted and executed in order. + * @param callable $func + * function(FilteredPearMailer $mailer, mixed $recipients, array $headers, string $body). + * The return value should generally be null/void. However, if you wish to + * short-circuit execution of the filters, then return a concrete value. + * @return static + */ + public function addFilter($id, $func) { + $this->_filters[$id] = $func; + ksort($this->_filters); + return $this; + } + + /** + * @return string + * Ex: 'smtp', 'sendmail', 'mail'. + */ + public function getDriver() { + return $this->_driver; + } + + public function &__get($name) { + return $this->_delegate->{$name}; + } + + public function __set($name, $value) { + return $this->_delegate->{$name} = $value; + } + + public function __isset($name) { + return isset($this->_delegate->{$name}); + } + + public function __unset($name) { + unset($this->_delegate->{$name}); + } + +} diff --git a/CRM/Utils/Mail/Logger.php b/CRM/Utils/Mail/Logger.php new file mode 100644 index 000000000000..453554eddd41 --- /dev/null +++ b/CRM/Utils/Mail/Logger.php @@ -0,0 +1,76 @@ + $val) { + $content .= "$key: $val\n"; + } + $content .= "\n" . $message . "\n"; + + if (is_numeric(CIVICRM_MAIL_LOG)) { + $config = CRM_Core_Config::singleton(); + // create the directory if not there + $dirName = $config->configAndLogDir . 'mail' . DIRECTORY_SEPARATOR; + CRM_Utils_File::createDir($dirName); + $fileName = md5(uniqid(CRM_Utils_String::munge($fileName))) . '.txt'; + file_put_contents($dirName . $fileName, + $content + ); + } + else { + file_put_contents(CIVICRM_MAIL_LOG, $content, FILE_APPEND); + } + } + +} diff --git a/tests/phpunit/CRM/Utils/Mail/FilteredPearMailerTest.php b/tests/phpunit/CRM/Utils/Mail/FilteredPearMailerTest.php new file mode 100644 index 000000000000..56b257c9d6a8 --- /dev/null +++ b/tests/phpunit/CRM/Utils/Mail/FilteredPearMailerTest.php @@ -0,0 +1,54 @@ +buf['recipients'] = $recipients; + $this->buf['headers'] = $headers; + $this->buf['body'] = $body; + return 'all the fruits in the basket'; + } + + }; + + $fm = new CRM_Utils_Mail_FilteredPearMailer('mock', [], $mock); + $fm->addFilter('1000_apple', function ($mailer, &$recipients, &$headers, &$body) { + $body .= ' with apples!'; + }); + $fm->addFilter('1000_banana', function ($mailer, &$recipients, &$headers, &$body) { + $headers['Banana'] = 'Cavendish'; + }); + $r = $fm->send(['recip'], ['Subject' => 'Fruit loops'], 'body'); + + $this->assertEquals('Fruit loops', $mock->buf['headers']['Subject']); + $this->assertEquals('Cavendish', $mock->buf['headers']['Banana']); + $this->assertEquals('body with apples!', $mock->buf['body']); + $this->assertEquals('all the fruits in the basket', $r); + } + + public function testFilter_shortCircuit() { + $mock = new class() extends \Mail { + + public function send($recipients, $headers, $body) { + return 'all the fruits in the basket'; + } + + }; + + $fm = new CRM_Utils_Mail_FilteredPearMailer('mock', [], $mock); + $fm->addFilter('1000_short_circuit', function ($mailer, &$recipients, &$headers, &$body) { + return 'the triumph of veggies over fruits'; + }); + $r = $fm->send(['recip'], ['Subject' => 'Fruit loops'], 'body'); + $this->assertEquals('the triumph of veggies over fruits', $r); + } + +} diff --git a/tools/scripts/composer/patches/pear-mail.patch.txt b/tools/scripts/composer/patches/pear-mail.patch.txt index e2f488dd3310..23fa638c9d40 100644 --- a/tools/scripts/composer/patches/pear-mail.patch.txt +++ b/tools/scripts/composer/patches/pear-mail.patch.txt @@ -17,21 +17,6 @@ diff --git a/Mail/mail.php b/Mail/mail.php index ee1ecef..ae6e2e8 100644 --- a/Mail/mail.php +++ b/Mail/mail.php -@@ -114,6 +114,14 @@ class Mail_mail extends Mail { - */ - public function send($recipients, $headers, $body) - { -+ if (defined('CIVICRM_MAIL_LOG')) { -+ CRM_Utils_Mail::logger($recipients, $headers, $body); -+ // Note: "CIVICRM_MAIL_LOG_AND SEND" (space not underscore) was a typo that existed for some years, so kept here for compatibility, but it should not be used. -+ if (!defined('CIVICRM_MAIL_LOG_AND_SEND') && !defined('CIVICRM_MAIL_LOG_AND SEND')) { -+ return true; -+ } -+ } -+ - if (!is_array($headers)) { - return PEAR::raiseError('$headers must be an array'); - } @@ -145,7 +153,12 @@ class Mail_mail extends Mail { if (is_a($headerElements, 'PEAR_Error')) { return $headerElements; @@ -46,41 +31,3 @@ index ee1ecef..ae6e2e8 100644 // We only use mail()'s optional fifth parameter if the additional // parameters have been provided and we're not running in safe mode. -diff --git a/Mail/sendmail.php b/Mail/sendmail.php -index 7e8f804..e0300a0 100644 ---- a/Mail/sendmail.php -+++ b/Mail/sendmail.php -@@ -132,6 +132,14 @@ class Mail_sendmail extends Mail { - */ - public function send($recipients, $headers, $body) - { -+ if (defined('CIVICRM_MAIL_LOG')) { -+ CRM_Utils_Mail::logger($recipients, $headers, $body); -+ // Note: "CIVICRM_MAIL_LOG_AND SEND" (space not underscore) was a typo that existed for some years, so kept here for compatibility, but it should not be used. -+ if (!defined('CIVICRM_MAIL_LOG_AND_SEND') && !defined('CIVICRM_MAIL_LOG_AND SEND')) { -+ return true; -+ } -+ } -+ - if (!is_array($headers)) { - return PEAR::raiseError('$headers must be an array'); - } -diff --git a/Mail/smtp.php b/Mail/smtp.php -index 5e698fe..5f057e2 100644 ---- a/Mail/smtp.php -+++ b/Mail/smtp.php -@@ -255,6 +255,14 @@ class Mail_smtp extends Mail { - */ - public function send($recipients, $headers, $body) - { -+ if (defined('CIVICRM_MAIL_LOG')) { -+ CRM_Utils_Mail::logger($recipients, $headers, $body); -+ // Note: "CIVICRM_MAIL_LOG_AND SEND" (space not underscore) was a typo that existed for some years, so kept here for compatibility, but it should not be used. -+ if (!defined('CIVICRM_MAIL_LOG_AND_SEND') && !defined('CIVICRM_MAIL_LOG_AND SEND')) { -+ return true; -+ } -+ } -+ - $result = $this->send_or_fail($recipients, $headers, $body); - - /* If persistent connections are disabled, destroy our SMTP object. */