Files
nextcloud-workflow-deck-aut…/lib/Service/NotificationMailer.php
T
Patrick Niebeling 860e8b7bbb
Build package / xml-lint (push) Successful in 30s
Build package / unit-tests (push) Successful in 1m14s
Build package / php-lint (8.4) (push) Successful in 55s
Build package / php-lint (8.5) (push) Successful in 56s
Build package / package (push) Successful in 1m17s
Leave the card description out of the moved-cards mail
2026-09-30 10:41:20 +02:00

181 lines
6.5 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 {
// Single-argument addBodyText() runs htmlspecialchars() itself, so
// these card-, workflow-, board- and stack-titles (user input) are
// safe as-is. The card description is deliberately left out.
$template->addBodyText(sprintf(
'Die Karte „%1$s“ wurde verschoben, weil sie überfällig ist (Workflow „%2$s“).',
$card->getTitle(),
$workflow->getTitle(),
));
$template->addBodyText(sprintf(
'Board „%1$s“: von „%2$s“ nach „%3$s“ verschoben.',
$targets['board'],
$targets['sourceStack'],
$targets['targetStack'],
));
$template->addBodyButton(
'Karte öffnen',
$this->urlGenerator->getAbsoluteURL('/apps/deck/board/' . $workflow->getBoardId() . '/card/' . $card->getId()),
);
}
/**
* 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,
]);
}
}
}