View source
<?php
namespace Drupal\simplenews\Mail;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Psr\Log\LoggerInterface;
use Drupal\Core\Mail\MailManagerInterface;
use Drupal\Core\Session\AccountSwitcherInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
use Drupal\simplenews\Entity\Subscriber;
use Drupal\simplenews\AbortSendingException;
use Drupal\simplenews\SkipMailException;
use Drupal\simplenews\Spool\SpoolStorageInterface;
use Drupal\simplenews\SubscriberInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Link;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Language\LanguageManagerInterface;
class Mailer implements MailerInterface {
use MessengerTrait;
use StringTranslationTrait;
const SEND_CHECK_INTERVAL = 100;
const SEND_TIME_LIMIT = 0.8;
const TRACK_RESULTS = [
SpoolStorageInterface::STATUS_DONE => TRUE,
SpoolStorageInterface::STATUS_FAILED => TRUE,
];
protected $spoolStorage;
protected $mailManager;
protected $state;
protected $logger;
protected $accountSwitcher;
protected $lock;
protected $config;
protected $startTime;
protected $entityTypeManager;
protected $languageManager;
protected $mailCache;
protected $moduleHandler;
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;
}
public function attemptImmediateSend(array $conditions = [], $use_batch = TRUE) {
if ($this->config
->get('mail.use_cron')) {
return FALSE;
}
if ($use_batch) {
$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,
],
];
}
$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 {
$this
->sendSpool(SpoolStorageInterface::UNLIMITED, $conditions);
$this->spoolStorage
->clear();
$this
->updateSendStatus();
}
return TRUE;
}
public function sendSpool($limit = SpoolStorageInterface::UNLIMITED, array $conditions = []) {
$check_counter = 0;
$spool = $this->spoolStorage
->getMails($limit, $conditions);
if (count($spool) > 0) {
$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);
if (++$check_counter >= static::SEND_CHECK_INTERVAL && ini_get('max_execution_time') > 0) {
$check_counter = 0;
$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 !cron_throttle_setting.', [
'!cron_throttle_setting' => Link::fromTextAndUrl($this
->t('Cron throttle setting'), Url::fromRoute('simplenews.settings_mail')),
]);
break;
}
}
}
} catch (AbortSendingException $e) {
$this->logger
->error($e
->getMessage());
}
$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->langcode][$row->result];
$item = ($item ?? 0) + 1;
}
}
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 => $languages) {
$storage
->resetCache([
$entity_id,
]);
$entity = $storage
->load($entity_id);
foreach ($languages as $langcode => $counts) {
$translation = $entity
->getTranslation($langcode);
$translation->simplenews_issue->sent_count += $counts[SpoolStorageInterface::STATUS_DONE] ?? 0;
$translation->simplenews_issue->error_count += $counts[SpoolStorageInterface::STATUS_FAILED] ?? 0;
}
$entity
->save();
}
}
$this->lock
->release('simplenews_update_sent_count');
}
$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', REQUEST_TIME);
$this->state
->set('simplenews.last_sent', $freq[SpoolStorageInterface::STATUS_DONE]);
$this->accountSwitcher
->switchBack();
return $freq[SpoolStorageInterface::STATUS_DONE];
}
}
public function sendMail(MailInterface $mail) {
$params['simplenews_mail'] = $mail;
if ($mail
->getKey('test') == 'node') {
$params['_error_message'] = FALSE;
}
try {
$message = $this->mailManager
->mail('simplenews', $mail
->getKey(), $mail
->getRecipient(), $mail
->getLanguage(), $params, $mail
->getFromFormatted());
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'],
]);
}
}
$result = $message['result'] ? SpoolStorageInterface::STATUS_DONE : SpoolStorageInterface::STATUS_PENDING;
$this->moduleHandler
->alter('simplenews_mail_result', $result, $message);
} catch (SkipMailException $e) {
$result = SpoolStorageInterface::STATUS_SKIPPED;
}
return $result;
}
public function sendTest(ContentEntityInterface $issue, array $test_addresses) {
$this->accountSwitcher
->switchTo(new AnonymousUserSession());
$recipients = [
'anonymous' => [],
'user' => [],
];
foreach ($test_addresses as $mail) {
$mail = trim($mail);
if (!empty($mail)) {
$subscriber = Subscriber::loadByMail($mail, 'create', $this->languageManager
->getCurrentLanguage());
if ($account = $subscriber
->getUser()) {
$recipients['user'][] = $account
->getDisplayName() . ' <' . $mail . '>';
}
else {
$recipients['anonymous'][] = $mail;
}
$mail = new MailEntity($issue, $subscriber, $this->mailCache);
$mail
->setKey('test');
$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();
}
public function sendCombinedConfirmation(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']);
}
public function updateSendStatus() {
$counts = [];
$sum = [];
$send = [];
$query = \Drupal::entityQuery('node');
$nids = $query
->condition('simplenews_issue.status', SIMPLENEWS_STATUS_SEND_PENDING)
->accessCheck(FALSE)
->execute();
$nodes = Node::loadMultiple($nids);
if ($nodes) {
foreach ($nodes as $nid => $node) {
$counts[$node->simplenews_issue->target_id][$nid] = $this->spoolStorage
->countMails([
'entity_id' => $nid,
'entity_type' => 'node',
]);
}
}
foreach ($counts as $newsletter_id => $node_count) {
$sum[$newsletter_id] = array_sum($node_count);
foreach ($node_count as $nid => $count) {
if ($newsletter_id != '0' && $sum[$newsletter_id] == '0') {
$send[] = $nid;
}
elseif ($newsletter_id == '0' && $count == '0') {
$send[] = $nid;
}
}
}
if (!empty($send)) {
foreach ($send as $nid) {
$node = Node::load($nid);
$node->simplenews_issue->status = SIMPLENEWS_STATUS_SEND_READY;
$node
->save();
}
}
}
public function getFrom() {
$address = $this->config
->get('newsletter.from_address');
$name = $this->config
->get('newsletter.from_name');
$formatted_address = mb_substr(PHP_OS, 0, 3) == 'WIN' ? $address : '"' . addslashes(Unicode::mimeHeaderEncode($name)) . '" <' . $address . '>';
return [
'address' => $address,
'formatted' => $formatted_address,
];
}
protected function startTimer() {
if (!function_exists('getrusage')) {
return;
}
$usage = getrusage();
$this->startTime = (double) ($usage['ru_stime.tv_sec'] . '.' . $usage['ru_stime.tv_usec']) + (double) ($usage['ru_utime.tv_sec'] . '.' . $usage['ru_utime.tv_usec']);
}
protected function getCurrentExecutionTime() {
if (!function_exists('getrusage')) {
return NULL;
}
$usage = getrusage();
$now = (double) ($usage['ru_stime.tv_sec'] . '.' . $usage['ru_stime.tv_usec']) + (double) ($usage['ru_utime.tv_sec'] . '.' . $usage['ru_utime.tv_usec']);
return $now - $this->startTime;
}
}