Files
nextcloud-workflow-deck-aut…/lib/Service/NotificationMailer.php
T
Patrick Niebeling ebf48b0707
Build package / xml-lint (push) Successful in 1m14s
Build package / php-lint (8.4) (push) Successful in 2m14s
Build package / php-lint (8.5) (push) Successful in 2m15s
Build package / unit-tests (push) Successful in 2m17s
Build package / package (push) Successful in 59s
Link the card title instead of an extra button in the mail
The plain-text part gets the card URL on its own line. Bumps to 35.2.2.
2026-09-30 11:05:22 +02:00

191 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
namespace OCA\WorkflowDeckAutomation\Service;
use OCA\Deck\Db\Card;
use OCA\WorkflowDeckAutomation\Db\Workflow;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Throwable;
/**
* The app is used internally only and speaks German, like its settings
* page: texts are written in German directly instead of going through
* IL10N with English source strings and a translation file.
*/
class NotificationMailer {
private const APP_ID = 'workflow_deck_automation';
private const SENDER_NAME = 'Deck Workflow-Automatisierung';
/**
* Language of Nextcloud's own footer text ("automatically sent email,
* please do not reply"), which would otherwise follow the instance
* default — in cron there is no request to take a language from.
*/
private const FOOTER_LANGUAGE = 'de';
public function __construct(
private IMailer $mailer,
private IURLGenerator $urlGenerator,
private LoggerInterface $logger,
) {
}
/**
* One mail listing every card moved for $recipient in this run — they
* may come from several workflows and boards.
*
* @param non-empty-list<array{workflow: Workflow, card: Card, targets: array{board: string, sourceStack: string, targetStack: string}}> $entries
* targets are the titles resolved by DeckIntegrationService::describeTargets()
*/
public function sendCardsMovedNotification(IUser $recipient, array $entries): void {
$email = $recipient->getEMailAddress();
if ($email === null || $email === '') {
$this->logger->info('Skipping moved-cards mail: user ' . $recipient->getUID() . ' has no email address', [
'app' => self::APP_ID,
]);
return;
}
$count = count($entries);
try {
$template = $this->mailer->createEMailTemplate(self::APP_ID . '.CardsMoved', [
'count' => $count,
'cardTitles' => array_map(static fn (array $entry) => $entry['card']->getTitle(), $entries),
]);
$template->setSubject($count === 1
? sprintf('Deck-Karte verschoben: %s', $entries[0]['card']->getTitle())
: sprintf('%d Deck-Karten verschoben', $count));
$template->addHeader();
$template->addHeading($count === 1
? 'Eine Karte wurde automatisch verschoben'
: sprintf('%d Karten wurden automatisch verschoben', $count), false);
foreach ($entries as $entry) {
$this->addCard($template, $entry['workflow'], $entry['card'], $entry['targets']);
}
$template->addFooter('', self::FOOTER_LANGUAGE);
$message = $this->mailer->createMessage();
$message->setTo([$email => $recipient->getDisplayName()]);
$message->setFrom([Util::getDefaultEmailAddress('noreply') => self::SENDER_NAME]);
$message->useTemplate($template);
$failedRecipients = $this->mailer->send($message);
if (!empty($failedRecipients)) {
$this->logger->error('Moved-cards mail failed for: ' . implode(', ', $failedRecipients), [
'app' => self::APP_ID,
]);
}
} catch (Throwable $e) {
$this->logger->error('Could not send moved-cards mail to ' . $recipient->getUID() . ': ' . $e->getMessage(), [
'app' => self::APP_ID,
'exception' => $e,
]);
}
}
/**
* @param array{board: string, sourceStack: string, targetStack: string} $targets
*/
private function addCard(IEMailTemplate $template, Workflow $workflow, Card $card, array $targets): void {
// The card description and the workflow's name are deliberately left
// out — the mail only says what moved where. The card title itself is
// the link to the card.
$sentence = '„%1$s“ (Board „%2$s“) ist überfällig und wurde von „%3$s“ nach „%4$s“ verschoben.';
$url = $this->urlGenerator->getAbsoluteURL('/apps/deck/board/' . $workflow->getBoardId() . '/card/' . $card->getId());
// With both parts passed, addBodyText() escapes nothing: every title
// is user input and has to be escaped here for the HTML part.
$html = sprintf(
$sentence,
'<a href="' . self::escape($url) . '">' . self::escape($card->getTitle()) . '</a>',
self::escape($targets['board']),
self::escape($targets['sourceStack']),
self::escape($targets['targetStack']),
);
$plain = sprintf(
$sentence,
$card->getTitle(),
$targets['board'],
$targets['sourceStack'],
$targets['targetStack'],
) . PHP_EOL . $url;
$template->addBodyText($html, $plain);
}
private static function escape(string $text): string {
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}
/**
* One-off notice that a workflow was switched off because the board or
* stack it points at is gone.
*
* @param WorkflowRunner::TARGET_* $brokenTarget
*/
public function sendWorkflowDisabledNotification(IUser $user, Workflow $workflow, string $brokenTarget): void {
$email = $user->getEMailAddress();
if ($email === null || $email === '') {
$this->logger->info('Cannot notify about disabled workflow ' . $workflow->getId() . ': user ' . $user->getUID() . ' has no email address', [
'app' => self::APP_ID,
]);
return;
}
$reason = match ($brokenTarget) {
WorkflowRunner::TARGET_SOURCE_STACK => 'sein Quell-Stapel nicht mehr existiert',
WorkflowRunner::TARGET_TARGET_STACK => 'sein Ziel-Stapel nicht mehr existiert',
default => 'sein Board nicht mehr existiert oder für dich nicht mehr verfügbar ist',
};
try {
$settingsLink = $this->urlGenerator->getAbsoluteURL('/settings/user/' . self::APP_ID);
$template = $this->mailer->createEMailTemplate(self::APP_ID . '.WorkflowDisabled', [
'workflowTitle' => $workflow->getTitle(),
]);
$template->setSubject(sprintf('Deck-Workflow deaktiviert: %s', $workflow->getTitle()));
$template->addHeader();
$template->addHeading('Ein Workflow wurde deaktiviert', false);
$template->addBodyText(sprintf(
'Der Workflow „%1$s“ wurde automatisch ausgeschaltet, weil %2$s. Er verschiebt keine Karten mehr.',
$workflow->getTitle(),
$reason,
));
$template->addBodyText(
'Lösche den Workflow oder stelle ein vorhandenes Board und vorhandene Stapel ein, um ihn wieder zu aktivieren. An diesen Workflow wirst du nicht noch einmal erinnert.',
);
$template->addBodyButton('Einstellungen öffnen', $settingsLink);
$template->addFooter('', self::FOOTER_LANGUAGE);
$message = $this->mailer->createMessage();
$message->setTo([$email => $user->getDisplayName()]);
$message->setFrom([Util::getDefaultEmailAddress('noreply') => self::SENDER_NAME]);
$message->useTemplate($template);
$failedRecipients = $this->mailer->send($message);
if (!empty($failedRecipients)) {
$this->logger->error('Deactivation mail for workflow ' . $workflow->getId() . ' failed for: ' . implode(', ', $failedRecipients), [
'app' => self::APP_ID,
]);
}
} catch (Throwable $e) {
$this->logger->error('Could not send deactivation mail for workflow ' . $workflow->getId() . ': ' . $e->getMessage(), [
'app' => self::APP_ID,
'exception' => $e,
]);
}
}
}