diff --git a/Civi/FlexMailer/Event/AlterBatchEvent.php b/Civi/FlexMailer/Event/AlterBatchEvent.php new file mode 100644 index 000000000000..3e8d4d41bfce --- /dev/null +++ b/Civi/FlexMailer/Event/AlterBatchEvent.php @@ -0,0 +1,52 @@ + + */ + private $tasks; + + public function __construct($context, $tasks) { + parent::__construct($context); + $this->tasks = $tasks; + } + + /** + * @return array + */ + public function getTasks() { + return $this->tasks; + } + +} diff --git a/Civi/FlexMailer/Event/BaseEvent.php b/Civi/FlexMailer/Event/BaseEvent.php new file mode 100644 index 000000000000..6f5974c574f3 --- /dev/null +++ b/Civi/FlexMailer/Event/BaseEvent.php @@ -0,0 +1,72 @@ +context = $context; + } + + /** + * @return \CRM_Mailing_BAO_Mailing + */ + public function getMailing() { + return $this->context['mailing']; + } + + /** + * @return \CRM_Mailing_BAO_MailingJob + */ + public function getJob() { + return $this->context['job']; + } + + /** + * @return array|NULL + */ + public function getAttachments() { + return $this->context['attachments']; + } + +} diff --git a/Civi/FlexMailer/Event/ComposeBatchEvent.php b/Civi/FlexMailer/Event/ComposeBatchEvent.php new file mode 100644 index 000000000000..dfa81c71ea2a --- /dev/null +++ b/Civi/FlexMailer/Event/ComposeBatchEvent.php @@ -0,0 +1,52 @@ + + */ + private $tasks; + + public function __construct($context, $tasks) { + parent::__construct($context); + $this->tasks = $tasks; + } + + /** + * @return array + */ + public function getTasks() { + return $this->tasks; + } + +} diff --git a/Civi/FlexMailer/Event/RunEvent.php b/Civi/FlexMailer/Event/RunEvent.php new file mode 100644 index 000000000000..1b31d55f2f9d --- /dev/null +++ b/Civi/FlexMailer/Event/RunEvent.php @@ -0,0 +1,64 @@ +stopPropagation()` + * and `$event->setCompleted($bool)`. + */ +class RunEvent extends BaseEvent { + + /** + * @var bool|NULL + */ + private $isCompleted = NULL; + + /** + * @return bool|NULL + */ + public function getCompleted() { + return $this->isCompleted; + } + + /** + * @param bool|NULL $isCompleted + * @return RunEvent + */ + public function setCompleted($isCompleted) { + $this->isCompleted = $isCompleted; + return $this; + } + +} diff --git a/Civi/FlexMailer/Event/SendBatchEvent.php b/Civi/FlexMailer/Event/SendBatchEvent.php new file mode 100644 index 000000000000..f2b7d221f3dd --- /dev/null +++ b/Civi/FlexMailer/Event/SendBatchEvent.php @@ -0,0 +1,73 @@ + + */ + private $tasks; + + /** + * @var bool|NULL + */ + private $isCompleted = NULL; + + public function __construct($context, $tasks) { + parent::__construct($context); + $this->tasks = $tasks; + } + + /** + * @return array + */ + public function getTasks() { + return $this->tasks; + } + + /** + * @return bool|NULL + */ + public function getCompleted() { + return $this->isCompleted; + } + + /** + * @param bool|NULL $isCompleted + * @return SendBatchEvent + */ + public function setCompleted($isCompleted) { + $this->isCompleted = $isCompleted; + return $this; + } + +} diff --git a/Civi/FlexMailer/Event/WalkBatchesEvent.php b/Civi/FlexMailer/Event/WalkBatchesEvent.php new file mode 100644 index 000000000000..6e290276f467 --- /dev/null +++ b/Civi/FlexMailer/Event/WalkBatchesEvent.php @@ -0,0 +1,74 @@ +callback = $callback; + } + + /** + * @return bool|NULL + */ + public function getCompleted() { + return $this->isDelivered; + } + + /** + * @param bool|NULL $isCompleted + * @return WalkBatchesEvent + */ + public function setCompleted($isCompleted) { + $this->isDelivered = $isCompleted; + return $this; + } + + /** + * @param array $tasks + * @return mixed + */ + public function visit($tasks) { + return call_user_func($this->callback, $tasks); + } + +} diff --git a/Civi/FlexMailer/FlexMailer.php b/Civi/FlexMailer/FlexMailer.php new file mode 100644 index 000000000000..64d0d4c9e816 --- /dev/null +++ b/Civi/FlexMailer/FlexMailer.php @@ -0,0 +1,257 @@ +setDefinition('mymod_subscriber', new Definition('MymodSubscriber', array())) + * ->addTag('kernel.event_subscriber'); + * } + * + * FlexMailer includes default listeners for all of these events. They + * behaves in basically the same way as CiviMail's traditional BAO-based + * delivery system (respecting mailerJobSize, mailThrottleTime, + * mailing_backend, hook_civicrm_alterMailParams, etal). However, you + * can replace any of the major functions, e.g. + * + * - If you send large blasts across multiple servers, then you may + * prefer a different algorithm for splitting the recipient list. + * Listen for WalkBatchesEvent. + * - If you want to compose messages in a new way (e.g. a different + * templating language), then listen for ComposeBatchEvent. + * - If you want to add extra email headers or tracking codes, + * then listen for AlterBatchEvent. + * - If you want to deliver messages through a different medium + * (such as web-services or batched SMTP), listen for SendBatchEvent. + * + * In all cases, your function can listen to the event and then decide what + * to do. If your listener does the work required for the event, then + * you can disable the default listener by calling `$event->stopPropagation()`. + * + * @see CRM_Utils_Hook::container + * @see Civi\Core\Container + * @see DefaultBatcher + * @see DefaultComposer + * @see DefaultSender + * @see HookAdapter + * @see OpenTracker + * @link http://symfony.com/doc/current/components/event_dispatcher.html + */ +class FlexMailer { + + const EVENT_RUN = 'civi.flexmailer.run'; + const EVENT_WALK = 'civi.flexmailer.walk'; + const EVENT_COMPOSE = 'civi.flexmailer.compose'; + const EVENT_ALTER = 'civi.flexmailer.alter'; + const EVENT_SEND = 'civi.flexmailer.send'; + + /** + * @return array + * Array(string $event => string $class). + */ + public static function getEventTypes() { + return array( + self::EVENT_RUN => 'Civi\\FlexMailer\\Event\\RunEvent', + self::EVENT_WALK => 'Civi\\FlexMailer\\Event\\WalkBatchesEvent', + self::EVENT_COMPOSE => 'Civi\\FlexMailer\\Event\\ComposeBatchEvent', + self::EVENT_ALTER => 'Civi\\FlexMailer\\Event\\AlterBatchEvent', + self::EVENT_SEND => 'Civi\\FlexMailer\\Event\\SendBatchEvent', + ); + } + + /** + * @var array + * An array which must define options: + * - mailing: \CRM_Mailing_BAO_Mailing + * - job: \CRM_Mailing_BAO_MailingJob + * - attachments: array + * + * Additional options may be passed. To avoid naming conflicts, use prefixing. + */ + public $context; + + /** + * @var EventDispatcherInterface + */ + private $dispatcher; + + /** + * Create a new FlexMailer instance, using data available in the CiviMail runJobs(). + * + * @param \CRM_Mailing_BAO_MailingJob $job + * @param object $deprecatedMessageMailer + * @param array $deprecatedTestParams + * @return bool + * TRUE if delivery completed. + */ + public static function createAndRun($job, $deprecatedMessageMailer, $deprecatedTestParams) { + $flexMailer = new \Civi\FlexMailer\FlexMailer(array( + 'mailing' => \CRM_Mailing_BAO_Mailing::findById($job->mailing_id), + 'job' => $job, + 'attachments' => \CRM_Core_BAO_File::getEntityFile('civicrm_mailing', $job->mailing_id), + 'deprecatedMessageMailer' => $deprecatedMessageMailer, + 'deprecatedTestParams' => $deprecatedTestParams, + )); + return $flexMailer->run(); + } + + /** + * FlexMailer constructor. + * @param array $context + * An array which must define options: + * - mailing: \CRM_Mailing_BAO_Mailing + * - job: \CRM_Mailing_BAO_MailingJob + * - attachments: array + * @param EventDispatcherInterface $dispatcher + */ + public function __construct($context = array(), EventDispatcherInterface $dispatcher = NULL) { + $this->context = $context; + $this->dispatcher = $dispatcher ? $dispatcher : \Civi::service('dispatcher'); + } + + /** + * @return bool + * TRUE if delivery completed. + * @throws \CRM_Core_Exception + */ + public function run() { + $flexMailer = $this; // PHP 5.3 + + if (count($this->validate()) > 0) { + throw new \CRM_Core_Exception("FlexMailer cannot execute: invalid context"); + } + + $run = $this->fireRun(); + if ($run->isPropagationStopped()) { + return $run->getCompleted(); + } + + $walkBatches = $this->fireWalkBatches(function ($tasks) use ($flexMailer) { + $flexMailer->fireComposeBatch($tasks); + $flexMailer->fireAlterBatch($tasks); + $sendBatch = $flexMailer->fireSendBatch($tasks); + return $sendBatch->getCompleted(); + }); + + return $walkBatches->getCompleted(); + } + + /** + * @return array + * List of error messages + */ + public function validate() { + $errors = array(); + if (empty($this->context['mailing'])) { + $errors['mailing'] = 'Missing \"mailing\"'; + } + if (empty($this->context['job'])) { + $errors['job'] = 'Missing \"job\"'; + } + return $errors; + } + + /** + * @return RunEvent + */ + public function fireRun() { + $event = new RunEvent($this->context); + $this->dispatcher->dispatch(self::EVENT_RUN, $event); + return $event; + } + + /** + * @param callable $onVisitBatch + * @return WalkBatchesEvent + */ + public function fireWalkBatches($onVisitBatch) { + $event = new WalkBatchesEvent($this->context, $onVisitBatch); + $this->dispatcher->dispatch(self::EVENT_WALK, $event); + return $event; + } + + /** + * @param array $tasks + * @return ComposeBatchEvent + */ + public function fireComposeBatch($tasks) { + $event = new ComposeBatchEvent($this->context, $tasks); + $this->dispatcher->dispatch(self::EVENT_COMPOSE, $event); + return $event; + } + + /** + * @param array $tasks + * @return AlterBatchEvent + */ + public function fireAlterBatch($tasks) { + $event = new AlterBatchEvent($this->context, $tasks); + $this->dispatcher->dispatch(self::EVENT_ALTER, $event); + return $event; + } + + /** + * @param array $tasks + * @return SendBatchEvent + */ + public function fireSendBatch($tasks) { + $event = new SendBatchEvent($this->context, $tasks); + $this->dispatcher->dispatch(self::EVENT_SEND, $event); + return $event; + } + +} diff --git a/Civi/FlexMailer/FlexMailerTask.php b/Civi/FlexMailer/FlexMailerTask.php new file mode 100644 index 000000000000..9e36d1248d1f --- /dev/null +++ b/Civi/FlexMailer/FlexMailerTask.php @@ -0,0 +1,140 @@ +setMailParams(...)); + * - During delivery, we read the message ($task->getMailParams()) + * and send it. + */ +class FlexMailerTask { + + /** + * @var int + */ + private $eventQueueId; + + /** + * @var int + */ + private $contactId; + + /** + * @var string + */ + private $hash; + + /** + * @var string + * + * WAS: email + */ + private $address; + + /** + * The individual email message to send (per alterMailPrams). + * + * @var array|NULL + * @see MailParams + */ + private $mailParams = NULL; + + /** + * FlexMailerTask constructor. + * + * @param int $eventQueueId + * @param int $contactId + * @param string $hash + * @param string $address + */ + public function __construct( + $eventQueueId, + $contactId, + $hash, + $address + ) { + $this->eventQueueId = $eventQueueId; + $this->contactId = $contactId; + $this->hash = $hash; + $this->address = $address; + } + + /** + * @return int + */ + public function getEventQueueId() { + return $this->eventQueueId; + } + + /** + * @return int + */ + public function getContactId() { + return $this->contactId; + } + + /** + * @return string + */ + public function getHash() { + return $this->hash; + } + + /** + * @return string + */ + public function getAddress() { + return $this->address; + } + + /** + * @return array + * @see CRM_Utils_Hook::alterMailParams + */ + public function getMailParams() { + return $this->mailParams; + } + + /** + * @param \array $mailParams + * @return FlexMailerTask + * @see CRM_Utils_Hook::alterMailParams + */ + public function setMailParams($mailParams) { + $this->mailParams = $mailParams; + return $this; + } + +} diff --git a/Civi/FlexMailer/Listener/Abdicator.php b/Civi/FlexMailer/Listener/Abdicator.php new file mode 100644 index 000000000000..cfad69e35af8 --- /dev/null +++ b/Civi/FlexMailer/Listener/Abdicator.php @@ -0,0 +1,68 @@ +getMailing(); + if ($mailing->template_type && $mailing->template_type !== 'traditional' && !$mailing->sms_provider_id) { + return; // OK, we'll continue running. + } + + // Nope, we'll abdicate. + $e->stopPropagation(); + $isDelivered = $e->getJob()->deliver( + $e->context['deprecatedMessageMailer'], + $e->context['deprecatedTestParams'] + ); + $e->setCompleted($isDelivered); + } + +} diff --git a/Civi/FlexMailer/Listener/BaseListener.php b/Civi/FlexMailer/Listener/BaseListener.php new file mode 100644 index 000000000000..123a33345e6d --- /dev/null +++ b/Civi/FlexMailer/Listener/BaseListener.php @@ -0,0 +1,49 @@ +active; + } + + /** + * @param bool $active + */ + public function setActive($active) { + $this->active = $active; + } + +} diff --git a/Civi/FlexMailer/Listener/DefaultBatcher.php b/Civi/FlexMailer/Listener/DefaultBatcher.php new file mode 100644 index 000000000000..2dbcfe9b36cf --- /dev/null +++ b/Civi/FlexMailer/Listener/DefaultBatcher.php @@ -0,0 +1,92 @@ +getJob()`), enumerate the recipients as + * a batch of FlexMailerTasks and visit each batch (`$e->visit($tasks)`). + * + * @param \Civi\FlexMailer\Event\WalkBatchesEvent $e + */ + public function onWalkBatches(WalkBatchesEvent $e) { + if (!$this->isActive()) { + return; + } + + $e->stopPropagation(); + + $job = $e->getJob(); + + // CRM-12376 + // This handles the edge case scenario where all the mails + // have been delivered in prior jobs. + $isDelivered = TRUE; + + // make sure that there's no more than $mailerBatchLimit mails processed in a run + $mailerBatchLimit = \CRM_Core_Config::singleton()->mailerBatchLimit; + + $eq = \CRM_Mailing_BAO_MailingJob::findPendingTasks($job->id, 'email'); + $tasks = array(); + while ($eq->fetch()) { + if ($mailerBatchLimit > 0 && \CRM_Mailing_BAO_MailingJob::$mailsProcessed >= $mailerBatchLimit) { + if (!empty($tasks)) { + $e->visit($tasks); + } + $eq->free(); + $e->setCompleted(FALSE); + return; + } + \CRM_Mailing_BAO_MailingJob::$mailsProcessed++; + + // FIXME: To support SMS, the address should be $eq->phone instead of $eq->email + $tasks[] = new FlexMailerTask($eq->id, $eq->contact_id, $eq->hash, + $eq->email); + if (count($tasks) == \CRM_Mailing_BAO_MailingJob::MAX_CONTACTS_TO_PROCESS) { + $isDelivered = $e->visit($tasks); + if (!$isDelivered) { + $eq->free(); + $e->setCompleted($isDelivered); + return; + } + $tasks = array(); + } + } + + $eq->free(); + + if (!empty($tasks)) { + $isDelivered = $e->visit($tasks); + } + $e->setCompleted($isDelivered); + } + +} diff --git a/Civi/FlexMailer/Listener/DefaultComposer.php b/Civi/FlexMailer/Listener/DefaultComposer.php new file mode 100644 index 000000000000..6d49cfd118a4 --- /dev/null +++ b/Civi/FlexMailer/Listener/DefaultComposer.php @@ -0,0 +1,150 @@ +isActive()) { + return; + } + + $e->stopPropagation(); + + $mailing = $e->getMailing(); + + if (property_exists($mailing, 'language') && $mailing->language && $mailing->language != 'en_US') { + $swapLang = \CRM_Utils_AutoClean::swap('global://dbLocale?getter', 'call://i18n/setLocale', $mailing->language); + } + + $tp = $this->createTokenProcessor($e)->evaluate(); + foreach ($tp->getRows() as $row) { + /** @var TokenRow $row */ + /** @var FlexMailerTask $task */ + $task = $row->context['flexMailerTask']; + + // Ugh, getVerpAndUrlsAndHeaders() is immensely silly. + list($verp) = $mailing->getVerpAndUrlsAndHeaders( + $e->getJob()->id, $task->getEventQueueId(), $task->getHash(), $task->getAddress()); + + $mailParams = array(); + + // Email headers + $mailParams['Return-Path'] = $verp['bounce']; + $mailParams['From'] = "\"{$mailing->from_name}\" <{$mailing->from_email}>"; + $mailParams['List-Unsubscribe'] = ""; + \CRM_Mailing_BAO_Mailing::addMessageIdHeader($mailParams, 'm', $e->getJob()->id, $task->getEventQueueId(), $task->getHash()); + $mailParams['Subject'] = $row->render('subject'); + //if ($isForward) {$mailParams['Subject'] = "[Fwd:{$this->subject}]";} + $mailParams['Precedence'] = 'bulk'; + $mailParams['X-CiviMail-Bounce'] = $verp['bounce']; + $mailParams['Reply-To'] = $verp['reply']; + if ($mailing->replyto_email && ($mailParams['From'] != $mailing->replyto_email)) { + $mailParams['Reply-To'] = $mailing->replyto_email; + } + + // Oddballs + $mailParams['text'] = $row->render('body_text'); + $mailParams['html'] = $row->render('body_html'); + $mailParams['attachments'] = $e->getAttachments(); + $mailParams['toName'] = $row->render('toName'); + $mailParams['toEmail'] = $task->getAddress(); + $mailParams['job_id'] = $e->getJob()->id; + + $task->setMailParams($mailParams); + } + } + + /** + * Instantiate a TokenProcessor, filling in the appropriate templates + * ("subject", "body_text", "body_html") as well the recipient metadata + * ("contactId", "mailingJobId", etc). + * + * @param \Civi\FlexMailer\Event\ComposeBatchEvent $e + * @return TokenProcessor + */ + protected function createTokenProcessor(ComposeBatchEvent $e) { + $templates = $e->getMailing()->getTemplates(); + + // This needs a better place to go. + if ($e->getMailing()->url_tracking) { + if (!empty($templates['html'])) { + $templates['html'] = TrackableURL::scanAndReplace($templates['html'], $e->getMailing()->id, '{action.eventQueueId}', TRUE); + } + if (!empty($templates['text'])) { + $templates['text'] = TrackableURL::scanAndReplace($templates['text'], $e->getMailing()->id, '{action.eventQueueId}', FALSE); + } + } + + $tp = new TokenProcessor(\Civi::service('dispatcher'), array( + 'controller' => '\Civi\FlexMailer\Listener\DefaultComposer', + // FIXME: Use template_type, template_options + 'smarty' => defined('CIVICRM_MAIL_SMARTY') && CIVICRM_MAIL_SMARTY ? TRUE : FALSE, + 'mailingId' => $e->getMailing()->id, + )); + $tp->addMessage('toName', '{contact.display_name}', 'text/plain'); + $tp->addMessage('subject', $templates['subject'], 'text/plain'); + $tp->addMessage('body_text', isset($templates['text']) ? $templates['text'] : '', 'text/plain'); + $tp->addMessage('body_html', isset($templates['html']) ? $templates['html'] : '', 'text/html'); + + foreach ($e->getTasks() as $key => $task) { + /** @var FlexMailerTask $task */ + + $tp->addRow()->context(array( + 'contactId' => $task->getContactId(), + 'mailingJobId' => $e->getJob()->id, + 'mailingActionTarget' => array( + 'id' => $task->getEventQueueId(), + 'hash' => $task->getHash(), + 'email' => $task->getAddress(), + ), + 'flexMailerTask' => $task, + )); + } + return $tp; + } + +} diff --git a/Civi/FlexMailer/Listener/DefaultSender.php b/Civi/FlexMailer/Listener/DefaultSender.php new file mode 100644 index 000000000000..5f2cb8b89eec --- /dev/null +++ b/Civi/FlexMailer/Listener/DefaultSender.php @@ -0,0 +1,178 @@ +isActive()) { + return; + } + + $e->stopPropagation(); + + $job = $e->getJob(); + $mailing = $e->getMailing(); + $job_date = \CRM_Utils_Date::isoToMysql($job->scheduled_date); + if (version_compare(\CRM_Utils_System::version(), '4.7.0', '>=')) { + $mailer = \Civi::service('pear_mail'); + } + else { + $mailer = \CRM_Core_Config::singleton()->getMailer(TRUE); + } + + $targetParams = $deliveredParams = array(); + $count = 0; + + foreach ($e->getTasks() as $key => $task) { + /** @var FlexMailerTask $task */ + /** @var \Mail_mime $message */ + $message = \Civi\FlexMailer\MailParams::convertMailParamsToMime($task->getMailParams()); + + if (empty($message)) { + // lets keep the message in the queue + // most likely a permissions related issue with smarty templates + // or a bad contact id? CRM-9833 + continue; + } + + // disable error reporting on real mailings (but leave error reporting for tests), CRM-5744 + if ($job_date) { + $errorScope = \CRM_Core_TemporaryErrorScope::ignoreException(); + } + + $headers = $message->headers(); + $result = $mailer->send($headers['To'], $message->headers(), + $message->get()); + + if ($job_date) { + unset($errorScope); + } + + if (is_a($result, 'PEAR_Error')) { + /** @var \PEAR_Error $result */ + // CRM-9191 + $message = $result->getMessage(); + if ( + strpos($message, 'Failed to write to socket') !== FALSE || + strpos($message, 'Failed to set sender') !== FALSE + ) { + // 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"); + + // these are socket write errors which most likely means smtp connection errors + // lets skip them + $smtpConnectionErrors++; + if ($smtpConnectionErrors <= 5) { + continue; + } + + // seems like we have too many of them in a row, we should + // write stuff to disk and abort the cron job + $job->writeToDB($deliveredParams, $targetParams, $mailing, $job_date); + + \CRM_Core_Error::debug_log_message("Too many SMTP Socket Errors. Exiting"); + \CRM_Utils_System::civiExit(); + } + + // Register the bounce event. + + $params = array( + 'event_queue_id' => $task->getEventQueueId(), + 'job_id' => $job->id, + 'hash' => $task->getHash(), + ); + $params = array_merge($params, + \CRM_Mailing_BAO_BouncePattern::match($result->getMessage()) + ); + \CRM_Mailing_Event_BAO_Bounce::create($params); + } + else { + // Register the delivery event. + $deliveredParams[] = $task->getEventQueueId(); + $targetParams[] = $task->getContactId(); + + $count++; + if ($count % self::BULK_MAIL_INSERT_COUNT == 0) { + $job->writeToDB($deliveredParams, $targetParams, $mailing, $job_date); + $count = 0; + + // hack to stop mailing job at run time, CRM-4246. + // to avoid making too many DB calls for this rare case + // lets do it when we snapshot + $status = \CRM_Core_DAO::getFieldValue( + 'CRM_Mailing_DAO_MailingJob', + $job->id, + 'status', + 'id', + TRUE + ); + + if ($status != 'Running') { + $e->setCompleted(FALSE); + return; + } + } + } + + unset($result); + + // seems like a successful delivery or bounce, lets decrement error count + // only if we have smtp connection errors + if ($smtpConnectionErrors > 0) { + $smtpConnectionErrors--; + } + + // If we have enabled the Throttle option, this is the time to enforce it. + $mailThrottleTime = \CRM_Core_Config::singleton()->mailThrottleTime; + if (!empty($mailThrottleTime)) { + usleep((int ) $mailThrottleTime); + } + } + + $e->setCompleted($job->writeToDB( + $deliveredParams, + $targetParams, + $mailing, + $job_date + )); + } + +} diff --git a/Civi/FlexMailer/Listener/HookAdapter.php b/Civi/FlexMailer/Listener/HookAdapter.php new file mode 100644 index 000000000000..360d1d09b369 --- /dev/null +++ b/Civi/FlexMailer/Listener/HookAdapter.php @@ -0,0 +1,54 @@ +isActive()) { + return; + } + + foreach ($e->getTasks() as $task) { + /** @var FlexMailerTask $task */ + $mailParams = $task->getMailParams(); + if ($mailParams) { + \CRM_Utils_Hook::alterMailParams($mailParams, 'flexmailer'); + $task->setMailParams($mailParams); + } + } + } + +} diff --git a/Civi/FlexMailer/Listener/OpenTracker.php b/Civi/FlexMailer/Listener/OpenTracker.php new file mode 100644 index 000000000000..98d8017b11a1 --- /dev/null +++ b/Civi/FlexMailer/Listener/OpenTracker.php @@ -0,0 +1,60 @@ +isActive() || !$e->getMailing()->open_tracking) { + return; + } + + $config = \CRM_Core_Config::singleton(); + + foreach ($e->getTasks() as $task) { + /** @var FlexMailerTask $task */ + $mailParams = $task->getMailParams(); + + if (!empty($mailParams) && !empty($mailParams['html'])) { + $mailParams['html'] .= "\n" . '"; + + $task->setMailParams($mailParams); + } + } + } + +} diff --git a/Civi/FlexMailer/MailParams.php b/Civi/FlexMailer/MailParams.php new file mode 100644 index 000000000000..90c8e51c707a --- /dev/null +++ b/Civi/FlexMailer/MailParams.php @@ -0,0 +1,117 @@ +"; + + // 2. Apply the other fields. + foreach ($mailParams as $key => $value) { + if (empty($value)) { + continue; + } + + switch ($key) { + case 'text': + $message->setTxtBody($mailParams['text']); + break; + + case 'html': + $message->setHTMLBody($mailParams['html']); + break; + + case 'attachments': + foreach ($mailParams['attachments'] as $fileID => $attach) { + $message->addAttachment($attach['fullPath'], + $attach['mime_type'], + $attach['cleanName'] + ); + } + break; + + case 'headers': + $message->headers($value); + break; + + default: + $message->headers(array($key => $value), TRUE); + } + } + + \CRM_Utils_Mail::setMimeParams($message); + + return $message; + } + +} diff --git a/Civi/FlexMailer/Services.php b/Civi/FlexMailer/Services.php new file mode 100644 index 000000000000..020ade948aa2 --- /dev/null +++ b/Civi/FlexMailer/Services.php @@ -0,0 +1,65 @@ +=')) { + $container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__)); + } + $container->setParameter('civi_flexmailer_callback', '\Civi\FlexMailer\FlexMailer::createAndRun'); + $container->setDefinition('civi_flexmailer_abdicator', new Definition('Civi\FlexMailer\Listener\Abdicator')); + $container->setDefinition('civi_flexmailer_default_batcher', new Definition('Civi\FlexMailer\Listener\DefaultBatcher')); + $container->setDefinition('civi_flexmailer_default_composer', new Definition('Civi\FlexMailer\Listener\DefaultComposer')); + $container->setDefinition('civi_flexmailer_open_tracker', new Definition('Civi\FlexMailer\Listener\OpenTracker')); + $container->setDefinition('civi_flexmailer_default_sender', new Definition('Civi\FlexMailer\Listener\DefaultSender')); + $container->setDefinition('civi_flexmailer_hooks', new Definition('Civi\FlexMailer\Listener\HookAdapter')); + } + + public static function registerListeners(ContainerAwareEventDispatcher $dispatcher) { + $terminalPriority = -100; + $dispatcher->addListenerService(FlexMailer::EVENT_RUN, array('civi_flexmailer_abdicator', 'onRun'), $terminalPriority); + $dispatcher->addListenerService(FlexMailer::EVENT_WALK, array('civi_flexmailer_default_batcher', 'onWalkBatches'), $terminalPriority); + $dispatcher->addListenerService(FlexMailer::EVENT_RUN, array('civi_flexmailer_default_composer', 'onRun'), 0); + $dispatcher->addListenerService(FlexMailer::EVENT_COMPOSE, array('civi_flexmailer_default_composer', 'onComposeBatch'), $terminalPriority); + $dispatcher->addListenerService(FlexMailer::EVENT_ALTER, array('civi_flexmailer_open_tracker', 'onAlterBatch'), -20); + $dispatcher->addListenerService(FlexMailer::EVENT_ALTER, array('civi_flexmailer_hooks', 'onAlterBatch'), -30); + $dispatcher->addListenerService(FlexMailer::EVENT_SEND, array('civi_flexmailer_default_sender', 'onSendBatch'), $terminalPriority); + } + +} diff --git a/Civi/FlexMailer/TrackableURL.php b/Civi/FlexMailer/TrackableURL.php new file mode 100644 index 000000000000..a27d1573c089 --- /dev/null +++ b/Civi/FlexMailer/TrackableURL.php @@ -0,0 +1,119 @@ +]*href *= *")([^">]+)(");', $callback, $html); + return preg_replace_callback(';(\<[^>]*href *= *\')([^">]+)(\');', $callback, $tmp); + } + +} diff --git a/tests/phpunit/Civi/FlexMailer/ConcurrentDeliveryTest.php b/tests/phpunit/Civi/FlexMailer/ConcurrentDeliveryTest.php new file mode 100644 index 000000000000..1310fa0f5e8a --- /dev/null +++ b/tests/phpunit/Civi/FlexMailer/ConcurrentDeliveryTest.php @@ -0,0 +1,85 @@ +assertTrue($ok, 'FlexMailer remained active during testing'); + } + + // ---- Boilerplate ---- + + // The remainder of this class contains dummy stubs which make it easier to + // work with the tests in an IDE. + + /** + * @dataProvider concurrencyExamples + * @see _testConcurrencyCommon + */ + public function testConcurrency($settings, $expectedTallies, $expectedTotal) { + parent::testConcurrency($settings, $expectedTallies, $expectedTotal); + } + + public function testBasic() { + parent::testBasic(); + } + +} diff --git a/tests/phpunit/Civi/FlexMailer/FlexMailerSystemTest.php b/tests/phpunit/Civi/FlexMailer/FlexMailerSystemTest.php new file mode 100644 index 000000000000..8641d6445b57 --- /dev/null +++ b/tests/phpunit/Civi/FlexMailer/FlexMailerSystemTest.php @@ -0,0 +1,139 @@ + $class) { + $dispatcher->addListener($event, array($this, 'handleEvent')); + } + + $hooks = \CRM_Utils_Hook::singleton(); + $hooks->setHook('civicrm_alterMailParams', + array($this, 'hook_alterMailParams')); + $this->counts = array(); + } + + public function handleEvent(Event $e) { + // We keep track of the events that fire during mail delivery. + // At the end, we'll ensure that the correct events fired. + $clazz = get_class($e); + if (!isset($this->counts[$clazz])) { + $this->counts[$clazz] = 1; + } + else { + $this->counts[$clazz]++; + } + } + + /** + * @see CRM_Utils_Hook::alterMailParams + */ + public function hook_alterMailParams(&$params, $context = NULL) { + $this->counts['hook_alterMailParams'] = 1; + $this->assertEquals('flexmailer', $context); + } + + public function tearDown() { + parent::tearDown(); + $this->assertNotEmpty($this->counts['hook_alterMailParams']); + foreach (FlexMailer::getEventTypes() as $event => $class) { + $this->assertTrue( + $this->counts[$class] > 0, + "If FlexMailer is active, $event should fire at least once." + ); + } + } + + // ---- Boilerplate ---- + + // The remainder of this class contains dummy stubs which make it easier to + // work with the tests in an IDE. + + /** + * Generate a fully-formatted mailing (with body_html content). + * + * @dataProvider urlTrackingExamples + */ + public function testUrlTracking( + $inputHtml, + $htmlUrlRegex, + $textUrlRegex, + $params + ) { + parent::testUrlTracking($inputHtml, $htmlUrlRegex, $textUrlRegex, $params); + } + + public function testBasicHeaders() { + parent::testBasicHeaders(); + } + + public function testText() { + parent::testText(); + } + + public function testHtmlWithOpenTracking() { + parent::testHtmlWithOpenTracking(); + } + + public function testHtmlWithOpenAndUrlTracking() { + parent::testHtmlWithOpenAndUrlTracking(); + } + +}