diff --git a/README.md b/README.md index c091bb5..4b9922c 100644 --- a/README.md +++ b/README.md @@ -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 ================== diff --git a/_config/email-templates.yml b/_config/email-templates.yml index a138503..3e05f0d 100644 --- a/_config/email-templates.yml +++ b/_config/email-templates.yml @@ -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 diff --git a/src/Admin/EmailTemplatesAdmin.php b/src/Admin/EmailTemplatesAdmin.php index 02a9671..566ef2f 100644 --- a/src/Admin/EmailTemplatesAdmin.php +++ b/src/Admin/EmailTemplatesAdmin.php @@ -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 @@ -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; } /** diff --git a/src/Models/Emailing.php b/src/Models/Emailing.php index a70eaed..ed0f3f8 100644 --- a/src/Models/Emailing.php +++ b/src/Models/Emailing.php @@ -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; } @@ -209,6 +212,17 @@ protected function previewTab() $iframe = new LiteralField('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", '


' . $varsHelperContent . '
'); + $tab->push($varsHelper); + return $tab; } @@ -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 @@ -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])) { @@ -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; }