You are here

EmailWebformHandler.php in Webform 6.x

Same filename and directory in other branches
  1. 8.5 src/Plugin/WebformHandler/EmailWebformHandler.php

File

src/Plugin/WebformHandler/EmailWebformHandler.php
View source
<?php

namespace Drupal\webform\Plugin\WebformHandler;

use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Utility\Mail;
use Drupal\Component\Utility\Html;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Markup;
use Drupal\Core\Url;
use Drupal\webform\Element\WebformAjaxElementTrait;
use Drupal\webform\Element\WebformMessage;
use Drupal\webform\Element\WebformSelectOther;
use Drupal\webform\Plugin\WebformElement\WebformCompositeBase;
use Drupal\webform\Plugin\WebformHandlerBase;
use Drupal\webform\Plugin\WebformHandlerMessageInterface;
use Drupal\webform\Twig\WebformTwigExtension;
use Drupal\webform\Utility\WebformElementHelper;
use Drupal\webform\Utility\WebformOptionsHelper;
use Drupal\webform\WebformSubmissionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Emails a webform submission.
 *
 * @WebformHandler(
 *   id = "email",
 *   label = @Translation("Email"),
 *   category = @Translation("Notification"),
 *   description = @Translation("Sends a webform submission via an email."),
 *   cardinality = \Drupal\webform\Plugin\WebformHandlerInterface::CARDINALITY_UNLIMITED,
 *   results = \Drupal\webform\Plugin\WebformHandlerInterface::RESULTS_PROCESSED,
 *   submission = \Drupal\webform\Plugin\WebformHandlerInterface::SUBMISSION_OPTIONAL,
 *   tokens = TRUE,
 * )
 */
class EmailWebformHandler extends WebformHandlerBase implements WebformHandlerMessageInterface {
  use WebformAjaxElementTrait;

  /**
   * Other option value.
   */
  const OTHER_OPTION = '_other_';

  /**
   * Default option value.
   */
  const EMPTY_OPTION = '_empty_';

  /**
   * Default option value.
   *
   * @see \Drupal\webform\Plugin\WebformHandler\EmailWebformHandler::getMessageEmails
   */
  const DEFAULT_OPTION = '_default_';

  /**
   * Default value. (This is used by the handler's settings.)
   */
  const DEFAULT_VALUE = '_default';

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

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

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

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

  /**
   * A mail manager for sending email.
   *
   * @var \Drupal\Core\Mail\MailManagerInterface
   */
  protected $mailManager;

  /**
   * The webform token manager.
   *
   * @var \Drupal\webform\WebformTokenManagerInterface
   */
  protected $tokenManager;

  /**
   * The webform theme manager.
   *
   * @var \Drupal\webform\WebformThemeManagerInterface
   */
  protected $themeManager;

  /**
   * The webform element plugin manager.
   *
   * @var \Drupal\webform\Plugin\WebformElementManagerInterface
   */
  protected $elementManager;

  /**
   * Cache of default configuration values.
   *
   * @var array
   */
  protected $defaultValues;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $instance->currentUser = $container
      ->get('current_user');
    $instance->moduleHandler = $container
      ->get('module_handler');
    $instance->languageManager = $container
      ->get('language_manager');
    $instance->mailManager = $container
      ->get('plugin.manager.mail');
    $instance->themeManager = $container
      ->get('webform.theme_manager');
    $instance->tokenManager = $container
      ->get('webform.token_manager');
    $instance->elementManager = $container
      ->get('plugin.manager.webform.element');
    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  public function getSummary() {
    $settings = $this
      ->getEmailConfiguration();

    // Simplify the [webform_submission:values:.*] tokens.
    array_walk($settings, function (&$value, $key) {
      if (is_string($value)) {
        $value = preg_replace('/\\[webform:([^:]+)\\]/', '[\\1]', $value);
        $value = preg_replace('/\\[webform_role:([^:]+)\\]/', '[\\1]', $value);
        $value = preg_replace('/\\[webform_access:type:([^:]+)\\]/', '[\\1]', $value);
        $value = preg_replace('/\\[webform_group:role:([^:]+)\\]/', '[group:\\1]', $value);
        $value = preg_replace('/\\[webform_group:owner:mail\\]/', '[group:owner]', $value);
        $value = preg_replace('/\\[webform_submission:(?:node|source_entity|values):([^]]+)\\]/', '[\\1]', $value);
        $value = preg_replace('/\\[webform_submission:([^]]+)\\]/', '[\\1]', $value);
        $value = preg_replace('/(:raw|:value)(:html)?\\]/', ']', $value);
      }
    });

    // Set state.
    $states = [
      WebformSubmissionInterface::STATE_DRAFT_CREATED => $this
        ->t('Draft created'),
      WebformSubmissionInterface::STATE_DRAFT_UPDATED => $this
        ->t('Draft updated'),
      WebformSubmissionInterface::STATE_CONVERTED => $this
        ->t('Converted'),
      WebformSubmissionInterface::STATE_COMPLETED => $this
        ->t('Completed'),
      WebformSubmissionInterface::STATE_UPDATED => $this
        ->t('Updated'),
      WebformSubmissionInterface::STATE_DELETED => $this
        ->t('Deleted'),
    ];
    $settings['states'] = array_intersect_key($states, array_combine($settings['states'], $settings['states']));

    // Set theme name.
    if ($settings['theme_name']) {
      $settings['theme_name'] = $this->themeManager
        ->getThemeName($settings['theme_name']);
    }
    return [
      '#settings' => $settings,
    ] + parent::getSummary();
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [
      'states' => [
        WebformSubmissionInterface::STATE_COMPLETED,
      ],
      'to_mail' => static::DEFAULT_VALUE,
      'to_options' => [],
      'cc_mail' => '',
      'cc_options' => [],
      'bcc_mail' => '',
      'bcc_options' => [],
      'from_mail' => static::DEFAULT_VALUE,
      'from_options' => [],
      'from_name' => static::DEFAULT_VALUE,
      'subject' => static::DEFAULT_VALUE,
      'body' => static::DEFAULT_VALUE,
      'excluded_elements' => [],
      'ignore_access' => FALSE,
      'exclude_empty' => TRUE,
      'exclude_empty_checkbox' => FALSE,
      'exclude_attachments' => FALSE,
      'html' => TRUE,
      'attachments' => FALSE,
      'twig' => FALSE,
      'debug' => FALSE,
      'reply_to' => '',
      'return_path' => '',
      'sender_mail' => '',
      'sender_name' => '',
      'theme_name' => '',
      'parameters' => [],
    ];
  }

  /**
   * Get configuration default values.
   *
   * @return array
   *   Configuration default values.
   */
  protected function getDefaultConfigurationValues() {
    if (isset($this->defaultValues)) {
      return $this->defaultValues;
    }
    $webform_settings = $this->configFactory
      ->get('webform.settings');
    $site_settings = $this->configFactory
      ->get('system.site');
    $body_format = $this->configuration['html'] ? 'html' : 'text';
    $default_to_mail = $webform_settings
      ->get('mail.default_to_mail') ?: $site_settings
      ->get('mail') ?: ini_get('sendmail_from');
    $default_from_mail = $webform_settings
      ->get('mail.default_from_mail') ?: $site_settings
      ->get('mail') ?: ini_get('sendmail_from');
    $this->defaultValues = [
      'states' => [
        WebformSubmissionInterface::STATE_COMPLETED,
      ],
      'to_mail' => $default_to_mail,
      'to_options' => [],
      'cc_mail' => $default_to_mail,
      'cc_options' => [],
      'bcc_mail' => $default_to_mail,
      'bcc_options' => [],
      'from_mail' => $default_from_mail,
      'from_options' => [],
      'from_name' => $webform_settings
        ->get('mail.default_from_name') ?: $site_settings
        ->get('name'),
      'subject' => $webform_settings
        ->get('mail.default_subject') ?: 'Webform submission from: [webform_submission:source-entity]',
      'body' => $this
        ->getBodyDefaultValues($body_format),
      'reply_to' => $webform_settings
        ->get('mail.default_reply_to') ?: '',
      'return_path' => $webform_settings
        ->get('mail.default_return_path') ?: '',
      'sender_mail' => $webform_settings
        ->get('mail.default_sender_mail') ?: '',
      'sender_name' => $webform_settings
        ->get('mail.default_sender_name') ?: '',
      'theme_name' => '',
    ];
    return $this->defaultValues;
  }

