Commit 0cd298da authored by Aegir user's avatar Aegir user
parents 4fea4f31 6e5fb4aa
# Change Log
## v0.2-alpha1
* Override core's `Mailing.preview` API to support rendering via
Flexmailer events.
* (BC Break) In the class `DefaultComposer`, change the signature for
`createMessageTemplates()` and `applyClickTracking()` to provide full
access to the event context (`$e`).
This diff is collapsed.
# org.civicrm.flexmailer
The FlexMailer is an email delivery system for CiviCRM v4.7+ which supports
batching and events, such as `WalkBatchesEvent`, `ComposeBatchEvent` and
`SendBatchEvent`.
FlexMailer includes default listeners for these events. The listeners behave
in basically the same way as CiviMail's traditional BAO-based delivery
system (respecting `mailerJobSize`, `mailThrottleTime`, `mailing_backend`,
`hook_civicrm_alterMailParams`, etal). However, this arrangement allows you
change behaviors in more fine-grained ways.
> NOTE: Some examples have not been tested well. This is an early revision of the doc+code!
## Installation
To download the latest alpha or beta version:
```
cv dl --dev flexmailer
```
To download the latest code from git:
```
git clone https://github.com/civicrm/org.civicrm.flexmailer.git $(cv path -x ./org.civicrm.flexmailer)
```
## Unit Tests
The headless unit tests are based on `phpunit` and `cv`. Simply run:
```
phpunit4
```
## Events: Inspection
To see what event listeners are configured, run:
```
cv debug:event-dispatcher /flexmail/
```
## Events: `ComposeBatchEvent`
The [`ComposeBatchEvent`](src/Event/ComposeBatchEvent.php) (`EVENT_COMPOSE`) builds the email messages. Each message
is represented as a [`FlexMailerTask`](src/FlexMailerTask.php) with a list of [`MailParams`](src/MailParams.php).
Some listeners are "under the hood" -- they define less visible parts of the message, e.g.
* `BasicHeaders` defines `Message-Id`, `Precedence`, `From`, `Reply-To`, and others.
* `BounceTracker` defines various headers for bounce-tracking.
* `OpenTracker` appends an HTML tracking code to any HTML messages.
The heavy-lifting of composing the message content is also handled by a listener, such as
[`DefaultComposer`](src/Listener/DefaultComposer.php). `DefaultComposer` replicates
traditional CiviMail functionality:
* Reads email content from `$mailing->body_text` and `$mailing->body_html`.
* Interprets tokens like `{contact.display_name}` and `{mailing.viewUrl}`.
* Loads data in batches.
* Post-processes the message with Smarty (if `CIVICRM_SMARTY` is enabled).
The traditional CiviMail semantics have some problems -- e.g. the Smarty post-processing is incompatible with Smarty's
template cache, and it is difficult to securely post-process the message with Smarty. However, changing the behavior
would break existing templates.
A major goal of FlexMailer is to facilitate a migration toward different template semantics. For example, an
extension might (naively) implement support for Mustache templates using:
```php
function mustache_civicrm_container($container) {
$container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
$container->findDefinition('dispatcher')->addMethodCall('addListener',
array(\Civi\FlexMailer\FlexMailer::EVENT_COMPOSE, '_mustache_compose_batch')
);
}
function _mustache_compose_batch(\Civi\FlexMailer\Event\ComposeBatchEvent $event) {
if ($event->getMailing()->template_type !== 'mustache') return;
$m = new Mustache_Engine();
foreach ($event->getTasks() as $task) {
if ($task->hasContent()) continue;
$contact = civicrm_api3('Contact', 'getsingle', array(
'id' => $task->getContactId(),
));
$task->setMailParam('text', $m->render($event->getMailing()->body_text, $contact));
$task->setMailParam('html', $m->render($event->getMailing()->body_html, $contact));
}
}
```
This implementation is naive in a few ways -- it performs separate SQL queries for each recipient; it doesn't optimize
the template compilation; it has a very limited range of tokens; and it doesn't handle click-through tracking. For
more ideas about these issues, review [`DefaultComposer`](src/Listener/DefaultComposer.php).
> FIXME: Core's `TokenProcessor` is useful for batch-loading token data.
> However, you currently have to use `addMessage()` and `render()` to kick it
> off -- but those are based on CiviMail template notation. We should provide
> another function that doesn't depend on the template notation -- so that
> other templates can leverage our token library.
> **Tip**: When you register a listener for `EVENT_COMPOSE`, note the weight.
> The default weight puts your listener in the middle of pipeline -- right
> before the `DefaultComposer`. However, you might want to position
> relative to other places -- e.g. `WEIGHT_PREPARE`, `WEIGHT_MAIN`,
> `WEIGHT_ALTER`, or `WEIGHT_END`.
## Events: `SendBatchEvent`
The [`SendBatchEvent`](src/Event/SendBatchEvent.php) (`EVENT_SEND`) takes a
batch of recipients and messages, and it delivers the messages. For
example, suppose you wanted to replace the built-in delivery mechanism with
a batch-oriented web-service:
```php
function example_civicrm_container($container) {
$container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
$container->findDefinition('dispatcher')->addMethodCall('addListener',
array(\Civi\FlexMailer\FlexMailer::EVENT_SEND, '_example_send_batch')
);
}
function _example_send_batch(\Civi\FlexMailer\Event\SendBatchEvent $event) {
$event->stopPropagation(); // Disable standard delivery
$context = stream_context_create(array(
'http' => array(
'method' => 'POST',
'header' => 'Content-type: application/vnd.php.serialize',
'content' => serialize($event->getTasks()),
),
));
return file_get_contents('https://example.org/batch-delivery', false, $context);
}
```
## Events: `WalkBatchesEvent`
The [`WalkBatchesEvent`](src/Event/WalkBatchesEvent.php) (`EVENT_WALK`)
examines the recipient list and pulls out a subset for whom you want to send
email. This is useful if you need strategies for chunking-out deliveries.
The basic formula for defining your own batch logic is:
```php
function example_civicrm_container($container) {
$container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
$container->findDefinition('dispatcher')->addMethodCall('addListener',
array(\Civi\FlexMailer\FlexMailer::EVENT_WALK, '_example_walk_batches')
);
}
function _example_walk_batches(\Civi\FlexMailer\Event\WalkBatchesEvent $event) {
$event->stopPropagation(); // Disable standard delivery
while (...) {
$tasks = array();
$task[] = new FlexMailerTask(...);
$task[] = new FlexMailerTask(...);
$task[] = new FlexMailerTask(...);
$event->visit($tasks);
}
}
```
## FAQ: How do you register a listener?
The examples above use `hook_civicrm_container` to manipulate the `dispatcher`;
and, specifically, they use `addListener()` to register a single global
function. However, you can also add multievent "subscribers" via
`addSubscriber()`, and you can register object-oriented services via
`addListenerService()` or `addSubscriberService()`.
For more discussion, see http://symfony.com/doc/current/components/event_dispatcher/introduction.html
## FAQ: Why use Symfony `EventDispatcher` instead of `CRM_Utils_Hook`?
There are several advantages of Symfony's event system -- for example, it
supports type-hinting, better in-source documentation, better
object-oriented modeling, and better refactoring. However, that's not why
FlexMailer uses it. Firstly, Symfony allows you to have multiple listeners
in the same module. Secondly, when you have multiple parties influencing an
event, the EventDispatcher allows you to both (a) set priorities among them
and (b) allow one listener to veto other listeners.
Some of these characteristics are also available in CMS event systems, but
not consistently so. The challenge of `CRM_Utils_Hook` is that it must
support the lowest-common denominator of functionality.
This diff is collapsed.
<?php
/**
* Civi v4.6 does not provide all the API's we would need to define
* FlexMailer in an extension, but you can patch core to simulate them.
* These define()s tell core to enable any such hacks (if available).
*/
define('CIVICRM_FLEXMAILER_HACK_DELIVER', '\Civi\FlexMailer\FlexMailer::createAndRun');
//define('CIVICRM_FLEXMAILER_HACK_SERVICES', '\Civi\FlexMailer\Services::registerServices');
//define('CIVICRM_FLEXMAILER_HACK_LISTENERS', '\Civi\FlexMailer\Services::registerListeners');
require_once 'flexmailer.civix.php';
/**
* Define an autoloader for FlexMailer.
*
* FlexMailer uses the namespace 'Civi\FlexMailer', but the
* autoloader in Civi v4.6 doesn't support this, so we provide
* our own autoloader.
*
* TODO: Whenever v4.6 dies, remove this file and define the
* autoloader in info.xml
*
* @link http://www.php-fig.org/psr/psr-4/examples/
*/
function _flexmailer_autoload($class) {
$prefix = 'Civi\\FlexMailer\\';
$base_dir = __DIR__ . '/src/';
$len = strlen($prefix);
if (strncmp($prefix, $class, $len) !== 0) {
return;
}
$relative_class = substr($class, $len);
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
if (file_exists($file)) {
require $file;
}
}
spl_autoload_register('_flexmailer_autoload');
/**
* Implements hook_civicrm_config().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_config
*/
function flexmailer_civicrm_config(&$config) {
_flexmailer_civix_civicrm_config($config);
}
/**
* Implements hook_civicrm_xmlMenu().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_xmlMenu
*/
function flexmailer_civicrm_xmlMenu(&$files) {
_flexmailer_civix_civicrm_xmlMenu($files);
}
/**
* Implements hook_civicrm_install().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_install
*/
function flexmailer_civicrm_install() {
_flexmailer_civix_civicrm_install();
}
/**
* Implements hook_civicrm_postInstall().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_postInstall
*/
function flexmailer_civicrm_postInstall() {
_flexmailer_civix_civicrm_postInstall();
}
/**
* Implements hook_civicrm_uninstall().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_uninstall
*/
function flexmailer_civicrm_uninstall() {
_flexmailer_civix_civicrm_uninstall();
}
/**
* Implements hook_civicrm_enable().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_enable
*/
function flexmailer_civicrm_enable() {
_flexmailer_civix_civicrm_enable();
}
/**
* Implements hook_civicrm_disable().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_disable
*/
function flexmailer_civicrm_disable() {
_flexmailer_civix_civicrm_disable();
}
/**
* Implements hook_civicrm_upgrade().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_upgrade
*/
function flexmailer_civicrm_upgrade($op, CRM_Queue_Queue $queue = NULL) {
return _flexmailer_civix_civicrm_upgrade($op, $queue);
}
/**
* Implements hook_civicrm_managed().
*
* Generate a list of entities to create/deactivate/delete when this module
* is installed, disabled, uninstalled.
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_managed
*/
function flexmailer_civicrm_managed(&$entities) {
_flexmailer_civix_civicrm_managed($entities);
}
/**
* Implements hook_civicrm_caseTypes().
*
* Generate a list of case-types.
*
* Note: This hook only runs in CiviCRM 4.4+.
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes
*/
function flexmailer_civicrm_caseTypes(&$caseTypes) {
_flexmailer_civix_civicrm_caseTypes($caseTypes);
}
/**
* Implements hook_civicrm_angularModules().
*
* Generate a list of Angular modules.
*
* Note: This hook only runs in CiviCRM 4.5+. It may
* use features only available in v4.6+.
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_caseTypes
*/
function flexmailer_civicrm_angularModules(&$angularModules) {
_flexmailer_civix_civicrm_angularModules($angularModules);
}
/**
* Implements hook_civicrm_alterSettingsFolders().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_alterSettingsFolders
*/
function flexmailer_civicrm_alterSettingsFolders(&$metaDataFolders = NULL) {
_flexmailer_civix_civicrm_alterSettingsFolders($metaDataFolders);
}
/**
* Functions below this ship commented out. Uncomment as required.
*
/**
* Implements hook_civicrm_preProcess().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_preProcess
*
function flexmailer_civicrm_preProcess($formName, &$form) {
} // */
/**
* Implements hook_civicrm_navigationMenu().
*
* @link http://wiki.civicrm.org/confluence/display/CRMDOC/hook_civicrm_navigationMenu
*
function flexmailer_civicrm_navigationMenu(&$menu) {
_flexmailer_civix_insert_navigation_menu($menu, NULL, array(
'label' => ts('The Page', array('domain' => 'org.civicrm.flexmailer')),
'name' => 'the_page',
'url' => 'civicrm/the-page',
'permission' => 'access CiviReport,access CiviContribute',
'operator' => 'OR',
'separator' => 0,
));
_flexmailer_civix_navigationMenu($menu);
} // */
/**
* Implements hook_civicrm_container().
*/
function flexmailer_civicrm_container($container) {
if (version_compare(\CRM_Utils_System::version(), '4.7.0', '>=')) {
$container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
}
\Civi\FlexMailer\Services::registerServices($container);
}
<?xml version="1.0"?>
<extension key="org.civicrm.flexmailer" type="module">
<file>flexmailer</file>
<name>FlexMailer</name>
<description>Flexible APIs for email delivery</description>
<license>AGPL-3.0</license>
<maintainer>
<author>Tim Otten</author>
<email>totten@civicrm.org</email>
</maintainer>
<urls>
<url desc="Main Extension Page">https://github.com/civicrm/org.civicrm.flexmailer</url>
<url desc="Documentation">https://github.com/civicrm/org.civicrm.flexmailer</url>
<url desc="Support">http://civicrm.stackexchange.com/</url>
<url desc="Licensing">http://www.gnu.org/licenses/agpl-3.0.html</url>
</urls>
<releaseDate>2017-03-15</releaseDate>
<version>0.2-alpha2</version>
<comments>
FlexMailer is an email delivery engine which replaces the internal guts
of CiviMail. By default, it supports the same delivery algorithms and
use-cases as CiviMail, but it also provides more APIs/events for
extension-developers. This enables other extensions to change the
template language, tracking codes, delivery mechanism, batching
algorithm, and other aspects of delivery. For API details, see the README.md.
</comments>
<compatibility>
<ver>4.7</ver>
</compatibility>
<civix>
<namespace>CRM/Flexmailer</namespace>
</civix>
</extension>
<?xml version="1.0"?>
<phpunit backupGlobals="false" backupStaticAttributes="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" syntaxCheck="false" bootstrap="tests/phpunit/bootstrap.php">
<testsuites>
<testsuite name="My Test Suite">
<directory>./tests/phpunit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">./</directory>
</whitelist>
</filter>
<listeners>
<listener class="Civi\Test\CiviTestListener">
<arguments/>
</listener>
</listeners>
</phpunit>
<?php
namespace Civi\FlexMailer\API;
use Civi\FlexMailer\FlexMailer;
use Civi\FlexMailer\FlexMailerTask;
use Civi\FlexMailer\Listener\Abdicator;
class MailingPreview {
/**
* Generate a preview of how a mailing would look.
*
* @param array $apiRequest
* - entity: string
* - action: string
* - params: array
* - id: int
* - contact_id: int
* @return array
* @throws \CRM_Core_Exception
*/
public static function preview($apiRequest) {
$params = $apiRequest['params'];
/** @var \CRM_Mailing_BAO_Mailing $mailing */
$mailing = \CRM_Mailing_BAO_Mailing::findById($params['id']);
if (!Abdicator::isFlexmailPreferred($mailing)) {
require_once 'api/v3/Mailing.php';
return civicrm_api3_mailing_preview($params);
}
$contactID = \CRM_Utils_Array::value('contact_id', $params,
\CRM_Core_Session::singleton()->get('userID'));
$job = new \CRM_Mailing_BAO_MailingJob();
$job->mailing_id = $mailing->id;
$job->is_test = 1;