You are here

public function SendGridMail::mail in SendGrid Integration 8

Same name and namespace in other branches
  1. 8.2 src/Plugin/Mail/SendGridMail.php \Drupal\sendgrid_integration\Plugin\Mail\SendGridMail::mail()

Sends a message composed by \Drupal\Core\Mail\MailManagerInterface->mail().

Parameters

array $message: Message array with at least the following elements:

  • id: A unique identifier of the email type. Examples: 'contact_user_copy', 'user_password_reset'.
  • to: The mail address or addresses where the message will be sent to. The formatting of this string will be validated with the PHP email validation filter. Some examples:

  • subject: Subject of the email to be sent. This must not contain any newline characters, or the mail may not be sent properly. The subject is converted to plain text by the mail plugin manager.
  • body: Message to be sent. Accepts both CRLF and LF line-endings. Email bodies must be wrapped. For smart plain text wrapping you can use \Drupal\Core\Mail\MailFormatHelper::wrapMail() .
  • headers: Associative array containing all additional mail headers not defined by one of the other parameters. PHP's mail() looks for Cc and Bcc headers and sends the mail to addresses in these headers too.

Return value

bool TRUE if the mail was successfully accepted for delivery, otherwise FALSE.

Overrides MailInterface::mail

File

src/Plugin/Mail/SendGridMail.php, line 121
Implements Drupal MailSystemInterface.

Class

SendGridMail
@file Implements Drupal MailSystemInterface.

Namespace

Drupal\sendgrid_integration\Plugin\Mail

Code

