Skip to content
Snippets Groups Projects
SMTPMailSystem.php 24.2 KiB
Newer Older
bcweaver's avatar
bcweaver committed
<?php

namespace Drupal\smtp\Plugin\Mail;

use Drupal\Component\Utility\Unicode;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Mail\MailFormatHelper;
use Drupal\Core\Mail\MailInterface;
use Drupal\Core\Messenger\Messenger;
bcweaver's avatar
bcweaver committed
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Egulias\EmailValidator\EmailValidator;
bcweaver's avatar
bcweaver committed
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\smtp\PHPMailer\PHPMailer;
bcweaver's avatar
bcweaver committed

/**
 * Modify the drupal mail system to use smtp when sending emails.
 *
 * Include the option to choose between plain text or HTML.
bcweaver's avatar
bcweaver committed
 *
 * @Mail(
 *   id = "SMTPMailSystem",
 *   label = @Translation("SMTP Mailer"),
 *   description = @Translation("Sends the message, using SMTP.")
 * )
 */
class SMTPMailSystem implements MailInterface, ContainerFactoryPluginInterface {
  protected $AllowHtml;
  protected $smtpConfig;

  /**
   * Logger.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
bcweaver's avatar
bcweaver committed
   */
  protected $logger;

  /**
   * Messenger.
   *
   * @var \Drupal\Core\Messenger\Messenger
   */
  protected $messenger;

  /**
   * Email validator.
   *
   * @var Egulias\EmailValidator\EmailValidator
   */
  protected $emailValidator;

bcweaver's avatar
bcweaver committed
  /**
   * Constructs a SMPTMailSystem object.
bcweaver's avatar
bcweaver committed
   * @param array $configuration
   *   The configuration array.
   * @param string $plugin_id
   *   The plug-in ID.
   * @param mixed $plugin_definition
   *   The plug-in definition.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger
   *   The logger object.
   * @param \Drupal\Core\Messenger\Messenger $messenger
   *   The messenger object.
   * @param \Egulias\EmailValidator\EmailValidator $emailValidator
   *   The messenger object.
bcweaver's avatar
bcweaver committed
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerChannelFactoryInterface $logger, Messenger $messenger, EmailValidator $emailValidator) {
bcweaver's avatar
bcweaver committed
    $this->smtpConfig = \Drupal::config('smtp.settings');
    $this->logger = $logger;
    $this->messenger = $messenger;
    $this->emailValidator = $emailValidator;
bcweaver's avatar
bcweaver committed
  }

  /**
   * Creates an instance of the plugin.
   *
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The container to pull out services used in the plugin.
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin ID for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   *
   * @return static
   *   Returns an instance of this plugin.
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('logger.factory'),
      $container->get('messenger'),
      $container->get('email.validator')
    );
bcweaver's avatar
bcweaver committed
  }

  /**
   * Concatenate and wrap the e-mail body for either plain-text or HTML emails.
bcweaver's avatar
bcweaver committed
   *
   * @param array $message
bcweaver's avatar
bcweaver committed
   *   A message array, as described in hook_mail_alter().
   *
bcweaver's avatar
bcweaver committed
   *   The formatted $message.
   */
  public function format(array $message) {
    $this->AllowHtml = $this->smtpConfig->get('smtp_allowhtml');
bcweaver's avatar
bcweaver committed
    // Join the body array into one string.
    $message['body'] = implode("\n\n", $message['body']);
    if ($this->AllowHtml == 0) {
      // Convert any HTML to plain-text.
      $message['body'] = MailFormatHelper::htmlToText($message['body']);
      // Wrap the mail body for sending.
      $message['body'] = MailFormatHelper::wrapMail($message['body']);
    }
bcweaver's avatar
bcweaver committed
    return $message;
  }

