You are here

SendGridMail.php in SendGrid Integration 8.2

Same filename and directory in other branches
  1. 8 src/Plugin/Mail/SendGridMail.php

Implements Drupal MailSystemInterface.

Plugin annotation


@Mail(
  id = "sendgrid_integration",
  label = @Translation("Sendgrid Integration"),
  description = @Translation("Sends the message using Sendgrid API.")
)

File

src/Plugin/Mail/SendGridMail.php
View source
<?php

namespace Drupal\sendgrid_integration\Plugin\Mail;

use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Utility\Xss;
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 Html2Text\Html2Text;
use SendGrid\Client;
use SendGrid\Exception\SendgridException;
use SendGrid\Mail\Attachment;
use SendGrid\Mail\Bcc;
use SendGrid\Mail\Cc;
use SendGrid\Mail\ClickTracking;
use SendGrid\Mail\From;
use SendGrid\Mail\Mail;
use SendGrid\Mail\OpenTracking;
use SendGrid\Mail\Personalization;
use SendGrid\Mail\ReplyTo;
use SendGrid\Mail\SandBoxMode;
use SendGrid\Mail\SpamCheck;
use SendGrid\Mail\To;
use SendGrid\Mail\TrackingSettings;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * @file
 * Implements Drupal MailSystemInterface.
 *
 * @Mail(
 *   id = "sendgrid_integration",
 *   label = @Translation("Sendgrid Integration"),
 *   description = @Translation("Sends the message using Sendgrid API.")
 * )
 */
class SendGridMail implements MailInterface, ContainerFactoryPluginInterface {

  /**
   * {@inheritdoc}
   */
  const SENDGRID_INTEGRATION_EMAIL_REGEX = '/^\\s*"?(.+?)"?\\s*<\\s*([^>]+)\\s*>$/';

  /**
   * The config factory service.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The logger service for the sendgrid_integration module.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $logger;

  /**
   * The module handler service.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The queue factory service.
   *
   * @var \Drupal\Core\Queue\QueueFactory
   */
  protected $queueFactory;

  /**
   * The MIME Type Guesser service.
   *
   * @var \Drupal\Core\File\MimeType\MimeTypeGuesser
   */
  protected $mimeTypeGuesser;

  /**
   * 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 $configFactory
   *   The configuration factory service.
   * @param \Drupal\Core\logger\LoggerChannelFactoryInterface $loggerChannelFactory
   *   The logger channel factory service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   The module handler service.
   * @param \Drupal\Core\Queue\QueueFactory $queueFactory
   *   The queue factory service.
   */
  public function __construct(array $configuration, string $plugin_id, $plugin_definition, ConfigFactoryInterface $configFactory, LoggerChannelFactoryInterface $loggerChannelFactory, ModuleHandlerInterface $moduleHandler, QueueFactory $queueFactory) {
    $this->configFactory = $configFactory;
    $this->logger = $loggerChannelFactory
      ->get('sendgrid_integration');
    $this->moduleHandler = $moduleHandler;
    $this->queueFactory = $queueFactory;
    $this->mimeTypeGuesser = \Drupal::service('file.mime_type.guesser');
  }

