getAffectedUsers($limit, $sendTime, $forceSending, $restrictEmails);
if (empty($affectedUsers)) {
// No users found to notify, mission abort
return 0;
}
$userLanguages = $this->config->getUserValueForUsers('core', 'lang', $affectedUsers);
$userTimezones = $this->config->getUserValueForUsers('core', 'timezone', $affectedUsers);
$userEnabled = $this->config->getUserValueForUsers('core', 'enabled', $affectedUsers);
// Send Email
$default_lang = $this->config->getSystemValue('default_language', 'en');
$defaultTimeZone = date_default_timezone_get();
$deleteItemsForUsers = [];
$this->activityManager->setRequirePNG(true);
foreach ($affectedUsers as $user) {
if (isset($userEnabled[$user]) && $userEnabled[$user] === 'false') {
$deleteItemsForUsers[] = $user;
continue;
}
try {
$userObject = $this->userManager->get($user);
} catch (\Exception $e) {
$this->logger->error('An error happened while trying to find ' . $user . ', skipping', ['exception' => $e]);
continue;
}
$email = $userObject ? $userObject->getEMailAddress() : '';
if (empty($email)) {
// The user did not setup an email address
// So we will not send an email :(
$this->logger->debug("Couldn't send notification email to user '{user}' (email address isn't set for that user)", ['user' => $user, 'app' => 'activity']);
$deleteItemsForUsers[] = $user;
continue;
}
$language = (!empty($userLanguages[$user])) ? $userLanguages[$user] : $default_lang;
$timezone = (!empty($userTimezones[$user])) ? $userTimezones[$user] : $defaultTimeZone;
try {
if ($this->sendEmailToUser($user, $email, $language, $timezone, $sendTime)) {
$deleteItemsForUsers[] = $user;
} else {
$this->logger->warning("Failed sending activity email to user '{user}'.", ['user' => $user, 'app' => 'activity']);
}
} catch (\Exception $e) {
$this->logger->error('Failed creating activity email for user "{user}"', [
'exception' => $e,
'user' => $user,
'app' => 'activity',
]);
// continue;
}
}
$this->activityManager->setRequirePNG(false);
// Delete all entries we dealt with
$this->deleteSentItems($deleteItemsForUsers, $sendTime);
return count($affectedUsers);
}
/**
* Get the users we want to send an email to
*/
protected function getAffectedUsers(?int $limit, int $latestSend, bool $forceSending, ?int $restrictEmails): array {
$query = $this->connection->getQueryBuilder();
$query->select('amq_affecteduser')
->selectAlias($query->createFunction('MIN(' . $query->getColumnName('amq_latest_send') . ')'), 'amq_trigger_time')
->from('activity_mq')
->groupBy('amq_affecteduser')
->orderBy('amq_trigger_time', 'ASC');
if ($limit > 0) {
$query->setMaxResults($limit);
}
if ($restrictEmails !== null) {
if ($restrictEmails === UserSettings::EMAIL_SEND_HOURLY) {
$query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(3600))));
} elseif ($restrictEmails === UserSettings::EMAIL_SEND_DAILY) {
$query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(3600 * 24))));
} elseif ($restrictEmails === UserSettings::EMAIL_SEND_WEEKLY) {
$query->where($query->expr()->eq('amq_timestamp', $query->func()->subtract('amq_latest_send', $query->expr()->literal(3600 * 24 * 7))));
} elseif ($restrictEmails === UserSettings::EMAIL_SEND_ASAP) {
$query->where($query->expr()->eq('amq_timestamp', 'amq_latest_send'));
}
$result = $query->executeQuery();
$affectedUsers = [];
while ($row = $result->fetch()) {
$affectedUsers[] = $row['amq_affecteduser'];
}
$result->closeCursor();
return $affectedUsers;
}
if ($forceSending) {
$query->where($query->expr()->lt('amq_timestamp', $query->createNamedParameter($latestSend)));
} else {
$query->where($query->expr()->lt('amq_latest_send', $query->createNamedParameter($latestSend)));
}
$result = $query->executeQuery();
$affectedUsers = [];
while ($row = $result->fetch()) {
$affectedUsers[] = $row['amq_affecteduser'];
}
$result->closeCursor();
return $affectedUsers;
}
/**
* Get all items for the user we want to send an email to
*
* @return array [data of the first max. 200 entries, total number of entries]
*/
protected function getItemsForUser(string $affectedUser, int $maxTime, int $maxNumItems = self::ENTRY_LIMIT): array {
$query = $this->connection->getQueryBuilder();
$query->select('*')
->from('activity_mq')
->where($query->expr()->lte('amq_timestamp', $query->createNamedParameter($maxTime)))
->andWhere($query->expr()->eq('amq_affecteduser', $query->createNamedParameter($affectedUser)))
->orderBy('amq_timestamp', 'ASC')
->setMaxResults($maxNumItems);
$result = $query->executeQuery();
$activities = [];
while ($row = $result->fetch()) {
$activities[] = $row;
}
$result->closeCursor();
if (isset($activities[$maxNumItems - 1])) {
// Reached the limit, run a query to get the actual count.
$query = $this->connection->getQueryBuilder();
$query->selectAlias($query->func()->count('*'), 'actual_count')
->from('activity_mq')
->where($query->expr()->lte('amq_timestamp', $query->createNamedParameter($maxTime)))
->andWhere($query->expr()->eq('amq_affecteduser', $query->createNamedParameter($affectedUser)));
$result = $query->executeQuery();
$row = $result->fetch();
$result->closeCursor();
return [$activities, $row['actual_count'] - $maxNumItems];
}
return [$activities, 0];
}
public function purgeItemsForUser(string $affectedUser): void {
$queryBuilder = $this->connection->getQueryBuilder();
$queryBuilder->delete('activity_mq')
->where($queryBuilder->expr()->eq('amq_affecteduser', $queryBuilder->createNamedParameter($affectedUser)));
$queryBuilder->executeStatement();
}
/**
* Get a language object for a specific language
*
* @param string $lang Language identifier
* @return IL10N Language object of $lang
*/
protected function getLanguage(string $lang): IL10N {
if (!isset($this->languages[$lang])) {
$this->languages[$lang] = $this->lFactory->get('activity', $lang);
}
return $this->languages[$lang];
}
/**
* Get the sender data
* @param string $setting Either `email` or `name`
*/
protected function getSenderData(string $setting): string {
if (empty($this->senderAddress)) {
$this->senderAddress = Util::getDefaultEmailAddress('no-reply');
}
if (empty($this->senderName)) {
$defaults = new Defaults();
$this->senderName = $defaults->getName();
}
if ($setting === 'email') {
return $this->senderAddress;
}
return $this->senderName;
}
/**
* Send a notification to one user
*
* @return bool True if the entries should be removed, false otherwise
* @throws \UnexpectedValueException
*/
protected function sendEmailToUser(string $userName, string $email, string $lang, string $timezone, int $maxTime): bool {
$user = $this->userManager->get($userName);
if (!$user instanceof IUser) {
return true;
}
if (!$this->mailer->validateMailAddress($email)) {
$this->logger->warning('Notification for user "{user}" not sent because the email address "{email}" is invalid.', ['user' => $userName, 'email' => $email]);
return true;
}
[$mailData, $skippedCount] = $this->getItemsForUser($userName, $maxTime);
$l = $this->getLanguage($lang);
$this->activityManager->setCurrentUserId($userName);
$this->groupHelper->resetEvents();
$this->groupHelper->setL10n($l);
foreach ($mailData as $activity) {
$event = $this->activityManager->generateEvent();
try {
$event->setApp((string)$activity['amq_appid'])
->setType((string)$activity['amq_type'])
->setAffectedUser((string)$activity['amq_affecteduser'])
->setTimestamp((int)$activity['amq_timestamp'])
->setSubject((string)$activity['amq_subject'], (array)json_decode($activity['amq_subjectparams'], true))
->setObject((string)$activity['object_type'], (int)$activity['object_id']);
} catch (\InvalidArgumentException $e) {
continue;
}
$this->groupHelper->addEvent($activity['mail_id'], $event);
}
$activityEvents = array_map(
function ($event) use ($timezone, $l) {
return [
'event' => $event,
'dateTime' => $this->dateFormatter->formatDateTime(
$event->getTimestamp(),
'long', 'short',
new \DateTimeZone($timezone), $l
)
];
},
$this->groupHelper->getEvents()
);
$template = $this->mailer->createEMailTemplate('activity.Notification', [
'displayname' => $user->getDisplayName(),
'url' => $this->urlGenerator->getAbsoluteURL('/'),
'activityEvents' => $activityEvents,
'skippedCount' => $skippedCount,
]);
$template->setSubject($l->t('Activity at %s', $this->getSenderData('name')));
$template->addHeader();
$template->addHeading($l->t('Hello %s', [$user->getDisplayName()]), $l->t('Hello %s,', [$user->getDisplayName()]));
$homeLink = '' . htmlspecialchars($this->getSenderData('name')) . '';
$template->addBodyText(
$l->t('There was some activity at %s', [$homeLink]),
$l->t('There was some activity at %s', [$this->urlGenerator->getAbsoluteURL('/')])
);
foreach ($activityEvents as $activity) {
$event = $activity['event'];
$activityDateTime = $activity['dateTime'];
$template->addBodyListItem($this->getHTMLSubject($event), $activityDateTime, $event->getIcon(), $event->getParsedSubject());
}
if ($skippedCount) {
$template->addBodyListItem($l->n('and %n more ', 'and %n more ', $skippedCount));
}
$template->addBodyText(
$l->t('You can change the frequency of these emails or disable them in the settings.', $this->urlGenerator->linkToRouteAbsolute('settings.PersonalSettings.index', ['section' => 'notifications'])),
$l->t('You can change the frequency of these emails or disable them in the settings: %s', $this->urlGenerator->linkToRouteAbsolute('settings.PersonalSettings.index', ['section' => 'notifications']))
);
$template->addFooter('', $lang);
$message = $this->mailer->createMessage();
$message->setTo([$email => $user->getDisplayName()]);
$message->useTemplate($template);
$message->setFrom([$this->getSenderData('email') => $this->getSenderData('name')]);
// We don't want auto generated responses to autogenerated activity notifications
$message->setAutoSubmitted(AutoSubmitted::VALUE_AUTO_GENERATED);
try {
$this->mailer->send($message);
} catch (\Exception $e) {
$this->logger->error('Failed sending activity email to user "{user}"', [
'exception' => $e,
'user' => $userName,
'app' => 'activity',
]);
return false;
}
$this->activityManager->setCurrentUserId(null);
return true;
}
protected function getHTMLSubject(IEvent $event): string {
if ($event->getRichSubject() === '') {
return htmlspecialchars($event->getParsedSubject());
}
$placeholders = $replacements = [];
foreach ($event->getRichSubjectParameters() as $placeholder => $parameter) {
$placeholders[] = '{' . $placeholder . '}';
if ($parameter['type'] === 'file') {
$replacement = (string)$parameter['path'];
} else {
$replacement = (string)$parameter['name'];
}
if (isset($parameter['link'])) {
$replacements[] = '' . htmlspecialchars($replacement) . '';
} else {
$replacements[] = '' . htmlspecialchars($replacement) . '';
}
}
return str_replace($placeholders, $replacements, $event->getRichSubject());
}
/**
* Delete all entries we dealt with
*/
protected function deleteSentItems(array $affectedUsers, int $maxTime): void {
if (empty($affectedUsers)) {
return;
}
$query = $this->connection->getQueryBuilder();
$query->delete('activity_mq')
->where($query->expr()->lte('amq_timestamp', $query->createNamedParameter($maxTime, IQueryBuilder::PARAM_INT)))
->andWhere($query->expr()->in('amq_affecteduser', $query->createNamedParameter($affectedUsers, IQueryBuilder::PARAM_STR_ARRAY), IQueryBuilder::PARAM_STR));
$query->executeStatement();
}
}