  /**
   * Send the e-mail message.
   *
   * @param array $message
bcweaver's avatar
bcweaver committed
   *   A message array, as described in hook_mail_alter().
bcweaver's avatar
bcweaver committed
   *   TRUE if the mail was successfully accepted, otherwise FALSE.
bcweaver's avatar
bcweaver committed
   */
  public function mail(array $message) {
    $to = $message['to'];
    $from = $message['from'];
    $body = $message['body'];
    $headers = $message['headers'];
    $subject = $message['subject'];

    // Create a new PHPMailer object - autoloaded from registry.
    $mailer = new PHPMailer();
    $mailer->Timeout = $this->smtpConfig->get('smtp_timeout');
bcweaver's avatar
bcweaver committed

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

    // Set the from name.
    $from_name = $this->smtpConfig->get('smtp_fromname');
    if (empty($from_name)) {
      // If value is not defined in settings, use site_name.
      $from_name = \Drupal::config('system.site')->get('name');
    }

    // Set SMTP module email from.
    if ($this->emailValidator->isValid($this->smtpConfig->get('smtp_from'))) {
      $from = $this->smtpConfig->get('smtp_from');
      $headers['Sender'] = $from;
      $headers['Return-Path'] = $from;
      $headers['Reply-To'] = $from;
bcweaver's avatar
bcweaver committed
    }

    // Defines the From value to what we expect.
    $mailer->From = $from;
    $mailer->FromName = Unicode::mimeHeaderEncode($from_name);
bcweaver's avatar
bcweaver committed
    $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);
bcweaver's avatar
bcweaver committed
      $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 (strtolower($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.
bcweaver's avatar
bcweaver committed
            // from to headers.
            $from = $value;
            $mailer->From = $value;

bcweaver's avatar
bcweaver committed
            $mailer->FromName = '';
            $mailer->Sender = $value;
bcweaver's avatar
bcweaver committed
          }
          break;
bcweaver's avatar
bcweaver committed
        case 'content-type':
          // Parse several values on the Content-type header,
          // storing them in an array like.
          // key=value -> $vars['key']='value'.
bcweaver's avatar
bcweaver committed
          $vars = explode(';', $value);
bcweaver's avatar
bcweaver committed
          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).
bcweaver's avatar
bcweaver committed
          $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] : '';
bcweaver's avatar
bcweaver committed

          switch ($vars[0]) {
            case 'text/plain':
              // The message includes only a plain text part.
              $mailer->IsHTML(FALSE);
              $content_type = 'text/plain';
              break;
bcweaver's avatar
bcweaver committed
            case 'text/html':
              // The message includes only an HTML part.
              $mailer->IsHTML(TRUE);
              $content_type = 'text/html';
              break;
bcweaver's avatar
bcweaver committed
            case 'multipart/related':
              // Get the boundary ID from the Content-Type header.
              $boundary = $this->getSubstring($value, 'boundary', '"', '"');
bcweaver's avatar
bcweaver committed

              // The message includes an HTML part w/inline attachments.
              $mailer->ContentType = $content_type = 'multipart/related; boundary="' . $boundary . '"';
bcweaver's avatar
bcweaver committed
            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;

bcweaver's avatar
bcweaver committed
            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;

bcweaver's avatar
bcweaver committed
            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');
              $this->logger->error(t('The %header of your message is not supported by PHPMailer and will be sent as text/plain instead.', ['%header' => "Content-Type: $value"]));
bcweaver's avatar
bcweaver committed

              // 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']);
bcweaver's avatar
bcweaver committed
          }
          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':
          $ccrecipients = explode(',', $value);
          foreach ($ccrecipients as $ccrecipient) {
            $cc_comp = $this->getComponents($ccrecipient);
            $mailer->AddCC($cc_comp['email'], $cc_comp['name']);
bcweaver's avatar
bcweaver committed
          }
          break;

        case 'bcc':
          $bccrecipients = explode(',', $value);
          foreach ($bccrecipients as $bccrecipient) {
            $bcc_comp = $this->_get_components($bccrecipient);
            $mailer->AddBCC($bcc_comp['email'], Unicode::mimeHeaderEncode($bcc_comp['name']));
bcweaver's avatar
bcweaver committed
          }
          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);
    // }
    //
bcweaver's avatar
bcweaver committed
    // Add the message's subject.
    $mailer->Subject = Unicode::mimeHeaderEncode($subject);
