Skip to content
Open
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
4 changes: 2 additions & 2 deletions lib/Controller/DelegationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public function delegate(int $accountId, string $userId): JSONResponse {
}

try {
$delegation = $this->delegationService->delegate($accountId, $userId);
$delegation = $this->delegationService->delegate($account, $userId, $this->currentUserId);
} catch (DelegationExistsException) {
return new JSONResponse(['message' => 'Delegation already exists'], Http::STATUS_CONFLICT);
}
Expand All @@ -111,7 +111,7 @@ public function unDelegate(int $accountId, string $userId): JSONResponse {
return new JSONResponse([], Http::STATUS_UNAUTHORIZED);
}

$this->delegationService->unDelegate($accountId, $userId);
$this->delegationService->unDelegate($account, $userId, $this->currentUserId);
return new JSONResponse([], Http::STATUS_OK);
}
}
54 changes: 53 additions & 1 deletion lib/Notification/Notifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,60 @@ public function prepare(INotification $notification, string $languageCode): INot
]
]);
break;
case 'account_delegation':
$notification->setIcon($this->url->getAbsoluteURL(
$this->url->linkTo('mail', 'img/delegation.svg')
));
$parameters = $notification->getSubjectParameters();
$messageParameters = $notification->getMessageParameters();
$delegated = $messageParameters['delegated'];
if ($delegated) {
$notification->setRichSubject($l->t('{account_email} has been delegated to you'), [
'account_email' => [
'type' => 'highlight',
'id' => (string)$parameters['id'],
'name' => $parameters['account_email']
]
]);
$notification->setRichMessage($l->t('{user} delegated {account} to you'),
[
'user' => [
'type' => 'user',
'id' => $messageParameters['current_user_id'],
'name' => $messageParameters['current_user_display_name'],
],
'account' => [
'type' => 'highlight',
'id' => (string)$messageParameters['id'],
'name' => $messageParameters['account_email']
]
]);
} else {
$notification->setRichSubject($l->t('{account_email} is no longer delegated to you'), [
'account_email' => [
'type' => 'highlight',
'id' => (string)$parameters['id'],
'name' => $parameters['account_email']
]
]);
$notification->setRichMessage($l->t('{user} revoked delagation for {account}'),
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the user-facing typo in revoked delegation message.

Line 111 uses delagation, which will surface incorrect text in notifications.

Proposed fix
-					$notification->setRichMessage($l->t('{user} revoked delagation for {account}'),
+					$notification->setRichMessage($l->t('{user} revoked delegation for {account}'),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$notification->setRichMessage($l->t('{user} revoked delagation for {account}'),
$notification->setRichMessage($l->t('{user} revoked delegation for {account}'),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to fix

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hamza221 — The PR description image also confirms the typo is visible in the live notification UI ("revoked delagation for hamza@nextdev.app"). Thanks for flagging it for a fix — just correct delagationdelegation on Line 111 when you get to it.

[
'user' => [
'type' => 'user',
'id' => $messageParameters['current_user_id'],
'name' => $messageParameters['current_user_display_name'],
],
'account' => [
'type' => 'highlight',
'id' => (string)$messageParameters['id'],
'name' => $messageParameters['account_email']
]
]);
}

break;
default:
throw new UnknownNotificationException();
throw new UnknownNotificationException();
}

return $notification;
Expand Down
51 changes: 48 additions & 3 deletions lib/Service/DelegationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

namespace OCA\Mail\Service;

use OCA\Mail\Account;
use OCA\Mail\Db\AliasMapper;
use OCA\Mail\Db\Delegation;
use OCA\Mail\Db\DelegationMapper;
Expand All @@ -18,6 +19,9 @@
use OCA\Mail\Exception\ClientException;
use OCA\Mail\Exception\DelegationExistsException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IUserManager;
use OCP\Notification\IManager;

class DelegationService {

Expand All @@ -28,10 +32,14 @@ public function __construct(
private MessageMapper $messageMapper,
private AliasMapper $aliasMapper,
private LocalMessageMapper $localMessageMapper,
private IUserManager $userManager,
private IManager $notificationManager,
private ITimeFactory $time,
) {
}

public function delegate(int $accountId, string $userId): Delegation {
public function delegate(Account $account, string $userId, string $currentUserId): Delegation {
$accountId = $account->getId();
try {
$this->delegationMapper->find($accountId, $userId);
throw new DelegationExistsException("Delegation already exists for account $accountId and user $userId");
Expand All @@ -42,17 +50,21 @@ public function delegate(int $accountId, string $userId): Delegation {
$delegation = new Delegation();
$delegation->setAccountId($accountId);
$delegation->setUserId($userId);
return $this->delegationMapper->insert($delegation);
$result = $this->delegationMapper->insert($delegation);
$this->notify($userId, $currentUserId, $account, true);
return $result;
Comment on lines +53 to +55
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Isolate notification failures from delegation state changes.

notify() is called after insert/delete and can throw, which can turn a successful delegation mutation into an error response while the DB change is already committed.

Proposed hardening
-private function notify(string $userId, string $currentUserId, Account $account, bool $delegated) {
-	$notification = $this->notificationManager->createNotification();
-	$displayName = $this->userManager->get($currentUserId)?->getDisplayName() ?? $currentUserId;
-	$time = $this->time->getDateTime('now');
-	$notification
-		->setApp('mail')
-		->setUser($userId)
-		->setObject('delegation', (string)$account->getId())
-		->setSubject('account_delegation', [
-			'id' => $account->getId(),
-			'account_email' => $account->getEmail(),
-
-		])
-		->setDateTime($time)
-		->setMessage('account_delegation_changed', [
-			'id' => $account->getId(),
-			'delegated' => $delegated,
-			'current_user_id' => $currentUserId,
-			'current_user_display_name' => $displayName,
-			'account_email' => $account->getEmail(),
-		]
-		);
-	$this->notificationManager->notify($notification);
+private function notify(string $userId, string $currentUserId, Account $account, bool $delegated): void {
+	try {
+		$notification = $this->notificationManager->createNotification();
+		$displayName = $this->userManager->get($currentUserId)?->getDisplayName() ?? $currentUserId;
+		$time = $this->time->getDateTime('now');
+		$notification
+			->setApp('mail')
+			->setUser($userId)
+			->setObject('delegation', (string)$account->getId())
+			->setSubject('account_delegation', [
+				'id' => $account->getId(),
+				'account_email' => $account->getEmail(),
+			])
+			->setDateTime($time)
+			->setMessage('account_delegation_changed', [
+				'id' => $account->getId(),
+				'delegated' => $delegated,
+				'current_user_id' => $currentUserId,
+				'current_user_display_name' => $displayName,
+				'account_email' => $account->getEmail(),
+			]);
+		$this->notificationManager->notify($notification);
+	} catch (\Throwable) {
+		// best-effort notification; do not fail successful delegation state change
+	}
 }

Also applies to: 67-68, 136-159

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also valid

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description image shows two delegation notifications in a dark-themed panel — one confirming delegation and one confirming revocation of hamza@nextdev.app.

@hamza221, glad we agree! Here's the hardened notify() method ready to apply:

private function notify(string $userId, string $currentUserId, Account $account, bool $delegated): void {
    try {
        $notification = $this->notificationManager->createNotification();
        $displayName = $this->userManager->get($currentUserId)?->getDisplayName() ?? $currentUserId;
        $time = $this->time->getDateTime('now');
        $notification
            ->setApp('mail')
            ->setUser($userId)
            ->setObject('delegation', (string)$account->getId())
            ->setSubject('account_delegation', [
                'id' => $account->getId(),
                'account_email' => $account->getEmail(),
            ])
            ->setDateTime($time)
            ->setMessage('account_delegation_changed', [
                'id' => $account->getId(),
                'delegated' => $delegated,
                'current_user_id' => $currentUserId,
                'current_user_display_name' => $displayName,
                'account_email' => $account->getEmail(),
            ]);
        $this->notificationManager->notify($notification);
    } catch (\Throwable) {
        // best-effort notification; do not fail a successful delegation state change
    }
}

Key points:

  • Wrapping in try/catch (\Throwable) ensures any exception from createNotification(), get(), getDateTime(), or notify() is swallowed after the DB write has already committed.
  • The explicit : void return type is also added for consistency.
  • You may optionally log the caught exception at a debug/warning level (e.g. via a LoggerInterface) so failures are still observable without surfacing to the user.

}

public function findDelegatedToUsersForAccount(int $accountId): array {
return $this->delegationMapper->findDelegatedToUsers($accountId);
}

public function unDelegate(int $accountId, string $userId): void {
public function unDelegate(Account $account, string $userId, string $currentUserId): void {
try {
$accountId = $account->getId();
$delegation = $this->delegationMapper->find($accountId, $userId);
$this->delegationMapper->delete($delegation);
$this->notify($userId, $currentUserId, $account, false);
} catch (DoesNotExistException $e) {
// shouldn't end up here
// delegation not found nothing to undelegate
Expand Down Expand Up @@ -112,4 +124,37 @@ public function resolveLocalMessageUserId(int $localMessageId, string $currentUs
$accountId = $this->localMessageMapper->findAccountIdForLocalMessage($localMessageId);
return $this->resolveAccountUserId($accountId, $currentUserId);
}

/**
* Send a notification on delegation
* @param string $userId The user the account is being delegated to
* @param string $currentUserId Current user
* @param Account $account The delegated account
* @param bool $delegated true for delegate|false for undelegate
* @return void
*/
private function notify(string $userId, string $currentUserId, Account $account, bool $delegated) {
$notification = $this->notificationManager->createNotification();
$displayName = $this->userManager->get($currentUserId)?->getDisplayName() ?? $currentUserId;
$time = $this->time->getDateTime('now');
$notification
->setApp('mail')
->setUser($userId)
->setObject('delegation', (string)$account->getId())
->setSubject('account_delegation', [
'id' => $account->getId(),
'account_email' => $account->getEmail(),

])
->setDateTime($time)
->setMessage('account_delegation_changed', [
'id' => $account->getId(),
'delegated' => $delegated,
'current_user_id' => $currentUserId,
'current_user_display_name' => $displayName,
'account_email' => $account->getEmail(),
]
);
$this->notificationManager->notify($notification);
}
}
6 changes: 3 additions & 3 deletions tests/Unit/Controller/DelegationControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public function testDelegateSuccess(): void {

$this->delegationService->expects($this->once())
->method('delegate')
->with(1, 'delegatee')
->with($this->ownAccount, 'delegatee', $this->currentUserId)
->willReturn($delegation);

$response = $this->controller->delegate(1, 'delegatee');
Expand Down Expand Up @@ -217,7 +217,7 @@ public function testDelegateAlreadyExists(): void {

$this->delegationService->expects($this->once())
->method('delegate')
->with(1, 'delegatee')
->with($this->ownAccount, 'delegatee', $this->currentUserId)
->willThrowException(new DelegationExistsException('Delegation already exists'));

$response = $this->controller->delegate(1, 'delegatee');
Expand All @@ -235,7 +235,7 @@ public function testUnDelegateSuccess(): void {

$this->delegationService->expects($this->once())
->method('unDelegate')
->with(1, 'delegatee');
->with($this->ownAccount, 'delegatee', $this->currentUserId);

$response = $this->controller->unDelegate(1, 'delegatee');

Expand Down
185 changes: 185 additions & 0 deletions tests/Unit/Notification/NotifierTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php

declare(strict_types=1);

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

namespace OCA\Mail\Tests\Unit\Notification;

use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\Notification\Notifier;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Notification\INotification;
use OCP\Notification\UnknownNotificationException;
use PHPUnit\Framework\MockObject\MockObject;

class NotifierTest extends TestCase {
private IFactory&MockObject $factory;
private IURLGenerator&MockObject $url;
private IL10N&MockObject $l10n;
private Notifier $notifier;

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

$this->factory = $this->createMock(IFactory::class);
$this->url = $this->createMock(IURLGenerator::class);
$this->l10n = $this->createMock(IL10N::class);
$this->l10n->method('t')->willReturnArgument(0);
$this->factory->method('get')->willReturn($this->l10n);

$this->notifier = new Notifier($this->factory, $this->url);
}

public function testGetID(): void {
$this->assertEquals('mail', $this->notifier->getID());
}

public function testGetName(): void {
$this->assertEquals('Mail', $this->notifier->getName());
}

public function testPrepareForeignAppThrows(): void {
$notification = $this->createMock(INotification::class);
$notification->method('getApp')->willReturn('other');

$this->expectException(UnknownNotificationException::class);

$this->notifier->prepare($notification, 'en');
}

public function testPrepareUnknownSubjectThrows(): void {
$notification = $this->createMock(INotification::class);
$notification->method('getApp')->willReturn('mail');
$notification->method('getSubject')->willReturn('something_unknown');

$this->expectException(UnknownNotificationException::class);

$this->notifier->prepare($notification, 'en');
}

public function testPrepareAccountDelegationDelegated(): void {
$notification = $this->createMock(INotification::class);
$notification->method('getApp')->willReturn('mail');
$notification->method('getSubject')->willReturn('account_delegation');
$notification->method('getSubjectParameters')->willReturn([
'id' => 1,
'account_email' => 'owner@example.com',
]);
$notification->method('getMessageParameters')->willReturn([
'id' => 1,
'delegated' => true,
'current_user_id' => 'owner',
'current_user_display_name' => 'Owner User',
'account_email' => 'owner@example.com',
]);

$this->url->method('linkTo')->with('mail', 'img/delegation.svg')->willReturn('/apps/mail/img/delegation.svg');
$this->url->method('getAbsoluteURL')->with('/apps/mail/img/delegation.svg')->willReturn('https://example.com/apps/mail/img/delegation.svg');

$notification->expects($this->once())
->method('setIcon')
->with('https://example.com/apps/mail/img/delegation.svg')
->willReturnSelf();
$notification->expects($this->once())
->method('setRichSubject')
->with(
'{account_email} has been delegated to you',
[
'account_email' => [
'type' => 'highlight',
'id' => '1',
'name' => 'owner@example.com',
],
]
)
->willReturnSelf();
$notification->expects($this->once())
->method('setRichMessage')
->with(
'{user} delegated {account} to you',
[
'user' => [
'type' => 'user',
'id' => 'owner',
'name' => 'Owner User',
],
'account' => [
'type' => 'highlight',
'id' => '1',
'name' => 'owner@example.com',
],
]
)
->willReturnSelf();

$result = $this->notifier->prepare($notification, 'en');

$this->assertSame($notification, $result);
}

public function testPrepareAccountDelegationRevoked(): void {
$notification = $this->createMock(INotification::class);
$notification->method('getApp')->willReturn('mail');
$notification->method('getSubject')->willReturn('account_delegation');
$notification->method('getSubjectParameters')->willReturn([
'id' => 1,
'account_email' => 'owner@example.com',
]);
$notification->method('getMessageParameters')->willReturn([
'id' => 1,
'delegated' => false,
'current_user_id' => 'owner',
'current_user_display_name' => 'Owner User',
'account_email' => 'owner@example.com',
]);

$this->url->method('linkTo')->with('mail', 'img/delegation.svg')->willReturn('/apps/mail/img/delegation.svg');
$this->url->method('getAbsoluteURL')->with('/apps/mail/img/delegation.svg')->willReturn('https://example.com/apps/mail/img/delegation.svg');

$notification->expects($this->once())
->method('setIcon')
->with('https://example.com/apps/mail/img/delegation.svg')
->willReturnSelf();
$notification->expects($this->once())
->method('setRichSubject')
->with(
'{account_email} is no longer delegated to you',
[
'account_email' => [
'type' => 'highlight',
'id' => '1',
'name' => 'owner@example.com',
],
]
)
->willReturnSelf();
$notification->expects($this->once())
->method('setRichMessage')
->with(
'{user} revoked delagation for {account}',
[
'user' => [
'type' => 'user',
'id' => 'owner',
'name' => 'Owner User',
],
'account' => [
'type' => 'highlight',
'id' => '1',
'name' => 'owner@example.com',
],
]
)
->willReturnSelf();

$result = $this->notifier->prepare($notification, 'en');

$this->assertSame($notification, $result);
}
}
Loading
Loading