  /**
   * {@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) : array {

    // Join message array.
    $message['body'] = implode("\n\n", $message['body']);
    return $message;
  }

  /**
   * {@inheritdoc}
   * @throws \SendGrid\Exception\SendgridException
   */
  public function mail(array $message) : bool {

    # Begin by creating instances of objects needed.
    $sendgrid_message = new Mail();
    $personalization0 = $sendgrid_message
      ->getPersonalization();
    $sandbox_mode = new SandBoxMode();
    $site_config = $this->configFactory
      ->get('system.site');
    $sendgrid_config = $this->configFactory
      ->get('sendgrid_integration.settings');
    if (isset($message['params']['apikey'])) {
      $key_secret = $message['params']['apikey'];
      unset($message['apikey']);
    }
    else {
      $key_secret = $sendgrid_config
        ->get('apikey');
      if ($this->moduleHandler
        ->moduleExists('key')) {
        $key = \Drupal::service('key.repository')
          ->getKey($key_secret);
        if ($key) {
          $key_value = $key
            ->getKeyValue();
          if ($key_value) {
            $key_secret = $key_value;
          }
        }
      }
    }
    if (empty($key_secret)) {

      // Set a error in the logs if there is no API key.
      $this->logger
        ->error('No API Secret key has been set');

      // Return false to indicate message was not able to send.
      return FALSE;
    }
    $options = [
      'turn_off_ssl_verification' => FALSE,
      'protocol' => 'https',
      'port' => NULL,
      'url' => NULL,
      'raise_exceptions' => FALSE,
    ];

    // Create a new SendGrid object.
    $client = new Client($key_secret, $options);
    $sitename = $site_config
      ->get('name');

    // If this is a password reset. Bypass spam filters.
    if (strpos($message['id'], 'password')) {
      $spam_check = new SpamCheck();
      $spam_check
        ->setEnable(FALSE);
    }

    // If this is a Drupal Commerce message. Bypass spam filters.
    if (strpos($message['id'], 'commerce')) {
      $spam_check = new SpamCheck();
      $spam_check
        ->setEnable(FALSE);
    }

    # Add UID metadata to the message that matches the drupal user ID.
    if (isset($message['params']['account']->uid)) {
      $mailuser = $message['params']['account'];
      $uid = $mailuser
        ->get('uid')->value;
      $sendgrid_message
        ->addCustomArg("uid", strval($uid));
    }

    // Checking if 'From' email-address already exists.
    if (isset($message['headers']['From'])) {
      $fromaddrarray = $this
        ->parseAddress($message['headers']['From']);
      $data['from'] = $fromaddrarray[0];
      $data['fromname'] = strval($fromaddrarray[1]);
      unset($fromaddrarray);
    }
    else {
      $data['from'] = $site_config
        ->get('mail');
      $data['fromname'] = strval($sitename);
    }

    // Check if $send is set to be true.
    if ($message['send'] != 1) {
      $this->logger
        ->notice('Email was not sent because send value was disabled.');
      return TRUE;
    }

    // Build the Sendgrid mail object.
    // The message MODULE and ID is used for the Category. Category is the only
    // thing in the Sendgrid UI you can use to sort mail.
    // This is an array of categories for Sendgrid statistics.
    $categories = [
      $sitename,
      $message['module'],
      $message['id'],
    ];

    // Allow other modules to modify categories.
    $result = $this->moduleHandler
      ->invokeAll('sendgrid_integration_categories_alter', [
      $message,
      $categories,
    ]);

    // Check if we got any variable back.
    if (!empty($result)) {
      $categories = $result;
    }
    $sendgrid_message
      ->addCategories($categories);
    $personalization0
      ->setSubject($message['subject']);
    $from = new From($data['from'], $data['fromname']);
    unset($data);

    # Set the from address and add a name if it exists.
    $sendgrid_message
      ->setFrom($from);

    // If there are multiple recipients we have to explode and walk the values.
    if (strpos($message['to'], ',')) {
      $sendtosarry = explode(',', $message['to']);
      foreach ($sendtosarry as $value) {
        $sendtoarrayparsed = $this
          ->parseAddress($value);
        $personalization0
          ->addTo(new To($sendtoarrayparsed[0], isset($sendtoarrayparsed[1]) ? $sendtoarrayparsed[1] : NULL));
      }
    }
    else {
      $toaddrarray = $this
        ->parseAddress($message['to']);
      if (isset($toaddrarray[1])) {
        $toname = $toaddrarray[1];
      }
      else {
        $toname = NULL;
      }
      $personalization0
        ->addTo(new To($toaddrarray[0], $toname));
    }

    // Beginning of consolidated header parsing.
    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] = isset($vars[0]) ? $vars[0] : '';