bcweaver's avatar
bcweaver committed

    // 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);
bcweaver's avatar
bcweaver committed
        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)));
bcweaver's avatar
bcweaver committed
            // Include it as part of the mail object.
            $mailer->AltBody = $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)));
bcweaver's avatar
bcweaver committed
            // 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);
bcweaver's avatar
bcweaver committed

        // Determine if there is an HTML part.
        $text_html = FALSE;
bcweaver's avatar
bcweaver committed
        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')) {
bcweaver's avatar
bcweaver committed
            // Get boundary ID from the Content-Type header.
            $boundary2 = $this->getSubstring($body_part, 'boundary', '"', '"');
bcweaver's avatar
bcweaver committed
            // Clean up the text.
            $body_part = trim($this->removeHeaders(trim($body_part)));
bcweaver's avatar
bcweaver committed
            // Split the body based on the boundary ID.
            $body_parts2 = $this->boundarySplit($body_part, $boundary2);
bcweaver's avatar
bcweaver committed

            foreach ($body_parts2 as $body_part2) {
bcweaver's avatar
bcweaver committed
              // 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)));
bcweaver's avatar
bcweaver committed
                // Include it as part of the mail object.
                $mailer->AltBody = $body_part2;
                $mailer->ContentType = 'multipart/mixed';
              }
              // If plain/html within the body part, add it to $mailer->Body.
              elseif (strpos($body_part2, 'text/html')) {
                // Get the encoding.
                $body_part2_encoding = $this->getSubstring($body_part2, 'Content-Transfer-Encoding', ' ', "\n");
bcweaver's avatar
bcweaver committed
                // Clean up the text.
                $body_part2 = trim($this->removeHeaders(trim($body_part2)));
bcweaver's avatar
bcweaver committed
                // Check whether the encoding is base64, and if so, decode it.
                if (Unicode::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';
              }
            }
          }
bcweaver's avatar
bcweaver committed
          // If text/plain within the body part, add it to $mailer->Body.
          elseif (strpos($body_part, 'text/plain')) {
            // Clean up the text.
            $body_part = trim($this->removeHeaders(trim($body_part)));
bcweaver's avatar
bcweaver committed

            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';
            }
          }
          // 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)));
bcweaver's avatar
bcweaver committed
            // Include it as part of the mail object.
            $mailer->Body = $body_part;
            $mailer->IsHTML(TRUE);
            $mailer->ContentType = 'multipart/mixed';
          }
          // Add the attachment.
          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', ' ', ';');
bcweaver's avatar
bcweaver committed

            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.'));
