Last active
          April 11, 2022 14:24 
        
      - 
      
- 
        Save benfavre/097a59d4e5e4bea31b3a533d0fd2bc10 to your computer and use it in GitHub Desktop. 
Revisions
- 
        benfavre revised this gist Apr 11, 2022 . 1 changed file with 6 additions and 6 deletions.There are no files selected for viewingThis file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -289,10 +289,10 @@ public function prepareEmail($subscriber, $server = null, $fromCache = false, $e } $message->setReplyTo($this->reply_to); // $message->setEncoder(new \Swift_Mime_ContentEncoder_PlainContentEncoder('8bit')); // FIX $factory = new \Swift_CharacterReaderFactory_SimpleCharacterReaderFactory(); // FIX $stream = new \Swift_CharacterStream_ArrayCharacterStream($factory, 'utf-8'); // FIX $message->setEncoder(new \Swift_Mime_ContentEncoder_NullContentEncoder('quoted-printable')); // FIX if (is_null($this->type) || $this->type == self::TYPE_REGULAR) { @@ -308,7 +308,7 @@ public function prepareEmail($subscriber, $server = null, $fromCache = false, $e // IMPORTANT: add plain part first, then html part $message->addPart($plain, 'text/plain'); $message->addPart(quoted_printable_encode($html), 'text/html'); // FIX } else { // Get plain content is for PLAIN campaign only $plain = $this->getPlainContent($subscriber, $msgId, $server); @@ -479,7 +479,7 @@ public function getBaseHtmlContent($fromCache = false, $expiresInSeconds = 600) $pipeline = new PipelineBuilder(); $pipeline->add(new AddDoctype()); $pipeline->add(new RemoveTitleTag()); // $pipeline->add(new ReplaceBareLineFeed()); // FIX $pipeline->add(new AppendHtml($this->getHtmlFooter())); $pipeline->add(new ParseRss()); $pipeline->add(new MakeInlineCss($this->template->findCssFiles())); 
- 
        benfavre revised this gist Apr 11, 2022 . No changes.There are no files selected for viewing
