Skip to content

feat: Account delegation#12727

Open
hamza221 wants to merge 9 commits into
mainfrom
feat/delegation-mvp
Open

feat: Account delegation#12727
hamza221 wants to merge 9 commits into
mainfrom
feat/delegation-mvp

Conversation

@hamza221
Copy link
Copy Markdown
Contributor

@hamza221 hamza221 commented Apr 14, 2026

Mvp for #12403

1 2 3 4
image image image image

Summary by CodeRabbit

  • New Features

    • Add mail delegation: modal UI to add/remove delegates and API endpoints to list/create/remove delegations
    • Backend delegation service with DB migration to store delegations
  • Improvements

    • Delegated accounts shown alongside your own and labeled "(delegated)"
    • Server settings and account removal hidden for delegated accounts; "Delegate account" added to account menu
    • App version bumped to 5.9.0-dev.3
  • Tests

    • New and updated unit/integration tests covering delegation flows

@hamza221 hamza221 changed the title feat: add delegation backend feat: Account delegation Apr 14, 2026
@hamza221 hamza221 added enhancement 3. to review skill:backend Issues and PRs that require backend development skills skill:frontend Issues and PRs that require JavaScript/Vue/styling development skills labels Apr 14, 2026
@hamza221 hamza221 self-assigned this Apr 14, 2026
@hamza221 hamza221 requested a review from nimishavijay April 14, 2026 15:05
@hamza221 hamza221 marked this pull request as ready for review April 14, 2026 15:30
@hamza221 hamza221 requested a review from kra-mo April 14, 2026 15:32
@hamza221
Copy link
Copy Markdown
Contributor Author

Copied over design review from the other pr

Nimisha

Awesome! Looks really nice! Only some small changes. Overall:

  • The icon for delegation (across the board) can be supervisor account. The one you've chosen is also very similar but we use that for indicating users and groups
  • Instead of "Delegation", I'm wondering if we can use the wording "Delegate account" in the 3 dot menu and the modal title
  • Use tertiary buttons for all the "Cancel" buttons and don't use the icon.

In the "Delegation" modal:

  • There is some whitespace on top which shouldn't be there (somewhow not there in the "Add delegates" modal)
  • We can also center align the "Delegates" heading. Ik in kra-mo's mockups it was left aligned, but because in the component we always have the heading in the center it looks inconsistent now.
  • I see that there's a h3 { font-weight: bold; } which makes the "Delegates" heading stronger than its parent heading. We could remove that

In the "Add delegates" modal:

  • the select component should be full width

Laura

We can also center align the "Delegates" heading.

I was going off of form group components, not a dialog, in this context, I think the two different headings don't make sense either, we could just have one "Delegates" title :)

Laura

Adding to what nimishavijay said, the "Revoke" button should also probably not have an icon, a checkmark with danger styling is always confusing :)

Additionally, the user selection dropdown should be full-width and there should be a bit more padding between the explanation text beneath and the buttons, also in the revoke access dialog.

Comment thread lib/Controller/DelegationController.php Outdated
Comment thread lib/Controller/DelegationController.php Outdated
Comment thread lib/Controller/DelegationController.php
Comment thread lib/Controller/DelegationController.php Outdated
@kesselb
Copy link
Copy Markdown
Contributor

kesselb commented Apr 20, 2026

For provisioned accounts I would suggest to hide the ui option to create a delegation (and maybe even api wise) for now.


We have currently two (soon 3) way how authentication works: 1) by using the password from the user session, 2) static password, 3) master password (which is similar to static password).

Case 1: This needs the password of the owner and hence, delegating would only work if the user has been logged in once. That should be the case anyway, however this still seems a bit fragile (think of password change cases, etc).

Case 2: Static password should work, but in the current version wa also use the middleware to update the password which probably wont work currently.

Case 3: Master password should work but is currently not merged. Community PR that I took over #12442. It needs a bit of polishing and we wanted to move the password updating from the middleware to the factory.

@hamza221
Copy link
Copy Markdown
Contributor Author

For provisioned accounts I would suggest to hide the ui option to create a delegation (and maybe even api wise) for now.

We have currently two (soon 3) way how authentication works: 1) by using the password from the user session, 2) static password, 3) master password (which is similar to static password).

Case 1: This needs the password of the owner and hence, delegating would only work if the user has been logged in once. That should be the case anyway, however this still seems a bit fragile (think of password change cases, etc).

Case 2: Static password should work, but in the current version wa also use the middleware to update the password which probably wont work currently.

Case 3: Master password should work but is currently not merged. Community PR that I took over #12442. It needs a bit of polishing and we wanted to move the password updating from the middleware to the factory.

Done, It's enough to check if the account has a provisioning id right ?

@hamza221 hamza221 requested a review from kesselb April 27, 2026 18:36
@hamza221 hamza221 requested a review from Copilot May 5, 2026 12:31
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Implements an MVP for “account delegation” so a user can act on behalf of another user’s mail account, including backend delegation storage/lookup and frontend UI for managing delegates.

Changes:

  • Adds delegation persistence and domain logic (DB table + mapper/entity + service + controller + routes).
  • Updates multiple controllers to resolve an “effective user” via delegation when accessing accounts/mailboxes/messages/local messages/aliases.
  • Adds frontend delegation modal + navigation entry and updates API/account payloads with an isDelegated flag; updates unit/integration tests accordingly.

Reviewed changes