  /**
   * Get configuration default value.
   *
   * @param string $name
   *   Configuration name.
   *
   * @return string|array
   *   Configuration default value.
   */
  protected function getDefaultConfigurationValue($name) {
    $default_values = $this
      ->getDefaultConfigurationValues();
    return $default_values[$name];
  }

  /**
   * Get mail configuration values.
   *
   * @return array
   *   An associative array containing email configuration values,
   *   along with the default configuration values.
   */
  public function getEmailConfiguration() {
    $settings = $this
      ->getSettings();
    $email = [];
    foreach ($settings as $key => $value) {
      $email[$key] = $value === static::DEFAULT_VALUE ? $this
        ->getDefaultConfigurationValue($key) : $value;
    }
    return $email;
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $this
      ->applyFormStateToConfiguration($form_state);

    // Get options, mail, and text elements as options (text/value).
    $text_element_options_value = [];
    $text_element_options_raw = [];
    $name_element_options = [];
    $mail_element_options = [];
    $options_element_options = [];
    $elements = $this->webform
      ->getElementsInitializedAndFlattened();
    foreach ($elements as $element_key => $element) {
      $element_plugin = $this->elementManager
        ->getElementInstance($element);
      if (!$element_plugin
        ->isInput($element) || !isset($element['#type'])) {
        continue;
      }

      // Set title.
      $element_title = isset($element['#title']) ? new FormattableMarkup('@title (@key)', [
        '@title' => $element['#title'],
        '@key' => $element_key,
      ]) : $element_key;

      // Add options element token, which can include multiple values.
      if (isset($element['#options'])) {
        $options_element_options["[webform_submission:values:{$element_key}:raw]"] = $element_title;
      }

      // Multiple value elements can NOT be used as a tokens.
      if ($element_plugin
        ->hasMultipleValues($element)) {
        continue;
      }
      if (!$element_plugin
        ->isComposite()) {

        // Add text element value and raw tokens.
        $text_element_options_value["[webform_submission:values:{$element_key}:value]"] = $element_title;
        $text_element_options_raw["[webform_submission:values:{$element_key}:raw]"] = $element_title;

        // Add name element token.
        $name_element_options["[webform_submission:values:{$element_key}:raw]"] = $element_title;

        // Add mail element token.
        if (in_array($element['#type'], [
          'email',
          'hidden',
          'value',
          'textfield',
          'webform_email_multiple',
          'webform_email_confirm',
        ])) {
          $mail_element_options["[webform_submission:values:{$element_key}:raw]"] = $element_title;
        }
      }

      // Element type specific tokens.
      switch ($element['#type']) {
        case 'webform_name':

          // Allow 'webform_name' composite to be used a value token.
          $name_element_options["[webform_submission:values:{$element_key}:value]"] = $element_title;
          break;
        case 'text_format':

          // Allow 'text_format' composite to be used a value token.
          $text_element_options_value["[webform_submission:values:{$element_key}]"] = $element_title;
          break;
      }

      // Handle composite sub elements.
      if ($element_plugin instanceof WebformCompositeBase) {
        $composite_elements = $element_plugin
          ->getCompositeElements();
        foreach ($composite_elements as $composite_key => $composite_element) {
          $composite_element_plugin = $this->elementManager
            ->getElementInstance($element);
          if (!$composite_element_plugin
            ->isInput($element) || !isset($composite_element['#type'])) {
            continue;
          }

          // Set composite title.
          if (isset($element['#title'])) {
            $f_args = [
              '@title' => $element['#title'],
              '@composite_title' => $composite_element['#title'],
              '@key' => $element_key,
              '@composite_key' => $composite_key,
            ];
            $composite_title = new FormattableMarkup('@title: @composite_title (@key: @composite_key)', $f_args);
          }
          else {
            $composite_title = "{$element_key}:{$composite_key}";
          }

          // Add name element token. Only applies to basic (not composite) elements.
          $name_element_options["[webform_submission:values:{$element_key}:{$composite_key}:raw]"] = $composite_title;

          // Add mail element token.
          if (in_array($composite_element['#type'], [
            'email',
            'webform_email_multiple',
            'webform_email_confirm',
          ])) {
            $mail_element_options["[webform_submission:values:{$element_key}:{$composite_key}:raw]"] = $composite_title;
          }
        }
      }
    }

    // Get roles.
    $roles_element_options = [];
    if ($roles = $this->configFactory
      ->get('webform.settings')
      ->get('mail.roles')) {
      $role_names = array_map('\\Drupal\\Component\\Utility\\Html::escape', user_role_names(TRUE));
      if (!in_array('authenticated', $roles)) {
        $role_names = array_intersect_key($role_names, array_combine($roles, $roles));
      }
      foreach ($role_names as $role_name => $role_label) {
        $roles_element_options["[webform_role:{$role_name}]"] = new FormattableMarkup('@title (@key)', [
          '@title' => $role_label,
          '@key' => $role_name,
        ]);
      }
    }

    // Get email and name other.
    $other_element_email_options = [
      '[site:mail]' => 'Site email address',
      '[current-user:mail]' => 'Current user email address [Authenticated only]',
      '[webform:author:mail]' => 'Webform author email address',
      '[webform_submission:user:mail]' => 'Webform submission owner email address [Authenticated only]',
    ];
    $other_element_name_options = [
      '[site:name]' => 'Site name',
      '[current-user:display-name]' => 'Current user display name',
      '[current-user:account-name]' => 'Current user account name',
      '[webform:author:display-name]' => 'Webform author display name',
      '[webform:author:account-name]' => 'Webform author account name',
      '[webform_submission:author:display-name]' => 'Webform submission author display name',
      '[webform_submission:author:account-name]' => 'Webform submission author account name',
    ];

    // Disable client-side HTML5 validation which is having issues with hidden
    // element validation.
    // @see http://stackoverflow.com/questions/22148080/an-invalid-form-control-with-name-is-not-focusable
    $form['#attributes']['novalidate'] = 'novalidate';

    // To.
    $form['to'] = [
      '#type' => 'details',
      '#title' => $this
        ->t('Send to'),
      '#help' => $this
        ->t('This is the "To:" email header which will be the person(s) responsible for receiving this webform.'),
      '#open' => TRUE,
    ];
    $form['to']['to_mail'] = $this
      ->buildElement('to_mail', $this
      ->t('To email'), $this
      ->t('To email address'), TRUE, $mail_element_options, $options_element_options, $roles_element_options, $other_element_email_options);
    $form['to']['cc_mail'] = $this
      ->buildElement('cc_mail', $this
      ->t('CC email'), $this
      ->t('CC email address'), FALSE, $mail_element_options, $options_element_options, $roles_element_options, $other_element_email_options);
    $form['to']['bcc_mail'] = $this
      ->buildElement('bcc_mail', $this
      ->t('BCC email'), $this
      ->t('BCC email address'), FALSE, $mail_element_options, $options_element_options, $roles_element_options, $other_element_email_options);
    $token_types = [
      'webform',
      'webform_submission',
    ];

    // Show webform role tokens if they have been specified.
    if (!empty($roles_element_options)) {
      $token_types[] = 'webform_role';
    }
    if ($this->moduleHandler
      ->moduleExists('webform_access')) {
      $token_types[] = 'webform_access';
    }
    if ($this->moduleHandler
      ->moduleExists('webform_group')) {
      $token_types[] = 'webform_group';
    }
    $form['to']['token_tree_link'] = $this
      ->buildTokenTreeElement($token_types);
    if (empty($roles_element_options) && $this->currentUser
      ->hasPermission('administer webform')) {
      $route_name = 'webform.config.handlers';
      $route_destination = Url::fromRoute('entity.webform.handlers', [
        'webform' => $this
          ->getWebform()
          ->id(),
      ])
        ->toString();
      $route_options = [
        'query' => [
          'destination' => $route_destination,
        ],
      ];
      $t_args = [
        ':href' => Url::fromRoute($route_name, [], $route_options)
          ->toString(),
      ];
      $form['to']['roles_message'] = [
        '#type' => 'webform_message',
        '#message_type' => 'warning',
        '#message_message' => $this
          ->t('Please note: You can select which <strong>user roles</strong> are available to receive webform emails by going to the Webform module\'s <a href=":href">admin settings</a> form.', $t_args),
        '#message_close' => TRUE,
        '#message_id' => 'webform_email_roles_message',
        '#message_storage' => WebformMessage::STORAGE_USER,
      ];
    }

    // From.
    $form['from'] = [
      '#type' => 'details',
      '#title' => $this
        ->t('Send from (website/domain)'),
      '#help' => $this
        ->t('This is the "From:" email header which should come from <em>you</em>.  It should be your brand, company, organization, or website entity.'),
      '#open' => TRUE,
    ];
    $form['from']['from_mail'] = $this
      ->buildElement('from_mail', $this
      ->t('From email'), $this
      ->t('From email address'), TRUE, $mail_element_options, $options_element_options, NULL, $other_element_email_options);
    $form['from']['from_name'] = $this
      ->buildElement('from_name', $this
      ->t('From name'), $this
      ->t('From name'), FALSE, $name_element_options, NULL, NULL, $other_element_name_options);
    $form['from']['token_tree_link'] = $this
      ->buildTokenTreeElement();

    // 'From name' is not used if it contains multiple email addresses.
    $form['from']['from_name']['from_name']['#states'] = [
      '!visible' => [
        ':input[name="settings[from_mail][other]"]' => [
          'value' => [
            'pattern' => ',',
          ],
        ],
      ],
    ];

    // Settings: Reply-to.
    $form['reply_to'] = [
      '#type' => 'details',
      '#title' => $this
        ->t('Reply to (individual/organization)'),
      '#help' => $this
        ->t('The "Reply-To:" email header is used for replying to the email that is received.  For example, if you collect a customers email, you would want to reply-to them. If you collect a lead generation form and want to reply to the coordinator, you would reply-to them.'),
      '#open' => TRUE,
    ];
    $form['reply_to']['reply_to'] = $this
      ->buildElement('reply_to', $this
      ->t('Reply-to email'), $this
      ->t('Reply-to email address'), FALSE, $mail_element_options, NULL, NULL, $other_element_email_options);
    $form['reply_to']['token_tree_link'] = $this
      ->buildTokenTreeElement($token_types);

    // Message.
    $form['message'] = [
      '#type' => 'details',
      '#title' => $this
        ->t('Message'),
      '#open' => TRUE,
    ];
    $form['message'] += $this
      ->buildElement('subject', $this
      ->t('Subject'), $this
      ->t('subject'), FALSE, $text_element_options_raw);
    $has_edit_twig_access = WebformTwigExtension::hasEditTwigAccess() || $this->configuration['twig'];

    // Message: Body.
    // Building a custom select other element that toggles between
    // HTML (CKEditor), Plain text (CodeMirror), and Twig (CodeMirror)
    // custom body elements.
    $body_options = [];
    $body_options[WebformSelectOther::OTHER_OPTION] = $this
      ->t('Custom body…');
    if ($has_edit_twig_access) {
      $body_options['twig'] = $this
        ->t('Twig template…');
    }
    $body_options[static::DEFAULT_VALUE] = $this
      ->t('Default');
    $body_options[(string) $this
      ->t('Elements')] = $text_element_options_value;

    // Get default format.
    $body_default_format = $this->configuration['html'] ? 'html' : 'text';

    // Get default values.
    $body_default_values = $this
      ->getBodyDefaultValues();

    // Get custom default values which are the same as default values.
    $body_custom_default_values = $this
      ->getBodyDefaultValues();

    // Set up default Twig body and convert tokens to use the
    // webform_token() Twig function.
    // @see \Drupal\webform\Twig\WebformTwigExtension
    $twig_default_body = $body_custom_default_values[$body_default_format];
    $twig_default_body = preg_replace('/(\\[[^]]+\\])/', '{{ webform_token(\'\\1\', webform_submission, [], options) }}', $twig_default_body);
    $body_custom_default_values['twig'] = $twig_default_body;

    // Look at the 'body' and determine the body select and custom
    // default values.
    if (WebformOptionsHelper::hasOption($this->configuration['body'], $body_options)) {
      $body_select_default_value = $this->configuration['body'];
    }
    elseif ($this->configuration['twig']) {
      $body_select_default_value = 'twig';
      $body_custom_default_values['twig'] = $this->configuration['body'];
    }
    else {
      $body_select_default_value = WebformSelectOther::OTHER_OPTION;
      $body_custom_default_values[$body_default_format] = $this->configuration['body'];
    }

    // Build body select menu.
    $form['message']['body'] = [
      '#type' => 'select',
      '#title' => $this
        ->t('Body'),
      '#options' => $body_options,
      '#required' => TRUE,
      '#default_value' => $body_select_default_value,
    ];
    foreach ($body_default_values as $format => $default_value) {
      if ($format === 'html') {
        $form['message']['body_custom_' . $format] = [
          '#type' => 'webform_html_editor',
          '#format' => $this->configFactory
            ->get('webform.settings')
            ->get('html_editor.mail_format'),
        ];
      }
      else {
        $form['message']['body_custom_' . $format] = [
          '#type' => 'webform_codemirror',
          '#mode' => $format,
        ];
      }
      $form['message']['body_custom_' . $format] += [
        '#title' => $this
          ->t('Body custom value (@format)', [
          '@format' => $format,
        ]),
        '#title_display' => 'hidden',
        '#default_value' => $body_custom_default_values[$format],
        '#states' => [
          'visible' => [
            ':input[name="settings[body]"]' => [
              'value' => WebformSelectOther::OTHER_OPTION,
            ],
            ':input[name="settings[html]"]' => [
              'checked' => $format === 'html' ? TRUE : FALSE,
            ],
          ],
          'required' => [
            ':input[name="settings[body]"]' => [
              'value' => WebformSelectOther::OTHER_OPTION,
            ],
            ':input[name="settings[html]"]' => [
              'checked' => $format === 'html' ? TRUE : FALSE,
            ],
          ],
        ],
      ];

      // Must set #parents because body_custom_* is not a configuration value.
      // @see \Drupal\webform\Plugin\WebformHandler\EmailWebformHandler::validateConfigurationForm
      $form['message']['body_custom_' . $format]['#parents'] = [
        'settings',
        'body_custom_' . $format,
      ];

      // Default body.
      $form['message']['body_default_' . $format] = [
        '#type' => 'webform_codemirror',
        '#mode' => $format,
        '#title' => $this
          ->t('Body default value (@format)', [
          '@format' => $format,
        ]),
        '#title_display' => 'hidden',
        '#default_value' => $body_default_values[$format],
        '#attributes' => [
          'readonly' => 'readonly',
          'disabled' => 'disabled',
        ],
        '#states' => [
          'visible' => [
            ':input[name="settings[body]"]' => [
              'value' => static::DEFAULT_VALUE,
            ],
            ':input[name="settings[html]"]' => [
              'checked' => $format === 'html' ? TRUE : FALSE,
            ],
          ],
        ],
      ];
    }

    // Twig body with help.
    $form['message']['body_custom_twig'] = [
      '#type' => 'webform_codemirror',
      '#mode' => 'twig',
      '#title' => $this
        ->t('Body custom value (Twig)'),
      '#title_display' => 'hidden',
      '#default_value' => $body_custom_default_values['twig'],
      '#access' => $has_edit_twig_access,
      '#states' => [
        'visible' => [
          ':input[name="settings[body]"]' => [
            'value' => 'twig',
          ],
        ],
        'required' => [
          ':input[name="settings[body]"]' => [
            'value' => 'twig',
          ],
        ],
      ],
      // Must set #parents because body_custom_twig is not a configuration value.
      // @see \Drupal\webform\Plugin\WebformHandler\EmailWebformHandler::validateConfigurationForm
      '#parents' => [
        'settings',
        'body_custom_twig',
      ],
    ];
    $form['message']['body_custom_twig_help'] = WebformTwigExtension::buildTwigHelp() + [
      '#access' => $has_edit_twig_access,
      '#states' => [
        'visible' => [
          ':input[name="settings[body]"]' => [
            'value' => 'twig',
          ],
        ],
      ],
    ];

    // Tokens.
    $form['message']['token_tree_link'] = $this
      ->buildTokenTreeElement();

    // Elements.
    $form['elements'] = [
      '#type' => 'details',
      '#title' => $this
        ->t('Included email values/markup'),
      '#description' => $this
        ->t('The selected elements will be included in the [webform_submission:values] token. Individual values may still be printed if explicitly specified as a [webform_submission:values:?] in the email body template.'),
      '#open' => $this->configuration['excluded_elements'] ? TRUE : FALSE,
    ];
    $form['elements']['excluded_elements'] = [
      '#type' => 'webform_excluded_elements',
      '#exclude_markup' => FALSE,
      '#webform_id' => $this->webform
        ->id(),
      '#default_value' => $this->configuration['excluded_elements'],
    ];
    $form['elements']['ignore_access'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Always include elements with private and restricted access'),
      '#description' => $this
        ->t('If checked, access controls for included element will be ignored.'),
      '#return_value' => TRUE,
      '#default_value' => $this->configuration['ignore_access'],
    ];
    $form['elements']['exclude_empty'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Exclude empty elements'),
      '#description' => $this
        ->t('If checked, empty elements will be excluded from the email values.'),
      '#return_value' => TRUE,
      '#default_value' => $this->configuration['exclude_empty'],
    ];
    $form['elements']['exclude_empty_checkbox'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Exclude unselected checkboxes'),
      '#description' => $this
        ->t('If checked, empty checkboxes will be excluded from the email values.'),
      '#return_value' => TRUE,
      '#default_value' => $this->configuration['exclude_empty_checkbox'],
    ];
    $form['elements']['exclude_attachments'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Exclude file elements with attachments'),
      '#return_value' => TRUE,
      '#description' => $this
        ->t('If checked, file attachments will be excluded from the email values, but the selected element files will still be attached to the email.'),
      '#default_value' => $this->configuration['exclude_attachments'],
      '#access' => $this
        ->getWebform()
        ->hasAttachments(),
      '#disabled' => !$this
        ->supportsAttachments(),
      '#states' => [
        'visible' => [
          ':input[name="settings[attachments]"]' => [
            'checked' => TRUE,
          ],
        ],
      ],
    ];
    $elements = $this->webform
      ->getElementsInitializedFlattenedAndHasValue();
    foreach ($elements as $element) {
      if (!empty($element['#access_view_roles']) || !empty($element['#private'])) {
        $form['elements']['ignore_access_message'] = [
          '#type' => 'webform_message',
          '#message_message' => $this
            ->t('This webform contains private and/or restricted access elements, which will only be included if the user submitting the form has access to these elements.'),
          '#message_type' => 'warning',
          '#states' => [
            'visible' => [
              ':input[name="settings[ignore_access]"]' => [
                'checked' => FALSE,
              ],
            ],
          ],
        ];
        break;
      }
    }

    // Attachments.
    $form['attachments'] = [
      '#type' => 'details',
      '#title' => $this
        ->t('Attachments'),
      '#access' => $this
        ->getWebform()
        ->hasAttachments(),
    ];
    if (!$this
      ->supportsAttachments()) {
      $t_args = [
        ':href_smtp' => 'https://www.drupal.org/project/smtp',
        ':href_mailsystem' => 'https://www.drupal.org/project/mailsystem',
        ':href_swiftmailer' => 'https://www.drupal.org/project/swiftmailer',
      ];
      $form['attachments']['attachments_message'] = [
        '#type' => 'webform_message',
        '#message_message' => $this
          ->t('To send email attachments, please install and configure the <a href=":href_smtp">SMTP Authentication Support</a> module or the <a href=":href_mailsystem">Mail System</a> and <a href=":href_swiftmailer">SwiftMailer</a> module.', $t_args),
        '#message_type' => 'warning',
        '#message_close' => TRUE,
        '#message_storage' => WebformMessage::STORAGE_SESSION,
      ];
    }
    $form['attachments']['attachments'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Include files as attachments'),
      '#description' => $this
        ->t('If checked, only file upload elements selected in the above included email values will be attached to the email.'),
      '#return_value' => TRUE,
      '#disabled' => !$this
        ->supportsAttachments(),
      '#default_value' => $this->configuration['attachments'],
    ];

    // Additional.
    $results_disabled = $this
      ->getWebform()
      ->getSetting('results_disabled');
    $form['additional'] = [
      '#type' => 'fieldset',
      '#title' => $this
        ->t('Additional settings'),
    ];

    // Settings: States.
    $form['additional']['states'] = [
      '#type' => 'checkboxes',
      '#title' => $this
        ->t('Send email'),
      '#options' => [
        WebformSubmissionInterface::STATE_DRAFT_CREATED => $this
          ->t('…when <b>draft is created</b>.'),
        WebformSubmissionInterface::STATE_DRAFT_UPDATED => $this
          ->t('…when <b>draft is updated</b>.'),
        WebformSubmissionInterface::STATE_CONVERTED => $this
          ->t('…when anonymous <b>submission is converted</b> to authenticated.'),
        WebformSubmissionInterface::STATE_COMPLETED => $this
          ->t('…when <b>submission is completed</b>.'),
        WebformSubmissionInterface::STATE_UPDATED => $this
          ->t('…when <b>submission is updated</b>.'),
        WebformSubmissionInterface::STATE_DELETED => $this
          ->t('…when <b>submission is deleted</b>.'),
        WebformSubmissionInterface::STATE_LOCKED => $this
          ->t('…when <b>submission is locked</b>.'),
      ],
      '#access' => $results_disabled ? FALSE : TRUE,
      '#default_value' => $results_disabled ? [
        WebformSubmissionInterface::STATE_COMPLETED,
      ] : $this->configuration['states'],
    ];
    $form['additional']['states_message'] = [
      '#type' => 'webform_message',
      '#message_message' => $this
        ->t("Because no submission state is checked, this email can only be sent using the 'Resend' form and/or custom code."),
      '#message_type' => 'warning',
      '#states' => [
        'visible' => [
          ':input[name^="settings[states]"]' => [
            'checked' => FALSE,
          ],
        ],
      ],
    ];

    // Settings: Return path.
    $form['additional']['return_path'] = $this
      ->buildElement('return_path', $this
      ->t('Return path'), $this
      ->t('Return path email address'), FALSE, $mail_element_options, NULL, NULL, $other_element_email_options);

    // Settings: Sender mail.
    $form['additional']['sender_mail'] = $this
      ->buildElement('sender_mail', $this
      ->t('Sender email'), $this
      ->t('Sender email address'), FALSE, $mail_element_options, $options_element_options, NULL, $other_element_email_options);

    // Settings: Sender name.
    $form['additional']['sender_name'] = $this
      ->buildElement('sender_name', $this
      ->t('Sender name'), $this
      ->t('Sender name'), FALSE, $name_element_options, NULL, NULL, $other_element_name_options);

    // Settings: HTML.
    $form['additional']['html'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Send email as HTML'),
      '#return_value' => TRUE,
      '#access' => $this
        ->supportsHtml(),
      '#default_value' => $this->configuration['html'],
    ];

    // Setting: Themes.
    $form['additional']['theme_name'] = [
      '#type' => 'select',
      '#title' => $this
        ->t('Theme to render this email'),
      '#description' => $this
        ->t('Select the theme that will be used to render this email.'),
      '#options' => $this->themeManager
        ->getThemeNames(),
      '#default_value' => $this->configuration['theme_name'],
    ];
    $form['additional']['parameters'] = [
      '#type' => 'webform_codemirror',
      '#mode' => 'yaml',
      '#title' => $this
        ->t('Custom parameters'),
      '#description' => $this
        ->t('Enter additional custom parameters to be appended to the email message\'s parameters. Custom parameters are used by <a href=":href">email related add-on modules</a>.', [
        ':href' => 'https://www.drupal.org/docs/8/modules/webform/webform-add-ons#mail',
      ]),
      '#default_value' => $this->configuration['parameters'],
    ];

    // Development.
    $form['development'] = [
      '#type' => 'details',
      '#title' => $this
        ->t('Development settings'),
    ];
    $form['development']['debug'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Enable debugging'),
      '#description' => $this
        ->t('If checked, sent emails will be displayed onscreen to all users.'),
      '#return_value' => TRUE,
      '#default_value' => $this->configuration['debug'],
    ];

    // ISSUE: TranslatableMarkup is breaking the #ajax.
    // WORKAROUND: Convert all Render/Markup to strings.
    WebformElementHelper::convertRenderMarkupToStrings($form);
    $this
      ->elementTokenValidate($form, $token_types);
    return $this
      ->setSettingsParents($form);
  }

