Skip to content

Commit

Permalink
support merge vars
Browse files Browse the repository at this point in the history
  • Loading branch information
lekoala committed Apr 14, 2020
1 parent 8e24afc commit edd2ba4
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 42 deletions.
28 changes: 28 additions & 0 deletions README.md
Expand Up @@ -170,6 +170,34 @@ If you want to add new group, add an extension to Emailing class and implement t
{
}

Merge vars:
This module supports merge vars using *|MERGETAG|* notation in Emailings
It expects your email gateway to support the X-MC-MergeVars convention.
You can change the email header being used.

LeKoala\EmailTemplates\Models\Emailing:
mail_merge_header: 'X-Mail-Header-Here'

By default, we use the mandrill template syntax replacement. If you use other
gateways you may need to replace them. For example, mailgun would be this:

LeKoala\EmailTemplates\Models\Emailing:
mail_merge_syntax: '%recipient.MERGETAG%'

Batch sending:
By default, this module will send emails in batch of 1000. You can change this with

LeKoala\EmailTemplates\Models\Emailing:
batch_count: 1000

Sending as bcc:
By default, this module send email as bcc in order to avoid displaying
recipients. If your email gateway (like, mailgun) supports hiding recipients, you can
use a real recipient with the following yml config

LeKoala\EmailTemplates\Models\Emailing:
send_bcc: false

Finding a good template
==================

Expand Down
5 changes: 5 additions & 0 deletions _config/email-templates.yml
Expand Up @@ -29,6 +29,11 @@ LeKoala\EmailTemplates\Models\SentEmail:
# possible values : 'time' or 'max'
cleanup_method: 'max'
cleanup_time: '-7 days'
LeKoala\EmailTemplates\Models\Emailing:
batch_count: 1000
mail_merge_header: 'X-MC-MergeVars'
mail_merge_syntax: '*|MERGETAG|*'
send_bcc: true
SilverStripe\Core\Injector\Injector:
SilverStripe\Control\Email\Email:
class: LeKoala\EmailTemplates\Email\BetterEmail
50 changes: 30 additions & 20 deletions src/Admin/EmailTemplatesAdmin.php
Expand Up @@ -3,13 +3,14 @@
namespace LeKoala\EmailTemplates\Admin;

use Exception;
use LeKoala\EmailTemplates\Helpers\FluentHelper;
use SilverStripe\Admin\ModelAdmin;
use SilverStripe\Control\Director;
use SilverStripe\View\Requirements;
use SilverStripe\Control\HTTPResponse;
use LeKoala\EmailTemplates\Models\Emailing;
use LeKoala\EmailTemplates\Models\SentEmail;
use LeKoala\EmailTemplates\Helpers\FluentHelper;
use LeKoala\EmailTemplates\Models\EmailTemplate;
use SilverStripe\Control\Director;

/**
* Manage your email templates
Expand Down Expand Up @@ -59,39 +60,48 @@ public function SendEmailing()

/* @var $Emailing Emailing */
$Emailing = Emailing::get()->byID($id);
$emails = $Emailing->getEmailByLocales();
$emails = $Emailing->getEmailsByLocales();

$errors = 0;
foreach ($emails as $locale => $email) {
// Wrap with withLocale to make sure any environment variable (urls, etc) are properly set when sending
$res = null;
FluentHelper::withLocale($locale, function () use ($email, &$res) {
try {
$res = $email->send();
} catch (Exception $ex) {
return $ex->getMessage();
$messages = [];
foreach ($emails as $locale => $emails) {
foreach ($emails as $email) {
$res = null;
$msg = null;
// Wrap with withLocale to make sure any environment variable (urls, etc) are properly set when sending
FluentHelper::withLocale($locale, function () use ($email, &$res, &$msg) {
try {
$res = $email->send();
} catch (Exception $ex) {
$res = false;
$msg = $ex->getMessage();
}
});
if (!$res) {
$errors++;
$messages[] = $msg;
}
return $res;
});
if (!$res) {
$errors++;
}
}

$message = _t('EmailTemplatesAdmin.EMAILING_ERROR', 'There was an error sending email');

if ($errors == 0) {
$Emailing->LastSent = date('Y-m-d H:i:s');
$Emailing->write();

$message = _t('EmailTemplatesAdmin.EMAILING_SENT', 'Emailing sent');
} else {
$message = _t('EmailTemplatesAdmin.EMAILING_ERROR', 'There was an error sending email');
$message .= ": " . implode(", ", $messages);
}

if (Director::is_ajax()) {
return $message;
$this->getResponse()->addHeader('X-Status', rawurlencode($message));
if ($errors > 0) {
// $this->getResponse()->setStatusCode(400);
}
return $this->getResponse();
}

return $this->redirectBack();
return $message;
}

/**
Expand Down
135 changes: 113 additions & 22 deletions src/Models/Emailing.php
Expand Up @@ -154,6 +154,9 @@ public function getNormalizedRecipientsList()
foreach ($perLine as $line) {
$items = explode(',', $line);
foreach ($items as $item) {
// Prevent whitespaces from messing up our queries
$item = trim($item);

if (!$item) {
continue;
}
Expand Down Expand Up @@ -209,6 +212,17 @@ protected function previewTab()
$iframe = new LiteralField('iframe', '<iframe src="' . $iframeSrc . '" style="width:800px;background:#fff;border:1px solid #ccc;min-height:500px;vertical-align:top"></iframe>');
$tab->push($iframe);

// Merge var helper
$vars = $this->collectMergeVars();
$syntax = self::config()->mail_merge_syntax;
if (empty($vars)) {
$varsHelperContent = "You can use $syntax notation to use mail merge variable for the recipients";
} else {
$varsHelperContent = "The following mail merge variables are used : " . implode(", ", $vars);
}
$varsHelper = new LiteralField("varsHelpers", '<div><br/><br/>' . $varsHelperContent . '</div>');
$tab->push($varsHelper);

return $tab;
}

Expand Down Expand Up @@ -247,6 +261,34 @@ public function renderTemplate()
return $html;
}

/**
* Collect all merge vars
*
* @return array
*/
public function collectMergeVars()
{
$fields = ['Subject', 'Content', 'Callout'];

$syntax = self::config()->mail_merge_syntax;

$regex = $syntax;
$regex = preg_quote($regex);
$regex = str_replace("MERGETAG", "([\w\.]+)", $regex);

$allMatches = [];
foreach ($fields as $field) {
$content = $this->$field;
$matches = [];
preg_match_all('/' . $regex . '/', $content, $matches);
if (!empty($matches[1])) {
$allMatches = array_merge($allMatches, $matches[1]);
}
}

return $allMatches;
}


/**
* Returns an instance of an Email with the content of the emailing
Expand All @@ -272,12 +314,37 @@ public function getEmail()
}

/**
* Returns an array of email with members by locale
* Various email providers use various types of mail merge headers
* By default, we use mandrill that is expected to work for other platforms through compat layer
*
* X-Mailgun-Recipient-Variables: {"bob@example.com": {"first":"Bob", "id":1}, "alice@example.com": {"first":"Alice", "id": 2}}
* Template syntax: %recipient.first%
* @link https://documentation.mailgun.com/en/latest/user_manual.html#batch-sending
*
* X-MC-MergeVars [{"rcpt":"recipient.email@example.com","vars":[{"name":"merge2","content":"merge2 content"}]}]
* Template syntax: *|MERGETAG|*
* @link https://mandrill.zendesk.com/hc/en-us/articles/205582117-How-to-Use-SMTP-Headers-to-Customize-Your-Messages
*
* @link https://developers.sparkpost.com/api/smtp/#header-using-the-x-msys-api-custom-header
*
* @return string
*/
public function getMergeVarsHeader()
{
return self::config()->mail_merge_header;
}

/**
* Returns an array of emails with members by locale, grouped by a given number of recipients
* Some apis prevent sending too many emails at the same time
*
* @return array
*/
public function getEmailByLocales()
public function getEmailsByLocales()
{
$batchCount = self::config()->batch_count ?? 1000;
$sendBcc = self::config()->send_bcc;

$membersByLocale = [];
foreach ($this->getAllRecipients() as $r) {
if (!isset($membersByLocale[$r->Locale])) {
Expand All @@ -286,29 +353,53 @@ public function getEmailByLocales()
$membersByLocale[$r->Locale][] = $r;
}

$mergeVars = $this->collectMergeVars();
$mergeVarHeader = $this->getMergeVarsHeader();

$emails = [];
foreach ($membersByLocale as $locale => $membersList) {
$email = Email::create();
if (!$email instanceof BetterEmail) {
throw new Exception("Make sure you are injecting the BetterEmail class instead of your base Email class");
}
if ($this->Sender) {
$email->setFrom($this->Sender);
}
foreach ($membersList as $r) {
$email->addBCC($r->Email, $r->FirstName . ' ' . $r->Surname);
$emails[$locale] = [];
$chunks = array_chunk($membersList, $batchCount);
foreach ($chunks as $chunk) {
$email = Email::create();
if (!$email instanceof BetterEmail) {
throw new Exception("Make sure you are injecting the BetterEmail class instead of your base Email class");
}
if ($this->Sender) {
$email->setFrom($this->Sender);
}
$mergeVarsData = [];
foreach ($chunk as $r) {
if ($sendBcc) {
$email->addBCC($r->Email, $r->FirstName . ' ' . $r->Surname);
} else {
$email->addTo($r->Email, $r->FirstName . ' ' . $r->Surname);
}
if (!empty($mergeVars)) {
$vars = [];
foreach ($mergeVars as $mergeVar) {
$vars[$mergeVar] = $r->$mergeVar;
}
$mergeVarsData[] = [
'rcpt' => $r->Email,
'vars' => $vars
];
}
}
// Merge vars
if (!empty($mergeVars)) {
$email->getSwiftMessage()->getHeaders()->addTextHeader($mergeVarHeader, json_encode($mergeVarsData));
}
// Localize
$EmailingID = $this->ID;
FluentHelper::withLocale($locale, function () use ($EmailingID, $email) {
$Emailing = Emailing::get()->byID($EmailingID);
$email->setSubject($Emailing->Subject);
$email->addData('EmailContent', $Emailing->Content);
$email->addData('Callout', $Emailing->Callout);
});
$emails[$locale][] = $email;
}

// Localize
$EmailingID = $this->ID;
FluentHelper::withLocale($locale, function () use ($EmailingID, $email) {
$Emailing = Emailing::get()->byID($EmailingID);
$email->setSubject($Emailing->Subject);
$email->addData('EmailContent', $Emailing->Content);
$email->addData('Callout', $Emailing->Callout);
});

$emails[$locale] = $email;
}
return $emails;
}
Expand Down

0 comments on commit edd2ba4

Please sign in to comment.