          // Nested switch to process the various content types. We only care
          // about the first entry in the array.
          switch ($vars[0]) {
            case 'text/plain':

              // The message includes only a plain text part.
              $sendgrid_message
                ->addContent('text/plain', MailFormatHelper::wrapMail(MailFormatHelper::htmlToText($message['body'])));
              break;
            case 'text/html':

              // Ensure body is a string before using it as HTML.
              $body = $message['body'];
              if ($body instanceof MarkupInterface) {
                $body = $body
                  ->__toString();
              }

              // The message includes only an HTML part.
              $sendgrid_message
                ->addContent('text/html', $body);

              // Also include a text only version of the email.
              $converter = new Html2Text($message['body']);
              $body_plain = $converter
                ->getText();
              $sendgrid_message
                ->addContent('text/plain', MailFormatHelper::wrapMail($body_plain));
              break;
            case 'multipart/alternative':

              // Get the boundary ID from the Content-Type header.
              $boundary = $this
                ->getSubString($message['body'], 'boundary', '"', '"');

              // Parse text and HTML portions.
              // Split the body based on the boundary ID.
              $body_parts = $this
                ->boundrySplit($message['body'], $boundary);
              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.
                  $sendgrid_message
                    ->addContent('text/plain', MailFormatHelper::wrapMail(MailFormatHelper::htmlToText($body_part)));
                }
                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.
                  $sendgrid_message
                    ->addContent('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)));
                      $sendgrid_message
                        ->addContent('text/plain', MailFormatHelper::wrapMail(MailFormatHelper::htmlToText($body_part2)));
                    }
                    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.
                        $sendgrid_message
                          ->addContent('text/html', base64_decode($body_part2));
                      }
                      else {

                        // Save the HTML content.
                        $sendgrid_message
                          ->addContent('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.
                    $sendgrid_message
                      ->addContent('text/plain', MailFormatHelper::wrapMail(MailFormatHelper::htmlToText($body_part)));
                  }
                  elseif (strpos($body_part, 'text/html')) {

                    // Clean up the text.
                    $body_part = trim($this
                      ->removeHeaders(trim($body_part)));

                    // Set the HTML message.
                    $sendgrid_message
                      ->addContent('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.");

              // Force the email to be text.
              $sendgrid_message
                ->addContent('text/plain', MailFormatHelper::wrapMail(MailFormatHelper::htmlToText($message['body'])));
          }
          break;

        // End Content-type parsing
        case 'reply-to':
          $sendreplyto = $this
            ->parseAddress($message['headers'][$key]);
          $reply_to = new ReplyTo($sendreplyto[0], isset($sendreplyto[1]) ? $sendreplyto[1] : NULL);
          $sendgrid_message
            ->setReplyTo($reply_to);
          break;
      }

      // -----------------------
      // BCC and CC Address Handling
      // -----------------------
      // Array to use for processing bcc and cc options.
      $cc_bcc_keys = [
        'cc',
        'bcc',
      ];

      // Empty array to process addresses.
      $address_cc_bcc = [];

      // Handle latter case issue for cc and bcc key.
      if (in_array(mb_strtolower($key), $cc_bcc_keys)) {
        $mail_ids = explode(',', $value);
        foreach ($mail_ids as $mail_id) {
          [
            $mail_cc_address,
            $cc_name,
          ] = $this
            ->parseAddress($mail_id);
          $address_cc_bcc[mb_strtolower($key)][] = [
            'mail' => $mail_cc_address,
            'name' => $cc_name,
          ];
        }
      }
    }
    if (isset($address_cc_bcc) && array_key_exists('cc', $address_cc_bcc)) {
      foreach ($address_cc_bcc['cc'] as $item) {
        $personalization0
          ->addCc(new Cc($item['mail'], $item['name']));
      }
    }
    if (isset($address_cc_bcc) && array_key_exists('bcc', $address_cc_bcc)) {
      foreach ($address_cc_bcc['bcc'] as $item) {
        $personalization0
          ->addBcc(new Bcc($item['mail'], $item['name']));
      }
    }

    // -----------------------
    // END - BCC and CC Address Handling
    // -----------------------
    // Prepare message attachments and params attachments.
    if (isset($message['attachments']) && !empty($message['attachments'])) {
      foreach ($message['attachments'] as $attachmentitem) {
        if (is_file($attachmentitem)) {
          try {
            $attach = new Attachment();
            $struct = $this
              ->getAttachmentStruct($attachmentitem);
            $attach
              ->setContent($struct['content']);
            $attach
              ->setType($struct['type']);
            $attach
              ->setFilename($struct['filename']);
            $attach
              ->setDisposition("attachment");
            $sendgrid_message
              ->addAttachment($attach);
          } catch (SendgridException $e) {
            $message = Xss::filter($e
              ->getMessage());
            $this->logger
              ->error('Attachment processing failed' . $message);
          } catch (\Exception $e) {
            $this->logger
              ->error('Attachment processing failed' . $e
              ->getMessage());
          }
        }
      }
    }

    // The Mime Mail module (mimemail) expects attachments as an array of file
    // arrays in $message['params']['attachments']. As many modules assume you
    // will be using Mime Mail to handle attachments, we need to parse this
    // array as well.
    if (isset($message['params']['attachments']) && !empty($message['params']['attachments'])) {
      foreach ($message['params']['attachments'] as $attachment) {
        $attach = new Attachment();
        if (isset($attachment['uri'])) {
          $attachment_path = \Drupal::service('file_system')
            ->realpath($attachment['uri']);
          if (is_file($attachment_path)) {
            try {
              $struct = $this
                ->getAttachmentStruct($attachment_path);

              // Allow for customised filenames.
              if (!empty($attachment['filename'])) {
                $struct['name'] = $attachment['filename'];
              }
              $attach
                ->setContent($struct['content']);
              $attach
                ->setType($struct['type']);
              $attach
                ->setFilename($struct['filename']);
              $attach
                ->setDisposition("attachment");
            } catch (SendgridException $e) {
              $json = json_decode(Xss::filter($e
                ->getMessage()));
              if ($e instanceof SendgridException) {
                $this->logger
                  ->error(json_encode($json, JSON_PRETTY_PRINT));
              }
            } catch (\Exception $e) {
              $this->logger
                ->error('Error processing attachments' . $e
                ->getMessage());
            }
            $sendgrid_message
              ->addAttachment($attach);
          }
        }
        elseif (isset($attachment['filecontent'])) {
          $attach
            ->setFilename($attachment['filename']);
          $attach
            ->setType($attachment['filemime']);
          $attach
            ->setContent(base64_encode($attachment['filecontent']));
          $sendgrid_message
            ->addAttachment($attach);
        }
      }

      // Remove the file objects from $message['params']['attachments'].
      // (This prevents double-attaching in the drupal_alter hook below.)
      unset($message['params']['attachments']);
    }

    // Add template ID.
    if (isset($message['sendgrid']['template_id'])) {
      $sendgrid_message
        ->setTemplateId($message['sendgrid']['template_id']);
    }

    // Add substitutions.
    if (isset($message['sendgrid']['substitutions'])) {
      foreach ($message['sendgrid']['substitutions'] as $key => $value) {
        $personalization0
          ->addSubstitution($key, $value);
      }
    }

    // Tracking settings.
    $tracking_settings = new TrackingSettings();
    $click_tracking = new ClickTracking();
    if (!empty($sendgrid_config
      ->get('trackclicks'))) {
      $click_tracking
        ->setEnable(TRUE);
      $click_tracking
        ->setEnableText(TRUE);
    }
    else {
      $click_tracking
        ->setEnable(FALSE);
      $click_tracking
        ->setEnableText(FALSE);
    }
    $tracking_settings
      ->setClickTracking($click_tracking);
    $open_tracking = new OpenTracking();
    if (!empty($sendgrid_config
      ->get('trackopens'))) {
      $open_tracking
        ->setEnable(TRUE);
    }
    else {
      $open_tracking
        ->setEnable(FALSE);
    }
    $tracking_settings
      ->setOpenTracking($open_tracking);
    $sendgrid_message
      ->setTrackingSettings($tracking_settings);

    // Allow other modules to alter the Mandrill message.
    $sendgrid_params = [
      'message' => $sendgrid_message,
    ];
    \Drupal::moduleHandler()
      ->alter('sendgrid_integration', $sendgrid_params, $message);

    // Lets try and send the message and catch the error.
    try {
      $response = $client
        ->send($sendgrid_message);
    } catch (SendgridException $e) {
      $this->logger
        ->error('Sending emails to Sendgrid service failed with error code ' . Xss::filter($e
        ->getCode()));
      $json = json_decode(Xss::filter($e
        ->getMessage()));
      if ($e instanceof SendgridException) {
        $this->logger
          ->error(json_encode($json, JSON_PRETTY_PRINT));
      }
      else {
        $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)) {
        $this->queueFactory
          ->get('SendGridResendQueue')
          ->createItem($message);
      }
      return FALSE;
    }

    // Creating hook, allowing other modules react on sent email.
    $hook_args = [
      $message['to'],
      $response,
    ];
    $this->moduleHandler
      ->invokeAll('sendgrid_integration_sent', $hook_args);
    $good_response_codes = [
      200,
      202,
    ];
    if (in_array($response
      ->getCode(), $good_response_codes)) {

      // In the special case the email is coming from the module test we log
      // an info level message.
      if (isset($message['key']) && $message['key'] == 'sengrid_integration_troubleshooting_test') {
        $this->logger
          ->info('Troubleshooting test email has been sent to %address.', [
          '%address' => $message['to'],
        ]);
      }

      // If the code is 200 we are good to finish and proceed.
      return TRUE;
    }

    // Default to low. Sending failed.
    $this->logger
      ->error('Sending emails to Sendgrid service failed with error message %message.', [
      '%message' => 'Response Code: ' . $response
        ->getCode() . "\n" . $response
        ->getBody(),
    ]);
    return FALSE;
  }

  /**
   * 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(string $source, string $target, string $beginning_character, string $ending_character) : string {
    $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(string $input, string $boundary) : array {
    $parts = [];
    $bs_possible = mb_substr($boundary, 2, -2);
    $bs_check = '\\"' . $bs_possible . '\\"';
    if ($boundary == $bs_check) {
      $boundary = $bs_possible;
    }
    $tmp = explode('--' . $boundary, $input);
    for ($i = 1; $i < count($tmp); $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(string $input) : string {
    $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;
      }
    }
    return implode("\n", $part_array);
  }

  /**
   * Split an email address into it's name and address components.
   * Returns an array with the first element as the email address and the
   * second element as the name.
   *
   * @param $email string
   *
   * @return mixed
   */
  protected function parseAddress(string $email) {
    if (preg_match(self::SENDGRID_INTEGRATION_EMAIL_REGEX, $email, $matches)) {
      return [
        $matches[2],
        strval($matches[1]),
      ];
    }
    else {
      return [
        $email,
      ];
    }
  }

  /**
   * Return an array structure for a message attachment.
   *
   * @param string $path
   *   Attachment path.
   *
   * @return array
   *   Attachment structure.
   *
   * @throws \Exception
   */
  public function getAttachmentStruct(string $path) : array {
    $struct = [];
    if (!@is_file($path)) {
      throw new \Exception($path . ' is not a valid file.');
    }
    $filename = basename($path);
    $file_content = base64_encode(file_get_contents($path));
    $mime_type = $this->mimeTypeGuesser
      ->guess($path);
    if (!$this
      ->isValidContentType($mime_type)) {
      throw new \Exception($mime_type . ' is not a valid content type.');
    }
    $struct['type'] = $mime_type;
    $struct['filename'] = $filename;
    $struct['content'] = $file_content;
    return $struct;
  }

  /**
   * Helper to determine if an attachment is valid.
   *
   * @param string $file_type
   *   The file mime type.
   *
   * @return bool
   *   True or false.
   */
  protected function isValidContentType(string $file_type) : bool {
    $valid_types = [
      'image/',
      'text/',
      'application/pdf',
      'application/x-zip',
    ];

    // Allow modules to alter the valid types array.
    \Drupal::moduleHandler()
      ->alter('sendgrid_integration_valid_attachment_types', $valid_types);
    foreach ($valid_types as $vct) {
      if (strpos($file_type, $vct) !== FALSE) {
        return TRUE;
      }
    }
    return FALSE;
  }

}

Classes

Namesort descending Description
SendGridMail @file Implements Drupal MailSystemInterface.