Copilot reviewed 51 out of 52 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tests/Unit/Service/DelegationServiceTest.php Adds unit tests for delegation service operations and effective user resolution.
tests/Unit/Service/AccountServiceTest.php Updates constructor wiring to include the new delegation mapper dependency.
tests/Unit/Controller/ThreadControllerTest.php Adapts tests for ThreadController’s new DelegationService dependency and behavior.
tests/Unit/Controller/SieveControllerTest.php Mocks delegation-based account user resolution for sieve endpoints.
tests/Unit/Controller/PageControllerTest.php Updates expectations for delegated accounts + isDelegated field in initial state.
tests/Unit/Controller/OutboxControllerTest.php Adds DelegationService dependency/mocks for delegated local message/account access.
tests/Unit/Controller/MessagesControllerTest.php Adds DelegationService dependency/mocks for delegated message/mailbox access.
tests/Unit/Controller/MessageApiControllerTest.php Adds DelegationService dependency/mocks for delegated account/message access.
tests/Unit/Controller/MailboxesControllerTest.php Adds DelegationService dependency/mocks for delegated account/mailbox access.
tests/Unit/Controller/MailboxesApiControllerTest.php Adds DelegationService dependency/mocks for delegated account/mailbox access.
tests/Unit/Controller/ListControllerTest.php Mocks delegation-based message user resolution for list actions.
tests/Unit/Controller/DraftsControllerTest.php Adds DelegationService dependency/mocks for delegated draft/local message access.
tests/Unit/Controller/DelegationControllerTest.php Adds unit tests for new DelegationController endpoints and authorization paths.
tests/Unit/Controller/AliasesControllerTest.php Adds DelegationService dependency/mocks for delegated alias/account access.
tests/Unit/Controller/AccountsControllerTest.php Adds DelegationService dependency/mocks and expects delegated accounts list calls.
tests/Unit/Controller/AccountApiControllerTest.php Adds DelegationService dependency and expects delegated accounts list calls.
tests/Integration/MailboxSynchronizationTest.php Wires DelegationService into MailboxesController in integration setup.
src/service/DelegationService.js Adds frontend API wrapper for delegation CRUD endpoints.
src/components/NavigationAccount.vue Adds “Delegate account” action + modal trigger and delegation eligibility logic.
src/components/DelegationModal.vue Adds UI to list/add/revoke delegates and user search integration.
src/components/AppSettingsMenu.vue Separates delegated accounts in settings menu and filters normal account list.
lib/Service/DelegationService.php Implements delegation CRUD and effective user resolution for account/mailbox/message/alias/local message.
lib/Service/AccountService.php Adds fetching delegated accounts for the current user.
lib/ResponseDefinitions.php Extends account list response typing with isDelegated.
lib/Migration/Version5008Date20260312152753.php Adds mail_delegations table with unique index and FK to mail_accounts.
lib/Exception/DelegationExistsException.php Adds a typed exception for existing delegations.
lib/Db/MessageMapper.php Adds query method to get account id from message.
lib/Db/MailboxMapper.php Adds query method to get account id from mailbox.
lib/Db/LocalMessageMapper.php Adds query method to get account id from local message.
lib/Db/DelegationMapper.php Adds DB access methods for delegation lookup and owner resolution.
lib/Db/Delegation.php Adds delegation entity and JSON serialization.
lib/Db/AliasMapper.php Adds query method to get account id from alias.
lib/Controller/ThreadController.php Resolves effective user for thread actions (move/snooze/delete/summarize/etc.).
lib/Controller/SieveController.php Resolves effective user for sieve operations.
lib/Controller/PageController.php Adds delegated accounts to initial state / accounts JSON with isDelegated.
lib/Controller/OutboxController.php Resolves effective user for outbox/draft/local message operations.
lib/Controller/MessagesController.php Resolves effective user for mailbox/message operations + AI actions.
lib/Controller/MessageApiController.php Resolves effective user for message API operations (send/get/raw/attachments).
lib/Controller/MailboxesController.php Resolves effective user for mailbox operations (index/patch/sync/etc.).
lib/Controller/MailboxesApiController.php Resolves effective user for mailboxes API list and listMessages endpoints.
lib/Controller/ListController.php Resolves effective user for unsubscribe based on message id.
lib/Controller/FollowUpController.php Resolves effective user for mailbox access when checking follow-up message ids.
lib/Controller/FilterController.php Resolves effective user for filter get/update authorization.
lib/Controller/DraftsController.php Resolves effective user for draft/local message operations.
lib/Controller/DelegationController.php Adds API endpoints to list/add/remove delegates for an account.
lib/Controller/AliasesController.php Resolves effective user for alias CRUD based on alias/account ownership/delegation.
lib/Controller/AccountsController.php Adds delegated accounts to index and resolves effective user for account actions.
lib/Controller/AccountApiController.php Returns owned + delegated accounts with isDelegated and aliases.
img/delegation.svg Adds delegation icon asset for UI.
appinfo/routes.php Registers delegation API routes.
appinfo/info.xml Bumps app version.
REUSE.toml Adds new SVG asset to REUSE annotations.
Comments suppressed due to low confidence (4)

src/components/DelegationModal.vue:1

  • The modal uses this.account.accountId for API calls, but other components (e.g., NavigationAccount) treat the account identifier as account.id. If the passed account object doesn’t contain accountId, these calls will send undefined to the backend routes and fail. Align on a single field (likely account.id) or adapt the prop shape so the ID is consistently available.
    src/components/DelegationModal.vue:1
  • The modal uses this.account.accountId for API calls, but other components (e.g., NavigationAccount) treat the account identifier as account.id. If the passed account object doesn’t contain accountId, these calls will send undefined to the backend routes and fail. Align on a single field (likely account.id) or adapt the prop shape so the ID is consistently available.
    src/components/DelegationModal.vue:1
  • The modal uses this.account.accountId for API calls, but other components (e.g., NavigationAccount) treat the account identifier as account.id. If the passed account object doesn’t contain accountId, these calls will send undefined to the backend routes and fail. Align on a single field (likely account.id) or adapt the prop shape so the ID is consistently available.
    lib/Service/DelegationService.php:1
  • This is subject to a race: two concurrent requests can both pass find() (non-existent) and then one insert will violate the unique index. If the insert throws a DB exception, it will likely surface as a 500. Prefer relying on the DB uniqueness and catching the unique-constraint violation on insert (mapping it to DelegationExistsException), or keep the existence check but still handle insert-time uniqueness violations.

Comment thread lib/Controller/OutboxController.php Outdated
Comment thread lib/Controller/DraftsController.php Outdated
Comment thread lib/Controller/AccountsController.php Outdated
Comment thread lib/Service/AccountService.php Outdated
Comment thread lib/Db/DelegationMapper.php
Comment thread lib/Controller/DelegationController.php Outdated
Comment thread lib/Controller/AccountApiController.php Outdated
@hamza221
Copy link
Copy Markdown
Contributor Author

hamza221 commented May 5, 2026

@copilot Any undesired behaviors or bugs introduced by this PR to the SMIME encryption feature

Copy link
Copy Markdown
Contributor

Copilot AI commented May 5, 2026

@copilot Any undesired behaviors or bugs introduced by this PR to the SMIME encryption feature

Yes, there is one S/MIME bug introduced by the delegation changes.

Bug: In DraftsController and OutboxController, when creating or updating messages with S/MIME signing/encryption, the certificate was looked up using $this->userId (the current user — potentially a delegate) instead of $effectiveUserId (the account owner). Since S/MIME certificates are scoped per user, a delegate creating/updating a draft or outbox message for an owned account would get a DoesNotExistException on findCertificate() because the certificate belongs to the owner, not the delegate.

Fixed in commit 65bca97 — replaced $this->userId with $effectiveUserId in 4 locations across DraftsController.create(), DraftsController.update(), OutboxController.create(), and OutboxController.update().