  /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
    parent::validateConfigurationForm($form, $form_state);
    $values = $form_state
      ->getValues();

    // Set custom body based on the selected format.
    $values['twig'] = FALSE;
    switch ($values['body']) {
      case 'twig':
        $values['body'] = $values['body_custom_twig'];
        $values['twig'] = TRUE;
        break;
      case WebformSelectOther::OTHER_OPTION:
        $body_format = $values['html'] ? 'html' : 'text';
        $values['body'] = $values['body_custom_' . $body_format];
        break;
    }
    $form_state
      ->setValues($values);
    $this
      ->applyFormStateToConfiguration($form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    parent::submitConfigurationForm($form, $form_state);
    $values = $form_state
      ->getValues();

    // Cleanup states.
    $values['states'] = array_values(array_filter($values['states']));
    foreach ($this->configuration as $name => $value) {
      if (isset($values[$name])) {

        // Convert options array to safe config array to prevent errors.
        // @see https://www.drupal.org/node/2297311
        if (preg_match('/_options$/', $name)) {
          $this->configuration[$name] = WebformOptionsHelper::encodeConfig($values[$name]);
        }
        else {
          $this->configuration[$name] = $values[$name];
        }
      }
    }

    // Cast debug.
    $this->configuration['debug'] = (bool) $this->configuration['debug'];
  }

  /**
   * {@inheritdoc}
   */
  public function postSave(WebformSubmissionInterface $webform_submission, $update = TRUE) {
    $state = $webform_submission
      ->getWebform()
      ->getSetting('results_disabled') ? WebformSubmissionInterface::STATE_COMPLETED : $webform_submission
      ->getState();
    if ($this->configuration['states'] && in_array($state, $this->configuration['states'])) {
      $message = $this
        ->getMessage($webform_submission);
      $this
        ->sendMessage($webform_submission, $message);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function postDelete(WebformSubmissionInterface $webform_submission) {
    if (in_array(WebformSubmissionInterface::STATE_DELETED, $this->configuration['states'])) {
      $message = $this
        ->getMessage($webform_submission);
      $this
        ->sendMessage($webform_submission, $message);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getMessage(WebformSubmissionInterface $webform_submission) {
    $theme_name = $this->configuration['theme_name'];

    // Switch to custom or default theme.
    $this->themeManager
      ->setCurrentTheme($theme_name);
    $token_options = [
      'email' => TRUE,
      'excluded_elements' => $this->configuration['excluded_elements'],
      'ignore_access' => $this->configuration['ignore_access'],
      'exclude_empty' => $this->configuration['exclude_empty'],
      'exclude_empty_checkbox' => $this->configuration['exclude_empty_checkbox'],
      'exclude_attachments' => $this->configuration['exclude_attachments'],
      'html' => $this->configuration['html'] && $this
        ->supportsHtml(),
    ];
    $token_data = [];
    $message = [];

    // Copy configuration to $message.
    foreach ($this->configuration as $configuration_key => $configuration_value) {

      // Get configuration name (to, cc, bcc, from, name, subject, mail)
      // and type (mail, options, or text).
      list($configuration_name, $configuration_type) = strpos($configuration_key, '_') !== FALSE ? explode('_', $configuration_key) : [
        $configuration_key,
        'text',
      ];

      // Set options and continue.
      if ($configuration_type === 'options') {
        $message[$configuration_key] = $configuration_value;
        continue;
      }

      // Determine if configuration value set to '_default'.
      $is_default_configuration = $configuration_value === static::DEFAULT_VALUE;

      // Determine if configuration value should use global configuration.
      $is_global_configuration = in_array($configuration_key, [
        'reply_to',
        'return_path',
        'sender_mail',
        'sender_name',
      ]);
      if ($is_default_configuration || !$configuration_value && $is_global_configuration) {
        $configuration_value = $this
          ->getDefaultConfigurationValue($configuration_key);
      }

      // Set email addresses.
      if ($configuration_type === 'mail') {
        $emails = $this
          ->getMessageEmails($webform_submission, $configuration_name, $configuration_value);
        $configuration_value = implode(',', array_unique($emails));
      }

      // If Twig enabled render and body, render the Twig template.
      if ($configuration_key === 'body' && $this->configuration['twig']) {
        $message[$configuration_key] = WebformTwigExtension::renderTwigTemplate($webform_submission, $configuration_value, $token_options);
      }
      else {

        // Clear tokens from email values.
        $token_options['clear'] = strpos($configuration_key, '_mail') !== FALSE ? TRUE : FALSE;

        // Get replace token values.
        $token_value = $this
          ->replaceTokens($configuration_value, $webform_submission, $token_data, $token_options);

        // Decode entities for all message values except the HTML message body.
        if (!empty($token_value) && is_string($token_value) && !($token_options['html'] && $configuration_key === 'body')) {
          $token_value = Html::decodeEntities($token_value);
        }
        $message[$configuration_key] = $token_value;
      }
    }

    // Trim the message body.
    $message['body'] = trim($message['body']);

    // Convert message body to HTML.
    if ($this->configuration['html'] && $this
      ->supportsHtml()) {

      // Apply optional global format to body.
      // NOTE: $message['body'] is not passed-thru Xss::filter() to allow
      // style tags to be supported.
      if ($format = $this->configFactory
        ->get('webform.settings')
        ->get('html_editor.mail_format')) {
        $build = [
          '#type' => 'processed_text',
          '#text' => $message['body'],
          '#format' => $format,
        ];
        $message['body'] = $this->themeManager
          ->renderPlain($build);
      }
    }

    // Add attachments.
    $message['attachments'] = $this
      ->getMessageAttachments($webform_submission);

    // Add webform submission.
    $message['webform_submission'] = $webform_submission;

    // Add handler.
    $message['handler'] = $this;

    // Switch back to active theme.
    $this->themeManager
      ->setActiveTheme();
    return $message;
  }

  /**
   * Get message to, cc, bcc, and from email addresses.
   *
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission.
   * @param string $configuration_name
   *   The email configuration name. (i.e. to, cc, bcc, or from)
   * @param string $configuration_value
   *   The email configuration value.
   *
   * @return array
   *   An array of email addresses and/or tokens.
   */
  protected function getMessageEmails(WebformSubmissionInterface $webform_submission, $configuration_name, $configuration_value) {
    $emails = [];

    // Get element from token and make sure the element has #options.
    $element_name = $this
      ->getElementKeyFromToken($configuration_value);
    $element = $element_name ? $this->webform
      ->getElement($element_name) : NULL;
    $element_has_options = $element && isset($element['#options']) ? TRUE : FALSE;

    // Check that email handle configuration has email #options.
    $email_has_options = !empty($this->configuration[$configuration_name . '_options']) ? TRUE : FALSE;

    // Get emails from options.
    if ($element_has_options && $email_has_options) {
      $email_options = WebformOptionsHelper::decodeConfig($this->configuration[$configuration_name . '_options']);

      // Set default email address.
      if (!empty($email_options[static::DEFAULT_OPTION])) {
        $emails[] = $email_options[static::DEFAULT_OPTION];
      }

      // Get submission email addresses as an array.
      $options_element_value = $webform_submission
        ->getElementData($element_name);
      if (is_array($options_element_value)) {
        $options_values = $options_element_value;
      }
      elseif ($options_element_value) {
        $options_values = [
          $options_element_value,
        ];
      }

      // Set empty email address.
      if (empty($options_values)) {
        if (!empty($email_options[static::EMPTY_OPTION])) {
          $emails[] = $email_options[static::EMPTY_OPTION];
        }
      }
      else {
        foreach ($options_values as $option_value) {
          if (!empty($email_options[$option_value])) {
            $emails[] = $email_options[$option_value];
          }
          elseif (!empty($email_options[static::OTHER_OPTION])) {
            $emails[] = $email_options[static::OTHER_OPTION];
          }
        }
      }
    }
    else {
      $emails[] = $configuration_value;
    }

    // Implode unique emails and tokens.
    $emails = implode(',', array_unique($emails));

    // Add user role email addresses to 'To', 'CC', and 'BCC'.
    // IMPORTANT: This is the only place where user email addresses can be
    // used as tokens. This prevents the webform module from being used to
    // spam users or worse… expose user email addresses to malicious users.
    if (in_array($configuration_name, [
      'to',
      'cc',
      'bcc',
    ])) {
      $roles = $this->configFactory
        ->get('webform.settings')
        ->get('mail.roles');
      $token_data = [];
      $token_data['webform_role'] = $roles;
      if ($this->moduleHandler
        ->moduleExists('webform_access')) {
        $token_data['webform_access'] = $webform_submission;
      }
      if ($this->moduleHandler
        ->moduleExists('webform_group')) {
        $token_data['webform_group'] = $webform_submission;
      }
      $emails = $this
        ->replaceTokens($emails, $webform_submission, $token_data);
    }

    // Resplit emails to make sure that emails are unique.
    $emails = preg_split('/\\s*,\\s*/', $emails);

    // Remove all empty email addresses.
    $emails = array_filter($emails);

    // Make sure all email addresses are unique.
    $emails = array_unique($emails);

    // Sort email addresses to make it easier to debug queuing and/or sending
    // issues.
    asort($emails);
    return $emails;
  }

  /**
   * Get message file attachments.
   *
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission.
   *
   * @return array
   *   A array of file attachments.
   */
  protected function getMessageAttachments(WebformSubmissionInterface $webform_submission) {
    if (empty($this->configuration['attachments']) || !$this
      ->supportsAttachments()) {
      return [];
    }
    $attachments = [];
    $elements = $this
      ->getWebform()
      ->getElementsInitializedAndFlattened();
    $element_attachments = $this
      ->getWebform()
      ->getElementsAttachments();
    foreach ($element_attachments as $element_attachment) {

      // Check if the element attachment key is excluded and should not attach any files.
      if (isset($this->configuration['excluded_elements'][$element_attachment])) {
        continue;
      }
      $element = $elements[$element_attachment];

      /** @var \Drupal\webform\Plugin\WebformElementAttachmentInterface $element_plugin */
      $element_plugin = $this->elementManager
        ->getElementInstance($element);
      $attachments = array_merge($attachments, $element_plugin
        ->getEmailAttachments($element, $webform_submission));
    }
    return $attachments;
  }

  /**
   * {@inheritdoc}
   */
  public function sendMessage(WebformSubmissionInterface $webform_submission, array $message) {
    $to = $message['to_mail'];
    $from = $message['from_mail'];

    // Remove less than (<) and greater (>) than from name.
    $message['from_name'] = preg_replace('/[<>]/', '', $message['from_name']);
    if (!empty($message['from_name'])) {
      $from = Mail::formatDisplayName($message['from_name']) . ' <' . $from . '>';
    }
    $current_langcode = $this->languageManager
      ->getCurrentLanguage()
      ->getId();

    // @todo [Drupal 9.x] Remove below class exists check.
    // Issue #84883: Unicode::mimeHeaderEncode() doesn't correctly
    // follow RFC 2047.
    // @see https://www.drupal.org/project/drupal/issues/84883
    // Don't send the message if the From address is not valid.
    if (class_exists('\\Symfony\\Component\\Mime\\Address')) {
      try {
        \Symfony\Component\Mime\Address::create($from);
      } catch (\Exception $exception) {
        if ($this->configuration['debug']) {
          $t_args = [
            '%form' => $this
              ->getWebform()
              ->label(),
            '%handler' => $this
              ->label(),
            '%from_email' => $from,
          ];
          $this
            ->messenger()
            ->addWarning($this
            ->t('%form: Email not sent for %handler handler because the <em>From</em> email (%from_email) is not valid.', $t_args), TRUE);
        }
        $context = [
          '@form' => $this
            ->getWebform()
            ->label(),
          '@handler' => $this
            ->label(),
          '@from_email' => $from,
        ];
        $this
          ->getLogger('webform_submission')
          ->error("@form: Email not sent for '@handler' handler because the 'From' email (@from_email) is not valid.", $context);
        return;
      }
    }

    // Don't send the message if To, CC, and BCC is empty.
    if (!$this
      ->hasRecipient($webform_submission, $message)) {
      if ($this->configuration['debug']) {
        $t_args = [
          '%form' => $this
            ->getWebform()
            ->label(),
          '%handler' => $this
            ->label(),
        ];
        $this
          ->messenger()
          ->addWarning($this
          ->t('%form: Email not sent for %handler handler because a <em>To</em>, <em>CC</em>, or <em>BCC</em> email was not provided.', $t_args), TRUE);
      }
      return;
    }

    // Render body using webform email message (wrapper) template.
    $build = [
      '#theme' => 'webform_email_message_' . ($this->configuration['html'] ? 'html' : 'text'),
      '#message' => [
        'body' => is_string($message['body']) ? Markup::create($message['body']) : $message['body'],
      ] + $message,
      '#webform_submission' => $webform_submission,
      '#handler' => $this,
    ];
    $theme_name = $this->configuration['theme_name'];
    $message['body'] = trim((string) $this->themeManager
      ->renderPlain($build, $theme_name));

    // Html body needs to be Markup so that relative URLs are converted
    // to absolute.
    // @see \Drupal\Core\Mail\MailManager::doMail
    if ($this->configuration['html']) {
      $message['body'] = Markup::create($message['body']);
    }

    // Send message.
    $key = $this
      ->getWebform()
      ->id() . '_' . $this
      ->getHandlerId();

    // Remove webform_submission and handler to prevent memory limit
    // issues during testing.
    if (drupal_valid_test_ua()) {
      unset($message['webform_submission'], $message['handler']);
    }

    // Append additional custom parameters.
    if (!empty($this->configuration['parameters'])) {
      $message += $this
        ->replaceTokens($this->configuration['parameters'], $webform_submission);
    }

    // Remove parameters.
    unset($message['parameters']);
    $result = $this->mailManager
      ->mail('webform', $key, $to, $current_langcode, $message, $from);
    if ($webform_submission
      ->getWebform()
      ->hasSubmissionLog()) {

      // Log detailed message to the 'webform_submission' log.
      $context = [
        '@from_name' => $message['from_name'],
        '@from_mail' => $message['from_mail'],
        '@to_mail' => $message['to_mail'],
        '@subject' => $message['subject'],
        'link' => $webform_submission
          ->id() ? $webform_submission
          ->toLink($this
          ->t('View'))
          ->toString() : NULL,
        'webform_submission' => $webform_submission,
        'handler_id' => $this
          ->getHandlerId(),
        'operation' => 'sent email',
      ];
      $this
        ->getLogger('webform_submission')
        ->notice("'@subject' sent to '@to_mail' from '@from_name' [@from_mail]'.", $context);
    }
    else {

      // Log general message to the 'webform' log.
      $context = [
        '@form' => $this
          ->getWebform()
          ->label(),
        '@title' => $this
          ->label(),
        'link' => $this
          ->getWebform()
          ->toLink($this
          ->t('Edit'), 'handlers')
          ->toString(),
      ];
      $this
        ->getLogger('webform')
        ->notice('@form webform sent @title email.', $context);
    }

    // Debug by displaying send email onscreen.
    if ($this->configuration['debug']) {
      $t_args = [
        '%from_name' => $message['from_name'],
        '%from_mail' => $message['from_mail'],
        '%to_mail' => $message['to_mail'],
        '%subject' => $message['subject'],
      ];
      $this
        ->messenger()
        ->addWarning($this
        ->t("%subject sent to %to_mail from %from_name [%from_mail].", $t_args), TRUE);
      $debug_message = $this
        ->buildDebugMessage($webform_submission, $message);
      $this
        ->messenger()
        ->addWarning($this->themeManager
        ->renderPlain($debug_message), TRUE);
    }
    return $result['send'];
  }

  /**
   * {@inheritdoc}
   */
  public function hasRecipient(WebformSubmissionInterface $webform_submission, array $message) {

    // Don't send the message if To, CC, and BCC is empty.
    if (empty($message['to_mail']) && empty($message['cc_mail']) && empty($message['bcc_mail'])) {
      return FALSE;
    }
    else {
      return TRUE;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function resendMessageForm(array $message) {
    $element = [];
    $element['to_mail'] = [
      '#type' => 'webform_email_multiple',
      '#title' => $this
        ->t('To email'),
      '#default_value' => $message['to_mail'],
    ];
    $element['cc_mail'] = [
      '#type' => 'webform_email_multiple',
      '#title' => $this
        ->t('CC email'),
      '#default_value' => $message['cc_mail'],
    ];
    $element['bcc_mail'] = [
      '#type' => 'webform_email_multiple',
      '#title' => $this
        ->t('BCC email'),
      '#default_value' => $message['bcc_mail'],
    ];
    $element['from_divider'] = [
      '#markup' => '<hr/>',
    ];
    $element['from_mail'] = [
      '#type' => 'email',
      '#title' => $this
        ->t('From email'),
      '#required' => TRUE,
      '#default_value' => $message['from_mail'],
    ];
    $element['from_name'] = [
      '#type' => 'textfield',
      '#title' => $this
        ->t('From name'),
      '#required' => TRUE,
      '#default_value' => $message['from_name'],
    ];
    $element['reply_to_divider'] = [
      '#markup' => '<hr/>',
    ];
    $element['reply_to'] = [
      '#type' => 'email',
      '#title' => $this
        ->t('Reply to'),
      '#default_value' => $message['reply_to'],
    ];
    $element['message_divider'] = [
      '#markup' => '<hr/>',
    ];
    $element['subject'] = [
      '#type' => 'textfield',
      '#title' => $this
        ->t('Subject'),
      '#default_value' => $message['subject'],
    ];
    $element['body'] = [
      '#type' => $message['html'] ? 'webform_html_editor' : 'webform_codemirror',
      '#title' => $this
        ->t('Message'),
      '#required' => TRUE,
      '#default_value' => $message['body'],
    ];
    $element['return_path'] = [
      '#type' => 'value',
      '#value' => $message['return_path'],
    ];
    $element['html'] = [
      '#type' => 'value',
      '#value' => $message['html'],
    ];
    $element['attachments'] = [
      '#type' => 'value',
      '#value' => $message['attachments'],
    ];
    if ($message['attachments']) {
      $element['files'] = [
        '#type' => 'item',
        '#title' => $this
          ->t('Attachments'),
      ] + $this
        ->buildAttachments($message['attachments']);
    }

    // Preload HTML Editor and CodeMirror so that they can be properly
    // initialized when loaded via Ajax.
    $element['#attached']['library'][] = 'webform/webform.element.html_editor';
    $element['#attached']['library'][] = 'webform/webform.element.codemirror.text';
    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function getMessageSummary(array $message) {
    return [
      '#settings' => $message,
    ] + parent::getSummary();
  }

  /**
   * Check that HTML emails are supported.
   *
   * @return bool
   *   TRUE if HTML email is supported.
   */
  protected function supportsHtml() {
    return TRUE;
  }

  /**
   * Check that emailing files as attachments is supported.
   *
   * @return bool
   *   TRUE if emailing files as attachments is supported.
   */
  protected function supportsAttachments() {

    // If 'system.mail.interface.default' is 'test_mail_collector'
    // allow email attachments during testing.
    if ($this->configFactory
      ->get('system.mail')
      ->get('interface.default') === 'test_mail_collector') {
      return TRUE;
    }

    // If webform_test.module is installed and this is a test webform
    // allow email attachments.
    if (strpos($this
      ->getWebform()
      ->id(), 'test_') === 0 && $this->moduleHandler
      ->moduleExists('webform_test')) {
      return TRUE;
    }

    // The Mail System module, which supports a variety of mail handlers,
    // and the SMTP module support attachments.
    $mailsystem_installed = $this->moduleHandler
      ->moduleExists('mailsystem');
    $smtp_enabled = $this->moduleHandler
      ->moduleExists('smtp') && $this->configFactory
      ->get('smtp.settings')
      ->get('smtp_on');
    return $mailsystem_installed || $smtp_enabled;
  }

  /**
   * Build debug message.
   *
   * @param \Drupal\webform\WebformSubmissionInterface $webform_submission
   *   A webform submission.
   * @param array $message
   *   An email message.
   *
   * @return array
   *   Debug message.
   */
  protected function buildDebugMessage(WebformSubmissionInterface $webform_submission, array $message) {

    // Title.
    $build = [
      '#type' => 'details',
      '#title' => $this
        ->t('Debug: Email: @title', [
        '@title' => $this
          ->label(),
      ]),
    ];

    // Values.
    $values = [
      'from_name' => $this
        ->t('From name'),
      'from_mail' => $this
        ->t('From mail'),
      'to_mail' => $this
        ->t('To mail'),
      'cc_mail' => $this
        ->t('Cc mail'),
      'bcc_mail' => $this
        ->t('Bcc mail'),
      'reply_to' => $this
        ->t('Reply-to'),
      'return_path' => $this
        ->t('Return path'),
      '---' => '---',
      'subject' => $this
        ->t('Subject'),
    ];
    foreach ($values as $name => $title) {
      if ($title === '---') {
        $build[$name] = [
          '#markup' => '<hr />',
        ];
      }
      elseif (!empty($message[$name])) {
        $build[$name] = [
          '#type' => 'item',
          '#title' => $title,
          '#markup' => $message[$name],
          '#wrapper_attributes' => [
            'class' => [
              'container-inline',
            ],
            'style' => 'margin: 0',
          ],
        ];
      }
    }

    // Body.
    $build['body'] = [
      '#type' => 'item',
      '#title' => $this
        ->t('Body'),
      '#markup' => Markup::create('<pre>' . htmlentities($message['body']) . '</pre>'),
      '#wrapper_attributes' => [
        'style' => 'margin: 0',
      ],
    ];

    // Attachments.
    if (!empty($message['attachments'])) {
      $build['attachments_divider'] = [
        '#markup' => '<hr />',
      ];
      $build['attachments'] = [
        '#type' => 'item',
        '#title' => $this
          ->t('Attachments'),
        '#wrapper_attributes' => [
          'style' => 'margin: 0',
        ],
        'files' => $this
          ->buildAttachments($message['attachments']),
      ];
    }
    return $build;
  }

  /**
   * Get the Mail System's formatter module name.
   *
   * @return string
   *   The Mail System's formatter module name.
   */
  protected function getMailSystemFormatter() {
    $mailsystem_config = $this->configFactory
      ->get('mailsystem.settings');

    // Get the default formatter.
    $mailsystem_formatter = $mailsystem_config
      ->get('defaults.formatter');

    // Look for a global setting for the webform module.
    $mailsystem_formatter = $mailsystem_config
      ->get('modules.webform.none.formatter') ?: $mailsystem_formatter;

    // Look for a specific setting for this webform module's email.
    $key = 'email_' . $this
      ->getHandlerId();
    $mailsystem_formatter = $mailsystem_config
      ->get("modules.webform.{$key}.formatter") ?: $mailsystem_formatter;
    return $mailsystem_formatter;
  }

  /**
   * Get message body default values, which can be formatted as text or html.
   *
   * @param string $format
   *   If a format (text or html) is provided the default value for the
   *   specified format is return. If no format is specified an associative
   *   array containing the text and html default body values will be returned.
   *
   * @return string|array
   *   A single (text or html) default body value or an associative array
   *   containing both the text and html default body values.
   */
  protected function getBodyDefaultValues($format = NULL) {
    $webform_settings = $this->configFactory
      ->get('webform.settings');
    $formats = [
      'text' => $webform_settings
        ->get('mail.default_body_text') ?: '[webform_submission:values]',
      'html' => $webform_settings
        ->get('mail.default_body_html') ?: '[webform_submission:values]',
    ];
    return $format === NULL ? $formats : $formats[$format];
  }

  /**
   * Build A select other element for email address and names.
   *
   * @param string $name
   *   The element's key.
   * @param string $title
   *   The element's title.
   * @param string $label
   *   The element's label.
   * @param bool $required
   *   TRUE if the element is required.
   * @param array $element_options
   *   The element options.
   * @param array $options_options
   *   The options options.
   * @param array $role_options
   *   The (user) role options.
   * @param array $other_options
   *   The other options.
   *
   * @return array
   *   A select other element.
   */
  protected function buildElement($name, $title, $label, $required = FALSE, array $element_options = [], array $options_options = NULL, array $role_options = NULL, array $other_options = NULL) {
    list($element_name, $element_type) = strpos($name, '_') !== FALSE ? explode('_', $name) : [
      $name,
      'text',
    ];
    $default_option = $this
      ->getDefaultConfigurationValue($name);
    $options = [];
    $options[WebformSelectOther::OTHER_OPTION] = $this
      ->t('Custom @label…', [
      '@label' => $label,
    ]);
    if ($default_option) {
      $options[(string) $this
        ->t('Default')] = [
        static::DEFAULT_VALUE => $default_option,
      ];
    }
    if (!empty($element_options)) {
      $options[(string) $this
        ->t('Elements')] = $element_options;
    }
    if ($options_options) {
      $options[(string) $this
        ->t('Options')] = $options_options;
    }
    if ($role_options) {
      $options[(string) $this
        ->t('Roles')] = $role_options;
    }
    if ($other_options) {
      $options[(string) $this
        ->t('Other')] = $other_options;
    }
    $element = [];
    $element[$name] = [
      '#type' => 'webform_select_other',
      '#title' => $title,
      '#options' => $options,
      '#empty_option' => !$required ? $this
        ->t('- None -') : NULL,
      '#other__title' => $title,
      '#other__title_display' => 'invisible',
      '#other__placeholder' => $this
        ->t('Enter @label…', [
        '@label' => $label,
      ]),
      '#other__type' => $element_type === 'mail' ? 'webform_email_multiple' : 'textfield',
      '#other__allow_tokens' => TRUE,
      '#required' => $required,
      '#default_value' => $this->configuration[$name],
    ];

    // Set empty option.
    if (in_array($name, [
      'reply_to',
      'return_path',
      'sender_mail',
      'sender_name',
    ])) {
      $element[$name]['#empty_option'] = $this
        ->t('- Default -');
    }

    // Remove maxlength.
    if (in_array($name, [
      'subject',
    ])) {
      $element[$name]['#other__maxlength'] = NULL;
    }

    // Tweak elements.
    switch ($name) {
      case 'from_mail':
        $element[$name]['#other__description'] = $this
          ->t('Multiple email addresses may be separated by commas.') . ' ' . $this
          ->t("If multiple email addresses are entered the '@name' will be not included in the email.", [
          '@name' => $this
            ->t('From name'),
        ]);
        break;
      case 'reply_to':
        $element[$name]['#description'] = $this
          ->t('The email address that a recipient will see when they replying to an email.');
        break;
      case 'return_path':
        $element[$name]['#description'] = $this
          ->t('The email address to which bounce messages are delivered.');
        break;
      case 'sender_mail':
        $element[$name]['#description'] = $this
          ->t('The email address submitting the message, if other than shown by the From header');
        break;
    }

    // Use multiple email for reply_to, return_path, and sender_mail because
    // it supports tokens.
    if (in_array($name, [
      'reply_to',
      'return_path',
      'sender_mail',
    ])) {
      $element[$name]['#other__type'] = 'webform_email_multiple';
      $element[$name]['#other__cardinality'] = 1;
      $element[$name]['#other__description'] = '';
      $t_args = [
        '@title' => $title,
      ];
      if ($default_email = $this
        ->getDefaultConfigurationValue($name)) {
        $t_args['%email'] = $default_email;
        $element[$name]['#description'] .= ' ' . $this
          ->t("Leave blank to use %email as the '@title' email.", $t_args);
      }
      else {
        $element[$name]['#description'] .= ' ' . $this
          ->t("Leave blank to automatically use the 'From' address.", $t_args);
      }
    }

    // If no options options are defined return the element.
    if (!$options_options) {
      return $element;
    }
    $ajax_id = 'webform-email-handler-' . $name;
    $this
      ->buildAjaxElementTrigger($ajax_id, $element[$name]);
    $this
      ->buildAjaxElementUpdate($ajax_id, $element);

    // Get options name.
    $options_name = $element_name . '_options';
    if (isset($options_options[$this->configuration[$name]]) && ($token_element_name = $this
      ->getElementKeyFromToken($this->configuration[$name]))) {

      // Get options name and element.
      $options_element = $this->webform
        ->getElement($token_element_name);

      // Set mapping options.
      $mapping_options = $options_element['#options'];
      array_walk($mapping_options, function (&$value, $key) {
        $value = '<b>' . $value . '</b>';
      });
      if (preg_match('/_other$/', $options_element['#type'])) {
        $mapping_options[static::OTHER_OPTION] = $this
          ->t("Other (Used when 'other' value is entered)");
      }
      if (empty($options_element['#required'])) {
        $mapping_options[static::EMPTY_OPTION] = $this
          ->t('Empty (Used when no option is selected)');
      }
      $mapping_options[static::DEFAULT_OPTION] = $this
        ->t('Default (This email address will always be included)');

      // Set placeholder emails.
      $destination_placeholder_emails = [
        'example@example.com',
        '[site:mail]',
      ];
      if ($role_options) {
        $role_names = array_keys($role_options);
        $destination_placeholder_emails[] = $role_names[0] === '[webform_role:authenticated]' && isset($role_names[1]) ? $role_names[1] : $role_names[0];
      }
      $element[$options_name] = [
        '#type' => 'webform_mapping',
        '#title' => $this
          ->t('@title options', [
          '@title' => $title,
        ]),
        '#description' => $this
          ->t('The selected element has multiple options. You may enter email addresses for each choice. When that choice is selected, an email will be sent to the corresponding addresses. If a field is left blank, no email will be sent for that option. You may use tokens.') . '<br /><br />',
        '#description_display' => 'before',
        '#required' => TRUE,
        '#default_value' => WebformOptionsHelper::decodeConfig($this->configuration[$options_name]),
        '#source' => $mapping_options,
        '#source__title' => $this
          ->t('Option'),
        '#destination__type' => 'webform_email_multiple',
        '#destination__allow_tokens' => TRUE,
        '#destination__title' => $this
          ->t('Email addresses'),
        '#destination__description' => NULL,
        '#destination__placeholder' => implode(', ', $destination_placeholder_emails),
      ];
    }
    else {
      $element[$options_name] = [
        '#type' => 'value',
        '#value' => [],
      ];
    }
    $this
      ->buildAjaxElementWrapper($ajax_id, $element[$options_name]);
    return $element;
  }

  /**
   * Build attachment to be displayed via debug message and resend form.
   *
   * @param array $attachments
   *   An array of email attachments.
   *
   * @return array
   *   A renderable array containing links to attachments.
   */
  protected function buildAttachments(array $attachments) {
    $build = [];
    foreach ($attachments as $attachment) {
      $t_args = [
        '@filename' => $attachment['filename'],
        '@filemime' => $attachment['filemime'],
        '@filesize' => format_size(mb_strlen($attachment['filecontent'])),
      ];
      if (!empty($attachment['_fileurl'])) {
        $t_args[':href'] = $attachment['_fileurl'];
        $build[] = [
          '#markup' => $this
            ->t('<strong><a href=":href">@filename</a></strong> (@filemime) - @filesize ', $t_args),
        ];
      }
      else {
        $build[] = [
          '#markup' => $this
            ->t('<strong>@filename</strong> (@filemime) - @filesize ', $t_args),
        ];
      }
    }
    return $build;
  }

  /**
   * Get element key from webform token.
   *
   * @param string $token
   *   The token.
   * @param string $format
   *   The element format.
   *
   * @return string|null
   *   The element key or NULL if token can not be parsed.
   */
  protected function getElementKeyFromToken($token, $format = 'raw') {
    if (preg_match('/^\\[webform_submission:values:([^:]+):' . $format . '\\]$/', $token, $match)) {
      return $match[1];
    }
    else {
      return NULL;
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function buildTokenTreeElement(array $token_types = [
    'webform',
    'webform_submission',
  ], $description = NULL) {
    $description = $description ?: $this
      ->t('Use [webform_submission:values:ELEMENT_KEY:raw] to get plain text values.');
    return parent::buildTokenTreeElement($token_types, $description);
  }

}

Classes

Namesort descending Description
EmailWebformHandler Emails a webform submission.