class Mailer in Simplenews 8
Same name and namespace in other branches
- 8.2 src/Mail/Mailer.php \Drupal\simplenews\Mail\Mailer
- 3.x 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 32
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;
/**
* @var \Drupal\simplenews\Spool\SpoolStorageInterface
*/
protected $spoolStorage;
/**
* @var \Drupal\Core\Mail\MailManagerInterface
*/
protected $mailManager;
/**
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var \Drupal\Core\Session\AccountSwitcherInterface
*/
protected $accountSwitcher;
/**
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* @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;
/**
* 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.
*/
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;
}
/**
* {@inheritdoc}
*/
public function attemptImmediateSend(array $conditions = array(), $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 = array();
for ($i = 0; $i < $num_operations; $i++) {
$operations[] = array(
'_simplenews_batch_dispatcher',
array(
'simplenews.mailer:sendSpool',
$throttle,
$conditions,
),
);
}
// Add separate operations to clear the spool and update the send status.
$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 {
// 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 = array()) {
$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 = array();
$this
->startTimer();
while ($mail = $spool
->nextMail()) {
$mail
->setKey('node');
$result = $this
->sendMail($mail);
// Update spool status.
// This is not optimal for performance but prevents duplicate emails
// in case of PHP execution time overrun.
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++;
}
}
// 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;
// Break the 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.', array(
'!cron_throttle_setting' => Link::fromTextAndUrl($this
->t('Cron throttle setting'), Url::fromRoute('simplenews.settings_mail')),
));
break;
}
}
}
// It is possible that all or at the end some results failed to get
// prepared, report them separately.
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++;
}
}
// Update subscriber count.
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');
}
// Report sent result and elapsed time. On Windows systems getrusage() is
// not implemented and hence no elapsed time is available.
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;
}
}
/**
* {@inheritdoc}
*/
public function sendMail(MailInterface $mail) {
$params['simplenews_mail'] = $mail;
// 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', 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'],
));
}
}
// Build array of sent results for spool table and reporting.
if ($message['result']) {
$result = array(
'status' => SpoolStorageInterface::STATUS_DONE,
'error' => FALSE,
);
}
else {
// This error may be caused by faulty mailserver configuration or overload.
// Mark "pending" to keep trying.
$result = array(
'status' => SpoolStorageInterface::STATUS_PENDING,
'error' => TRUE,
);
}
} catch (SkipMailException $e) {
$result = array(
'status' => SpoolStorageInterface::STATUS_SKIPPED,
'error' => FALSE,
);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function sendTest(NodeInterface $node, 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 = 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) {
// Create a stub subscriber. Use values from the user having the given
// address, or if there is no such user, the anonymous user.
if ($user = user_load_by_mail($mail)) {
$subscriber = Subscriber::create()
->fillFromAccount($user);
}
else {
$subscriber = Subscriber::create([
'mail' => $mail,
]);
}
// Keep the current language.
$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();
}
/**
* {@inheritdoc}
*/
public function sendCombinedConfirmation(SubscriberInterface $subscriber) {
$params['from'] = $this
->getFrom();
$params['context']['simplenews_subscriber'] = $subscriber;
// Send multiple if there is more than one change for this subscriber
// single otherwise.
$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']);
}
}
}
/**
* {@inheritdoc}
*/
public function updateSendStatus() {
$counts = array();
// number of pending emails in the spool
$sum = array();
// sum of emails in the spool per tnid (translation id)
$send = array();
// nodes with the status 'send'
// For each pending newsletter count the number of pending emails in the spool.
$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',
));
}
}
// Determine which nodes are send per translation group and per 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 array(
'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
* 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;
}
$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 | ||
Mailer:: |
protected | property | ||
Mailer:: |
protected | property | The entity type manager. | |
Mailer:: |
protected | property | The language manager. | |
Mailer:: |
protected | property | ||
Mailer:: |
protected | property | ||
Mailer:: |
protected | property | The simplenews mail cache. | |
Mailer:: |
protected | property | ||
Mailer:: |
protected | property | ||
Mailer:: |
protected | property | Start time of the timer. | |
Mailer:: |
protected | property | ||
Mailer:: |
public | function |
Send mail spool immediatly 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:: |
public | function |
Update newsletter sent status. Overrides MailerInterface:: |
|
Mailer:: |
public | function | Constructs a Mailer. | |
MessengerTrait:: |
protected | property | The messenger. | 29 |
MessengerTrait:: |
public | function | Gets the messenger. | 29 |
MessengerTrait:: |
public | function | Sets the messenger. | |
StringTranslationTrait:: |
protected | property | The string translation service. | 1 |
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. |