Verified Commit 7b7712f0 authored by Dave Long's avatar Dave Long
Browse files

feat: #3397418 Ensure origin headers of mails sent using the symfony-based...

feat: #3397418 Ensure origin headers of mails sent using the symfony-based mailer service comply to RFC5322

By: znerol
By: adamps
parent c32f2c70
Loading
Loading
Loading
Loading
Loading
+151 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Core\Mailer\EventSubscriber;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;

/**
 * Message subscriber which sets the from and sender headers.
 */
class OriginatorSubscriber implements EventSubscriberInterface {

  public function __construct(
    protected readonly ConfigFactoryInterface $configFactory,
    protected readonly LanguageManagerInterface $languageManager,
  ) {
  }

  /**
   * Sets the default from header and a sender header if necessary.
   *
   * @param \Symfony\Component\Mailer\Event\MessageEvent $event
   *   The message event.
   */
  public function onMessage(MessageEvent $event): void {
    $message = $event->getMessage();
    if ($message instanceof Email) {
      $this->setDefaultFrom($message);
      $this->setDefaultSender($message);
      $this->removeRedundantSender($message);
    }
  }

  /**
   * Sets the default from address.
   *
   * @param \Symfony\Component\Mime\Email $message
   *   The email message.
   */
  protected function setDefaultFrom(Email $message): void {
    $from = $message->getFrom();
    if (count($from) === 0) {
      $langcode = $message->getHeaders()->get('Content-Language')?->getBodyAsString();
      $siteAddress = $this->getSiteAddress($langcode);
      $message->from($siteAddress);
    }
  }

  /**
   * Sets the default sender address.
   *
   * @param \Symfony\Component\Mime\Email $message
   *   The email message.
   */
  protected function setDefaultSender(Email $message): void {
    if (!$message->getSender()) {
      $langcode = $message->getHeaders()->get('Content-Language')?->getBodyAsString();
      $siteAddress = $this->getSiteAddress($langcode);
      $message->sender($siteAddress);
    }
  }

  /**
   * Removes the Sender address if it is redundant.
   *
   * Rules according to RFC 5322 section 3.6.2 (Originator Fields):
   * * If the from field contains more than one mailbox specification in the
   *   mailbox-list, then the sender field, containing the field name
   *   "Sender" and a single mailbox specification, MUST appear in the
   *   message.
   * * The "Sender:" field specifies the mailbox of the agent responsible
   *   for the actual transmission of the message.
   * * If the originator of the message can be indicated by a single mailbox
   *   and the author and transmitter are identical, the "Sender:" field
   *   SHOULD NOT be used.
   *
   * @param \Symfony\Component\Mime\Email $message
   *   The email message.
   *
   * @see https://www.rfc-editor.org/rfc/rfc5322.html#section-3.6.2
   */
  protected function removeRedundantSender(Email $message): void {
    $from = $message->getFrom();
    $sender = $message->getSender();
    $senderRedundant = count($from) === 1 &&
      $sender !== NULL &&
      $from[0]->getAddress() === $sender->getAddress();
    if ($senderRedundant) {
      $message->getHeaders()->remove('Sender');
    }
  }

  /**
   * Returns the site email address.
   *
   * @param string|null $langcode
   *   The language code from the email.
   */
  protected function getSiteAddress(?string $langcode): Address {
    return $this->executeInEnvironment($langcode, function () {
      $config = $this->configFactory->get('system.site');
      return new Address($config->get('mail'), $config->get('name'));
    });
  }

  /**
   * Invokes the given callback after switching the config language.
   *
   * @param string|null $langcode
   *   The language code.
   * @param callable(): T $callback
   *   Callback to execute inside substituted environment.
   *
   * @return T
   *   Returns the result returned by the callback.
   *
   * @template T
   *
   * @todo Replace with execution environment once that is available.
   * @see: https://www.drupal.org/i/3536307
   */
  protected function executeInEnvironment(?string $langcode, callable $callback): mixed {
    $originalLanguage = $this->languageManager->getConfigOverrideLanguage();
    if ($langcode && ($language = $this->languageManager->getLanguage($langcode))) {
      $this->languageManager->setConfigOverrideLanguage($language);
    }
    try {
      return $callback();
    }
    finally {
      $this->languageManager->setConfigOverrideLanguage($originalLanguage);
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      // Should be the last one to allow header changes by other listeners.
      MessageEvent::class => ['onMessage', -255],
    ];
  }

}
+2 −0
Original line number Diff line number Diff line
@@ -49,3 +49,5 @@ services:
  Symfony\Component\Mailer\Mailer:
    autowire: true
  Symfony\Component\Mailer\MailerInterface: '@Symfony\Component\Mailer\Mailer'
  Drupal\Core\Mailer\EventSubscriber\OriginatorSubscriber:
    autowire: true
+5 −0
Original line number Diff line number Diff line
name: 'Mailer event subscriber test'
type: module
description: 'Support module for mailer event subscriber testing.'
package: Testing
version: VERSION
+6 −0
Original line number Diff line number Diff line
services:
  mailer_event_subscriber_test.message_event_test_subscriber:
    class: Drupal\mailer_event_subscriber_test\EventSubscriber\MessageEventTestSubscriber
    arguments: ['@state']
    tags:
      - { name: 'event_subscriber' }
+84 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\mailer_event_subscriber_test\EventSubscriber;

use Drupal\Core\State\StateInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;

/**
 * A message event test subscriber.
 */
class MessageEventTestSubscriber implements EventSubscriberInterface {

  /**
   * Constructs a new message event test subscriber.
   *
   * @param \Drupal\Core\State\StateInterface $state
   *   The state service.
   */
  public function __construct(
    protected StateInterface $state,
  ) {
  }

  /**
   * Sets a custom from header.
   *
   * @param \Symfony\Component\Mailer\Event\MessageEvent $event
   *   The message event.
   */
  public function setCustomFrom(MessageEvent $event): void {
    $customFrom = $this->state->get('mailer_event_subscriber_test.set_custom_from');
    $message = $event->getMessage();
    if (!empty($customFrom) && $message instanceof Email) {
      $message->from(Address::create($customFrom));
    }
  }

  /**
   * Sets a custom message sender header.
   *
   * @param \Symfony\Component\Mailer\Event\MessageEvent $event
   *   The message event.
   */
  public function setCustomMessageSender(MessageEvent $event): void {
    $customFrom = $this->state->get('mailer_event_subscriber_test.set_custom_message_sender');
    $message = $event->getMessage();
    if (!empty($customFrom) && $message instanceof Email) {
      $message->sender(Address::create($customFrom));
    }
  }

  /**
   * Sets a custom envelope sender.
   *
   * @param \Symfony\Component\Mailer\Event\MessageEvent $event
   *   The message event.
   */
  public function setCustomEnvelopeSender(MessageEvent $event): void {
    $customEnvelopeSender = $this->state->get('mailer_event_subscriber_test.set_custom_envelope_sender');
    if (!empty($customEnvelopeSender)) {
      $envelope = $event->getEnvelope();
      $envelope->setSender(Address::create($customEnvelopeSender));
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      MessageEvent::class => [
        ['setCustomFrom', 0],
        ['setCustomMessageSender', 0],
        ['setCustomEnvelopeSender', 0],
      ],
    ];
  }

}
Loading