(REF) CRM_Utils_Mail - Split LoggingMailer into FilteredPearMailer and Logger
authorTim Otten <totten@civicrm.org>
Tue, 11 Feb 2020 01:48:50 +0000 (17:48 -0800)
committerTim Otten <totten@civicrm.org>
Tue, 11 Feb 2020 04:32:07 +0000 (20:32 -0800)
1. Captures more context (i.e. the original driver and params)
2. Changes various property names to avoid potential for conflict with delegated properties
3. Adds `addFilter($id, $func)` so that one can move more filters in here
4. Consolidates the various references ot CIVICRM_MAIL_LOG* into one file.

CRM/Utils/Mail.php
CRM/Utils/Mail/FilteredPearMailer.php [new file with mode: 0644]
CRM/Utils/Mail/Logger.php [new file with mode: 0644]
CRM/Utils/Mail/LoggingMailer.php [deleted file]
tests/phpunit/CRM/Utils/Mail/FilteredPearMailerTest.php [new file with mode: 0644]

index c6a982c587bb9d4ad361da13e626f39f24abe22c..02dc32cd317a4e5456e9328f7ef646dfe2762a19 100644 (file)
@@ -123,10 +123,17 @@ class CRM_Utils_Mail {
     }
     else {
       $mailer = Mail::factory($driver, $params);
-      // Previously, CiviCRM bundled patches to change the behavior of these three classes. Use a decorator to avoid patching.
-      if ($mailer instanceof Mail_smtp || $mailer instanceof Mail_mail || $mailer instanceof  Mail_sendmail) {
-        $mailer = new CRM_Utils_Mail_LoggingMailer($mailer);
-      }
+    }
+
+    // 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;
@@ -330,34 +337,10 @@ class CRM_Utils_Mail {
    * @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 (file)
index 0000000..1c11e23
--- /dev/null
@@ -0,0 +1,112 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * The filtered-mailer is a utility to wrap an existing PEAR Mail class
+ * and apply extra filters. It is primarily intended for resolving
+ * quirks in the standard implementations.
+ *
+ * This wrapper acts a bit like a chameleon, passing-through properties
+ * from the underlying object. Consequently, internal properties are
+ * prefixed with `_` to avoid conflict.
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+class CRM_Utils_Mail_FilteredPearMailer extends Mail {
+
+  /**
+   * @var string
+   *   Ex: 'smtp' or 'sendmail'
+   */
+  protected $_driver;
+
+  /**
+   * @var array
+   */
+  protected $_params;
+
+  /**
+   * @var Mail
+   */
+  protected $_delegate;
+
+  /**
+   * @var callable[]
+   */
+  protected $_filters = [];
+
+  /**
+   * CRM_Utils_Mail_FilteredPearMailer constructor.
+   * @param string $driver
+   * @param array $params
+   * @param Mail $mailer
+   */
+  public function __construct($driver, $params, $mailer) {
+    $this->_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 (file)
index 0000000..453554e
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * An attachment to PEAR Mail which logs emails to files based on
+ * the CIVICRM_MAIL_LOG configuration.
+ *
+ * (Produced by refactoring; specifically, extracting log-related functions
+ * from CRM_Utils_Mail.)
+ *
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ */
+class CRM_Utils_Mail_Logger {
+
+  /**
+   * @param CRM_Utils_Mail_FilteredPearMailer $mailer
+   * @param mixed $recipients
+   * @param array $headers
+   * @param string $body
+   * @return mixed
+   *   Normally returns null/void. But if the filter process is to be
+   *   short-circuited, then returns a concrete value.
+   */
+  public static function filter($mailer, &$recipients, &$headers, &$body) {
+    if (defined('CIVICRM_MAIL_LOG')) {
+      static::log($recipients, $headers, $body);
+      if (!defined('CIVICRM_MAIL_LOG_AND_SEND') && !defined('CIVICRM_MAIL_LOG_AND SEND')) {
+        return TRUE;
+      }
+    }
+  }
+
+  /**
+   * @param $to
+   * @param $headers
+   * @param $message
+   */
+  public static function log(&$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);
+    }
+  }
+
+}
diff --git a/CRM/Utils/Mail/LoggingMailer.php b/CRM/Utils/Mail/LoggingMailer.php
deleted file mode 100644 (file)
index 3f7b59b..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-/*
- +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC. All rights reserved.                        |
- |                                                                    |
- | This work is published under the GNU AGPLv3 license with some      |
- | permitted exceptions and without any warranty. For full license    |
- | and copyright information, see https://civicrm.org/licensing       |
- +--------------------------------------------------------------------+
- */
-
-/**
- * The logging-mailer is a utility to wrap an existing PEAR Mail class
- * and apply extra logging functionality.
- *
- * It replaces a set of patches which had been previously applied directly
- * to a few specific PEAR Mail classes.
- *
- * @package CRM
- * @copyright CiviCRM LLC https://civicrm.org/licensing
- */
-class CRM_Utils_Mail_LoggingMailer extends Mail {
-
-  /**
-   * @var Mail
-   */
-  protected $delegate;
-
-  /**
-   * @param Mail $delegate
-   */
-  public function __construct($delegate) {
-    $this->delegate = $delegate;
-  }
-
-  public function send($recipients, $headers, $body) {
-    if (defined('CIVICRM_MAIL_LOG')) {
-      CRM_Utils_Mail::logger($recipients, $headers, $body);
-      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');
-    }
-
-    return $this->delegate->send($recipients, $headers, $body);
-  }
-
-  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/tests/phpunit/CRM/Utils/Mail/FilteredPearMailerTest.php b/tests/phpunit/CRM/Utils/Mail/FilteredPearMailerTest.php
new file mode 100644 (file)
index 0000000..56b257c
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * Class CRM_Utils_Mail_FilteredPearMailerTest
+ * @group headless
+ */
+class CRM_Utils_Mail_FilteredPearMailerTest extends CiviUnitTestCase {
+
+  public function testFilter() {
+    $mock = new class() extends \Mail {
+      public $buf = [];
+
+      public function send($recipients, $headers, $body) {
+        $this->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);
+  }
+
+}