From e496cf179f887f37bd5d1d193cd7aba3c90ce3c7 Mon Sep 17 00:00:00 2001 From: Sander Potjer Date: Thu, 2 Jun 2022 17:40:07 +0200 Subject: [PATCH 1/2] #3 Support for Craft 4 Signed-off-by: Sander Potjer --- README.md | 2 +- composer.json | 17 +- src/MailchimpTransactional.php | 2 +- src/mail/MailchimpTransactionalAdapter.php | 36 +- src/mail/MailchimpTransactionalTransport.php | 549 ++++++------------- 5 files changed, 186 insertions(+), 420 deletions(-) diff --git a/README.md b/README.md index 4e89e7e..2ad0e02 100755 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This plugin provides a [Mailchimp Transactional](https://mailchimp.com/features/ ## Requirements -This plugin requires Craft CMS 3.7.0 or later. +This plugin requires Craft CMS 4.0 or later. ## Installation diff --git a/composer.json b/composer.json index 75966bf..ac27922 100755 --- a/composer.json +++ b/composer.json @@ -2,7 +2,6 @@ "name": "perfectwebteam/craft-mailchimp-transactional", "description": "Mailchimp Transactional mailer adapter for Craft CMS.", "type": "craft-plugin", - "version": "1.0.3", "keywords": [ "craft", "cms", @@ -10,7 +9,8 @@ "craft-plugin", "mailchimp transactional", "mailchimp", - "transactional" + "transactional", + "mandrill" ], "support": { "docs": "https://github.com/perfectwebteam/craft-mailchimp-transactional/blob/main/README.md", @@ -25,8 +25,8 @@ } ], "require": { - "craftcms/cms": "^3.7.0", - "mailchimp/transactional": "*" + "php": "^8.0.2", + "craftcms/cms": "^4.0.0" }, "autoload": { "psr-4": { @@ -41,5 +41,14 @@ "documentationUrl": "https://github.com/perfectwebteam/craft-mailchimp-transactional/blob/main/README.md", "changelogUrl": "https://raw.githubusercontent.com/perfectwebteam/craft-mailchimp-transactional/main/CHANGELOG.md", "class": "perfectwebteam\\mailchimptransactional\\MailchimpTransactional" + }, + "config": { + "platform": { + "php": "8.0.2" + }, + "allow-plugins": { + "yiisoft/yii2-composer": true, + "craftcms/plugin-installer": true + } } } diff --git a/src/MailchimpTransactional.php b/src/MailchimpTransactional.php index 5774e2c..6a2b4e3 100755 --- a/src/MailchimpTransactional.php +++ b/src/MailchimpTransactional.php @@ -1,6 +1,6 @@ MailchimpTransactionalTransport::class, - 'constructArgs' => [ - [ - 'class' => Swift_Events_SimpleEventDispatcher::class - ] - ], - 'apiKey' => Craft::parseEnv($this->apiKey), - 'subAccount' => Craft::parseEnv($this->subaccount) ?: null, - 'template' => Craft::parseEnv($this->template) ?: null - ]; + $transport = new MailchimpTransactionalTransport(App::parseEnv($this->apiKey)); + + if ($this->template) { + $transport->setTemplate(App::parseEnv($this->template)); + } + + if ($this->subaccount) { + $transport->setSubaccount(App::parseEnv($this->subaccount)); + } + + return $transport; } } diff --git a/src/mail/MailchimpTransactionalTransport.php b/src/mail/MailchimpTransactionalTransport.php index 4160187..2bd55d0 100644 --- a/src/mail/MailchimpTransactionalTransport.php +++ b/src/mail/MailchimpTransactionalTransport.php @@ -1,6 +1,6 @@ dispatcher = $dispatcher; - $this->apiKey = null; - $this->async = false; - $this->subAccount = null; - $this->template = null; - } + private string $key; - /** - * Not used - */ - public function isStarted(): bool - { - return false; - } + private string $template = ''; - /** - * Not used - */ - public function start(): void - { - } + private string $subaccount = ''; /** - * Not used + * @param string $key + * @param HttpClientInterface|null $client + * @param EventDispatcherInterface|null $dispatcher + * @param LoggerInterface|null $logger */ - public function stop(): void + public function __construct(string $key, HttpClientInterface $client = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null) { - } + $this->key = $key; - /** - * Not used - */ - public function ping(): void - { + parent::__construct($client, $dispatcher, $logger); } /** - * @param string $apiKey - * @return $this + * @return string */ - public function setApiKey(string $apiKey) + public function __toString(): string { - $this->apiKey = $apiKey; - - return $this; + return sprintf('mandrill+api://%s', $this->getEndpoint()); } /** - * @return null|string + * @param SentMessage $sentMessage + * @param Email $email + * @param Envelope $envelope + * @return ResponseInterface + * @throws TransportExceptionInterface + * @throws \Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface + * @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface + * @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface */ - public function getApiKey(): ?string + protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface { - return $this->apiKey; - } + $response = $this->client->request('POST', 'https://' . $this->getEndpoint() . '/api/1.0/messages/' . $this->getApiCall() . '.json', [ + 'json' => $this->getPayload($email, $envelope), + ]); - /** - * @param bool $async - * @return $this - */ - public function setAsync(bool $async) - { - $this->async = $async; + try { + $statusCode = $response->getStatusCode(); + $result = $response->toArray(false); + } catch (DecodingExceptionInterface $e) { + throw new HttpTransportException('Unable to send an email: ' . $response->getContent(false) . sprintf(' (code %d).', $statusCode), $response); + } catch (TransportExceptionInterface $e) { + throw new HttpTransportException('Could not reach the remote Mandrill server.', $response, 0, $e); + } - return $this; - } + if (200 !== $statusCode) { + if ('error' === ($result['status'] ?? false)) { + throw new HttpTransportException('Unable to send an email: ' . $result['message'] . sprintf(' (code %d).', $result['code']), $response); + } - /** - * @return null|bool - */ - public function getAsync(): ?bool - { - return $this->async; - } + throw new HttpTransportException(sprintf('Unable to send an email (code %d).', $result['code']), $response); + } - /** - * @param string|null $subAccount - * @return $this - */ - public function setSubAccount(?string $subAccount) - { - $this->subAccount = $subAccount; + $firstRecipient = reset($result); + $sentMessage->setMessageId($firstRecipient['_id']); - return $this; + return $response; } /** - * @return null|string + * @return string|null */ - public function getSubAccount(): ?string + private function getEndpoint(): ?string { - return $this->subAccount; + return ($this->host ?: self::HOST) . ($this->port ? ':' . $this->port : ''); } /** - * @param string|null $template - * @return $this + * @return string|null */ - public function setTemplate(?string $template) + private function getApiCall(): ?string { - $this->template = $template; - - return $this; + return $this->template ? 'send-template' : 'send'; } /** - * @return null|string + * @param Email $email + * @param Envelope $envelope + * @return array */ - public function getTemplate(): ?string - { - return $this->template; - } + private function getPayload(Email $email, Envelope $envelope): array + { + $payload = [ + 'key' => $this->key, + 'message' => [ + 'html' => $email->getHtmlBody(), + 'text' => $email->getTextBody(), + 'subject' => $email->getSubject(), + 'from_email' => $envelope->getSender()->getAddress(), + 'to' => $this->getRecipients($email, $envelope), + 'subaccount' => $this->subaccount ?: null + ], + 'template_name' => $this->template ?: null, + 'template_content' => [ + [ + 'name' => 'body', + 'content' => $email->getHtmlBody() + ] + ] + ]; - /** - * @return ApiClient - * @throws Swift_TransportException - */ - protected function createMailchimpTransactional() - { - if ($this->apiKey === null) { - throw new Swift_TransportException('Cannot create instance of Mailchimp Transactional while API key is NULL'); + if ('' !== $envelope->getSender()->getName()) { + $payload['message']['from_name'] = $envelope->getSender()->getName(); } - return (new ApiClient())->setApiKey($this->apiKey); - } + foreach ($email->getAttachments() as $attachment) { + $headers = $attachment->getPreparedHeaders(); + $disposition = $headers->getHeaderBody('Content-Disposition'); - /** - * Send mail via Mailchimp Transactional - * - * @param Swift_Mime_SimpleMessage $message - * @param null $failedRecipients - * @return int Number of messages sent - * @throws Swift_TransportException - */ - public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null): int - { - $this->resultApi = null; + $att = [ + 'content' => $attachment->bodyToString(), + 'type' => $headers->get('Content-Type')->getBody(), + ]; - if ($event = $this->dispatcher->createSendEvent($this, $message)) { - $this->dispatcher->dispatchEvent($event, 'beforeSendPerformed'); + if ($name = $headers->getHeaderParameter('Content-Disposition', 'name')) { + $att['name'] = $name; + } - if ($event->bubbleCancelled()) { - return 0; + if ('inline' === $disposition) { + $payload['message']['images'][] = $att; + } else { + $payload['message']['attachments'][] = $att; } } - $sendCount = 0; - - $mailchimpTransactionalMessage = $this->getMailchimpTransactionalMessage($message); - $mailchimpTransactional = $this->createMailchimpTransactional(); - - // Use template if configured - if ($this->template) { - $response = $mailchimpTransactional->messages->sendTemplate([ - 'template_name' => $this->template, - 'template_content' => [ - [ - 'name' => 'body', - 'content' => $mailchimpTransactionalMessage['html'] - ] - ], - 'message' => $mailchimpTransactionalMessage, - 'async' => $this->async - ]); - } else { - $response = $mailchimpTransactional->messages->send([ - 'message' => $mailchimpTransactionalMessage, - 'async' => $this->async - ]); - } + $headersToBypass = ['from', 'to', 'cc', 'bcc', 'subject', 'content-type']; - if ($response instanceof \Throwable) { - throw $response; - } + foreach ($email->getHeaders()->all() as $name => $header) { + if (\in_array($name, $headersToBypass, true)) { + continue; + } - $this->resultApi = $response; + if ($header instanceof TagHeader) { + $payload['message']['tags'] = array_merge( + $payload['message']['tags'] ?? [], + explode(',', $header->getValue()) + ); - foreach ($this->resultApi as $item) { - if ($item->status === 'sent' || $item->status === 'queued') { - $sendCount++; - } else { - $failedRecipients[] = $item->email; + continue; } - } - if ($event) { - if ($sendCount > 0) { - $event->setResult(Swift_Events_SendEvent::RESULT_SUCCESS); - } else { - $event->setResult(Swift_Events_SendEvent::RESULT_FAILED); + if ($header instanceof MetadataHeader) { + $payload['message']['metadata'][$header->getKey()] = $header->getValue(); + + continue; } - $this->dispatcher->dispatchEvent($event, 'sendPerformed'); + $payload['message']['headers'][$header->getName()] = $header->getBodyAsString(); } - return $sendCount; - } - - /** - * @param Swift_Events_EventListener $plugin - */ - public function registerPlugin(Swift_Events_EventListener $plugin): void - { - $this->dispatcher->bindEventListener($plugin); + return $payload; } /** + * @param Email $email + * @param Envelope $envelope * @return array */ - protected function getSupportedContentTypes(): array + protected function getRecipients(Email $email, Envelope $envelope): array { - return [ - 'text/plain', - 'text/html' - ]; - } + $recipients = []; - /** - * @param string $contentType - * @return bool - */ - protected function supportsContentType(string $contentType): bool - { - return in_array($contentType, $this->getSupportedContentTypes()); - } + foreach ($envelope->getRecipients() as $recipient) { + $type = 'to'; - /** - * @param Swift_Mime_SimpleMessage $message - * @return string|null - */ - protected function getMessagePrimaryContentType(Swift_Mime_SimpleMessage $message): ?string - { - $contentType = $message->getContentType(); + if (\in_array($recipient, $email->getBcc(), true)) { + $type = 'bcc'; + } elseif (\in_array($recipient, $email->getCc(), true)) { + $type = 'cc'; + } - if ($this->supportsContentType($contentType)) { - return $contentType; - } + $recipientPayload = [ + 'email' => $recipient->getAddress(), + 'type' => $type, + ]; - // SwiftMailer hides the content type set in the constructor of Swift_Mime_SimpleMessage as soon - // as you add another part to the message. We need to access the protected property - // userContentType to get the original type. - $messageRef = new ReflectionClass($message); + if ('' !== $recipient->getName()) { + $recipientPayload['name'] = $recipient->getName(); + } - if ($messageRef->hasProperty('userContentType')) { - $propRef = $messageRef->getProperty('userContentType'); - $propRef->setAccessible(true); - $contentType = $propRef->getValue($message); + $recipients[] = $recipientPayload; } - return $contentType; + return $recipients; } /** - * Format message for Mailchimp Transactional - * - * https://mailchimp.com/developer/transactional/api/messages/send-new-message/ - * - * @author https://github.com/AccordGroup/MandrillSwiftMailer - * - * @param Swift_Mime_SimpleMessage $message - * @return array Mailchimp Transactional Send Message + * @param string $template + * @return $this */ - public function getMailchimpTransactionalMessage(Swift_Mime_SimpleMessage $message): array + public function setTemplate(string $template): static { - $contentType = $this->getMessagePrimaryContentType($message); - - $fromAddresses = $message->getFrom(); - $fromEmails = array_keys($fromAddresses); - - $toAddresses = $message->getTo(); - $ccAddresses = $message->getCc() ?: []; - $bccAddresses = $message->getBcc() ?: []; - $replyToAddresses = $message->getReplyTo() ?: []; - - $to = []; - $attachments = []; - $images = []; - $headers = []; - $tags = []; - - foreach ($toAddresses as $toEmail => $toName) { - $to[] = [ - 'email' => $toEmail, - 'name' => $toName, - 'type' => 'to' - ]; - } - - foreach ($replyToAddresses as $replyToEmail => $replyToName) { - if ($replyToName) { - $headers['Reply-To'] = sprintf('%s <%s>', $replyToEmail, $replyToName); - } else { - $headers['Reply-To'] = $replyToEmail; - } - } - - foreach ($ccAddresses as $ccEmail => $ccName) { - $to[] = [ - 'email' => $ccEmail, - 'name' => $ccName, - 'type' => 'cc' - ]; - } - - foreach ($bccAddresses as $bccEmail => $bccName) { - $to[] = [ - 'email' => $bccEmail, - 'name' => $bccName, - 'type' => 'bcc' - ]; - } - - $bodyHtml = $bodyText = null; - - if ($contentType === 'text/plain') { - $bodyText = $message->getBody(); - } elseif ($contentType === 'text/html') { - $bodyHtml = $message->getBody(); - } else { - $bodyHtml = $message->getBody(); - } - - foreach ($message->getChildren() as $child) { - if ($child instanceof Swift_Image) { - $images[] = [ - 'type' => $child->getContentType(), - 'name' => $child->getId(), - 'content' => base64_encode($child->getBody()), - ]; - } elseif ($child instanceof Swift_Attachment) { - $attachments[] = [ - 'type' => $child->getContentType(), - 'name' => $child->getFilename(), - 'content' => base64_encode($child->getBody()) - ]; - } elseif ($child instanceof Swift_MimePart && $this->supportsContentType($child->getContentType())) { - if ($child->getContentType() === 'text/html') { - $bodyHtml = $child->getBody(); - } elseif ($child->getContentType() === 'text/plain') { - $bodyText = $child->getBody(); - } - } - } - - $mailchimpTransactionalMessage = [ - 'html' => $bodyHtml, - 'text' => $bodyText, - 'subject' => $message->getSubject(), - 'from_email' => $fromEmails[0], - 'from_name' => $fromAddresses[$fromEmails[0]], - 'to' => $to, - 'headers' => $headers, - 'tags' => $tags, - 'inline_css' => null - ]; - - if (count($attachments) > 0) { - $mailchimpTransactionalMessage['attachments'] = $attachments; - } - - if (count($images) > 0) { - $mailchimpTransactionalMessage['images'] = $images; - } - - foreach ($message->getHeaders()->getAll() as $header) { - if ($header->getFieldType() === Swift_Mime_Header::TYPE_TEXT) { - switch ($header->getFieldName()) { - case 'List-Unsubscribe': - $headers['List-Unsubscribe'] = $header->getValue(); - $mailchimpTransactionalMessage['headers'] = $headers; - break; - case 'X-MC-InlineCSS': - $mailchimpTransactionalMessage['inline_css'] = $header->getValue(); - break; - case 'X-MC-Tags': - $tags = $header->getValue(); - if (!is_array($tags)) { - $tags = explode(',', $tags); - } - $mailchimpTransactionalMessage['tags'] = $tags; - break; - case 'X-MC-Autotext': - $autoText = $header->getValue(); - if (in_array($autoText, ['true', 'on', 'yes', 'y', true], true)) { - $mailchimpTransactionalMessage['auto_text'] = true; - } - if (in_array($autoText, ['false', 'off', 'no', 'n', false], true)) { - $mailchimpTransactionalMessage['auto_text'] = false; - } - break; - case 'X-MC-GoogleAnalytics': - $analyticsDomains = explode(',', $header->getValue()); - if (is_array($analyticsDomains)) { - $mailchimpTransactionalMessage['google_analytics_domains'] = $analyticsDomains; - } - break; - case 'X-MC-GoogleAnalyticsCampaign': - $mailchimpTransactionalMessage['google_analytics_campaign'] = $header->getValue(); - break; - case 'X-MC-TrackingDomain': - $mailchimpTransactionalMessage['tracking_domain'] = $header->getValue(); - break; - default: - if (strncmp($header->getFieldName(), 'X-', 2) === 0) { - $headers[$header->getFieldName()] = $header->getValue(); - $mailchimpTransactionalMessage['headers'] = $headers; - } - break; - } - } - } - - if ($this->getSubaccount()) { - $mailchimpTransactionalMessage['subaccount'] = $this->getSubaccount(); - } + $this->template = $template; - return $mailchimpTransactionalMessage; + return $this; } /** - * @return null|array + * @param string $subaccount + * @return $this */ - public function getResultApi(): ?array + public function setSubaccount(string $subaccount): static { - return $this->resultApi; + $this->subaccount = $subaccount; + + return $this; } -} +} \ No newline at end of file From 9b82052e34c1d3eb0202f010a5bfac67c52812fd Mon Sep 17 00:00:00 2001 From: Sander Potjer Date: Thu, 2 Jun 2022 17:41:42 +0200 Subject: [PATCH 2/2] Release v2.0.0 Signed-off-by: Sander Potjer --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 766400c..e062e6c 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ Mailchimp Transactional mailer adapter for Craft CMS. +## 2.0.0 - 2022-06-02 +### Added +- Added Craft 4 compatibility. +- +### Changed +- Using Symfony Mailer rather than Swift Mailer. + ## 1.0.3 - 2022-03-30 ### Fixed - Improve the Mailchimp Transactional API response error logging (thanks @nstCactus)