Repository URL to install this package:
|
Version:
2.0.1 ▾
|
<?php
namespace Drupal\dds_sendgrid\Plugin\Mail;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Annotation\Translation;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Mail\MailFormatHelper;
use Drupal\Core\Mail\MailInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\file\FileInterface;
use Html2Text\Html2Text;
use SendGrid\Mail\Attachment;
use SendGrid\Mail\Bcc;
use SendGrid\Mail\Cc;
use SendGrid\Mail\Content;
use SendGrid\Mail\From;
use SendGrid\Mail\Mail;
use SendGrid\Mail\MailSettings;
use SendGrid\Mail\To;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class SendGridMailSystem
*
* @package Drupal\dds_sendgrid\Plugin\Mail
*
* @Mail(
* id = "sendgrid",
* label = @Translation("SendGrid"),
* description = @Translation("Sends the message through the SendGrid API.")
* )
*/
class SendGridMailSystem implements MailInterface, ContainerFactoryPluginInterface {
protected $configFactory;
protected $logger;
protected $moduleHandler;
protected $queueFactory;
/**
* SendGridMailSystem constructor.
*
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory service.
* @param \Drupal\Core\logger\LoggerChannelFactoryInterface $logger_channel_factory
* The logger channel factory service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Queue\QueueFactory $queue_factory
* The queue factory service.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
ConfigFactoryInterface $config_factory,
LoggerChannelFactoryInterface $logger_channel_factory,
ModuleHandlerInterface $module_handler,
QueueFactory $queue_factory
) {
$this->configFactory = $config_factory;
$this->logger = $logger_channel_factory->get('DDS Sendgrid');
$this->moduleHandler = $module_handler;
$this->queueFactory = $queue_factory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('config.factory'),
$container->get('logger.factory'),
$container->get('module_handler'),
$container->get('queue')
);
}
/**
* {@inheritdoc}
*/
public function format(array $message) {
// Join message array.
$message['body'] = implode("\n\n", $message['body']);
return $message;
}
/**
* {@inheritdoc}
*/
public function mail(array $message) {
$site_config = $this->configFactory->get('system.site');
$sendgrid_config = $this->configFactory->get('dds_sendgrid.settings');
$api_key = $sendgrid_config->get('api_key');
// Set log messages and return false if no api key is set.
if (empty($api_key)) {
$this->logger->error('Missing API Key!');
return FALSE;
}
$sendgrid = new \SendGrid($api_key);
$mail = new Mail();
$mail_settings = new MailSettings();
$site_name = $site_config->get('name');
$site_email = $site_config->get('mail');
// If this is a password reset, bypass list and spam management.
if (strpos($message['id'], 'password')) {
$mail_settings->setBypassListManagement(TRUE);
$mail_settings->setSpamCheck(FALSE);
}
// If this is a Drupal Commerce mail, bypass list and spam management.
if (strpos($message['id'], 'commerce')) {
$mail_settings->setBypassListManagement(TRUE);
$mail_settings->setSpamCheck(FALSE);
}
// Set bypass list management if it's defined in params.
if (isset($message['params']['sendgrid']['bypass_list_management'])) {
$mail_settings->setBypassListManagement((bool)$message['params']['sendgrid']['bypass_list_management']);
}
// Set the spam check if it's defined in params.
if (isset($message['params']['sendgrid']['spam_check'])) {
$mail_settings->setSpamCheck((bool)$message['params']['sendgrid']['spam_check']);
}
// Define custom arguments
$custom_args = [
'id' => $message['id'],
'module' => $message['module']
];
// Add account UID if it's been defined
if (isset($message['params']['account']->uid)) {
$custom_args['uid'] = $message['params']['account']->uid;
}
// Let other modules modify the custom arguments
$altered_custom_args = $this->moduleHandler->invokeAll('sendgrid_custom_arguments_alter', [
$custom_args,
$message
]);
// If any hooks have defined any arguments, set them as the custom arguments.
if (!empty($altered_custom_args)) {
$custom_args = $altered_custom_args;
}
// Define categories
$categories = [
$site_name,
$message['module'],
$message['id']
];
// Let other modules modify the categories.
$altered_categories = $this->moduleHandler->invokeAll('sendgrid_categories_alter', [
$categories,
$message
]);
// If any hooks have altered the categories, set them as the categories.
if (!empty($altered_categories)) {
$categories = $altered_categories;
}
// Make sure all characters in the categories are non-ascii.
foreach ($categories as $category_index => $category) {
$categories[$category_index] = transliterator_transliterate('Any-Latin; Latin-ASCII; [\u0080-\u7fff] remove', $category);
}
// Configure the "From" sender.
$from = new From($site_email, $site_name);
// Checking if 'from' email-address already exist.
if (isset($message['headers']['from'])) {
if (isset($message['headers']['From']) && $message['headers']['from'] == $message['headers']['From']) {
$parsed_address = $this->parseAddress($message['headers']['from']);
$from = new From($parsed_address['email'], $parsed_address['name']);
unset($parsed_address);
}
}
// If there are multiple recipients we loop over them.
if (strpos($message['to'], ',')) {
$recipients = [];
$recipient_array = explode(',', $message['to']);
foreach ($recipient_array as $recipient_data) {
$parsed_address = $this->parseAddress($recipient_data);
$recipients[] = new To($parsed_address['email'], $parsed_address['name']);
unset($parsed_address);
}
$mail->addTos($recipients);
} else {
$parsed_address = $this->parseAddress($message['to']);
$mail->addTo(
new To(
$parsed_address['email'],
$parsed_address['name']
)
);
unset($parsed_address);
}
$mail->setFrom($from);
$mail->setSubject($message['subject']);
$mail->addCustomArgs($custom_args);
$mail->addCategories($categories);
$mail->setMailSettings($mail_settings);
// Logic for the various header types.
foreach ($message['headers'] as $key => $value) {
switch (mb_strtolower($key)) {
case 'content-type':
// Parse several values on the Content-type header, storing them in an array like
// key=value -> $vars['key']='value'.
$vars = explode(';', $value);
foreach ($vars as $i => $var) {
if ($cut = strpos($var, '=')) {
$new_var = trim(mb_strtolower(mb_substr($var, $cut + 1)));
$new_key = trim(mb_substr($var, 0, $cut));
unset($vars[$i]);
$vars[$new_key] = $new_var;
}
}
// If $vars is empty then set an empty value at index 0 to avoid a PHP warning in the next statement.
$vars[0] = $vars[0] ?? '';
switch ($vars[0]) {
case 'text/plain':
// Add the plain text content.
$mail->addContent(
new Content(
'text/plain',
MailFormatHelper::wrapMail(
MailFormatHelper::htmlToText($message['body'])
)
)
);
break;
case 'text/html':
$body = $message['body'];
if ($body instanceof MarkupInterface) {
$body = $body->__toString();
}
// Add the HTML content
$mail->addContent(
new Content(
'text/html',
$body
)
);
// Add the plain text content.
$converter = new Html2Text($message['body']);
$plain_body = $converter->getText();
$mail->addContent(
new Content(
'text/plain',
$plain_body
)
);
break;
case 'multipart/related':
// @TODO Determine how to handle this content type.
break;
case 'multipart/alternative':
// Get the boundary ID from the Content-Type header.
$boundary = $this->getSubString($message['body'], 'boundary', '"', '"');
// Split the body based on the boundary ID.
$body_parts = $this->boundrySplit($message['body'], $boundary);
// Parse text and HTML portions.
foreach ($body_parts as $body_part) {
// If plain/text within the body part, add it to $mailer->AltBody.
if (strpos($body_part, 'text/plain')) {
// Clean up the text.
$body_part = trim($this->removeHeaders(trim($body_part)));
// Include it as part of the mail object.
$mail->addContent(
new Content(
'text/plain',
MailFormatHelper::wrapMail(
MailFormatHelper::htmlToText(
$body_part
)
)
)
);
}
// If plain/html within the body part, add it to $mailer->Body.
elseif (strpos($body_part, 'text/html')) {
// Clean up the text.
$body_part = trim($this->removeHeaders(trim($body_part)));
// Include it as part of the mail object.
$mail->addContent(
new Content(
'text/html',
$body_part
)
);
}
}
break;
case 'multipart/mixed':
// Get the boundary ID from the Content-Type header.
$boundary = $this->getSubString($value, 'boundary', '"', '"');
// Split the body based on the boundary ID.
$body_parts = $this->boundrySplit($message['body'], $boundary);
// Parse text and HTML portions.
foreach ($body_parts as $body_part) {
if (strpos($body_part, 'multipart/alternative')) {
// Get the second boundary ID from the Content-Type header.
$boundary2 = $this->getSubString($body_part, 'boundary', '"', '"');
// Clean up the text.
$body_part = trim($this->removeHeaders(trim($body_part)));
// Split the body based on the internal boundary ID.
$body_parts2 = $this->boundrySplit($body_part, $boundary2);
// Process the internal parts.
foreach ($body_parts2 as $body_part2) {
// If plain/text within the body part, add it to $mailer->AltBody.
if (strpos($body_part2, 'text/plain')) {
// Clean up the text.
$body_part2 = trim($this->removeHeaders(trim($body_part2)));
$mail->addContent(
new Content(
'text/plain',
MailFormatHelper::wrapMail(
MailFormatHelper::htmlToText(
$body_part2
)
)
)
);
}
// If plain/html within the body part, add it to $mailer->Body.
elseif (strpos($body_part2, 'text/html')) {
// Get the encoding.
$body_part2_encoding = trim($this->getSubString($body_part2, 'Content-Transfer-Encoding', ':', "\n"));
// Clean up the text.
$body_part2 = trim($this->removeHeaders(trim($body_part2)));
// Check whether the encoding is base64, and if so, decode it.
if (mb_strtolower($body_part2_encoding) === 'base64') {
// Save the decoded HTML content.
$mail->addContent(
new Content(
'text/html',
base64_decode($body_part2)
)
);
}
else {
// Save the HTML content.
$mail->addContent(
new Content(
'text/html',
$body_part2
)
);
}
}
}
}
else {
// This parses the message if there is no internal content
// type set after the multipart/mixed.
// If text/plain within the body part, add it to $mailer->Body.
if (strpos($body_part, 'text/plain')) {
// Clean up the text.
$body_part = trim($this->removeHeaders(trim($body_part)));
// Set the text message.
$mail->addContent(
new Content(
'text/plain',
MailFormatHelper::wrapMail(
MailFormatHelper::htmlToText(
$body_part
)
)
)
);
}
// If text/html within the body part, add it to $mailer->Body.
elseif (strpos($body_part, 'text/html')) {
// Clean up the text.
$body_part = trim($this->removeHeaders(trim($body_part)));
// Set the HTML message.
$mail->addContent(
new Content(
'text/html',
$body_part
)
);
}
}
}
break;
default:
// Everything else is unknown so we log and send the message as text.
\Drupal::messenger()
->addError(t('The %header of your message is not supported by SendGrid and will be sent as text/plain instead.', ['%header' => "Content-Type: $value"]));
$this->logger->error("The Content-Type: $value of your message is not supported by PHPMailer and will be sent as text/plain instead.");
$mail->addContent(
new Content(
'text/plain',
MailFormatHelper::wrapMail(
MailFormatHelper::htmlToText(
$message['body']
)
)
)
);
break;
}
break;
case 'reply-to':
$parsed_address = $this->parseAddress($message['headers'][$key]);
$mail->setReplyTo($parsed_address['email'], $parsed_address['name']);
break;
case 'cc':
if (strpos($value, ',')) {
$cc_mails = explode(',', $value);
foreach ($cc_mails as $cc_mail) {
$parsed_address = $this->parseAddress($cc_mail);
$mail->addCc(
new Cc(
$parsed_address['email'],
$parsed_address['name']
)
);
unset($parsed_address);
}
} else {
$parsed_address = $this->parseAddress($value);
$mail->addCc(
new Cc(
$parsed_address['email'],
$parsed_address['name']
)
);
unset($parsed_address);
}
break;
case 'bcc':
if (strpos($value, ',')) {
$bcc_mails = explode(',', $value);
foreach ($bcc_mails as $bcc_mail) {
$parsed_address = $this->parseAddress($bcc_mail);
$mail->addBcc(
new Bcc(
$parsed_address['email'],
$parsed_address['name']
)
);
unset($parsed_address);
}
} else {
$parsed_address = $this->parseAddress($value);
$mail->addBcc(
new Bcc(
$parsed_address['email'],
$parsed_address['name']
)
);
unset($parsed_address);
}
break;
}
}
// Handle attachments
$attachments = [];
if (isset($message['attachments']) && !empty($message['attachments'])) {
foreach ($message['attachments'] as $attachment) {
if (is_file($attachment)) {
// Get the file name.
$file_name = basename($attachment);
// Get the file content as base64 encoded.
$file_content = base64_encode(file_get_contents($attachment));
// Get the file mime type.
$file_mime = mime_content_type($attachment);
$attachments[$file_name] = new Attachment(
$file_content,
$file_mime,
$file_name
);
}
}
}
else if (isset($message['params']['attachments']) && !empty($message['params']['attachments'])) {
foreach ($message['params']['attachments'] as $attachment) {
// Get the file path.
if (isset($attachment['filepath']) && !empty($attachment['filepath'])) {
$filepath = $attachment['filepath'];
}
else if (isset($attachment['file']) && $attachment['file'] instanceof FileInterface) {
$filepath = \Drupal::service('file_system')
->realPath($attachment['file'])->getFileUri();
}
else {
continue;
}
// Get the file name.
if (isset($attachment['filename']) && !empty($attachment['filename'])) {
$file_name = $attachment['filename'];
} else {
$file_name = basename($filepath);
}
// Get the file content as base64 encoded.
$file_content = base64_encode(file_get_contents($filepath));
// Get the file mime type.
$file_mime = mime_content_type($filepath);
$attachments[$file_name] = new Attachment(
$file_content,
$file_mime,
$file_name
);
}
}
// If we have attachments, add them to the mail
if (!empty($attachments)) {
$mail->addAttachments($attachments);
}
// Set template
if (isset($message['params']['sendgrid']['template_id']) && !empty($message['params']['sendgrid']['template_id'])) {
$mail->setTemplateId($message['params']['sendgrid']['template_id']);
}
// Add substitution tags.
if (isset($message['params']['sendgrid']['substitutions']) && !empty($message['params']['sendgrid']['substitutions'])) {
$mail->addSubstitutions($message['params']['sendgrid']['substitutions']);
}
// Dynamic template data. Useful for the new templates that can use handlebars.
if (isset($message['params']['sendgrid']['dynamic_template_data']) && !empty($message['params']['sendgrid']['dynamic_template_data'])) {
$mail->addDynamicTemplateDatas($message['params']['sendgrid']['dynamic_template_data']);
}
// Do not send if sending is disabled
if ($message['send'] != 1) {
$this->logger->notice('Email was not sent because send value was disabled.');
return TRUE;
}
try {
$response = $sendgrid->send($mail);
} catch (\Exception $e) {
$this->logger->error('Sending emails to Sendgrid service failed with error code ' . $e->getCode());
$this->logger->error($e->getMessage());
// Add message to queue if reason for failing was timeout or
// another valid reason. This adds more error tolerance.
$codes = [
-110,
404,
408,
500,
502,
503,
504,
];
if (in_array($e->getCode(), $codes, FALSE)) {
$this->queueFactory->get('SendGridResendQueue')->createItem($message);
}
return FALSE;
}
// If the code is 200 we are good to finish and proceed. Otherwise defaults to sending failed.
return $response->statusCode() === 202;
}
/**
* Returns a string that is contained within another string.
*
* Returns the string from within $source that is some where after $target
* and is between $beginning_character and $ending_character.
*
* Swiped from SMTP module. Thanks!
*
* @param string $source
* A string containing the text to look through.
* @param string $target
* A string containing the text in $source to start looking from.
* @param string $beginning_character
* A string containing the character just before the sought after text.
* @param string $ending_character
* A string containing the character just after the sought after text.
*
* @return string
* A string with the text found between the $beginning_character and the
* $ending_character.
*/
protected function getSubString($source, $target, $beginning_character, $ending_character) {
$search_start = strpos($source, $target) + 1;
$first_character = strpos($source, $beginning_character, $search_start) + 1;
$second_character = strpos($source, $ending_character, $first_character) + 1;
$substring = mb_substr($source, $first_character, $second_character - $first_character);
$string_length = mb_strlen($substring) - 1;
if ($substring[$string_length] == $ending_character) {
$substring = mb_substr($substring, 0, $string_length);
}
return $substring;
}
/**
* Splits the input into parts based on the given boundary.
*
* Swiped from Mail::MimeDecode, with modifications based on Drupal's coding
* standards and this bug report: http://pear.php.net/bugs/bug.php?id=6495
*
* @param string $input
* A string containing the body text to parse.
* @param string $boundary
* A string with the boundary string to parse on.
*
* @return array
* An array containing the resulting mime parts
*/
protected function boundrySplit($input, $boundary) {
$parts = [];
$bs_possible = mb_substr($boundary, 2, -2);
$bs_check = '\"' . $bs_possible . '\"';
if ($boundary == $bs_check) {
$boundary = $bs_possible;
}
$tmp = explode('--' . $boundary, $input);
$count = count($tmp);
for ($i = 1; $i < $count; $i++) {
if (trim($tmp[$i])) {
$parts[] = $tmp[$i];
}
}
return $parts;
}
/**
* Strips the headers from the body part.
*
* @param string $input
* A string containing the body part to strip.
*
* @return string
* A string with the stripped body part.
*/
protected function removeHeaders($input) {
$part_array = explode("\n", $input);
// Will strip these headers according to RFC2045.
$headers_to_strip = [
'Content-Type',
'Content-Transfer-Encoding',
'Content-ID',
'Content-Disposition',
];
$pattern = '/^(' . implode('|', $headers_to_strip) . '):/';
while (count($part_array) > 0) {
// Ignore trailing spaces/newlines.
$line = rtrim($part_array[0]);
// If the line starts with a known header string.
if (preg_match($pattern, $line)) {
$line = rtrim(array_shift($part_array));
// Remove line containing matched header.
// If line ends in a ';' and the next line starts with four spaces, it's a continuation
// of the header split onto the next line. Continue removing lines while we have this condition.
while (substr($line, -1) == ';' && count($part_array) > 0 && substr($part_array[0], 0, 4) == ' ') {
$line = rtrim(array_shift($part_array));
}
}
else {
// No match header, must be past headers; stop searching.
break;
}
}
$output = implode("\n", $part_array);
return $output;
}
/**
* Split an email address into it's name and address components.
*
* @param string $email
*
* @return array
*/
protected function parseAddress($email) {
if (preg_match('/^\s*"?(.+?)"?\s*<\s*([^>]+)\s*>$/', $email, $matches)) {
return ['email' => $matches[2], 'name' => $matches[1]];
}
return ['email' => $email, 'name' => ''];
}
}