View source
<?php
namespace Drupal\simplenews\Mail;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
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\node\NodeInterface;
use Drupal\simplenews\Entity\Subscriber;
use Drupal\simplenews\NewsletterInterface;
use Drupal\simplenews\Mail\MailEntity;
use Drupal\simplenews\Mail\MailInterface;
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;
protected $spoolStorage;
protected $mailManager;
protected $state;
protected $logger;
protected $accountSwitcher;
protected $lock;
protected $config;
protected $startTime;
protected $entityTypeManager;
protected $languageManager;
protected $mailCache;
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) {
$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;
}
public function attemptImmediateSend(array $conditions = array(), $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 = array();
for ($i = 0; $i < $num_operations; $i++) {
$operations[] = array(
'_simplenews_batch_dispatcher',
array(
'simplenews.mailer:sendSpool',
$throttle,
$conditions,
),
);
}
$operations[] = array(
'_simplenews_batch_dispatcher',
array(
'simplenews.spool_storage:clear',
),
);
$operations[] = array(
'_simplenews_batch_dispatcher',
array(
'simplenews.mailer:updateSendStatus',
),
);
$batch = array(
'operations' => $operations,
'title' => 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 = array()) {
$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 = array();
$this
->startTimer();
while ($mail = $spool
->nextMail()) {
$mail
->setKey('node');
$result = $this
->sendMail($mail);
foreach ($spool
->getProcessed() as $msid => $row) {
$row_result = isset($row->result) ? $row->result : $result;
$this->spoolStorage
->updateMails(array(
$msid,
), $row_result);
if ($row_result['status'] == SpoolStorageInterface::STATUS_DONE) {
$count_success++;
if (!isset($sent[$row->entity_type][$row->entity_id][$row->langcode])) {
$sent[$row->entity_type][$row->entity_id][$row->langcode] = 1;
}
else {
$sent[$row->entity_type][$row->entity_id][$row->langcode]++;
}
}
elseif ($row_result['status'] == SpoolStorageInterface::STATUS_SKIPPED) {
$count_skipped++;
}
if ($row_result['error']) {
$count_fail++;
}
}
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.', array(
'!cron_throttle_setting' => Link::fromTextAndUrl($this
->t('Cron throttle setting'), Url::fromRoute('simplenews.settings_mail')),
));
break;
}
}
}
foreach ($spool
->getProcessed() as $msid => $row) {
$row_result = $row->result;
$this->spoolStorage
->updateMails(array(
$msid,
), $row_result);
if ($row_result['status'] == SpoolStorageInterface::STATUS_DONE) {
$count_success++;
if (isset($row->langcode)) {
if (!isset($sent[$row->entity_type][$row->entity_id][$row->langcode])) {
$sent[$row->entity_type][$row->entity_id][$row->langcode] = 1;
}
else {
$sent[$row->entity_type][$row->entity_id][$row->langcode]++;
}
}
}
elseif ($row_result['status'] == SpoolStorageInterface::STATUS_SKIPPED) {
$count_skipped++;
}
if ($row_result['error']) {
$count_fail++;
}
}
if ($this->lock
->acquire('simplenews_update_sent_count')) {
foreach ($sent as $entity_type => $ids) {
foreach ($ids as $entity_id => $languages) {
$storage = $this->entityTypeManager
->getStorage($entity_type);
$storage
->resetCache(array(
$entity_id,
));
$entity = $storage
->load($entity_id);
foreach ($languages as $langcode => $count) {
$translation = $entity
->getTranslation($langcode);
$translation->simplenews_issue->sent_count = $translation->simplenews_issue->sent_count + $count;
}
$entity
->save();
}
}
$this->lock
->release('simplenews_update_sent_count');
}
if (function_exists('getrusage')) {
$this->logger
->notice('%success emails sent in %sec seconds, %skipped skipped, %fail failed sending.', array(
'%success' => $count_success,
'%sec' => round($this
->getCurrentExecutionTime(), 1),
'%skipped' => $count_skipped,
'%fail' => $count_fail,
));
}
else {
$this->logger
->notice('%success emails sent, %skipped skipped, %fail failed.', array(
'%success' => $count_success,
'%skipped' => $count_skipped,
'%fail' => $count_fail,
));
}
$this->state
->set('simplenews.last_cron', REQUEST_TIME);
$this->state
->set('simplenews.last_sent', $count_success);
$this->accountSwitcher
->switchBack();
return $count_success;
}
}
public function sendMail(MailInterface $mail) {
$params['simplenews_mail'] = $mail;
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', array(
'%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', array(
'%type' => $mail
->getKey(),
'%to' => $message['to'],
'%subject' => $message['subject'],
));
}
}
if ($message['result']) {
$result = array(
'status' => SpoolStorageInterface::STATUS_DONE,
'error' => FALSE,
);
}
else {
$result = array(
'status' => SpoolStorageInterface::STATUS_PENDING,
'error' => TRUE,
);
}
} catch (SkipMailException $e) {
$result = array(
'status' => SpoolStorageInterface::STATUS_SKIPPED,
'error' => FALSE,
);
}
return $result;
}
public function sendTest(NodeInterface $node, array $test_addresses) {
$this->accountSwitcher
->switchTo(new AnonymousUserSession());
$recipients = array(
'anonymous' => array(),
'user' => array(),
);
foreach ($test_addresses as $mail) {
$mail = trim($mail);
if (!empty($mail)) {
$subscriber = simplenews_subscriber_load_by_mail($mail);
if (!$subscriber) {
if ($user = user_load_by_mail($mail)) {
$subscriber = Subscriber::create()
->fillFromAccount($user);
}
else {
$subscriber = Subscriber::create([
'mail' => $mail,
]);
}
$subscriber
->setLangcode($this->languageManager
->getCurrentLanguage());
}
if ($subscriber
->getUserId()) {
$account = $subscriber->uid->entity;
$recipients['user'][] = $account
->getUserName() . ' <' . $mail . '>';
}
else {
$recipients['anonymous'][] = $mail;
}
$mail = new MailEntity($node, $subscriber, $this->mailCache);
$mail
->setKey('test');
$this
->sendMail($mail);
}
}
if (count($recipients['user'])) {
$recipients_txt = implode(', ', $recipients['user']);
$this
->messenger()
->addMessage(t('Test newsletter sent to user %recipient.', array(
'%recipient' => $recipients_txt,
)));
}
if (count($recipients['anonymous'])) {
$recipients_txt = implode(', ', $recipients['anonymous']);
$this
->messenger()
->addMessage(t('Test newsletter sent to anonymous %recipient.', array(
'%recipient' => $recipients_txt,
)));
}
$this->accountSwitcher
->switchBack();
}
public function sendCombinedConfirmation(SubscriberInterface $subscriber) {
$params['from'] = $this
->getFrom();
$params['context']['simplenews_subscriber'] = $subscriber;
$use_combined = $this->config
->get('subscription.use_combined');
$changes = $subscriber
->getChanges();
if (count($changes) > 1 && $use_combined != 'never' || $use_combined == 'always') {
$key = 'subscribe_combined';
$this->mailManager
->mail('simplenews', $key, $subscriber
->getMail(), $subscriber
->getLangcode(), $params, $params['from']['address']);
}
else {
foreach ($changes as $newsletter_id => $key) {
$params['context']['newsletter'] = simplenews_newsletter_load($newsletter_id);
$this->mailManager
->mail('simplenews', $key, $subscriber
->getMail(), $subscriber
->getLangcode(), $params, $params['from']['address']);
}
}
}
public function updateSendStatus() {
$counts = array();
$sum = array();
$send = array();
$query = \Drupal::entityQuery('node');
$nids = $query
->condition('simplenews_issue.status', SIMPLENEWS_STATUS_SEND_PENDING)
->execute();
$nodes = Node::loadMultiple($nids);
if ($nodes) {
foreach ($nodes as $nid => $node) {
$counts[$node->simplenews_issue->target_id][$nid] = $this->spoolStorage
->countMails(array(
'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 array(
'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;
}
$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;
}
}