public function mail(array $message) {
  $site_config = $this->configFactory
    ->get('system.site');
  $sendgrid_config = $this->configFactory
    ->get('sendgrid_integration.settings');
  $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);
  $sendgrid_message = new Email();
  $sitename = $site_config
    ->get('name');

  // Defining default unique args.
  $unique_args = [
    'id' => $message['id'],
    'module' => $message['module'],
  ];

  // If this is a password reset. Bypass spam filters.
  if (strpos($message['id'], 'password')) {
    $sendgrid_message
      ->addFilter('bypass_list_management', 'enable', 1);
  }

  // If this is a Drupal Commerce message. Bypass spam filters.
  if (strpos($message['id'], 'commerce')) {
    $sendgrid_message
      ->addFilter('bypass_list_management', 'enable', 1);
  }

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

  // Allow other modules to modify unique arguments.
  $args = $this->moduleHandler
    ->invokeAll('sendgrid_integration_unique_args_alter', [
    $unique_args,
    $message,
  ]);

  // Check if we got any variable back.
  if (!empty($args)) {
    $unique_args = $args;
  }

  // Checking if 'From' email-address already exists.
  if (isset($message['headers']['From'])) {
    $fromaddrarray = $this
      ->parseAddress($message['headers']['From']);
    $data['from'] = $fromaddrarray[0];
    $data['fromname'] = $fromaddrarray[1];
  }
  else {
    $data['from'] = $site_config
      ->get('mail');
    $data['fromname'] = $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
    ->setFrom($data['from'])
    ->setSubject($message['subject'])
    ->setCategories($categories)
    ->setUniqueArgs($unique_args);
  if (!empty($data['fromname'])) {
    $sendgrid_message
      ->setFromName($data['fromname']);
  }

  // If there are multiple recipients we use a different method for To:
  if (strpos($message['to'], ',')) {
    $sendtosarry = explode(',', $message['to']);

    // Use multiple `addSmtpapiTo`s as a superior alternative to `setBcc`.
    // As per email spec the addTo can be empty for that use case.
    // See https://sendgrid.com/docs/for-developers/sending-email/building-an-x-smtpapi-header/#bcc-behavior
    if (!isset($message['sendgrid']['smtpapito']['bcc'])) {

      // Don't bother putting anything in "to" and "toName" for
      // multiple addresses. Only put multiple addresses in the Smtp header.
      // For multi addresses as per https://packagist.org/packages/fastglass/sendgrid
      $sendgrid_message
        ->addTo($sendtosarry);
    }
    else {

      // Add SmtAPi header instead if we want BCC like method, see
      // https://github.com/taz77/sendgrid-php-ng/tree/1.0.12#bcc.
      $sendgrid_message
        ->setSmtpapiTos($sendtosarry);
    }
  }
  else {
    $toaddrarray = $this
      ->parseAddress($message['to']);
    $sendgrid_message
      ->addTo($toaddrarray[0]);
    if (!empty($toaddrarray[1])) {
      $sendgrid_message
        ->addToName($toaddrarray[1]);
    }
  }

  // Add cc and bcc in mail if they exist.
  $cc_bcc_keys = [
    'cc',
    'bcc',
  ];
  $address_cc_bcc = [];

  // 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
              ->setText(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
              ->setHtml($body);

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

            // @todo determine how to handle this content type.
            // Get the boundary ID from the Content-Type header.
            $boundary = $this
              ->getSubString($message['body'], 'boundary', '"', '"');
            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
                  ->setText(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
                  ->setHtml($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
                      ->setText(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
                        ->setHtml(base64_decode($body_part2));
                    }
                    else {

                      // Save the HTML content.
                      $sendgrid_message
                        ->setHtml($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
                    ->setText(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
                    ->setHtml($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
              ->setText(MailFormatHelper::wrapMail(MailFormatHelper::htmlToText($message['body'])));
        }
        break;
      case 'reply-to':
        $sendgrid_message
          ->setReplyTo($message['headers'][$key]);
        break;
    }

    // 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) {
        list($mail_cc_address, $cc_name) = $this
          ->parseAddress($mail_id);
        $address_cc_bcc[mb_strtolower($key)][] = [
          'mail' => $mail_cc_address,
          'name' => $cc_name,
        ];
      }
    }
  }
  if (array_key_exists('cc', $address_cc_bcc)) {
    foreach ($address_cc_bcc['cc'] as $item) {
      $sendgrid_message
        ->addCc($item['mail']);
      $sendgrid_message
        ->addCcName($item['name']);
    }
  }
  if (array_key_exists('bcc', $address_cc_bcc)) {
    foreach ($address_cc_bcc['bcc'] as $item) {
      $sendgrid_message
        ->addBcc($item['mail']);
      $sendgrid_message
        ->addBccName($item['name']);
    }
  }

  // Prepare message attachments and params attachments.
  $attachments = [];
  if (isset($message['attachments']) && !empty($message['attachments'])) {
    foreach ($message['attachments'] as $attachmentitem) {
      if (is_file($attachmentitem)) {
        $attachments[$attachmentitem] = $attachmentitem;
      }
    }
  }
  elseif (isset($message['params']['attachments']) && !empty($message['params']['attachments'])) {
    foreach ($message['params']['attachments'] as $attachment) {

      // Get filepath.
      if (isset($attachment['filepath']) && is_file($attachment['filepath'])) {
        $filepath = $attachment['filepath'];
      }
      elseif (isset($attachment['file']) && $attachment['file'] instanceof FileInterface) {
        $filepath = \Drupal::service('file_system')
          ->realpath($attachment['file']
          ->getFileUri());
      }
      else {
        continue;
      }

      // Get filename.
      if (isset($attachment['filename'])) {
        $filename = $attachment['filename'];
      }
      else {
        $filename = basename($filepath);
      }

      // Attach file.
      $attachments[$filename] = $filepath;
    }
  }

  // If we have attachments, add them.
  if (!empty($attachments)) {
    $sendgrid_message
      ->setAttachments($attachments);
  }

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

  // Add substitutions.
  if (isset($message['sendgrid']['substitutions'])) {
    $sendgrid_message
      ->setSubstitutions($message['sendgrid']['substitutions']);
  }

  // Lets try and send the message and catch the error.
  try {
    $response = $client
      ->send($sendgrid_message);
  } catch (\Exception $e) {
    $this->logger
      ->error('Sending emails to Sendgrid service failed with error code ' . $e
      ->getCode());
    if ($e instanceof Exception) {
      foreach ($e
        ->getErrors() as $error_info) {
        $this->logger
          ->error('Sendgrid generated error ' . $error_info);
      }
    }
    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'],
    $unique_args,
    $response,
  ];
  $this->moduleHandler
    ->invokeAll('sendgrid_integration_sent', $hook_args);
  if ($response
    ->getCode() == 200) {

    // 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
      ->getBody()->errors[0],
  ]);
  return FALSE;
}