bcweaver's avatar
bcweaver committed
              }
            }
            else {
              // Clean up the text.
              $body_part = trim($this->removeHeaders(trim($body_part)));
bcweaver's avatar
bcweaver committed

              if (Unicode::strtolower($file_encoding) == 'base64') {
                $attachment = base64_decode($body_part);
              }
              elseif (Unicode::strtolower($file_encoding) == 'quoted-printable') {
                $attachment = quoted_printable_decode($body_part);
              }
              else {
                $attachment = $body_part;
              }

              $attachment_new_filename = \Drupal::service('file_system')->tempnam('temporary://', 'smtp');
              $file_path = file_save_data($attachment, $attachment_new_filename, FILE_EXISTS_REPLACE);
              $real_path = \Drupal::service('file_system')->realpath($file_path->uri);

              if (!$mailer->AddAttachment($real_path, $file_name)) {
                $this->messenger->addMessage($this->t('Attachment could not be found or accessed.'));
bcweaver's avatar
bcweaver committed
              }
            }
          }
        }
        break;

      default:
        $mailer->Body = $body;
        break;
    }

    // Process mimemail attachments, which are prepared in mimemail_mail().
    if (isset($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'] : file_get_mimetype($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 = '';
    }

    // 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';

bcweaver's avatar
bcweaver committed
      'mailer' => $mailer,
      'to' => $to,
      'from' => $from,
bcweaver's avatar
bcweaver committed
    if ($this->smtpConfig->get('smtp_queue')) {
      $this->logger->info(t('Queue sending mail to: @to', ['@to' => $to]));
bcweaver's avatar
bcweaver committed
      smtp_send_queue($mailerArr);
    }
    else {
      return _smtp_mailer_send($mailerArr);
    }

    return TRUE;
  }

  /**
   * 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
bcweaver's avatar
bcweaver committed
   *   A string containing the body text to parse.
   * @param string $boundary
bcweaver's avatar
bcweaver committed
   *   A string with the boundary string to parse on.
bcweaver's avatar
bcweaver committed
   *   An array containing the resulting mime parts
   */
  protected function boundarySplit($input, $boundary) {
bcweaver's avatar
bcweaver committed
    $bs_possible = 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;
bcweaver's avatar
bcweaver committed

  /**
   * Strips the headers from the body part.
   *
   * @param string $input
bcweaver's avatar
bcweaver committed
   *   A string containing the body part to strip.
bcweaver's avatar
bcweaver committed
   *   A string with the stripped body part.
   */
  protected function removeHeaders($input) {
bcweaver's avatar
bcweaver committed
    $part_array = explode("\n", $input);

    // Will strip these headers according to RFC2045.
    $headers_to_strip = [
      'Content-Type',
      'Content-Transfer-Encoding',
      'Content-ID',
      'Content-Disposition',
    ];
bcweaver's avatar
bcweaver committed
    $pattern = '/^(' . implode('|', $headers_to_strip) . '):/';

    while (count($part_array) > 0) {

      // Ignore trailing spaces/newlines.
bcweaver's avatar
bcweaver committed
      $line = rtrim($part_array[0]);

      // If the line starts with a known header string.
bcweaver's avatar
bcweaver committed
      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.
bcweaver's avatar
bcweaver committed
        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.
bcweaver's avatar
bcweaver committed
        break;
      }
    }

    $output = implode("\n", $part_array);
    return $output;
bcweaver's avatar
bcweaver committed

  /**
   * 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.
   *
   * @param string $source
bcweaver's avatar
bcweaver committed
   *   A string containing the text to look through.
   * @param string $target
bcweaver's avatar
bcweaver committed
   *   A string containing the text in $source to start looking from.
   * @param string $beginning_character
bcweaver's avatar
bcweaver committed
   *   A string containing the character just before the sought after text.
   * @param string $ending_character
bcweaver's avatar
bcweaver committed
   *   A string containing the character just after the sought after text.
bcweaver's avatar
bcweaver committed
   *   A string with the text found between the $beginning_character and the
   *   $ending_character.
   */
  protected function getSubstring($source, $target, $beginning_character, $ending_character) {
bcweaver's avatar
bcweaver committed
    $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        = substr($source, $first_character, $second_character - $first_character);
    $string_length    = strlen($substring) - 1;

    if ($substring[$string_length] == $ending_character) {
      $substring = substr($substring, 0, $string_length);
    }

    return $substring;
bcweaver's avatar
bcweaver committed

  /**
   * Returns an array of name and email address from a string.
   *
   * @param string $input
   *   A string that contains different possible combinations of names and
   *   email address.
   *
   * @return array
   *   An array containing a name and an email address.
bcweaver's avatar
bcweaver committed
   */
  protected function getComponents($input) {
    $input = trim($input);
bcweaver's avatar
bcweaver committed
      'input' => $input,
      'name' => '',
      'email' => '',
bcweaver's avatar
bcweaver committed

    // If the input is a valid email address in its entirety,
    // then there is nothing to do, just return that.
    if ($this->emailValidator->isValid($input)) {
      $components['email'] = $input;
bcweaver's avatar
bcweaver committed
      return $components;
    }

    // Check if $input has one of the following formats, extract what we can:
    // some name <address@example.com>.
    // "another name" <address@example.com>.
    // <address@example.com>.
    if (preg_match('/^"?([^"\t\n]*)"?\s*<([^>\t\n]*)>$/', $input, $matches)) {
bcweaver's avatar
bcweaver committed
      $components['name'] = trim($matches[1]);
      $components['email'] = trim($matches[2]);
    }

    return $components;
  }
bcweaver's avatar
bcweaver committed
}