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
181 lines
6.5 KiB
PHP
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,
|
|
]);
|
|
}
|
|
}
|
|
}
|