- 
        benfavre created this gist Apr 11, 2022 .There are no files selected for viewingThis file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,587 @@ <?php namespace Acelle\Library\Traits; use Acelle\Model\Template; use Exception; use Acelle\Library\ExtendedSwiftMessage; use Acelle\Model\Setting; use Acelle\Library\StringHelper; use League\Pipeline\PipelineBuilder; use Acelle\Library\HtmlHandler\ParseRss; use Acelle\Library\HtmlHandler\ReplaceBareLineFeed; use Acelle\Library\HtmlHandler\AppendHtml; use Acelle\Library\HtmlHandler\TransformTag; use Acelle\Library\HtmlHandler\InjectTrackingPixel; use Acelle\Library\HtmlHandler\MakeInlineCss; use Acelle\Library\HtmlHandler\TransformUrl; use Acelle\Library\HtmlHandler\TransformWidgets; use Acelle\Library\HtmlHandler\AddDoctype; use Acelle\Library\HtmlHandler\RemoveTitleTag; use Acelle\Library\Lockable; use File; use Cache; use Soundasleep\Html2Text; trait HasTemplate { /** * Campaign has one template. */ public function template() { return $this->belongsTo('Acelle\Model\Template'); } /** * Get template. */ public function setTemplate($template, $name=null) { $campaignTemplate = $template->copy([ 'name' => $name ? $name : trans('messages.campaign.template_name', ['name' => $this->name]), 'customer_id' => $this->customer_id, ]); // remove exist template if ($this->template) { $this->template->deleteAndCleanup(); } $this->template_id = $campaignTemplate->id; $this->save(); $this->refresh(); if (\Schema::hasColumn($this->getTable(), 'plain')) { $this->updatePlainFromHtml(); } if (method_exists($this, 'updateLinks')) { $this->updateLinks(); } } /** * Upload a template. */ public function uploadTemplate($request) { $template = Template::uploadTemplate($request); $this->setTemplate($template); } /** * Check if email has template. */ public function hasTemplate() { return $this->template()->exists(); } /** * Get thumb. */ public function getThumbUrl() { if ($this->template) { return $this->template->getThumbUrl(); } else { return url('images/placeholder.jpg'); } } /** * Remove email template. */ public function removeTemplate() { $this->template->deleteAndCleanup(); } /** * Update campaign plain text. */ public function updatePlainFromHtml() { if (!$this->plain) { $this->plain = preg_replace('/\s+/', ' ', preg_replace('/\r\n/', ' ', strip_tags($this->getTemplateContent()))); $this->save(); } } /** * Set template content. */ public function setTemplateContent($content, $callback = null) { if (!$this->template) { throw new Exception('Cannot set content: campaign/email does not have template!'); } $template = $this->template; $template->content = $content; $template->save(); if (!is_null($callback)) { $callback($this); } } /** * Get template content. */ public function getTemplateContent() { if (!$this->template) { throw new Exception('Cannot get content: campaign/email does not have template!'); } return $this->template->content; } /** * Build Email Custom Headers. * * @return Hash list of custom headers */ public function getCustomHeaders($subscriber, $server) { $msgId = StringHelper::generateMessageId(StringHelper::getDomainFromEmail($this->from_email)); if ($this->isStdClassSubscriber($subscriber)) { $unsubscribeUrl = null; } else { $unsubscribeUrl = $subscriber->generateUnsubscribeUrl($msgId); if ($this->trackingDomain) { $unsubscribeUrl = $this->trackingDomain->buildTrackingUrl($unsubscribeUrl); } } $headers = array( 'X-Acelle-Campaign-Id' => $this->uid, 'X-Acelle-Subscriber-Id' => $subscriber->uid, 'X-Acelle-Customer-Id' => $this->customer->uid, 'X-Acelle-Message-Id' => $msgId, 'X-Acelle-Sending-Server-Id' => $server->uid, 'Precedence' => 'bulk', ); if ($unsubscribeUrl) { $headers['List-Unsubscribe'] = "<{$unsubscribeUrl}>"; } else { $sampleUnsubscribeUrl = route('campaign_message', ['message' => StringHelper::base64UrlEncode(trans('messages.email.test_link_note')) ]); $headers['List-Unsubscribe'] = "<{$sampleUnsubscribeUrl}>"; } return $headers; } /** * Check if the given variable is a subscriber object (for actually sending a email) * Or a stdClass subscriber (for sending test email). * * @param object $object */ public function isStdClassSubscriber($object) { return get_class($object) == 'stdClass'; } function indentContent($content, $tab="\t"){ // add marker linefeeds to aid the pretty-tokeniser (adds a linefeed between all tag-end boundaries) $content = preg_replace('/(>)(<)(\/*)/', "$1\n$2$3", $content); // now indent the tags $token = strtok($content, "\n"); $result = ''; // holds formatted version as it is built $pad = 0; // initial indent $matches = array(); // returns from preg_matches() // scan each line and adjust indent based on opening/closing tags while ($token !== false && strlen($token)>0) { $token = trim($token); // test for the various tag states // 1. open and closing tags on same line - no change if (preg_match('/.+<\/\w[^>]*>$/', $token, $matches)) $indent=0; // 2. closing tag - outdent now elseif (preg_match('/^<\/\w/', $token, $matches)) { $pad--; if($indent>0) $indent=0; if($nextTagNegative){ $pad--;$nextTagNegative=false; } } // 3. opening tag - don't pad this one, only subsequent tags (only if it isn't a void tag) elseif (preg_match('/^<\w[^>]*[^\/]>.*$/', $token, $matches)) { $voidTag = false; foreach ($matches as $m) { // Void elements according to http://www.htmlandcsswebdesign.com/articles/voidel.php if (preg_match('/^<(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)/im', $m)) { $voidTag = true; break; } } if (!$voidTag) $indent=1;$nextTagNegative=true; } // 4. no indentation needed else $indent = 0; // pad the line with the required number of leading spaces $line = str_pad($token, strlen($token)+$pad, $tab, STR_PAD_LEFT); $result .= $line."\n"; // add to the cumulative result, with linefeed $token = strtok("\n"); // get the next token $pad += $indent; // update the pad size for subsequent lines } return $result; } /** * Prepare the email content using Swift Mailer. * * @input object subscriber * @input object sending server * * @return MIME text message */ public function prepareEmail($subscriber, $server = null, $fromCache = false, $expiresInSeconds = 600) { // build the message $customHeaders = $this->getCustomHeaders($subscriber, $this); $msgId = $customHeaders['X-Acelle-Message-Id']; $message = new ExtendedSwiftMessage(); $message->setId($msgId); if (is_null($this->type) || $this->type == self::TYPE_REGULAR) { $message->setContentType('text/html; charset=utf-8'); } else { $message->setContentType('text/plain; charset=utf-8'); } foreach ($customHeaders as $key => $value) { $message->getHeaders()->addTextHeader($key, $value); } // @TODO for AWS, setting returnPath requires verified domain or email address if (!is_null($server) && $server->allowCustomReturnPath()) { $returnPath = $server->getVerp($subscriber->email); if ($returnPath) { $message->setReturnPath($returnPath); } } $message->setSubject('[test] ' . $this->getSubject($subscriber, $msgId)); $message->setFrom(array($this->from_email => $this->from_name)); $message->setTo($subscriber->email); if (!empty(Setting::get('campaign.bcc'))) { $addresses = array_filter(preg_split('/\s*,\s*/', Setting::get('campaign.bcc'))); $message->setBcc($addresses); } if (!empty(Setting::get('campaign.cc'))) { $addresses = array_filter(preg_split('/\s*,\s*/', Setting::get('campaign.cc'))); $message->setCc($addresses); } $message->setReplyTo($this->reply_to); // $message->setEncoder(new \Swift_Mime_ContentEncoder_PlainContentEncoder('8bit')); $factory = new \Swift_CharacterReaderFactory_SimpleCharacterReaderFactory(); $stream = new \Swift_CharacterStream_ArrayCharacterStream($factory, 'utf-8'); $message->setEncoder(new \Swift_Mime_ContentEncoder_NullContentEncoder('quoted-printable')); if (is_null($this->type) || $this->type == self::TYPE_REGULAR) { $html = $this->getHtmlContent($subscriber, $msgId, $server, $fromCache, $expiresInSeconds); $options = array( 'ignore_errors' => true, // other options go here ); $plain = Html2Text::convert($html, $options); // IMPORTANT: add plain part first, then html part $message->addPart($plain, 'text/plain'); $message->addPart(quoted_printable_encode($html), 'text/html'); } else { // Get plain content is for PLAIN campaign only $plain = $this->getPlainContent($subscriber, $msgId, $server); $message->addPart($plain, 'text/plain'); } if ($this->sign_dkim) { $message = $this->sign($message); } if ($this->attachments) { // Email model foreach ($this->attachments as $file) { $attachment = \Swift_Attachment::fromPath($file->file); $message->attach($attachment); // This is used by certain delivery services like ElasticEmail $message->extAttachments[] = [ 'path' => $file->file, 'type' => $attachment->getContentType()]; } } else { // Campaign model //@todo attach function used for any attachment of Campaign $path_campaign = $this->getAttachmentPath(); if (is_dir($path_campaign)) { $files = File::allFiles($path_campaign); foreach ($files as $file) { $attachment = \Swift_Attachment::fromPath((string) $file); $message->attach($attachment); // This is used by certain delivery services like ElasticEmail $message->extAttachments[] = [ 'path' => (string) $file, 'type' => $attachment->getContentType()]; } } } return array($message, $msgId); } /** * Get tagged Subject. * * @return string */ public function getSubject($subscriber, $msgId) { $pipeline = new PipelineBuilder(); $pipeline->add(new ReplaceBareLineFeed()); $pipeline->add(new TransformTag($this, $subscriber, $msgId)); return $pipeline->build()->process($this->subject); } /** * Check if email footer enabled. * * @return string * @deprecated this is a very poorly designed function with dependencies session! * @todo so, we are adding if/else to facilitate testing only */ public function footerEnabled() { if (is_null($this->customer)) { return; } return ($this->customer->getCurrentSubscription()->plan->getOption('email_footer_enabled') == 'yes') ? true : false; } /** * Get HTML footer. * * @return string * @deprecated this is a very poorly designed function with dependencies session! * @todo so, we are adding if/else to facilitate testing only */ public function getHtmlFooter() { if (is_null($this->customer)) { return; } return $this->customer->getCurrentSubscription()->plan->getOption('html_footer'); } /** * Find sending domain from email. * * @return mixed */ public function findSendingDomain($email) { $domainName = substr(strrchr($email, '@'), 1); if ($domainName == false) { return; } $domain = $this->customer->sendingDomains()->where('name', $domainName)->first(); return $domain; } /** * Sign the message with DKIM. * * @return mixed */ public function sign($message) { $sendingDomain = $this->findSendingDomain($this->from_email); if (is_null($sendingDomain)) { return $message; } $privateKey = $sendingDomain->dkim_private; $domainName = $sendingDomain->name; $selector = $sendingDomain->getDkimSelectorParts()[0]; $signer = new \Swift_Signers_DKIMSigner($privateKey, $domainName, $selector); $signer->ignoreHeader('Return-Path'); $message->attachSigner($signer); return $message; } public function getCachedHtmlId() { return "{$this->uid}-html"; } public function clearCache() { Cache::forget($this->getCachedHtmlId()); } /** * Build Email HTML content. * * @return string */ public function getHtmlContent($subscriber = null, $msgId = null, $server = null, $fromCache = false, $expiresInSeconds = 600) { $baseHtml = $this->getBaseHtmlContent($fromCache, $expiresInSeconds); // Bind subscriber/message/server information to email content $pipeline = new PipelineBuilder(); $pipeline->add(new TransformTag($this, $subscriber, $msgId, $server)); $pipeline->add(new InjectTrackingPixel($this, $msgId)); $pipeline->add(new TransformUrl($this->template, $msgId, $this->trackingDomain)); // Actually push HTML to pipeline for processing $html = $pipeline->build()->process($baseHtml); // Return subscriber's bound html return $html; } // Return the HTML content which has been processed through base handlers (pipeline) // Which is not associated with any subscriber/message/server public function getBaseHtmlContent($fromCache = false, $expiresInSeconds = 600) { if (!$this->template) { throw new Exception('No template available'); } $cacheId = $this->getCachedHtmlId(); $updateCacheFlag = $fromCache && !Cache::has($cacheId); $html = null; if (!$fromCache || $updateCacheFlag) { $pipeline = new PipelineBuilder(); $pipeline->add(new AddDoctype()); $pipeline->add(new RemoveTitleTag()); // $pipeline->add(new ReplaceBareLineFeed()); $pipeline->add(new AppendHtml($this->getHtmlFooter())); $pipeline->add(new ParseRss()); $pipeline->add(new MakeInlineCss($this->template->findCssFiles())); $pipeline->add(new TransformWidgets()); // // $pipeline->add(new TransformTag($this, $subscriber, $msgId, $server)); // // $pipeline->add(new InjectTrackingPixel($this, $msgId)); // // $pipeline->add(new TransformUrl($this->template, $msgId, $this->trackingDomain)); // // $html = $this->wooTransform($html); $html = $pipeline->build()->process($this->getTemplateContent()); } if ($updateCacheFlag) { $lockfile = storage_path('locks/campaign-cache-'.$this->uid); $lock = new Lockable($lockfile); $lock->getExclusiveLock(function ($f) use ($cacheId, $html, $expiresInSeconds) { Cache::put($cacheId, $html, $expiresInSeconds); }, $timeoutSeconds = 3, $timeoutCallback = function () { // echo "Quit me mememem"; // just quit, do not throw exception }); } // It is important to return $html in priority here, as cache update may not work! return $html ?: Cache::get($cacheId); } /** * Build Email HTML content. * Notice: this method is used for PLAIN CAMPAIGN only. To extract plain content from HTML, use Html2Text instead * * @return string */ public function getPlainContent($subscriber, $msgId, $server = null) { $plain = $this->plain.$this->getPlainTextFooter(); $pipeline = new PipelineBuilder(); $pipeline->add(new ReplaceBareLineFeed()); $pipeline->add(new TransformTag($this, $subscriber, $msgId, $server)); $plain = $pipeline->build()->process($plain); return $plain; } /** * Get PLAIN TEXT footer. * * @return string * @deprecated this is a very poorly designed function with dependencies session! * @todo so, we are adding if/else to facilitate testing only */ public function getPlainTextFooter() { if (is_null($this->customer)) { return; } return $this->customer->getCurrentSubscription()->plan->getOption('plain_text_footer'); } /** * Create a stdClass subscriber (for sending a campaign test email) * The campaign sending functions take a subscriber object as input * However, a test email address is not yet a subscriber object, so we have to build a fake stdClass object * which can be used as a real subscriber. * * @param array $subscriber */ public function createStdClassSubscriber($subscriber) { // default attributes that are required $jsonObj = [ 'uid' => uniqid(), ]; // append the customer specified attributes and build a stdClass object $stdObj = json_decode(json_encode(array_merge($jsonObj, $subscriber))); return $stdObj; } public function makeTrackingPixel($msgId) { if (!is_null($msgId)) { $url = route('openTrackingUrl', ['message_id' => StringHelper::base64UrlEncode($msgId)], true); if ($this->trackingDomain) { $url = $this->trackingDomain->buildTrackingUrl($url); } } else { $url = $this->makeSampleLink(); } return '<img alt="" src="'.$url.'" width="0" height="0" alt="" style="visibility:hidden" />' . PHP_EOL; } public function makeSampleLink() { $sampleLink = route('campaign_message', [ 'message' => StringHelper::base64UrlEncode(trans('messages.email.test_link_note')) ]); if ($this->trackingDomain) { $sampleLink = $this->trackingDomain->buildTrackingUrl($sampleLink); } return $sampleLink; } }