class Mailer in Simplenews 3.x
Same name and namespace in other branches
- 8.2 src/Mail/Mailer.php \Drupal\simplenews\Mail\Mailer
- 8 src/Mail/Mailer.php \Drupal\simplenews\Mail\Mailer
Default Mailer.
Hierarchy
- class \Drupal\simplenews\Mail\Mailer implements MailerInterface uses MessengerTrait, StringTranslationTrait
Expanded class hierarchy of Mailer
1 string reference to 'Mailer'
1 service uses Mailer
File
- src/
Mail/ Mailer.php, line 31
Namespace
Drupal\simplenews\MailView source
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 !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());
}
// 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->langcode][$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 => $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');
}
// 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', REQUEST_TIME);
$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('test') == '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'] ? SpoolStorageInterface::STATUS_DONE : SpoolStorageInterface::STATUS_PENDING;
$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) {
// 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());
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();
}
/**
* {@inheritdoc}
*/
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']);
}
/**
* {@inheritdoc}
*/
public function updateSendStatus() {
// Number of pending emails in the spool.
$counts = [];
// Sum of emails in the spool per tnid (translation id)
$sum = [];
// Nodes with the status 'send'.
$send = [];
// For each pending newsletter count pending emails in the spool.
$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',
]);
}
}
// Determine which nodes are sent per translation group and individual node.
foreach ($counts as $newsletter_id => $node_count) {
// The sum of emails per tnid is the combined status result for the group
// of translated nodes. Untranslated nodes have tnid == 0 which will be
// ignored later.
$sum[$newsletter_id] = array_sum($node_count);
foreach ($node_count as $nid => $count) {
// Translated nodes (tnid != 0)
if ($newsletter_id != '0' && $sum[$newsletter_id] == '0') {
$send[] = $nid;
}
elseif ($newsletter_id == '0' && $count == '0') {
$send[] = $nid;
}
}
}
// Update overall newsletter status.
if (!empty($send)) {
foreach ($send as $nid) {
$node = Node::load($nid);
$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');
// Windows based PHP systems don't accept formatted email addresses.
$formatted_address = mb_substr(PHP_OS, 0, 3) == 'WIN' ? $address : '"' . addslashes(Unicode::mimeHeaderEncode($name)) . '" <' . $address . '>';
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 = (double) ($usage['ru_stime.tv_sec'] . '.' . $usage['ru_stime.tv_usec']) + (double) ($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 = (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;
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
Mailer:: |
protected | property | Account switcher. | |
Mailer:: |
protected | property | The config factory. | |
Mailer:: |
protected | property | The entity type manager. | |
Mailer:: |
protected | property | The language manager. | |
Mailer:: |
protected | property | Lock service. | |
Mailer:: |
protected | property | A logger instance. | |
Mailer:: |
protected | property | The simplenews mail cache. | |
Mailer:: |
protected | property | The mail manager. | |
Mailer:: |
protected | property | The module handler. | |
Mailer:: |
protected | property | The simplenews spool storage. | |
Mailer:: |
protected | property | Start time of the timer. | |
Mailer:: |
protected | property | State service. | |
Mailer:: |
public | function |
Send mail spool immediately if cron should not be used. Overrides MailerInterface:: |
|
Mailer:: |
protected | function | Returns the current execution time. | |
Mailer:: |
public | function |
Build formatted from-name and email for a mail object. Overrides MailerInterface:: |
|
Mailer:: |
public | function |
Send collected confirmations. Overrides MailerInterface:: |
|
Mailer:: |
public | function |
Send a node to an email address. Overrides MailerInterface:: |
|
Mailer:: |
public | function |
Send simplenews newsletters from the spool. Overrides MailerInterface:: |
|
Mailer:: |
public | function |
Send test version of newsletter. Overrides MailerInterface:: |
|
Mailer:: |
constant | Amount of mails after which the execution time should be checked again. | ||
Mailer:: |
constant | At 80% of the PHP max execution time, sending is interrupted. | ||
Mailer:: |
protected | function | Starts the execution timer. | |
Mailer:: |
constant | Array indicating which status values to track results for. | ||
Mailer:: |
public | function |
Update newsletter sent status. Overrides MailerInterface:: |
|
Mailer:: |
public | function | Constructs a Mailer. | |
MessengerTrait:: |
protected | property | The messenger. | 27 |
MessengerTrait:: |
public | function | Gets the messenger. | 27 |
MessengerTrait:: |
public | function | Sets the messenger. | |
StringTranslationTrait:: |
protected | property | The string translation service. | 4 |
StringTranslationTrait:: |
protected | function | Formats a string containing a count of items. | |
StringTranslationTrait:: |
protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait:: |
protected | function | Gets the string translation service. | |
StringTranslationTrait:: |
public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait:: |
protected | function | Translates a string to the current language or to a given language. |