Note: S/MIME decryption when reading messages works correctly — getImapMessage() uses $account->getUserId() (the owner's ID), which is correctly resolved via $effectiveUserId in MessagesController.

Comment thread lib/Controller/AccountsController.php Outdated
Comment thread lib/Controller/DelegationController.php Outdated
Comment thread lib/Controller/AccountsController.php Outdated
@hamza221 hamza221 force-pushed the feat/delegation-mvp branch from 65bca97 to 99884ca Compare May 7, 2026 14:43
@hamza221
Copy link
Copy Markdown
Contributor Author

hamza221 commented May 7, 2026

resolved conflicts, proceeding with the reviews now

Comment thread lib/Migration/Version5008Date20260312152753.php
@hamza221 hamza221 requested review from ChristophWurst and Copilot May 7, 2026 15:35
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

hamza221 and others added 5 commits May 7, 2026 22:35
Signed-off-by: Hamza <hamzamahjoubi221@gmail.com>
Signed-off-by: Hamza <hamzamahjoubi221@gmail.com>
Signed-off-by: Hamza <hamzamahjoubi221@gmail.com>
Signed-off-by: Hamza <hamzamahjoubi221@gmail.com>
@kesselb kesselb force-pushed the feat/delegation-mvp branch from e91d8d4 to caa529d Compare May 7, 2026 20:35
@kesselb
Copy link
Copy Markdown
Contributor

kesselb commented May 7, 2026

For the final commit, I would prefer to have one "feat: Account delegation".

If you want to keep the current structure, please use the right types.

feat: add delegation backend => feat(delegation): Add delegation backend
feat(delegation): add unit tests => test(delegation): Add unit tests for delegation backend
feat: add delegation frontend => feat(delegation): Add delegation frontend

@hamza221
Copy link
Copy Markdown
Contributor Author

hamza221 commented May 7, 2026

For the final commit, I would prefer to have one "feat: Account delegation".

If you want to keep the current structure, please use the right types.

feat: add delegation backend => feat(delegation): Add delegation backend feat(delegation): add unit tests => test(delegation): Add unit tests for delegation backend feat: add delegation frontend => feat(delegation): Add delegation frontend

I'll squash into one commit once ready

Comment thread lib/Migration/Version5008Date20260312152753.php Outdated
Comment thread lib/Controller/AccountsController.php Outdated
Comment thread lib/Controller/AccountApiController.php Outdated
Comment thread lib/Controller/AccountApiController.php Outdated
Comment thread lib/Db/MessageMapper.php Outdated
Comment thread lib/Service/DelegationService.php Outdated
Comment thread src/components/DelegationModal.vue
Comment thread lib/Service/DelegationService.php Outdated
Signed-off-by: Hamza <hamzamahjoubi221@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5879e793-2b2e-42b5-b49b-c98704a1bbe7

📥 Commits

Reviewing files that changed from the base of the PR and between 6400561 and b4ab4b4.

📒 Files selected for processing (2)
  • lib/Controller/AccountApiController.php
  • tests/Unit/Controller/AccountApiControllerTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/Controller/AccountApiController.php
  • tests/Unit/Controller/AccountApiControllerTest.php

📝 Walkthrough

Walkthrough

Adds mailbox delegation: a DB migration and Delegation entity, mappers, a DelegationService with resolution helpers, three REST endpoints and DelegationController, delegation-aware wiring across Mail controllers, frontend modal and JS service to manage delegates, updated response types, updated REUSE metadata and version, and unit/integration tests.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Account delegation' clearly and concisely summarizes the main feature being added—a delegation system that allows users to delegate mail account access to others.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 53 out of 54 changed files in this pull request and generated 4 comments.

Comment on lines +238 to +239
$effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId);
$account = $this->accountService->find($effectiveUserId, $id);
Comment thread lib/Controller/SieveController.php
Comment on lines +44 to 48
$effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->currentUserId);
$account = $this->accountService->findById($accountId);

if ($account->getUserId() !== $this->currentUserId) {
if ($account->getUserId() !== $effectiveUserId) {
return new JSONResponse([], Http::STATUS_NOT_FOUND);
Comment on lines +34 to +46
public function delegate(int $accountId, string $userId): Delegation {
try {
$this->delegationMapper->find($accountId, $userId);
throw new DelegationExistsException("Delegation already exists for account $accountId and user $userId");
} catch (DoesNotExistException) {
// delegation doesn't exist, continue
}

$delegation = new Delegation();
$delegation->setAccountId($accountId);
$delegation->setUserId($userId);
return $this->delegationMapper->insert($delegation);
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/Controller/ThreadController.php (1)

5-8: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

License header uses AGPL-3.0-only instead of AGPL-3.0-or-later.

The SPDX license identifier should be AGPL-3.0-or-later per coding guidelines.

Proposed fix
 /**
  * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-License-Identifier: AGPL-3.0-or-later
  */

As per coding guidelines: "New files must use AGPL-3.0-or-later license identifier, never AGPL-3.0-only".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Controller/ThreadController.php` around lines 5 - 8, The SPDX license
header at the top of ThreadController.php uses "AGPL-3.0-only"; update the
SPDX-License-Identifier token to "AGPL-3.0-or-later" in the file header (the
comment block above the ThreadController class) so the file conforms to the
project's license identifier guideline.
lib/Controller/SieveController.php (1)

5-8: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

License header uses AGPL-3.0-only instead of AGPL-3.0-or-later.

The SPDX license identifier should be AGPL-3.0-or-later per coding guidelines.

Proposed fix
 /**
  * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
- * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-License-Identifier: AGPL-3.0-or-later
  */

As per coding guidelines: "New files must use AGPL-3.0-or-later license identifier, never AGPL-3.0-only".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Controller/SieveController.php` around lines 5 - 8, Update the SPDX
license identifier in the file header of SieveController.php: replace
"SPDX-License-Identifier: AGPL-3.0-only" with "SPDX-License-Identifier:
AGPL-3.0-or-later" in the top comment block so the file uses the required
AGPL-3.0-or-later identifier.
🧹 Nitpick comments (5)
src/components/AccountSettings.vue (1)

109-109: ⚡ Quick win

Guard scrollToAccountSettings when AccountForm can be absent.

With Line 109 hiding the mail-server section for delegated accounts, this.$refs.accountForm may be undefined when rename-primary-alias is emitted. Add a guard before dereferencing.

Suggested fix
scrollToAccountSettings() {
-	this.$refs.accountForm.$el.scrollIntoView({
+	const accountForm = this.$refs.accountForm
+	if (!accountForm?.$el) {
+		return
+	}
+	accountForm.$el.scrollIntoView({
		behavior: 'smooth',
	})
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/AccountSettings.vue` at line 109, The handler for the
rename-primary-alias event calls scrollToAccountSettings which dereferences
this.$refs.accountForm that can be undefined when the mail-server section is
hidden; update the event handler (where rename-primary-alias is handled) to
guard before accessing this.$refs.accountForm (or calling
scrollToAccountSettings) by checking that this.$refs.accountForm exists (or that
accountForm ref is present) and only then invoking scrollToAccountSettings,
ensuring no direct dereference of undefined AccountForm.
tests/Unit/Controller/AccountsControllerTest.php (1)

154-157: ⚡ Quick win

Add a positive delegated-account case in testIndex.

Line 154 currently validates only the empty delegated list path. Please add one case where findDelegatedAccounts returns at least one delegated account and assert isDelegated is true, so regressions in delegated serialization don’t slip through.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/Controller/AccountsControllerTest.php` around lines 154 - 157, Add
a positive delegated-account path inside the testIndex unit test: mock
AccountService::findDelegatedAccounts to return a non-empty array (e.g., one
delegated account) for $this->userId, call the controller/index flow as in the
existing empty-case, and add assertions that the serialized response marks that
account with isDelegated === true; update the mock expectation for
findDelegatedAccounts (or add a second expectation) and assert the output
contains the delegated account and its isDelegated flag is true to prevent
regression in delegated serialization.
lib/Controller/PageController.php (1)

187-205: ⚡ Quick win

Consider consolidating account transformation logic.

The account-to-JSON transformation logic for delegated accounts (lines 187-205) largely duplicates the owned account transformation (lines 169-186). Consider extracting this shared logic into a private helper method or a service to reduce duplication and improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Controller/PageController.php` around lines 187 - 205, The
delegated-account loop duplicates the owned-account JSON transformation; extract
the shared logic into a private helper like transformAccountToJson($account,
bool $isDelegated = false) and call it from both places (use current usages of
$delegatedAccount->jsonSerialize(), aliasesService->findAll(...),
mailManager->getMailboxes(...), and logger handling inside the helper). The
helper should accept an Account object, set isDelegated accordingly, populate
aliases via $this->aliasesService->findAll($account->getId(),
$account->getUserId()), attempt to load mailboxes with
$this->mailManager->getMailboxes($account) inside the same try/catch that logs
using $this->logger and sets mailboxes/error defaults, and return the assembled
array so callers (both owned and delegated loops) push the returned array into
$accountsJson.
lib/Controller/DelegationController.php (1)

50-52: 💤 Low value

Consider using HTTP 403 FORBIDDEN instead of 401 UNAUTHORIZED for authorization failures.

When a user is authenticated but attempts to access another user's account, HTTP 403 FORBIDDEN is more semantically correct than 401 UNAUTHORIZED:

  • 401 UNAUTHORIZED: Authentication required (no valid credentials)
  • 403 FORBIDDEN: Authenticated but not authorized for this resource

The user has a valid session ($this->currentUserId is set via injection), they're just not the owner of the account.

Suggested change
         if ($account->getUserId() !== $this->currentUserId) {
-            return new JSONResponse([], Http::STATUS_UNAUTHORIZED);
+            return new JSONResponse([], Http::STATUS_FORBIDDEN);
         }

Also applies to: 72-74, 110-112

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Controller/DelegationController.php` around lines 50 - 52, Replace the
401 responses used for authorization failures with 403 FORBIDDEN: locate the
checks in DelegationController where you compare $account->getUserId() !==
$this->currentUserId (and the analogous checks at the other occurrences
referenced) and change the returned JSONResponse from Http::STATUS_UNAUTHORIZED
to Http::STATUS_FORBIDDEN so authenticated-but-unauthorized access returns 403;
keep the response body unchanged.
tests/Unit/Service/DelegationServiceTest.php (1)

41-44: ⚡ Quick win

Remove unused test fixtures.

The test declares and initializes mocks for IUserManager, IManager (notification manager), ITimeFactory, and IEventDispatcher (lines 41-44, 58-61), and defines a mockNotification() helper (lines 79-94) that's called in two tests (lines 97, 156). However, DelegationService's constructor (lines 63-70 in the service file) does not accept any of these dependencies—it only injects the mapper collaborators. These mocks are never used and represent dead test code.

🧹 Proposed cleanup

Remove the unused mock properties, their initialization in setUp(), and the mockNotification() calls:

 class DelegationServiceTest extends TestCase {
 	private DelegationMapper&MockObject $delegationMapper;
 	private AccountService&MockObject $accountService;
 	private MailboxMapper&MockObject $mailboxMapper;
 	private MessageMapper&MockObject $messageMapper;
 	private AliasMapper&MockObject $aliasMapper;
 	private LocalMessageMapper&MockObject $localMessageMapper;
-	private IUserManager&MockObject $userManager;
-	private IManager&MockObject $notificationManager;
-	private ITimeFactory&MockObject $timeFactory;
-	private IEventDispatcher&MockObject $eventDispatcher;
 	private DelegationService $service;

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

 		$this->delegationMapper = $this->createMock(DelegationMapper::class);
 		$this->accountService = $this->createMock(AccountService::class);
 		$this->mailboxMapper = $this->createMock(MailboxMapper::class);
 		$this->messageMapper = $this->createMock(MessageMapper::class);
 		$this->aliasMapper = $this->createMock(AliasMapper::class);
 		$this->localMessageMapper = $this->createMock(LocalMessageMapper::class);
-		$this->userManager = $this->createMock(IUserManager::class);
-		$this->notificationManager = $this->createMock(IManager::class);
-		$this->timeFactory = $this->createMock(ITimeFactory::class);
-		$this->eventDispatcher = $this->createMock(IEventDispatcher::class);

 		$this->service = new DelegationService(
 			$this->delegationMapper,
 			$this->accountService,
 			$this->mailboxMapper,
 			$this->messageMapper,
 			$this->aliasMapper,
 			$this->localMessageMapper,
 		);
 		...
 	}

-	private function mockNotification(): void {
-		...
-	}

 	public function testDelegateSuccess(): void {
-		$this->mockNotification();
 		...
 	}
 	
 	public function testUnDelegateSuccess(): void {
-		$this->mockNotification();
 		...
 	}

Also remove the unused imports (lines 26-31 in the test file).

Also applies to: 58-61, 79-94, 97-97, 156-156

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Unit/Service/DelegationServiceTest.php` around lines 41 - 44, Remove
the dead test fixtures: delete the mock properties for IUserManager, IManager
(notification manager), ITimeFactory, and IEventDispatcher, remove their
initialization in setUp(), and remove the mockNotification() helper and its
calls in the two tests; also drop the corresponding unused imports. Verify that
only the mapper collaborator mocks (the dependencies actually accepted by
DelegationService's constructor) remain and run tests to ensure nothing else
references those symbols.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/Controller/AccountsController.php`:
- Around line 128-131: The delegationService->resolveAccountUserId call in show
(and in patchAccount, updateSignature, draft, getQuota, updateSmimeCertificate,
testAccountConnection) can throw DoesNotExistException and currently isn't
handled; wrap the resolveAccountUserId($id, $this->currentUserId) invocation in
a try-catch that catches DoesNotExistException and returns an HTTP 403 JSON
response (matching MailboxesController's behavior), otherwise proceed to call
accountService->find (or the existing logic) with the resolved $effectiveUserId;
apply this same try-catch pattern to the listed methods (show, patchAccount,
updateSignature, draft, getQuota, updateSmimeCertificate, testAccountConnection)
to consistently handle delegation resolution failures.

In `@lib/Controller/AliasesController.php`:
- Around line 48-51: AliasesController's index (and other methods: update,
destroy, create, updateSignature) call
$this->delegationService->resolveAccountUserId($accountId, $this->currentUserId)
without catching DoesNotExistException, so delegation/account-not-found errors
propagate; update each method (index, update, destroy, create, updateSignature)
to wrap the resolveAccountUserId call in a try/catch that catches
DoesNotExistException and returns the same HTTP 403 response used in
MessagesController/MailboxesController (i.e., catch DoesNotExistException and
return a JSON/HTTP 403 response), keeping the rest of the method flow unchanged
when resolution succeeds.

In `@lib/Controller/OutboxController.php`:
- Around line 67-70: The show method calls
delegationService->resolveLocalMessageUserId($id, $this->userId) without
handling a DoesNotExistException; wrap that call in a try-catch that catches
DoesNotExistException (the same pattern used in
MessagesController/MailboxesController) and return an appropriate JsonResponse
(e.g., JsonResponse::notFound or JsonResponse::error with 404) when caught, then
proceed to call service->getMessage($id, $effectiveUserId) only if resolution
succeeds; apply the same try-catch pattern to other delegation resolution calls
in this controller.

In `@lib/Db/DelegationMapper.php`:
- Around line 52-70: In findAccountOwnerForDelegatedUser remove the incorrect
type argument passed to the column-to-column comparison in the join: the call to
$qb->expr()->eq('d.account_id', 'a.id', IQueryBuilder::PARAM_INT) should be
changed to a plain column comparison (no PARAM_* type) so expr()->eq compares
two columns; update the join invocation accordingly (leave other usages of
createNamedParameter with PARAM_INT unchanged).

In `@src/components/DelegationModal.vue`:
- Around line 268-281: The revokeDelegate method currently updates delegates and
shows success but does not close the modal; after a successful revoke (inside
revokeDelegate, after the this.delegates filter and the showSuccess call) call
this.closeDialog() to dismiss the 'revoke' dialog and return the user to the
main view; ensure you call this.closeDialog() only in the try path (not in
catch) and keep existing error logging in revokeDelegate unchanged.

---

Outside diff comments:
In `@lib/Controller/SieveController.php`:
- Around line 5-8: Update the SPDX license identifier in the file header of
SieveController.php: replace "SPDX-License-Identifier: AGPL-3.0-only" with
"SPDX-License-Identifier: AGPL-3.0-or-later" in the top comment block so the
file uses the required AGPL-3.0-or-later identifier.

In `@lib/Controller/ThreadController.php`:
- Around line 5-8: The SPDX license header at the top of ThreadController.php
uses "AGPL-3.0-only"; update the SPDX-License-Identifier token to
"AGPL-3.0-or-later" in the file header (the comment block above the
ThreadController class) so the file conforms to the project's license identifier
guideline.

---

Nitpick comments:
In `@lib/Controller/DelegationController.php`:
- Around line 50-52: Replace the 401 responses used for authorization failures
with 403 FORBIDDEN: locate the checks in DelegationController where you compare
$account->getUserId() !== $this->currentUserId (and the analogous checks at the
other occurrences referenced) and change the returned JSONResponse from
Http::STATUS_UNAUTHORIZED to Http::STATUS_FORBIDDEN so
authenticated-but-unauthorized access returns 403; keep the response body
unchanged.

In `@lib/Controller/PageController.php`:
- Around line 187-205: The delegated-account loop duplicates the owned-account
JSON transformation; extract the shared logic into a private helper like
transformAccountToJson($account, bool $isDelegated = false) and call it from
both places (use current usages of $delegatedAccount->jsonSerialize(),
aliasesService->findAll(...), mailManager->getMailboxes(...), and logger
handling inside the helper). The helper should accept an Account object, set
isDelegated accordingly, populate aliases via
$this->aliasesService->findAll($account->getId(), $account->getUserId()),
attempt to load mailboxes with $this->mailManager->getMailboxes($account) inside
the same try/catch that logs using $this->logger and sets mailboxes/error
defaults, and return the assembled array so callers (both owned and delegated
loops) push the returned array into $accountsJson.

In `@src/components/AccountSettings.vue`:
- Line 109: The handler for the rename-primary-alias event calls
scrollToAccountSettings which dereferences this.$refs.accountForm that can be
undefined when the mail-server section is hidden; update the event handler
(where rename-primary-alias is handled) to guard before accessing
this.$refs.accountForm (or calling scrollToAccountSettings) by checking that
this.$refs.accountForm exists (or that accountForm ref is present) and only then
invoking scrollToAccountSettings, ensuring no direct dereference of undefined
AccountForm.

In `@tests/Unit/Controller/AccountsControllerTest.php`:
- Around line 154-157: Add a positive delegated-account path inside the
testIndex unit test: mock AccountService::findDelegatedAccounts to return a
non-empty array (e.g., one delegated account) for $this->userId, call the
controller/index flow as in the existing empty-case, and add assertions that the
serialized response marks that account with isDelegated === true; update the
mock expectation for findDelegatedAccounts (or add a second expectation) and
assert the output contains the delegated account and its isDelegated flag is
true to prevent regression in delegated serialization.

In `@tests/Unit/Service/DelegationServiceTest.php`:
- Around line 41-44: Remove the dead test fixtures: delete the mock properties
for IUserManager, IManager (notification manager), ITimeFactory, and
IEventDispatcher, remove their initialization in setUp(), and remove the
mockNotification() helper and its calls in the two tests; also drop the
corresponding unused imports. Verify that only the mapper collaborator mocks
(the dependencies actually accepted by DelegationService's constructor) remain
and run tests to ensure nothing else references those symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 994d9c90-0537-4357-ab4e-fa4d54acd0a8

📥 Commits

Reviewing files that changed from the base of the PR and between 70bb3d3 and 7893e7b.

⛔ Files ignored due to path filters (1)
  • img/delegation.svg is excluded by !**/*.svg
📒 Files selected for processing (53)
  • REUSE.toml
  • appinfo/info.xml
  • appinfo/routes.php
  • lib/Controller/AccountApiController.php
  • lib/Controller/AccountsController.php
  • lib/Controller/AliasesController.php
  • lib/Controller/DelegationController.php
  • lib/Controller/DraftsController.php
  • lib/Controller/FilterController.php
  • lib/Controller/FollowUpController.php
  • lib/Controller/ListController.php
  • lib/Controller/MailboxesApiController.php
  • lib/Controller/MailboxesController.php
  • lib/Controller/MessageApiController.php
  • lib/Controller/MessagesController.php
  • lib/Controller/OutboxController.php
  • lib/Controller/PageController.php
  • lib/Controller/SieveController.php
  • lib/Controller/ThreadController.php
  • lib/Db/AliasMapper.php
  • lib/Db/Delegation.php
  • lib/Db/DelegationMapper.php
  • lib/Db/LocalMessageMapper.php
  • lib/Db/MailAccountMapper.php
  • lib/Db/MailboxMapper.php
  • lib/Db/MessageMapper.php
  • lib/Exception/DelegationExistsException.php
  • lib/Migration/Version5008Date20260312152753.php
  • lib/ResponseDefinitions.php
  • lib/Service/AccountService.php
  • lib/Service/DelegationService.php
  • src/components/AccountSettings.vue
  • src/components/AppSettingsMenu.vue
  • src/components/DelegationModal.vue
  • src/components/NavigationAccount.vue
  • src/service/DelegationService.js
  • tests/Integration/MailboxSynchronizationTest.php
  • tests/Unit/Controller/AccountApiControllerTest.php
  • tests/Unit/Controller/AccountsControllerTest.php
  • tests/Unit/Controller/AliasesControllerTest.php
  • tests/Unit/Controller/DelegationControllerTest.php
  • tests/Unit/Controller/DraftsControllerTest.php
  • tests/Unit/Controller/ListControllerTest.php
  • tests/Unit/Controller/MailboxesApiControllerTest.php
  • tests/Unit/Controller/MailboxesControllerTest.php
  • tests/Unit/Controller/MessageApiControllerTest.php
  • tests/Unit/Controller/MessagesControllerTest.php
  • tests/Unit/Controller/OutboxControllerTest.php
  • tests/Unit/Controller/PageControllerTest.php
  • tests/Unit/Controller/SieveControllerTest.php
  • tests/Unit/Controller/ThreadControllerTest.php
  • tests/Unit/Service/AccountServiceTest.php
  • tests/Unit/Service/DelegationServiceTest.php

Comment on lines 128 to 131
public function show(int $id): JSONResponse {
return new JSONResponse($this->accountService->find($this->currentUserId, $id));
$effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId);
return new JSONResponse($this->accountService->find($effectiveUserId, $id));
}
Copy link
Copy Markdown

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

Missing DoesNotExistException handling for delegation resolution.

The show method and several others (patchAccount, updateSignature, draft, getQuota, updateSmimeCertificate, testAccountConnection) call resolveAccountUserId without catching DoesNotExistException, unlike MailboxesController which wraps these calls in try-catch blocks returning HTTP 403.

Proposed fix for `show` method
 #[TrapError]
 public function show(int $id): JSONResponse {
+    try {
         $effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId);
+    } catch (DoesNotExistException $e) {
+        return new JSONResponse([], Http::STATUS_FORBIDDEN);
+    }
     return new JSONResponse($this->accountService->find($effectiveUserId, $id));
 }

Apply similar pattern to other affected methods.

📝 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
public function show(int $id): JSONResponse {
return new JSONResponse($this->accountService->find($this->currentUserId, $id));
$effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId);
return new JSONResponse($this->accountService->find($effectiveUserId, $id));
}
public function show(int $id): JSONResponse {
try {
$effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId);
} catch (DoesNotExistException $e) {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
return new JSONResponse($this->accountService->find($effectiveUserId, $id));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Controller/AccountsController.php` around lines 128 - 131, The
delegationService->resolveAccountUserId call in show (and in patchAccount,
updateSignature, draft, getQuota, updateSmimeCertificate, testAccountConnection)
can throw DoesNotExistException and currently isn't handled; wrap the
resolveAccountUserId($id, $this->currentUserId) invocation in a try-catch that
catches DoesNotExistException and returns an HTTP 403 JSON response (matching
MailboxesController's behavior), otherwise proceed to call accountService->find
(or the existing logic) with the resolved $effectiveUserId; apply this same
try-catch pattern to the listed methods (show, patchAccount, updateSignature,
draft, getQuota, updateSmimeCertificate, testAccountConnection) to consistently
handle delegation resolution failures.

Comment on lines 48 to 51
public function index(int $accountId): JSONResponse {
return new JSONResponse($this->aliasService->findAll($accountId, $this->currentUserId));
$effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->currentUserId);
return new JSONResponse($this->aliasService->findAll($accountId, $effectiveUserId));
}
Copy link
Copy Markdown

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

Missing DoesNotExistException handling for delegation resolution.

Unlike MessagesController and MailboxesController, this controller does not catch DoesNotExistException from the delegation resolution calls. If the delegation doesn't exist or the account is not found, this will propagate as an unhandled exception rather than returning HTTP 403.

While #[TrapError] may catch generic exceptions, the pattern in other controllers explicitly handles this case for consistent error responses.

Proposed fix
 #[TrapError]
 public function index(int $accountId): JSONResponse {
+    try {
         $effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->currentUserId);
+    } catch (DoesNotExistException $e) {
+        return new JSONResponse([], Http::STATUS_FORBIDDEN);
+    }
     return new JSONResponse($this->aliasService->findAll($accountId, $effectiveUserId));
 }

Apply similar pattern to update, destroy, create, and updateSignature methods.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Controller/AliasesController.php` around lines 48 - 51,
AliasesController's index (and other methods: update, destroy, create,
updateSignature) call $this->delegationService->resolveAccountUserId($accountId,
$this->currentUserId) without catching DoesNotExistException, so
delegation/account-not-found errors propagate; update each method (index,
update, destroy, create, updateSignature) to wrap the resolveAccountUserId call
in a try/catch that catches DoesNotExistException and returns the same HTTP 403
response used in MessagesController/MailboxesController (i.e., catch
DoesNotExistException and return a JSON/HTTP 403 response), keeping the rest of
the method flow unchanged when resolution succeeds.

Comment on lines 67 to 70
public function show(int $id): JsonResponse {
$message = $this->service->getMessage($id, $this->userId);
$effectiveUserId = $this->delegationService->resolveLocalMessageUserId($id, $this->userId);
$message = $this->service->getMessage($id, $effectiveUserId);
return JsonResponse::success($message);
Copy link
Copy Markdown

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

Missing DoesNotExistException handling for delegation resolution.

Same issue as AliasesController — the delegation resolution calls (resolveLocalMessageUserId, resolveAccountUserId) are not wrapped in try-catch blocks. This is inconsistent with the pattern established in MessagesController and MailboxesController.

Proposed fix for `show` method (apply similar pattern to other methods)
 #[TrapError]
 public function show(int $id): JsonResponse {
+    try {
         $effectiveUserId = $this->delegationService->resolveLocalMessageUserId($id, $this->userId);
+    } catch (DoesNotExistException $e) {
+        return JsonResponse::error('Not found', Http::STATUS_FORBIDDEN);
+    }
     $message = $this->service->getMessage($id, $effectiveUserId);
     return JsonResponse::success($message);
 }
📝 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
public function show(int $id): JsonResponse {
$message = $this->service->getMessage($id, $this->userId);
$effectiveUserId = $this->delegationService->resolveLocalMessageUserId($id, $this->userId);
$message = $this->service->getMessage($id, $effectiveUserId);
return JsonResponse::success($message);
#[TrapError]
public function show(int $id): JsonResponse {
try {
$effectiveUserId = $this->delegationService->resolveLocalMessageUserId($id, $this->userId);
} catch (DoesNotExistException $e) {
return JsonResponse::error('Not found', Http::STATUS_FORBIDDEN);
}
$message = $this->service->getMessage($id, $effectiveUserId);
return JsonResponse::success($message);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Controller/OutboxController.php` around lines 67 - 70, The show method
calls delegationService->resolveLocalMessageUserId($id, $this->userId) without
handling a DoesNotExistException; wrap that call in a try-catch that catches
DoesNotExistException (the same pattern used in
MessagesController/MailboxesController) and return an appropriate JsonResponse
(e.g., JsonResponse::notFound or JsonResponse::error with 404) when caught, then
proceed to call service->getMessage($id, $effectiveUserId) only if resolution
succeeds; apply the same try-catch pattern to other delegation resolution calls
in this controller.

Comment on lines +52 to +70
public function findAccountOwnerForDelegatedUser(int $accountId, string $delegatedUserId): string {
$qb = $this->db->getQueryBuilder();
$qb->select('a.user_id')
->from($this->getTableName(), 'd')
->join('d', 'mail_accounts', 'a',
$qb->expr()->eq('d.account_id', 'a.id', IQueryBuilder::PARAM_INT))
->where($qb->expr()->eq('d.account_id', $qb->createNamedParameter($accountId, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('d.user_id', $qb->createNamedParameter($delegatedUserId)));

$result = $qb->executeQuery();
$row = $result->fetch();
$result->closeCursor();

if ($row === false) {
throw new DoesNotExistException("No delegation found for account $accountId and user $delegatedUserId");
}

return (string)$row['user_id'];
}
Copy link
Copy Markdown

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

Remove incorrect parameter type from column-to-column comparison.

Line 57 passes IQueryBuilder::PARAM_INT as the third argument to eq() when joining two columns. The type parameter in expr()->eq() is only for comparing columns to literal values or bound parameters, not for column-to-column comparisons. This could cause unexpected query behavior.

🔧 Proposed fix
 		$qb->select('a.user_id')
 			->from($this->getTableName(), 'd')
 			->join('d', 'mail_accounts', 'a',
-				$qb->expr()->eq('d.account_id', 'a.id', IQueryBuilder::PARAM_INT))
+				$qb->expr()->eq('d.account_id', 'a.id'))
 			->where($qb->expr()->eq('d.account_id', $qb->createNamedParameter($accountId, IQueryBuilder::PARAM_INT)))
 			->andWhere($qb->expr()->eq('d.user_id', $qb->createNamedParameter($delegatedUserId)));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Db/DelegationMapper.php` around lines 52 - 70, In
findAccountOwnerForDelegatedUser remove the incorrect type argument passed to
the column-to-column comparison in the join: the call to
$qb->expr()->eq('d.account_id', 'a.id', IQueryBuilder::PARAM_INT) should be
changed to a plain column comparison (no PARAM_* type) so expr()->eq compares
two columns; update the join invocation accordingly (leave other usages of
createNamedParameter with PARAM_INT unchanged).

Comment on lines +268 to +281
async revokeDelegate() {
if (!this.revokeUser) {
return
}

try {
await unDelegate(this.account.accountId, this.revokeUser.userId)
this.delegates = this.delegates.filter((d) => d.userId !== this.revokeUser.userId)
showSuccess(t('mail', 'Revoked access for {userId}', { userId: this.revokeUser.userId }))
} catch (error) {
logger.error('Could not revoke delegation', { error })
showError(t('mail', 'Could not revoke delegation'))
}
},
Copy link
Copy Markdown

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

Missing closeDialog() call after successful revoke.

After successfully revoking delegation, the view remains on the 'revoke' dialog. The user should be returned to the main view.

Proposed fix
         async revokeDelegate() {
             if (!this.revokeUser) {
                 return
             }

             try {
                 await unDelegate(this.account.accountId, this.revokeUser.userId)
                 this.delegates = this.delegates.filter((d) => d.userId !== this.revokeUser.userId)
                 showSuccess(t('mail', 'Revoked access for {userId}', { userId: this.revokeUser.userId }))
+                this.closeDialog()
             } catch (error) {
                 logger.error('Could not revoke delegation', { error })
                 showError(t('mail', 'Could not revoke delegation'))
             }
+            this.revokeUser = null
         },
📝 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
async revokeDelegate() {
if (!this.revokeUser) {
return
}
try {
await unDelegate(this.account.accountId, this.revokeUser.userId)
this.delegates = this.delegates.filter((d) => d.userId !== this.revokeUser.userId)
showSuccess(t('mail', 'Revoked access for {userId}', { userId: this.revokeUser.userId }))
} catch (error) {
logger.error('Could not revoke delegation', { error })
showError(t('mail', 'Could not revoke delegation'))
}
},
async revokeDelegate() {
if (!this.revokeUser) {
return
}
try {
await unDelegate(this.account.accountId, this.revokeUser.userId)
this.delegates = this.delegates.filter((d) => d.userId !== this.revokeUser.userId)
showSuccess(t('mail', 'Revoked access for {userId}', { userId: this.revokeUser.userId }))
this.closeDialog()
} catch (error) {
logger.error('Could not revoke delegation', { error })
showError(t('mail', 'Could not revoke delegation'))
}
this.revokeUser = null
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/DelegationModal.vue` around lines 268 - 281, The
revokeDelegate method currently updates delegates and shows success but does not
close the modal; after a successful revoke (inside revokeDelegate, after the
this.delegates filter and the showSuccess call) call this.closeDialog() to
dismiss the 'revoke' dialog and return the user to the main view; ensure you
call this.closeDialog() only in the try path (not in catch) and keep existing
error logging in revokeDelegate unchanged.

@kesselb
Copy link
Copy Markdown
Contributor

kesselb commented May 8, 2026

@claude could you review that one please, just to make copilot and coderrabbitai didn't miss anything? Please use mythos if possible.

@hamza221
Copy link
Copy Markdown
Contributor Author

hamza221 commented May 8, 2026

@claude could you review that one please, just to make copilot and coderrabbitai didn't miss anything? Please use mythos if possible.

Rabbit was autorun 😅

Comment thread src/components/DelegationModal.vue Outdated
Comment thread src/components/DelegationModal.vue Outdated
Comment thread src/components/DelegationModal.vue
Comment thread src/components/DelegationModal.vue Outdated
Comment thread lib/Controller/AccountApiController.php Outdated
Signed-off-by: Hamza <hamzamahjoubi221@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/Controller/AccountApiController.php`:
- Around line 64-66: Remove the unused local variable $ownerUserId in the
array_map callback that builds $delegatedList: inside the closure used in
array_map (mapping Account objects to transformed data via
transformAccountList), delete the line assigning $ownerUserId =
$account->getUserId() so the closure only calls
$this->transformAccountList($account, true); this removes the PHPMD warning
while preserving behavior.

In `@src/components/DelegationModal.vue`:
- Around line 260-271: The current finally block always calls closeDialog(),
which closes the modal even on delegation errors and leaves transient state
(e.g., selectedUser, user suggestions) intact for a stale resubmit; change the
flow so that closeDialog() is only invoked on success (after showSuccess) and
not in the finally that runs on errors, and add explicit state cleanup inside
the closeDialog() or a new reset method to clear transient fields (reset
selectedUser, suggestion list, and any temporary flags) so reopening the
DelegationModal shows a fresh form; apply the same change to the other
delegation block referenced (around the delegation call at lines ~289-291) and
ensure delegating is still set to false in finally.
- Around line 13-15: The modal title currently renders t('mail', 'Delegation')
in the h2 with id "delegation-modal-title"; change it to use the same phrasing
as the rest of the flow (e.g., t('mail', 'Add delegate') or t('mail', 'Delegate
access') to match surrounding buttons/labels), update or add the corresponding
translation key in the i18n resource, and ensure any tests or snapshots
referencing "Delegation" are updated to the new string; locate the title in
DelegationModal.vue (h2 id="delegation-modal-title") to make the change.
- Around line 1-4: The SPDX header in the comment block uses extra bullet
prefixes; update the header comment so it exactly matches the repository's
required SPDX lines by removing the "- " prefixes and leaving two comment lines:
"SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors" and
"SPDX-Identifier: AGPL-3.0-or-later" (i.e., modify the comment at the top of
DelegationModal.vue to contain those exact SPDX keys/values).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c92ab44b-fcf6-4d38-b725-ff594bab75df

📥 Commits

Reviewing files that changed from the base of the PR and between 7893e7b and 97abbcd.

📒 Files selected for processing (3)
  • lib/Controller/AccountApiController.php
  • lib/Service/DelegationService.php
  • src/components/DelegationModal.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/Service/DelegationService.php

Comment thread lib/Controller/AccountApiController.php Outdated
Comment on lines +1 to +4
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
Copy link
Copy Markdown

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

Use the repository’s exact SPDX header format.

This header is close, but the repo rule for new src/**/*.vue files requires the exact SPDX keys and values without the bullet prefixes. As written, this can slip past automated license checks.

Proposed fix
 <!--
-  - SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
-  - SPDX-License-Identifier: AGPL-3.0-or-later
+SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+SPDX-Identifier: AGPL-3.0-or-later
 -->

As per coding guidelines, "SPDX license header format must be: SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors and SPDX-Identifier: AGPL-3.0-or-later"

📝 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
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<!--
SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
SPDX-Identifier: AGPL-3.0-or-later
-->
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/DelegationModal.vue` around lines 1 - 4, The SPDX header in
the comment block uses extra bullet prefixes; update the header comment so it
exactly matches the repository's required SPDX lines by removing the "- "
prefixes and leaving two comment lines: "SPDX-FileCopyrightText: 2026 Nextcloud
GmbH and Nextcloud contributors" and "SPDX-Identifier: AGPL-3.0-or-later" (i.e.,
modify the comment at the top of DelegationModal.vue to contain those exact SPDX
keys/values).

Comment on lines +13 to +15
<h2 id="delegation-modal-title">
{{ t('mail', 'Delegation') }}
</h2>
Copy link
Copy Markdown

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

Align the modal title with the rest of the delegation flow.

This still uses Delegation, while the surrounding actions read as Add delegate / Delegate access. Using one phrase throughout will make the flow feel less disjointed.

Proposed fix
-				{{ t('mail', 'Delegation') }}
+				{{ t('mail', 'Delegate 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
<h2 id="delegation-modal-title">
{{ t('mail', 'Delegation') }}
</h2>
<h2 id="delegation-modal-title">
{{ t('mail', 'Delegate account') }}
</h2>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/DelegationModal.vue` around lines 13 - 15, The modal title
currently renders t('mail', 'Delegation') in the h2 with id
"delegation-modal-title"; change it to use the same phrasing as the rest of the
flow (e.g., t('mail', 'Add delegate') or t('mail', 'Delegate access') to match
surrounding buttons/labels), update or add the corresponding translation key in
the i18n resource, and ensure any tests or snapshots referencing "Delegation"
are updated to the new string; locate the title in DelegationModal.vue (h2
id="delegation-modal-title") to make the change.

Comment on lines +260 to +271
this.delegating = true
try {
const delegation = await delegate(this.account.accountId, this.selectedUser.id)
this.delegates.push(delegation)
showSuccess(t('mail', 'Delegated access to {userId}', { userId: this.selectedUser.id }))
} catch (error) {
logger.error('Could not delegate access', { error })
showError(t('mail', 'Could not delegate access'))
} finally {
this.delegating = false
this.closeDialog()
}
Copy link
Copy Markdown

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

Keep the add dialog open on errors, and clear its transient state on close.

finally always routes back to main, so a failed delegation kicks the user out of the form immediately after the error toast. On top of that, closeDialog() only flips view, so reopening the dialog can keep the previous selection/suggestions around and allow a stale resubmit.

Proposed fix
 		async addDelegate() {
 			if (!this.selectedUser) {
 				return
 			}

 			this.delegating = true
 			try {
 				const delegation = await delegate(this.account.accountId, this.selectedUser.id)
 				this.delegates.push(delegation)
 				showSuccess(t('mail', 'Delegated access to {userId}', { userId: this.selectedUser.id }))
+				this.closeDialog()
 			} catch (error) {
 				logger.error('Could not delegate access', { error })
 				showError(t('mail', 'Could not delegate access'))
 			} finally {
 				this.delegating = false
-				this.closeDialog()
 			}
 		},
@@
 		closeDialog() {
+			this.selectedUser = null
+			this.userSuggestions = []
+			this.revokeUser = null
 			this.view = 'main'
 		},

Also applies to: 289-291

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/DelegationModal.vue` around lines 260 - 271, The current
finally block always calls closeDialog(), which closes the modal even on
delegation errors and leaves transient state (e.g., selectedUser, user
suggestions) intact for a stale resubmit; change the flow so that closeDialog()
is only invoked on success (after showSuccess) and not in the finally that runs
on errors, and add explicit state cleanup inside the closeDialog() or a new
reset method to clear transient fields (reset selectedUser, suggestion list, and
any temporary flags) so reopening the DelegationModal shows a fresh form; apply
the same change to the other delegation block referenced (around the delegation
call at lines ~289-291) and ensure delegating is still set to false in finally.

hamza221 added 2 commits May 8, 2026 16:27
Signed-off-by: Hamza <hamzamahjoubi221@gmail.com>
Signed-off-by: Hamza <hamzamahjoubi221@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review enhancement skill:backend Issues and PRs that require backend development skills skill:frontend Issues and PRs that require JavaScript/Vue/styling development skills

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants