simplenews-3.0.0-alpha1/src/Mail/Mailer.php

src/Mail/Mailer.php
<?php

namespace Drupal\simplenews\Mail;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Mail\MailManagerInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
use Drupal\simplenews\AbortSendingException;
use Drupal\simplenews\Entity\Subscriber;
use Drupal\simplenews\SkipMailException;
use Drupal\simplenews\Spool\SpoolStorageInterface;
use Drupal\simplenews\SubscriberInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Header\MailboxHeader;

/**
 * Default Mailer.
 */
class Mailer implements MailerInterface {

  use MessengerTrait;
  use StringTranslationTrait;

  /**
   * Amount of mails after which the execution time should be checked again.
   */
  const SEND_CHECK_INTERVAL = 100;

  /**
   * At 80% of the PHP max execution time, sending is interrupted.
   */
  const SEND_TIME_LIMIT = 0.8;

  /**
   * Array indicating which status values to track results for.
   */
  const TRACK_RESULTS = [
    SpoolStorageInterface::STATUS_DONE => TRUE,
    SpoolStorageInterface::STATUS_FAILED => TRUE,
  ];

  /**
   * The simplenews spool storage.
   *
   * @var \Drupal\simplenews\Spool\SpoolStorageInterface
   */
  protected $spoolStorage;

  /**
   * The mail manager.
   *
   * @var \Drupal\Core\Mail\MailManagerInterface
   */
  protected $mailManager;

  /**
   * State service.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * A logger instance.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

  /**
   * Account switcher.
   *
   * @var \Drupal\Core\Session\AccountSwitcherInterface
   */
  protected $accountSwitcher;

  /**
   * Lock service.
   *
   * @var \Drupal\Core\Lock\LockBackendInterface
   */
  protected $lock;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ImmutableConfig
   */
  protected $config;

  /**
   * Start time of the timer.
   *
   * @var float
   */
  protected $startTime;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

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

  /**
   * The simplenews mail cache.
   *
   * @var \Drupal\simplenews\Mail\MailCacheInterface
   */
  protected $mailCache;

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

  /**
   * Constructs a Mailer.
   *
   * @param \Drupal\simplenews\Spool\SpoolStorageInterface $spool_storage
   *   The simplenews spool storage.
   * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager
   *   The mail manager.
   * @param \Drupal\Core\State\StateInterface $state
   *   State service.
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   * @param \Drupal\Core\Session\AccountSwitcherInterface $account_switcher
   *   Account switcher.
   * @param \Drupal\Core\Lock\LockBackendInterface $lock
   *   Lock service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   * @param \Drupal\simplenews\Mail\MailCacheInterface $mail_cache
   *   The simplenews mail cache.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   */
  public function __construct(SpoolStorageInterface $spool_storage, MailManagerInterface $mail_manager, StateInterface $state, LoggerInterface $logger, AccountSwitcherInterface $account_switcher, LockBackendInterface $lock, ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, LanguageManagerInterface $language_manager, MailCacheInterface $mail_cache, ModuleHandlerInterface $module_handler) {
    $this->spoolStorage = $spool_storage;
    $this->mailManager = $mail_manager;
    $this->state = $state;
    $this->logger = $logger;
    $this->accountSwitcher = $account_switcher;
    $this->lock = $lock;
    $this->config = $config_factory->get('simplenews.settings');
    $this->entityTypeManager = $entity_type_manager;
    $this->languageManager = $language_manager;
    $this->mailCache = $mail_cache;
    $this->moduleHandler = $module_handler;
  }

  /**
   * {@inheritdoc}
   */
  public function attemptImmediateSend(array $conditions = [], $use_batch = TRUE) {
    if ($this->config->get('mail.use_cron')) {
      return FALSE;
    }
    if ($use_batch) {
      // Set up as many send operations as necessary to send all mails with the
      // defined throttle amount.
      $throttle = $this->config->get('mail.throttle');
      $spool_count = $this->spoolStorage->countMails($conditions);
      $num_operations = ceil($spool_count / $throttle);

      $operations = [];
      for ($i = 0; $i < $num_operations; $i++) {
        $operations[] = [
          '_simplenews_batch_dispatcher',
          ['simplenews.mailer:sendSpool', $throttle, $conditions],
        ];
      }

      // Add separate operations to clear the spool and update the send status.
      $operations[] = ['_simplenews_batch_dispatcher', ['simplenews.spool_storage:clear']];
      $operations[] = ['_simplenews_batch_dispatcher', ['simplenews.mailer:updateSendStatus']];

      $batch = [
        'operations' => $operations,
        'title' => $this->t('Sending mails'),
      ];
      batch_set($batch);
    }
    else {
      // Send everything that matches the conditions immediately.
      $this->sendSpool(SpoolStorageInterface::UNLIMITED, $conditions);
      $this->spoolStorage->clear();
      $this->updateSendStatus();
    }
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function sendSpool($limit = SpoolStorageInterface::UNLIMITED, array $conditions = []) {
    $check_counter = 0;

    // Send pending messages from database cache.
    $spool = $this->spoolStorage->getMails($limit, $conditions);
    if (count($spool) > 0) {

      // Switch to the anonymous user.
      $anonymous_user = new AnonymousUserSession();
      $this->accountSwitcher->switchTo($anonymous_user);

      $count_fail = $count_skipped = $count_success = 0;
      $sent = [];

      $this->startTimer();

      try {
        while ($mail = $spool->nextMail()) {
          $mail->setKey('node');
          $result = $this->sendMail($mail);
          $spool->setLastMailResult($result);

          // Check every n emails if we exceed the limit.
          // When PHP maximum execution time is almost elapsed we interrupt
          // sending. The remainder will be sent during the next cron run.
          if (++$check_counter >= static::SEND_CHECK_INTERVAL && ini_get('max_execution_time') > 0) {
            $check_counter = 0;
            // Stop sending if a percentage of max execution time was exceeded.
            $elapsed = $this->getCurrentExecutionTime();
            if ($elapsed > static::SEND_TIME_LIMIT * ini_get('max_execution_time')) {
              $this->logger->warning('Sending interrupted: PHP maximum execution time almost exceeded. Remaining newsletters will be sent during the next cron run. If this warning occurs regularly you should reduce the <a href=":cron_throttle_setting">Cron throttle setting</a>.', [
                ':cron_throttle_setting' => Url::fromRoute('simplenews.settings_mail')->toString(),
              ]);
              break;
            }
          }
        }
      }
      catch (AbortSendingException $e) {
        $this->logger->error($e->getMessage());
      }

      // Calculate counts.
      $results_table = [];
      $freq = array_fill(0, SpoolStorageInterface::STATUS_FAILED + 1, 0);
      foreach ($spool->getResults() as $row) {
        $freq[$row->result]++;
        if (isset(static::TRACK_RESULTS[$row->result])) {
          $item = &$results_table[$row->entity_type][$row->entity_id][$row->result];
          $item = ($item ?? 0) + 1;
        }
      }

      // Update subscriber count.
      if ($this->lock->acquire('simplenews_update_sent_count')) {
        foreach ($results_table as $entity_type => $ids) {
          $storage = $this->entityTypeManager->getStorage($entity_type);

          foreach ($ids as $entity_id => $counts) {
            $storage->resetCache([$entity_id]);
            $entity = $storage->load($entity_id);
            $entity->simplenews_issue->sent_count += $counts[SpoolStorageInterface::STATUS_DONE] ?? 0;
            $entity->simplenews_issue->error_count += $counts[SpoolStorageInterface::STATUS_FAILED] ?? 0;
            $entity->save();
          }
        }
        $this->lock->release('simplenews_update_sent_count');
      }

      // Report sent result and elapsed time. On Windows systems getrusage() is
      // not implemented and hence no elapsed time is available.
      $log_array = [
        '%success' => $freq[SpoolStorageInterface::STATUS_DONE],
        '%skipped' => $freq[SpoolStorageInterface::STATUS_SKIPPED],
        '%fail' => $freq[SpoolStorageInterface::STATUS_FAILED],
        '%retry' => $freq[SpoolStorageInterface::STATUS_PENDING],
      ];

      if (function_exists('getrusage')) {
        $log_array['%sec'] = round($this->getCurrentExecutionTime(), 1);
        $this->logger->notice('%success emails sent in %sec seconds, %skipped skipped, %fail failed permanently, %retry failed retrying.', $log_array);
      }
      else {
        $this->logger->notice('%success emails sent, %skipped skipped, %fail failed permanently, %retry failed retrying.', $log_array);
      }

      $this->state->set('simplenews.last_cron', \Drupal::time()->getRequestTime());
      $this->state->set('simplenews.last_sent', $freq[SpoolStorageInterface::STATUS_DONE]);

      $this->accountSwitcher->switchBack();
      return $freq[SpoolStorageInterface::STATUS_DONE];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function sendMail(MailInterface $mail) {
    $params['simplenews_mail'] = $mail;
    if ($mail->getKey() == 'node') {
      // Suppress error message as it causes cron failures.
      $params['_error_message'] = FALSE;
    }

    // Send mail.
    try {
      $message = $this->mailManager->mail('simplenews', $mail->getKey(), $mail->getRecipient(), $mail->getLanguage(), $params, $mail->getFromFormatted());

      // Log sent result in watchdog.
      if ($this->config->get('mail.debug')) {
        if ($message['result']) {
          $this->logger->debug('Outgoing email. Message type: %type<br />Subject: %subject<br />Recipient: %to', [
            '%type' => $mail->getKey(),
            '%to' => $message['to'],
            '%subject' => $message['subject'],
          ]);
        }
        else {
          $this->logger->error('Outgoing email failed. Message type: %type<br />Subject: %subject<br />Recipient: %to', [
            '%type' => $mail->getKey(),
            '%to' => $message['to'],
            '%subject' => $message['subject'],
          ]);
        }
      }

      // By default, failures are left in PENDING state to retry.
      $result = ($message['result'] === FALSE) ? SpoolStorageInterface::STATUS_PENDING : SpoolStorageInterface::STATUS_DONE;
      $this->moduleHandler->alter('simplenews_mail_result', $result, $message);
    }
    catch (SkipMailException $e) {
      $result = SpoolStorageInterface::STATUS_SKIPPED;
    }

    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function sendTest(ContentEntityInterface $issue, array $test_addresses, string $key = 'test') {
    // Force the current user to anonymous to ensure consistent permissions.
    $this->accountSwitcher->switchTo(new AnonymousUserSession());

    // Send the test newsletter to the test address(es) specified in the node.
    // Build array of test email addresses.
    // Send newsletter to test addresses.
    // Emails are send direct, not using the spool.
    $recipients = ['anonymous' => [], 'user' => []];
    foreach ($test_addresses as $mail) {
      $mail = trim($mail);
      if (!empty($mail)) {
        $subscriber = Subscriber::loadByMail($mail, 'create', $this->languageManager->getCurrentLanguage()->getId());

        if ($account = $subscriber->getUser()) {
          $recipients['user'][] = $account->getDisplayName() . ' <' . $mail . '>';
        }
        else {
          $recipients['anonymous'][] = $mail;
        }
        $mail = new MailEntity($issue, $subscriber, $this->mailCache);
        $mail->setKey($key);
        $this->sendMail($mail);
      }
    }
    if (count($recipients['user'])) {
      $recipients_txt = implode(', ', $recipients['user']);
      $this->messenger()->addMessage($this->t('Test newsletter sent to user %recipient.', ['%recipient' => $recipients_txt]));
    }
    if (count($recipients['anonymous'])) {
      $recipients_txt = implode(', ', $recipients['anonymous']);
      $this->messenger()->addMessage($this->t('Test newsletter sent to anonymous %recipient.', ['%recipient' => $recipients_txt]));
    }

    $this->accountSwitcher->switchBack();
  }

  /**
   * {@inheritdoc}
   */
  public function sendSubscribeConfirmation(SubscriberInterface $subscriber) {
    $params['from'] = $this->getFrom();
    $params['context']['simplenews_subscriber'] = $subscriber;
    $key = 'subscribe_combined';
    $this->mailManager->mail('simplenews', $key, $subscriber->getMail(), $subscriber->getLangcode(), $params, $params['from']['address']);
  }

  /**
   * {@inheritdoc}
   */
  public function updateSendStatus() {
    // For each pending newsletter count pending emails in the spool.
    // If 0, update status to send-ready.
    $query = \Drupal::entityQuery('node');
    $nids = $query
      ->condition('simplenews_issue.status', SIMPLENEWS_STATUS_SEND_PENDING)
      ->accessCheck(FALSE)
      ->execute();
    $nodes = Node::loadMultiple($nids);
    foreach ($nodes as $nid => $node) {
      $count = $this->spoolStorage->countMails(['entity_id' => $nid, 'entity_type' => 'node']);
      if ($count == 0) {
        $node->simplenews_issue->status = SIMPLENEWS_STATUS_SEND_READY;
        $node->save();
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getFrom() {
    $address = $this->config->get('newsletter.from_address');
    $name = $this->config->get('newsletter.from_name');

    $mailbox = new MailboxHeader('From', new Address($address, $name));

    // Windows based PHP systems don't accept formatted email addresses.
    $formatted_address = (mb_substr(PHP_OS, 0, 3) == 'WIN') ? $address : $mailbox->getBodyAsString();

    return [
      'address' => $address,
      'formatted' => $formatted_address,
    ];
  }

  /**
   * Starts the execution timer.
   */
  protected function startTimer() {
    // Windows systems don't implement getrusage(). There is no alternative.
    if (!function_exists('getrusage')) {
      return;
    }

    $usage = getrusage();
    $this->startTime = (float) ($usage['ru_stime.tv_sec'] . '.' . $usage['ru_stime.tv_usec']) + (float) ($usage['ru_utime.tv_sec'] . '.' . $usage['ru_utime.tv_usec']);
  }

  /**
   * Returns the current execution time.
   *
   * @return float|null
   *   The elapsed PHP execution time since the last start.
   *
   * @see self::startTime()
   */
  protected function getCurrentExecutionTime() {
    // Windows systems don't implement getrusage(). There is no alternative.
    if (!function_exists('getrusage')) {
      return NULL;
    }

    $usage = getrusage();
    $now = (float) ($usage['ru_stime.tv_sec'] . '.' . $usage['ru_stime.tv_usec']) + (float) ($usage['ru_utime.tv_sec'] . '.' . $usage['ru_utime.tv_usec']);

    return $now - $this->startTime;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc