Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
novicell/dds_sendgrid / src / Plugin / Mail / SendGridMailSystem.php
Size: Mime:
<?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' => ''];
  }

}