You are here

public function SMTPMailSystem::mail in SMTP Authentication Support 8

Send the e-mail message.

Parameters

array $message: A message array, as described in hook_mail_alter().

Return value

mixed TRUE if the mail was successfully accepted, otherwise FALSE.

Throws

\PHPMailer\PHPMailer\Exception

Overrides MailInterface::mail

See also

drupal_mail()

File

src/Plugin/Mail/SMTPMailSystem.php, line 198

Class

SMTPMailSystem
Modify the drupal mail system to use smtp when sending emails.

Namespace

Drupal\smtp\Plugin\Mail

Code

public function mail(array $message) {
  $logger = $this->logger
    ->get('smtp');
  if (!class_exists(PHPMailer::class)) {
    $logger
      ->error($this
      ->t('Unable to send mail: PHPMailer class was not found.'));
    return FALSE;
  }
  $to = $message['to'];
  $body = $message['body'];
  $headers = array_change_key_case($message['headers']);
  $subject = $message['subject'];

  // Optionally reroute all emails to a single address.
  list($to, $headers) = $this
    ->applyRerouting($to, $headers);

  // Create a new PHPMailer object - autoloaded from registry.
  $mailer = new PHPMailer(TRUE);

  // Use email.validator due to different validation standard by PHPMailer.
  $mailer::$validator = [
    $this->emailValidator,
    'isValid',
  ];

  // Override PHPMailer default timeout if requested.
  $smtp_timeout = $this->smtpConfig
    ->get('smtp_timeout');
  if (!empty($smtp_timeout)) {
    $mailer->Timeout = $smtp_timeout;
  }

  // Turn on debugging, if requested.
  if ($this->smtpConfig
    ->get('smtp_debugging') && $this->currentUser
    ->hasPermission('administer smtp module')) {
    $mailer->SMTPDebug = TRUE;
  }

  // Turn on KeepAlive feature if requested.
  if ($this->smtpConfig
    ->get('smtp_keepalive')) {
    $mailer->SMTPKeepAlive = TRUE;
  }

  // Prefer the from_name from the message.
  if (!empty($message['params']['from_name'])) {
    $from_name = $message['params']['from_name'];
  }
  elseif (!empty($this->smtpConfig
    ->get('smtp_fromname'))) {
    $from_name = $this->smtpConfig
      ->get('smtp_fromname');
  }
  if (empty($from_name)) {

    // If value is not defined in settings, use site_name.
    $from_name = $this->configFactory
      ->get('system.site')
      ->get('name');
  }

  // Set from email.
  if (!empty($message['params']['from_mail'])) {
    $from = $message['params']['from_mail'];
  }
  elseif ($this->emailValidator
    ->isValid($this->smtpConfig
    ->get('smtp_from'))) {
    $from = $this->smtpConfig
      ->get('smtp_from');
  }
  if (empty($from)) {
    $from = $message['from'];

    // The $from address might contain the "name" part. If it does, split it,
    // since PHPMailer expects $from to be the raw email address.
    $matches = [];
    if (preg_match('/^(.*)\\s\\<(.*)\\>$/', $from, $matches)) {
      $from = $matches[2];
    }
  }

  // Updates $headers fields.
  $headers['sender'] = isset($headers['sender']) ? $headers['sender'] . ',' . $from : $from;
  $headers['return-path'] = isset($headers['return-path']) ? $headers['return-path'] . ',' . $from : $from;
  $headers['reply-to'] = isset($headers['reply-to']) ? $headers['reply-to'] . ',' . $from : $from;

  // Defines the From value to what we expect.
  $mailer->From = $from;
  $mailer->FromName = Unicode::mimeHeaderEncode($from_name);
  $mailer->Sender = $from;
  $hostname = $this->smtpConfig
    ->get('smtp_client_hostname');
  if ($hostname != '') {
    $mailer->Hostname = $hostname;
  }
  $helo = $this->smtpConfig
    ->get('smtp_client_helo');
  if ($helo != '') {
    $mailer->Helo = $helo;
  }

  // Create the list of 'To:' recipients.
  $torecipients = explode(',', $to);
  foreach ($torecipients as $torecipient) {
    $to_comp = $this
      ->getComponents($torecipient);
    $mailer
      ->AddAddress($to_comp['email'], $to_comp['name']);
  }

  // Parse the headers of the message and set the PHPMailer object's settings
  // accordingly.
  foreach ($headers as $key => $value) {
    switch ($key) {
      case 'from':
        if ($from == NULL or $from == '') {

          // If a from value was already given, then set based on header.
          // Should be the most common situation since drupal_mail moves the.
          // from to headers.
          $from = $value;
          $mailer->From = $value;
          $mailer->FromName = '';
          $mailer->Sender = $value;
        }
        break;
      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(strtolower(substr($var, $cut + 1)));
            $new_key = trim(substr($var, 0, $cut));
            unset($vars[$i]);
            $vars[$new_key] = $new_var;
          }
        }

        // Set the charset based on the provided value,
        // otherwise set it to UTF-8 (which is Drupal's internal default).
        $mailer->CharSet = isset($vars['charset']) ? $vars['charset'] : 'UTF-8';

        // 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] : '';
        switch ($vars[0]) {
          case 'text/plain':

            // The message includes only a plain text part.
            $mailer
              ->IsHTML(FALSE);
            $content_type = 'text/plain';
            break;
          case 'text/html':

            // The message includes only an HTML part.
            $mailer
              ->IsHTML(TRUE);
            $content_type = 'text/html';
            break;
          case 'multipart/related':

            // Get the boundary ID from the Content-Type header.
            $boundary = $this
              ->getSubstring($value, 'boundary', '"', '"');

            // The message includes an HTML part w/inline attachments.
            $mailer->ContentType = $content_type = 'multipart/related; boundary="' . $boundary . '"';
            break;
          case 'multipart/alternative':

            // The message includes both a plain text and an HTML part.
            $mailer->ContentType = $content_type = 'multipart/alternative';

            // Get the boundary ID from the Content-Type header.
            $boundary = $this
              ->getSubstring($value, 'boundary', '"', '"');
            break;
          case 'multipart/mixed':

            // The message includes one or more attachments.
            $mailer->ContentType = $content_type = 'multipart/mixed';

            // Get the boundary ID from the Content-Type header.
            $boundary = $this
              ->getSubstring($value, 'boundary', '"', '"');
            break;
          default:

            // Everything else is unsupported by PHPMailer.
            $this->messenger
              ->addMessage($this
              ->t('The %header of your message is not supported by PHPMailer and will be sent as text/plain instead.', [
              '%header' => "Content-Type: {$value}",
            ]), 'error');
            $logger
              ->error($this
              ->t('The %header of your message is not supported by PHPMailer and will be sent as text/plain instead.', [
              '%header' => "Content-Type: {$value}",
            ]));

            // Force the Content-Type to be text/plain.
            $mailer
              ->IsHTML(FALSE);
            $content_type = 'text/plain';
        }
        break;
      case 'reply-to':

        // Only add a "reply-to" if it's not the same as "return-path".
        if ($value != $headers['return-path']) {
          $reply_to_comp = $this
            ->getComponents($value);
          $mailer
            ->AddReplyTo($reply_to_comp['email'], $reply_to_comp['name']);
        }
        break;
      case 'content-transfer-encoding':
        $mailer->Encoding = $value;
        break;
      case 'return-path':
        $mailer->Sender = $value;
        break;
      case 'mime-version':
      case 'x-mailer':

        // Let PHPMailer specify these.
        break;
      case 'errors-to':
        $mailer
          ->AddCustomHeader('Errors-To: ' . $value);
        break;
      case 'cc':
        $cc_recipients = explode(',', $value);
        foreach ($cc_recipients as $cc_recipient) {
          $cc_comp = $this
            ->getComponents($cc_recipient);
          $mailer
            ->AddCC($cc_comp['email'], $cc_comp['name']);
        }
        break;
      case 'bcc':
        $bcc_recipients = explode(',', $value);
        foreach ($bcc_recipients as $bcc_recipient) {
          $bcc_comp = $this
            ->getComponents($bcc_recipient);
          $mailer
            ->AddBCC($bcc_comp['email'], Unicode::mimeHeaderEncode($bcc_comp['name']));
        }
        break;
      default:

        // The header key is not special - add it as is.
        $mailer
          ->AddCustomHeader($key . ': ' . $value);
    }
  }

  // TODO
  // Need to figure out the following.
  //
  // Add one last header item, but not if it has already been added.
  // $errors_to = FALSE;
  // foreach ($mailer->CustomHeader as $custom_header) {
  //   if ($custom_header[0] = '') {
  //     $errors_to = TRUE;
  //   }
  // }
  // if ($errors_to) {
  //   $mailer->AddCustomHeader('Errors-To: '. $from);
  // }.
  // Add the message's subject.
  $mailer->Subject = $subject;

  // Processes the message's body.
  switch ($content_type) {
    case 'multipart/related':
      $mailer->Body = $body;

      // TODO: Figure out if there is anything more to handling this type.
      break;
    case 'multipart/alternative':

      // Split the body based on the boundary ID.
      $body_parts = $this
        ->boundarySplit($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.
          $mailer->AltBody = $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.
          $mailer->Body = $body_part;
        }
      }
      break;
    case 'multipart/mixed':

      // Split the body based on the boundary ID.
      $body_parts = $this
        ->boundarySplit($body, $boundary);

      // Determine if there is an HTML part.
      $text_html = FALSE;
      foreach ($body_parts as $body_part) {
        if (strpos($body_part, 'text/html')) {
          $text_html = TRUE;
        }
      }
      foreach ($body_parts as $body_part) {

        // If test/plain within the body part, add it to either
        // $mailer->AltBody or $mailer->Body, depending on whether there is
        // also a text/html part ot not.
        if (strpos($body_part, 'multipart/alternative')) {

          // Get 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 boundary ID.
          $body_parts2 = $this
            ->boundarySplit($body_part, $boundary2);
          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)));

              // Include it as part of the mail object.
              $mailer->AltBody = $body_part2;
              $mailer->ContentType = 'multipart/mixed';
            }
            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') {

                // Include it as part of the mail object.
                $mailer->Body = base64_decode($body_part2);

                // Ensure the whole message is recoded in the base64 format.
                $mailer->Encoding = 'base64';
              }
              else {

                // Include it as part of the mail object.
                $mailer->Body = $body_part2;
              }
              $mailer->ContentType = 'multipart/mixed';
            }
          }
        }
        elseif (strpos($body_part, 'text/plain')) {

          // Clean up the text.
          $body_part = trim($this
            ->removeHeaders(trim($body_part)));
          if ($text_html) {
            $mailer->AltBody = $body_part;
            $mailer
              ->IsHTML(TRUE);
            $mailer->ContentType = 'multipart/mixed';
          }
          else {
            $mailer->Body = $body_part;
            $mailer
              ->IsHTML(FALSE);
            $mailer->ContentType = 'multipart/mixed';
          }
        }
        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.
          $mailer->Body = $body_part;
          $mailer
            ->IsHTML(TRUE);
          $mailer->ContentType = 'multipart/mixed';
        }
        elseif (strpos($body_part, 'Content-Disposition: attachment;') && !isset($message['params']['attachments'])) {
          $file_path = $this
            ->getSubstring($body_part, 'filename=', '"', '"');
          $file_name = $this
            ->getSubstring($body_part, ' name=', '"', '"');
          $file_encoding = $this
            ->getSubstring($body_part, 'Content-Transfer-Encoding', ' ', "\n");
          $file_type = $this
            ->getSubstring($body_part, 'Content-Type', ' ', ';');
          if (file_exists($file_path)) {
            if (!$mailer
              ->AddAttachment($file_path, $file_name, $file_encoding, $file_type)) {
              $this->messenger
                ->addMessage($this
                ->t('Attachment could not be found or accessed.'));
            }
          }
          else {

            // Clean up the text.
            $body_part = trim($this
              ->removeHeaders(trim($body_part)));
            if (mb_strtolower($file_encoding) == 'base64') {
              $attachment = base64_decode($body_part);
            }
            elseif (mb_strtolower($file_encoding) == 'quoted-printable') {
              $attachment = quoted_printable_decode($body_part);
            }
            else {
              $attachment = $body_part;
            }
            $attachment_new_filename = $this->fileSystem
              ->tempnam('temporary://', 'smtp');
            $file_path = file_save_data($attachment, $attachment_new_filename, FileSystemInterface::EXISTS_REPLACE);
            $real_path = $this->fileSystem
              ->realpath($file_path->uri);
            if (!$mailer
              ->AddAttachment($real_path, $file_name)) {
              $this->messenger
                ->addMessage($this
                ->t('Attachment could not be found or accessed.'));
            }
          }
        }
      }
      break;
    default:
      $mailer->Body = $body;
      break;
  }

  // Process mimemail attachments, which are prepared in mimemail_mail().
  if (!empty($message['params']['attachments'])) {
    foreach ($message['params']['attachments'] as $attachment) {
      if (isset($attachment['filecontent'])) {
        $mailer
          ->AddStringAttachment($attachment['filecontent'], $attachment['filename'], 'base64', $attachment['filemime']);
      }
      if (isset($attachment['filepath'])) {
        $filename = isset($attachment['filename']) ? $attachment['filename'] : basename($attachment['filepath']);
        $filemime = isset($attachment['filemime']) ? $attachment['filemime'] : $this->mimeTypeGuesser
          ->guess($attachment['filepath']);
        $mailer
          ->AddAttachment($attachment['filepath'], $filename, 'base64', $filemime);
      }
    }
  }

  // Set the authentication settings.
  $username = $this->smtpConfig
    ->get('smtp_username');
  $password = $this->smtpConfig
    ->get('smtp_password');

  // If username and password are given, use SMTP authentication.
  if ($username != '' && $password != '') {
    $mailer->SMTPAuth = TRUE;
    $mailer->Username = $username;
    $mailer->Password = $password;
  }

  // Set the protocol prefix for the smtp host.
  switch ($this->smtpConfig
    ->get('smtp_protocol')) {
    case 'ssl':
      $mailer->SMTPSecure = 'ssl';
      break;
    case 'tls':
      $mailer->SMTPSecure = 'tls';
      break;
    default:
      $mailer->SMTPSecure = '';
  }
  $mailer->SMTPAutoTLS = $this->smtpConfig
    ->get('smtp_autotls');

  // Set other connection settings.
  $mailer->Host = $this->smtpConfig
    ->get('smtp_host') . ';' . $this->smtpConfig
    ->get('smtp_hostbackup');
  $mailer->Port = $this->smtpConfig
    ->get('smtp_port');
  $mailer->Mailer = 'smtp';
  $mailerArr = [
    'mailer' => $mailer,
    'to' => $to,
    'from' => $from,
  ];
  if ($this->smtpConfig
    ->get('smtp_queue')) {
    $logger
      ->info($this
      ->t('Queue sending mail to: @to', [
      '@to' => $to,
    ]));
    smtp_send_queue($mailerArr);
  }
  else {
    return $this
      ->smtpMailerSend($mailerArr);
  }
  return TRUE;
}