Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions apps/settings/lib/Hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Mail\EMailDetails;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\User\Events\PasswordUpdatedEvent;
use OCP\User\Events\UserChangedEvent;
Expand Down Expand Up @@ -109,7 +111,8 @@ public function onChangePassword(PasswordUpdatedEvent $handle): void {
$template->setSubject($l->t('Password for %1$s changed on %2$s', [$user->getDisplayName(), $instanceName]));
$template->addHeader();
$template->addHeading($l->t('Password changed for %s', [$user->getDisplayName()]), false);
$template->addBodyText($text . ' ' . $l->t('If you did not request this, please contact an administrator.'));
$template->addBodyText($text);
$template->addBodyNote($l->t('If you did not request this, please contact an administrator.'), '', IEMailTemplate::NOTE_WARNING);
$template->addFooter();

$message = $this->mailer->createMessage();
Expand Down Expand Up @@ -178,10 +181,17 @@ public function onChangeEmail(UserChangedEvent $handle): void {
$template->setSubject($l->t('Email address for %1$s changed on %2$s', [$user->getDisplayName(), $instanceName]));
$template->addHeader();
$template->addHeading($l->t('Email address changed for %s', [$user->getDisplayName()]), false);
$template->addBodyText($text . ' ' . $l->t('If you did not request this, please contact an administrator.'));
$template->addBodyText($text);
$details = (new EMailDetails($user->getDisplayName()))->setInitials($user->getDisplayName());
$details->addRow($l->t('Previous email address'))->text($oldMailAddress);
$newMailAddress = $details->addRow($l->t('New email address'));
if ($user->getEMailAddress()) {
$template->addBodyText($l->t('The new email address is %s', [$user->getEMailAddress()]));
$newMailAddress->text($user->getEMailAddress());
} else {
$newMailAddress->muted($l->t('No email address set'));
}
$template->addBodyDetails($details);
$template->addBodyNote($l->t('If you did not request this, please contact an administrator.'), '', IEMailTemplate::NOTE_WARNING);
$template->addFooter();

$message = $this->mailer->createMessage();
Expand Down
139 changes: 139 additions & 0 deletions apps/settings/tests/HooksTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Settings\Tests;

use OCA\Settings\Hooks;
use OCP\Activity\IEvent;
use OCP\Activity\IManager as IActivityManager;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IGroupManager;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Mail\EMailDetails;
use OCP\Mail\EMailDetailsRow;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Mail\IMessage;
use OCP\User\Events\PasswordUpdatedEvent;
use OCP\User\Events\UserChangedEvent;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;

class HooksTest extends TestCase {
private IMailer&MockObject $mailer;
private IEMailTemplate&MockObject $template;
private IUser&MockObject $user;
private Hooks $hooks;

protected function setUp(): void {
parent::setUp();

$activityManager = $this->createMock(IActivityManager::class);
$activityManager->method('generateEvent')->willReturn($this->createMock(IEvent::class));

$urlGenerator = $this->createMock(IURLGenerator::class);
$urlGenerator->method('getAbsoluteURL')->willReturn('https://cloud.example.com/');

$l10n = $this->createMock(IL10N::class);
$l10n->method('t')->willReturnCallback(fn (string $text, array $parameters = []): string => vsprintf($text, $parameters));
$l10nFactory = $this->createMock(IFactory::class);
$l10nFactory->method('get')->with('settings')->willReturn($l10n);

$this->user = $this->createMock(IUser::class);
$this->user->method('getUID')->willReturn('alice');
$this->user->method('getDisplayName')->willReturn('Alice');
$this->user->method('getLastLogin')->willReturn(1);

$userSession = $this->createMock(IUserSession::class);
$userSession->method('getUser')->willReturn($this->user);

$this->template = $this->createMock(IEMailTemplate::class);
$this->mailer = $this->createMock(IMailer::class);
$this->mailer->method('createEMailTemplate')->willReturn($this->template);
$this->mailer->method('createMessage')->willReturn($this->createMock(IMessage::class));

$this->hooks = new Hooks(
$activityManager,
$this->createMock(IGroupManager::class),
$this->createMock(IUserManager::class),
$userSession,
$urlGenerator,
$this->mailer,
$this->createMock(IConfig::class),
$l10nFactory,
$this->createMock(Defaults::class),
);
}

public function testPasswordChangedMail(): void {
$this->user->method('getEMailAddress')->willReturn('alice@example.com');

$this->template->expects($this->once())->method('addBodyText')
->with('Your password on https://cloud.example.com/ was changed.');
$this->template->expects($this->once())->method('addBodyNote')
->with('If you did not request this, please contact an administrator.', '', IEMailTemplate::NOTE_WARNING);
$this->mailer->expects($this->once())->method('send');

$this->hooks->handle(new PasswordUpdatedEvent($this->user, 'secret'));
}

public function testEmailChangedMail(): void {
$this->user->method('getEMailAddress')->willReturn('new@example.com');

$this->template->expects($this->once())->method('addBodyText')
->with('Your email address on https://cloud.example.com/ was changed.');
$this->template->expects($this->once())->method('addBodyDetails')
->with($this->callback(fn (EMailDetails $details): bool => $this->detailsToArray($details) === [
'title' => 'Alice',
'initials' => 'Alice',
'rows' => [
['Previous email address', [[EMailDetailsRow::PART_TEXT, 'old@example.com']]],
['New email address', [[EMailDetailsRow::PART_TEXT, 'new@example.com']]],
],
]));
$this->template->expects($this->once())->method('addBodyNote')
->with('If you did not request this, please contact an administrator.', '', IEMailTemplate::NOTE_WARNING);
$this->mailer->expects($this->once())->method('send');

$this->hooks->handle(new UserChangedEvent($this->user, 'eMailAddress', 'new@example.com', 'old@example.com'));
}

public function testEmailRemovedMail(): void {
$this->user->method('getEMailAddress')->willReturn(null);

$this->template->expects($this->once())->method('addBodyDetails')
->with($this->callback(fn (EMailDetails $details): bool => $this->detailsToArray($details)['rows'] === [
['Previous email address', [[EMailDetailsRow::PART_TEXT, 'old@example.com']]],
['New email address', [[EMailDetailsRow::PART_MUTED, 'No email address set']]],
]));
$this->mailer->expects($this->once())->method('send');

$this->hooks->handle(new UserChangedEvent($this->user, 'eMailAddress', null, 'old@example.com'));
}

private function detailsToArray(EMailDetails $details): array {
return [
'title' => $details->getTitle(),
'initials' => $details->getInitialsName(),
'rows' => array_map(
fn (EMailDetailsRow $row): array => [
$row->getLabel(),
array_map(fn (array $part): array => [$part['type'], $part['text']], $row->getParts()),
],
$details->getRows(),
),
];
}
}
18 changes: 9 additions & 9 deletions apps/settings/tests/Mailer/NewUserMailHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,9 @@ public function testGenerateTemplateWithPasswordResetToken(): void {
.nc-link{color:#ebebeb!important}
.nc-border{border-color:#3b3b3b!important}
.nc-note-neutral,.nc-secondary{background:#2c2c2c!important}
.nc-note-info{background:#10303f!important}
.nc-note-warning{background:#3a2e10!important}
.nc-note-error{background:#3d1717!important}
.nc-note-info{background:#003553!important;border-left-color:#00AEFF!important}
.nc-note-warning{background:#3D3010!important;border-left-color:#FFEEC5!important}
.nc-note-error{background:#552121!important;border-left-color:#FFCCCC!important}
}
</style>
</head>
Expand Down Expand Up @@ -289,9 +289,9 @@ public function testGenerateTemplateWithoutPasswordResetToken(): void {
.nc-link{color:#ebebeb!important}
.nc-border{border-color:#3b3b3b!important}
.nc-note-neutral,.nc-secondary{background:#2c2c2c!important}
.nc-note-info{background:#10303f!important}
.nc-note-warning{background:#3a2e10!important}
.nc-note-error{background:#3d1717!important}
.nc-note-info{background:#003553!important;border-left-color:#00AEFF!important}
.nc-note-warning{background:#3D3010!important;border-left-color:#FFEEC5!important}
.nc-note-error{background:#552121!important;border-left-color:#FFCCCC!important}
}
</style>
</head>
Expand Down Expand Up @@ -410,9 +410,9 @@ public function testGenerateTemplateWithoutUserId(): void {
.nc-link{color:#ebebeb!important}
.nc-border{border-color:#3b3b3b!important}
.nc-note-neutral,.nc-secondary{background:#2c2c2c!important}
.nc-note-info{background:#10303f!important}
.nc-note-warning{background:#3a2e10!important}
.nc-note-error{background:#3d1717!important}
.nc-note-info{background:#003553!important;border-left-color:#00AEFF!important}
.nc-note-warning{background:#3D3010!important;border-left-color:#FFEEC5!important}
.nc-note-error{background:#552121!important;border-left-color:#FFCCCC!important}
}
</style>
</head>
Expand Down
5 changes: 2 additions & 3 deletions core/BackgroundJobs/CheckCodeIntegrityJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\L10N\IFactory;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Notification\IManager as INotificationManager;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -196,9 +197,7 @@ private function sendMail(IUser $admin, array $summary): void {
$template->setSubject($l->t('The code integrity check result has changed'));
$template->addHeader();
$template->addHeading($l->t('The code integrity check result has changed'));
foreach (self::formatSummary($l, $summary) as $sentence) {
$template->addBodyText($sentence);
}
$template->addBodyNote(implode("\n", self::formatSummary($l, $summary)), '', IEMailTemplate::NOTE_WARNING);
$template->addBodyButton(
$l->t('Review integrity check results'),
$this->urlGenerator->linkToRouteAbsolute('settings.AdminSettings.index', ['section' => 'overview']),
Expand Down
13 changes: 9 additions & 4 deletions lib/private/Authentication/Listeners/RemoteWipeEmailListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory as IL10nFactory;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Mail\IMessage;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -107,8 +108,10 @@ private function getWipingStartedMessage(RemoteWipeStarted $event, IUser $user):
$htmlHeading,
$plainHeading
);
$emailTemplate->addBodyText(
$this->l10n->t('Device or application »%s« has started the remote wipe process. You will receive another email once the process has finished', [$event->getToken()->getName()])
$emailTemplate->addBodyNote(
$this->l10n->t('Device or application »%s« has started the remote wipe process. You will receive another email once the process has finished', [$event->getToken()->getName()]),
'',
IEMailTemplate::NOTE_WARNING,
);
$emailTemplate->addFooter();
$message->setTo([$user->getEMailAddress()]);
Expand All @@ -135,8 +138,10 @@ private function getWipingFinishedMessage(RemoteWipeFinished $event, IUser $user
$htmlHeading,
$plainHeading
);
$emailTemplate->addBodyText(
$this->l10n->t('Device or application »%s« has finished the remote wipe process.', [$event->getToken()->getName()])
$emailTemplate->addBodyNote(
$this->l10n->t('Device or application »%s« has finished the remote wipe process.', [$event->getToken()->getName()]),
'',
IEMailTemplate::NOTE_INFO,
);
$emailTemplate->addFooter();
$message->setTo([$user->getEMailAddress()]);
Expand Down
14 changes: 7 additions & 7 deletions lib/private/Mail/EMailTemplate.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ class EMailTemplate implements IEMailTemplate {
.nc-link{color:#ebebeb!important}
.nc-border{border-color:#3b3b3b!important}
.nc-note-neutral,.nc-secondary{background:#2c2c2c!important}
.nc-note-info{background:#10303f!important}
.nc-note-warning{background:#3a2e10!important}
.nc-note-error{background:#3d1717!important}
.nc-note-info{background:#003553!important;border-left-color:#00AEFF!important}
.nc-note-warning{background:#3D3010!important;border-left-color:#FFEEC5!important}
.nc-note-error{background:#552121!important;border-left-color:#FFCCCC!important}
}
</style>
</head>
Expand Down Expand Up @@ -181,7 +181,7 @@ class EMailTemplate implements IEMailTemplate {
protected string $note = <<<EOF
<table role="presentation" width="100%%" cellpadding="0" cellspacing="0" style="Margin:0 0 20px;border-collapse:separate;border-spacing:0;margin:0 0 20px;width:100%%">
<tr>
<td class="nc-note-%4\$s nc-text" style="background:%1\$s;border-left:4px solid %2\$s;border-radius:12px;color:#222222;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;line-height:1.5;padding:16px 20px">%3\$s</td>
<td class="nc-note-%4\$s nc-text" style="background:%1\$s;border-left:4px solid %2\$s;border-radius:4px;color:#222222;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen-Sans,Ubuntu,Cantarell,'Helvetica Neue',Arial,sans-serif;font-size:16px;line-height:1.5;padding:16px 20px">%3\$s</td>
</tr>
</table>
EOF;
Expand All @@ -193,9 +193,9 @@ class EMailTemplate implements IEMailTemplate {
/** @var array<string, array{background: string, border: string}> */
protected array $noteColors = [
IEMailTemplate::NOTE_NEUTRAL => ['background' => '#f4f4f5', 'border' => 'transparent'],
IEMailTemplate::NOTE_INFO => ['background' => '#e5f0f5', 'border' => '#0071ad'],
IEMailTemplate::NOTE_WARNING => ['background' => '#fdf3dc', 'border' => '#a37200'],
IEMailTemplate::NOTE_ERROR => ['background' => '#fbe5e5', 'border' => '#c50000'],
IEMailTemplate::NOTE_INFO => ['background' => '#D5F1FA', 'border' => '#0066AC'],
IEMailTemplate::NOTE_WARNING => ['background' => '#FFEEC5', 'border' => '#664700'],
IEMailTemplate::NOTE_ERROR => ['background' => '#FFE7E7', 'border' => '#8A0000'],
];

protected string $detailsBegin = <<<EOF
Expand Down
28 changes: 27 additions & 1 deletion tests/Core/BackgroundJobs/CheckCodeIntegrityJobTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class CheckCodeIntegrityJobTest extends TestCase {
private IGroupManager&MockObject $groupManager;
private INotificationManager&MockObject $notificationManager;
private IMailer&MockObject $mailer;
private IEMailTemplate&MockObject $template;
private IL10N&MockObject $l10n;
/** @var list<string> recipients collected by expectAdminNotifications() */
private array $users = [];
Expand Down Expand Up @@ -76,7 +77,8 @@ protected function setUp(): void {
=> vsprintf(str_replace('%n', (string)$count, $count === 1 ? $singular : $plural), $parameters)
);

$this->mailer->method('createEMailTemplate')->willReturn($this->createMock(IEMailTemplate::class));
$this->template = $this->createMock(IEMailTemplate::class);
$this->mailer->method('createEMailTemplate')->willReturn($this->template);
$this->mailer->method('createMessage')->willReturn($this->createMock(IMessage::class));

$this->notificationManager->method('createNotification')->willReturnCallback(
Expand Down Expand Up @@ -182,6 +184,13 @@ public function testNotifiesAndMailsAdminsWhenCheckStartsFailing(): void {
$this->notificationManager->expects($this->never())->method('markProcessed');
$this->expectAdminNotifications($fingerprint, ['files' => 1, 'unverified' => []]);
$this->mailer->expects($this->once())->method('send');
$this->template->expects($this->never())->method('addBodyText');
$this->template->expects($this->once())->method('addBodyNote')->with(
'1 file does not match the signed release. It may have been modified or added without authorization.',
'',
IEMailTemplate::NOTE_WARNING,
);
$this->template->expects($this->once())->method('addBodyButton');
$this->expectStoredFingerprint($fingerprint);

self::invokePrivate($this->job, 'run', [null]);
Expand Down Expand Up @@ -218,6 +227,23 @@ public function testReplacesNotificationWhenFailureChanges(): void {
self::invokePrivate($this->job, 'run', [null]);
}

public function testMailPutsEverySentenceInOneWarningNote(): void {
$this->mockAdmins();
$results = self::FAILED_RESULT;
$results['files'] = ['EXCEPTION' => ['class' => \Exception::class, 'message' => 'Signature data not found.']];
$this->checker->method('getResults')->willReturn($results);
$this->appConfig->method('getValueString')->willReturn('');

$this->template->expects($this->once())->method('addBodyNote')->with(
"1 file does not match the signed release. It may have been modified or added without authorization.\n"
. 'The signature of files is missing or invalid, so it could not be verified.',
'',
IEMailTemplate::NOTE_WARNING,
);

self::invokePrivate($this->job, 'run', [null]);
}

public function testClearsNotificationWhenCheckPassesAgain(): void {
$this->checker->method('getResults')->willReturn([]);
$previous = $this->fingerprint(self::FAILED_RESULT);
Expand Down
Loading
Loading