diff --git a/appinfo/info.xml b/appinfo/info.xml index 9ab13f49cb..ec6726f493 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -34,7 +34,7 @@ The rating depends on the installed text processing backend. See [the rating ove Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/). ]]> - 5.8.0-dev.2 + 5.8.0-dev.3 agpl Christoph Wurst GretaD diff --git a/appinfo/routes.php b/appinfo/routes.php index bda4b2d2fe..22350d2a1c 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -505,6 +505,21 @@ 'url' => '/api/follow-up/check-message-ids', 'verb' => 'POST', ], + [ + 'name' => 'delegation#getDelegatedUsers', + 'url' => '/api/delegations/{accountId}', + 'verb' => 'GET', + ], + [ + 'name' => 'delegation#delegate', + 'url' => '/api/delegations/{accountId}', + 'verb' => 'POST', + ], + [ + 'name' => 'delegation#unDelegate', + 'url' => '/api/delegations/{accountId}/{userId}', + 'verb' => 'DELETE', + ], [ 'name' => 'textBlockShares#getTextBlockShares', 'url' => '/api/textBlocks/{id}/shares', diff --git a/lib/Controller/AccountApiController.php b/lib/Controller/AccountApiController.php index 1105d78329..ba6e1cf4f6 100644 --- a/lib/Controller/AccountApiController.php +++ b/lib/Controller/AccountApiController.php @@ -14,6 +14,7 @@ use OCA\Mail\ResponseDefinitions; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AliasesService; +use OCA\Mail\Service\DelegationService; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; use OCP\AppFramework\Http\Attribute\NoAdminRequired; @@ -32,6 +33,7 @@ public function __construct( private readonly ?string $userId, private readonly AccountService $accountService, private readonly AliasesService $aliasesService, + private readonly DelegationService $delegationService, ) { parent::__construct($appName, $request); } @@ -54,17 +56,36 @@ public function list(): DataResponse { } $accounts = $this->accountService->findByUserId($userId); - return new DataResponse(array_map(function (Account $account) use ($userId) { + $result = array_map(function (Account $account) use ($userId) { $aliases = $this->aliasesService->findAll($account->getId(), $userId); return [ 'id' => $account->getId(), 'email' => $account->getEmail(), + 'isDelegated' => false, 'aliases' => array_map(static fn (Alias $alias) => [ 'id' => $alias->getId(), 'email' => $alias->getAlias(), 'name' => $alias->getName(), ], $aliases), ]; - }, $accounts)); + }, $accounts); + + $delegatedAccounts = $this->accountService->findDelegatedAccounts($userId); + foreach ($delegatedAccounts as $account) { + $ownerUserId = $account->getUserId(); + $aliases = $this->aliasesService->findAll($account->getId(), $ownerUserId); + $result[] = [ + 'id' => $account->getId(), + 'email' => $account->getEmail(), + 'isDelegated' => true, + 'aliases' => array_map(static fn (Alias $alias) => [ + 'id' => $alias->getId(), + 'email' => $alias->getAlias(), + 'name' => $alias->getName(), + ], $aliases), + ]; + } + + return new DataResponse($result); } } diff --git a/lib/Controller/AccountsController.php b/lib/Controller/AccountsController.php index 974b0d89df..49084d1149 100644 --- a/lib/Controller/AccountsController.php +++ b/lib/Controller/AccountsController.php @@ -25,6 +25,7 @@ use OCA\Mail\Model\NewMessageData; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AliasesService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\SetupService; use OCA\Mail\Service\Sync\SyncService; use OCP\AppFramework\Controller; @@ -52,6 +53,7 @@ class AccountsController extends Controller { private IConfig $config; private IRemoteHostValidator $hostValidator; private MailboxSync $mailboxSync; + private DelegationService $delegationService; public function __construct( string $appName, @@ -69,6 +71,7 @@ public function __construct( IRemoteHostValidator $hostValidator, MailboxSync $mailboxSync, private ITimeFactory $timeFactory, + DelegationService $delegationService, ) { parent::__construct($appName, $request); $this->accountService = $accountService; @@ -83,6 +86,7 @@ public function __construct( $this->config = $config; $this->hostValidator = $hostValidator; $this->mailboxSync = $mailboxSync; + $this->delegationService = $delegationService; } /** @@ -98,6 +102,15 @@ public function index(): JSONResponse { foreach ($mailAccounts as $mailAccount) { $conf = $mailAccount->jsonSerialize(); $conf['aliases'] = $this->aliasesService->findAll($conf['accountId'], $this->currentUserId); + $conf['isDelegated'] = false; + $json[] = $conf; + } + + $delegatedAccounts = $this->accountService->findDelegatedAccounts($this->currentUserId); + foreach ($delegatedAccounts as $delegatedAccount) { + $conf = $delegatedAccount->jsonSerialize(); + $conf['isDelegated'] = true; + $conf['aliases'] = $this->aliasesService->findAll($conf['accountId'], $delegatedAccount->getUserId()); $json[] = $conf; } return new JSONResponse($json); @@ -113,7 +126,8 @@ public function index(): JSONResponse { */ #[TrapError] 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)); } /** @@ -136,9 +150,10 @@ public function update(int $id, ?string $imapPassword = null, ?string $smtpPassword = null, string $authMethod = 'password'): JSONResponse { + $effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId); try { // Make sure the account actually exists - $this->accountService->find($this->currentUserId, $id); + $this->accountService->find($effectiveUserId, $id); } catch (ClientException $e) { return new JSONResponse([], Http::STATUS_BAD_REQUEST); } @@ -164,9 +179,11 @@ public function update(int $id, } try { - return MailJsonResponse::success( - $this->setup->createNewAccount($accountName, $emailAddress, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->currentUserId, $authMethod, $id) + $result = MailJsonResponse::success( + $this->setup->createNewAccount($accountName, $emailAddress, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $effectiveUserId, $authMethod, $id) ); + $this->delegationService->logDelegatedAction("$this->currentUserId updated account <$id> on behalf of $effectiveUserId"); + return $result; } catch (CouldNotConnectException $e) { $data = [ 'error' => $e->getReason(), @@ -221,28 +238,29 @@ public function patchAccount(int $id, ?bool $classificationEnabled = null, ?bool $imipCreate = null, ): JSONResponse { - $account = $this->accountService->find($this->currentUserId, $id); + $effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId); + $account = $this->accountService->find($effectiveUserId, $id); $dbAccount = $account->getMailAccount(); if ($draftsMailboxId !== null) { - $this->mailManager->getMailbox($this->currentUserId, $draftsMailboxId); + $this->mailManager->getMailbox($effectiveUserId, $draftsMailboxId); $dbAccount->setDraftsMailboxId($draftsMailboxId); } if ($sentMailboxId !== null) { - $this->mailManager->getMailbox($this->currentUserId, $sentMailboxId); + $this->mailManager->getMailbox($effectiveUserId, $sentMailboxId); $dbAccount->setSentMailboxId($sentMailboxId); } if ($trashMailboxId !== null) { - $this->mailManager->getMailbox($this->currentUserId, $trashMailboxId); + $this->mailManager->getMailbox($effectiveUserId, $trashMailboxId); $dbAccount->setTrashMailboxId($trashMailboxId); } if ($archiveMailboxId !== null) { - $this->mailManager->getMailbox($this->currentUserId, $archiveMailboxId); + $this->mailManager->getMailbox($effectiveUserId, $archiveMailboxId); $dbAccount->setarchiveMailboxId($archiveMailboxId); } if ($snoozeMailboxId !== null) { - $this->mailManager->getMailbox($this->currentUserId, $snoozeMailboxId); + $this->mailManager->getMailbox($effectiveUserId, $snoozeMailboxId); $dbAccount->setSnoozeMailboxId($snoozeMailboxId); } if ($editorMode !== null) { @@ -262,7 +280,7 @@ public function patchAccount(int $id, $dbAccount->setTrashRetentionDays($trashRetentionDays <= 0 ? null : $trashRetentionDays); } if ($junkMailboxId !== null) { - $this->mailManager->getMailbox($this->currentUserId, $junkMailboxId); + $this->mailManager->getMailbox($effectiveUserId, $junkMailboxId); $dbAccount->setJunkMailboxId($junkMailboxId); } if ($searchBody !== null) { @@ -274,9 +292,11 @@ public function patchAccount(int $id, if ($imipCreate !== null) { $dbAccount->setImipCreate($imipCreate); } - return new JSONResponse( + $result = new JSONResponse( new Account($this->accountService->save($dbAccount)) ); + $this->delegationService->logDelegatedAction("$this->currentUserId patched account <$id> on behalf of $effectiveUserId"); + return $result; } /** @@ -292,7 +312,9 @@ public function patchAccount(int $id, */ #[TrapError] public function updateSignature(int $id, ?string $signature = null): JSONResponse { - $this->accountService->updateSignature($id, $this->currentUserId, $signature); + $effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId); + $this->accountService->updateSignature($id, $effectiveUserId, $signature); + $this->delegationService->logDelegatedAction("$this->currentUserId updated signature for account <$id> on behalf of $effectiveUserId"); return new JSONResponse(); } @@ -307,7 +329,9 @@ public function updateSignature(int $id, ?string $signature = null): JSONRespons */ #[TrapError] public function destroy(int $id): JSONResponse { - $this->accountService->delete($this->currentUserId, $id); + $effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId); + $this->accountService->delete($effectiveUserId, $id); + $this->delegationService->logDelegatedAction("$this->currentUserId deleted account <$id> on behalf of $effectiveUserId"); return new JSONResponse(); } @@ -420,11 +444,12 @@ public function draft(int $id, $this->logger->info("Updating draft <$draftId> in account <$id>"); } - $account = $this->accountService->find($this->currentUserId, $id); + $effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId); + $account = $this->accountService->find($effectiveUserId, $id); $previousDraft = null; if ($draftId !== null) { try { - $previousDraft = $this->mailManager->getMessage($this->currentUserId, $draftId); + $previousDraft = $this->mailManager->getMessage($effectiveUserId, $draftId); } catch (ClientException $e) { $this->logger->info("Draft {$draftId} could not be loaded: {$e->getMessage()}"); } @@ -442,6 +467,7 @@ public function draft(int $id, null, [] ); + $this->delegationService->logDelegatedAction("$this->currentUserId saved draft in account <$id> on behalf of $effectiveUserId"); return new JSONResponse([ 'id' => $this->mailManager->getMessageIdForUid($draftsMailbox, $newUID) ]); @@ -460,7 +486,8 @@ public function draft(int $id, * @throws ClientException */ public function getQuota(int $id): JSONResponse { - $account = $this->accountService->find($this->currentUserId, $id); + $effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId); + $account = $this->accountService->find($effectiveUserId, $id); $quota = $this->mailManager->getQuota($account); if ($quota === null) { @@ -479,9 +506,11 @@ public function getQuota(int $id): JSONResponse { * @throws ClientException */ public function updateSmimeCertificate(int $id, ?int $smimeCertificateId = null) { - $account = $this->accountService->find($this->currentUserId, $id)->getMailAccount(); + $effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId); + $account = $this->accountService->find($effectiveUserId, $id)->getMailAccount(); $account->setSmimeCertificateId($smimeCertificateId); $this->accountService->update($account); + $this->delegationService->logDelegatedAction("$this->currentUserId updated S/MIME certificate for account <$id> on behalf of $effectiveUserId"); return MailJsonResponse::success(); } @@ -494,8 +523,9 @@ public function updateSmimeCertificate(int $id, ?int $smimeCertificateId = null) * @throws ClientException */ public function testAccountConnection(int $id) { + $effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId); return new JSONResponse([ - 'data' => $this->accountService->testAccountConnection($this->currentUserId, $id), + 'data' => $this->accountService->testAccountConnection($effectiveUserId, $id), ]); } diff --git a/lib/Controller/AliasesController.php b/lib/Controller/AliasesController.php index ccdf08d47a..d9fb03b0e7 100644 --- a/lib/Controller/AliasesController.php +++ b/lib/Controller/AliasesController.php @@ -12,6 +12,7 @@ use OCA\Mail\Exception\NotImplemented; use OCA\Mail\Http\TrapError; use OCA\Mail\Service\AliasesService; +use OCA\Mail\Service\DelegationService; use OCP\AppFramework\Controller; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; @@ -23,14 +24,17 @@ class AliasesController extends Controller { private AliasesService $aliasService; private string $currentUserId; + private DelegationService $delegationService; public function __construct(string $appName, IRequest $request, AliasesService $aliasesService, - string $userId) { + string $userId, + DelegationService $delegationService) { parent::__construct($appName, $request); $this->aliasService = $aliasesService; $this->currentUserId = $userId; + $this->delegationService = $delegationService; } /** @@ -42,7 +46,8 @@ public function __construct(string $appName, */ #[TrapError] 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)); } /** @@ -64,15 +69,16 @@ public function update(int $id, string $alias, string $aliasName, ?int $smimeCertificateId = null): JSONResponse { - return new JSONResponse( - $this->aliasService->update( - $this->currentUserId, - $id, - $alias, - $aliasName, - $smimeCertificateId, - ) + $effectiveUserId = $this->delegationService->resolveAliasUserId($id, $this->currentUserId); + $alias = $this->aliasService->update( + $effectiveUserId, + $id, + $alias, + $aliasName, + $smimeCertificateId, ); + $this->delegationService->logDelegatedAction("$this->currentUserId updated alias: $id on behalf of $effectiveUserId"); + return new JSONResponse($alias); } /** @@ -83,7 +89,10 @@ public function update(int $id, */ #[TrapError] public function destroy(int $id): JSONResponse { - return new JSONResponse($this->aliasService->delete($this->currentUserId, $id)); + $effectiveUserId = $this->delegationService->resolveAliasUserId($id, $this->currentUserId); + $alias = $this->aliasService->delete($effectiveUserId, $id); + $this->delegationService->logDelegatedAction("$this->currentUserId deleted alias: $id on behalf of $effectiveUserId"); + return new JSONResponse($alias); } /** @@ -98,8 +107,12 @@ public function destroy(int $id): JSONResponse { */ #[TrapError] public function create(int $accountId, string $alias, string $aliasName): JSONResponse { + $effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->currentUserId); + $alias = $this->aliasService->create($effectiveUserId, $accountId, $alias, $aliasName); + $id = $alias->getId(); + $this->delegationService->logDelegatedAction("$this->currentUserId created alias: $id on behalf of $effectiveUserId"); return new JSONResponse( - $this->aliasService->create($this->currentUserId, $accountId, $alias, $aliasName), + $alias, Http::STATUS_CREATED ); } @@ -115,6 +128,9 @@ public function create(int $accountId, string $alias, string $aliasName): JSONRe */ #[TrapError] public function updateSignature(int $id, ?string $signature = null): JSONResponse { - return new JSONResponse($this->aliasService->updateSignature($this->currentUserId, $id, $signature)); + $effectiveUserId = $this->delegationService->resolveAliasUserId($id, $this->currentUserId); + $alias = $this->aliasService->updateSignature($effectiveUserId, $id, $signature); + $this->delegationService->logDelegatedAction("$this->currentUserId updated alias: $id 's signature on behalf of $effectiveUserId"); + return new JSONResponse($alias); } } diff --git a/lib/Controller/DelegationController.php b/lib/Controller/DelegationController.php new file mode 100644 index 0000000000..0a982a4529 --- /dev/null +++ b/lib/Controller/DelegationController.php @@ -0,0 +1,115 @@ +currentUserId = $UserId; + } + + /** + * @NoAdminRequired + * + * @param int $accountId + * @return JSONResponse + */ + #[TrapError] + public function getDelegatedUsers(int $accountId): JSONResponse { + $account = $this->accountService->findById($accountId); + if ($account->getUserId() !== $this->currentUserId) { + return new JSONResponse([], Http::STATUS_UNAUTHORIZED); + } + + return new JSONResponse( + $this->delegationService->findDelegatedToUsersForAccount($accountId) + ); + } + + /** + * @NoAdminRequired + * + * @param int $accountId + * @param string $userId + * @return JSONResponse + */ + #[TrapError] + public function delegate(int $accountId, string $userId): JSONResponse { + + $account = $this->accountService->findById($accountId); + if ($this->currentUserId === null) { + return new JSONResponse([], Http::STATUS_UNAUTHORIZED); + } + + if ($account->getUserId() !== $this->currentUserId) { + return new JSONResponse([], Http::STATUS_UNAUTHORIZED); + } + + if ($userId === $this->currentUserId) { + return new JSONResponse(['message' => 'Cannot delegate to yourself'], Http::STATUS_BAD_REQUEST); + } + + if (!$this->userManager->userExists($userId)) { + return new JSONResponse([], Http::STATUS_NOT_FOUND); + } + + try { + $delegation = $this->delegationService->delegate($account, $userId, $this->currentUserId); + } catch (DelegationExistsException) { + return new JSONResponse(['message' => 'Delegation already exists'], Http::STATUS_CONFLICT); + } + + return new JSONResponse($delegation, Http::STATUS_CREATED); + } + + /** + * @NoAdminRequired + * + * @param int $accountId + * @param string $userId + * @return JSONResponse + */ + #[TrapError] + public function unDelegate(int $accountId, string $userId): JSONResponse { + $account = $this->accountService->findById($accountId); + + if ($this->currentUserId === null) { + return new JSONResponse([], Http::STATUS_UNAUTHORIZED); + } + + if ($account->getUserId() !== $this->currentUserId) { + return new JSONResponse([], Http::STATUS_UNAUTHORIZED); + } + + $this->delegationService->unDelegate($account, $userId, $this->currentUserId); + return new JSONResponse([], Http::STATUS_OK); + } +} diff --git a/lib/Controller/DraftsController.php b/lib/Controller/DraftsController.php index f1a98cee76..c8eb78b771 100644 --- a/lib/Controller/DraftsController.php +++ b/lib/Controller/DraftsController.php @@ -14,6 +14,7 @@ use OCA\Mail\Http\JsonResponse; use OCA\Mail\Http\TrapError; use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\DraftsService; use OCA\Mail\Service\SmimeService; use OCP\AppFramework\Controller; @@ -30,6 +31,7 @@ class DraftsController extends Controller { private AccountService $accountService; private ITimeFactory $timeFactory; private SmimeService $smimeService; + private DelegationService $delegationService; public function __construct(string $appName, @@ -38,13 +40,15 @@ public function __construct(string $appName, DraftsService $service, AccountService $accountService, ITimeFactory $timeFactory, - SmimeService $smimeService) { + SmimeService $smimeService, + DelegationService $delegationService) { parent::__construct($appName, $request); $this->userId = $userId; $this->service = $service; $this->accountService = $accountService; $this->timeFactory = $timeFactory; $this->smimeService = $smimeService; + $this->delegationService = $delegationService; } /** @@ -93,7 +97,8 @@ public function create( ?int $draftId = null, bool $requestMdn = false, bool $isPgpMime = false) : JsonResponse { - $account = $this->accountService->find($this->userId, $accountId); + $effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->userId); + $account = $this->accountService->find($effectiveUserId, $accountId); if ($draftId !== null) { $this->service->handleDraft($account, $draftId); } @@ -120,7 +125,8 @@ public function create( } $this->service->saveMessage($account, $message, $to, $cc, $bcc, $attachments); - + $id = $message->getId(); + $this->delegationService->logDelegatedAction("$this->userId created draft: $id on behalf of $effectiveUserId"); return JsonResponse::success($message, Http::STATUS_CREATED); } @@ -165,8 +171,9 @@ public function update(int $id, ?int $sendAt = null, bool $requestMdn = false, bool $isPgpMime = false): JsonResponse { - $message = $this->service->getMessage($id, $this->userId); - $account = $this->accountService->find($this->userId, $accountId); + $effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->userId); + $message = $this->service->getMessage($id, $effectiveUserId); + $account = $this->accountService->find($effectiveUserId, $accountId); $message->setType(LocalMessage::TYPE_DRAFT); $message->setAccountId($accountId); @@ -202,10 +209,12 @@ public function update(int $id, */ #[TrapError] public function destroy(int $id): JsonResponse { - $message = $this->service->getMessage($id, $this->userId); - $this->accountService->find($this->userId, $message->getAccountId()); + $effectiveUserId = $this->delegationService->resolveLocalMessageUserId($id, $this->userId); + $message = $this->service->getMessage($id, $effectiveUserId); + $this->accountService->find($effectiveUserId, $message->getAccountId()); - $this->service->deleteMessage($this->userId, $message); + $this->service->deleteMessage($effectiveUserId, $message); + $this->delegationService->logDelegatedAction("$this->userId deleted draft: $id on behalf of $effectiveUserId"); return JsonResponse::success('Message deleted', Http::STATUS_ACCEPTED); } @@ -217,10 +226,12 @@ public function destroy(int $id): JsonResponse { */ #[TrapError] public function move(int $id): JsonResponse { - $message = $this->service->getMessage($id, $this->userId); - $account = $this->accountService->find($this->userId, $message->getAccountId()); + $effectiveUserId = $this->delegationService->resolveLocalMessageUserId($id, $this->userId); + $message = $this->service->getMessage($id, $effectiveUserId); + $account = $this->accountService->find($effectiveUserId, $message->getAccountId()); $this->service->sendMessage($message, $account); + $this->delegationService->logDelegatedAction("$this->userId moved draft: $id to the IMAP server on behalf of $effectiveUserId"); return JsonResponse::success( 'Message moved to IMAP', Http::STATUS_ACCEPTED ); diff --git a/lib/Controller/FilterController.php b/lib/Controller/FilterController.php index f8a9da64d3..e173f7e8fc 100644 --- a/lib/Controller/FilterController.php +++ b/lib/Controller/FilterController.php @@ -11,6 +11,7 @@ use OCA\Mail\AppInfo\Application; use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\FilterService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; @@ -27,6 +28,7 @@ public function __construct( string $userId, private FilterService $mailFilterService, private AccountService $accountService, + private DelegationService $delegationService, ) { parent::__construct(Application::APP_ID, $request); $this->currentUserId = $userId; @@ -39,12 +41,12 @@ public function __construct( #[Route(Route::TYPE_FRONTPAGE, verb: 'GET', url: '/api/filter/{accountId}', requirements: ['accountId' => '[\d]+'])] #[NoAdminRequired] public function getFilters(int $accountId): JSONResponse { + $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); } - $result = $this->mailFilterService->parse($account->getMailAccount()); return new JSONResponse($result->getFilters()); @@ -56,13 +58,15 @@ public function getFilters(int $accountId): JSONResponse { #[Route(Route::TYPE_FRONTPAGE, verb: 'PUT', url: '/api/filter/{accountId}', requirements: ['accountId' => '[\d]+'])] #[NoAdminRequired] public function updateFilters(int $accountId, array $filters): JSONResponse { + $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); } $this->mailFilterService->update($account->getMailAccount(), $filters); + $this->delegationService->logDelegatedAction("$this->currentUserId updated account: $accountId 's filters on behalf of $effectiveUserId"); return new JSONResponse([]); } diff --git a/lib/Controller/FollowUpController.php b/lib/Controller/FollowUpController.php index 031280e784..6f61f8f61c 100644 --- a/lib/Controller/FollowUpController.php +++ b/lib/Controller/FollowUpController.php @@ -14,6 +14,7 @@ use OCA\Mail\Db\ThreadMapper; use OCA\Mail\Http\JsonResponse; use OCA\Mail\Http\TrapError; +use OCA\Mail\Service\DelegationService; use OCP\AppFramework\Controller; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; @@ -31,6 +32,7 @@ public function __construct( private ThreadMapper $threadMapper, private MessageMapper $messageMapper, private MailboxMapper $mailboxMapper, + private DelegationService $delegationService, ) { parent::__construct($appName, $request); } @@ -54,7 +56,8 @@ public function checkMessageIds(array $messageIds): JsonResponse { $mailboxId = $message->getMailboxId(); if (!isset($mailboxes[$mailboxId])) { try { - $mailboxes[$mailboxId] = $this->mailboxMapper->findByUid($mailboxId, $userId); + $effectiveUserId = $this->delegationService->resolveMailboxUserId($mailboxId, $userId); + $mailboxes[$mailboxId] = $this->mailboxMapper->findByUid($mailboxId, $effectiveUserId); } catch (DoesNotExistException $e) { continue; } diff --git a/lib/Controller/ListController.php b/lib/Controller/ListController.php index f91263b827..685b080ac9 100644 --- a/lib/Controller/ListController.php +++ b/lib/Controller/ListController.php @@ -15,6 +15,7 @@ use OCA\Mail\Http\JsonResponse; use OCA\Mail\IMAP\IMAPClientFactory; use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DelegationService; use OCP\AppFramework\Controller; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; @@ -31,6 +32,7 @@ class ListController extends Controller { private IClientService $httpClientService; private LoggerInterface $logger; private ?string $currentUserId; + private DelegationService $delegationService; public function __construct(IRequest $request, IMailManager $mailManager, @@ -38,7 +40,8 @@ public function __construct(IRequest $request, IMAPClientFactory $clientFactory, IClientService $httpClientService, LoggerInterface $logger, - ?string $userId) { + ?string $userId, + DelegationService $delegationService) { parent::__construct(Application::APP_ID, $request); $this->mailManager = $mailManager; $this->accountService = $accountService; @@ -47,6 +50,7 @@ public function __construct(IRequest $request, $this->httpClientService = $httpClientService; $this->logger = $logger; $this->currentUserId = $userId; + $this->delegationService = $delegationService; } /** @@ -59,9 +63,10 @@ public function unsubscribe(int $id): JsonResponse { } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return JsonResponse::fail(null, Http::STATUS_NOT_FOUND); } @@ -94,6 +99,7 @@ public function unsubscribe(int $id): JsonResponse { } finally { $client->logout(); } + $this->delegationService->logDelegatedAction("$this->currentUserId unsubscribed from mailing list: $id on behalf of $effectiveUserId"); return JsonResponse::success(); } diff --git a/lib/Controller/MailboxesApiController.php b/lib/Controller/MailboxesApiController.php index 106a8892e2..b7c49ca9f7 100644 --- a/lib/Controller/MailboxesApiController.php +++ b/lib/Controller/MailboxesApiController.php @@ -13,6 +13,7 @@ use OCA\Mail\Contracts\IMailSearch; use OCA\Mail\ResponseDefinitions; use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DelegationService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; @@ -33,6 +34,7 @@ public function __construct( private IMailManager $mailManager, private readonly AccountService $accountService, private IMailSearch $mailSearch, + private DelegationService $delegationService, ) { parent::__construct($appName, $request); } @@ -56,7 +58,8 @@ public function list(int $accountId): DataResponse { } try { - $account = $this->accountService->find($userId, $accountId); + $effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $userId); + $account = $this->accountService->find($effectiveUserId, $accountId); } catch (DoesNotExistException $e) { return new DataResponse([], Http::STATUS_NOT_FOUND); } @@ -97,8 +100,9 @@ public function listMessages(int $mailboxId, return new DataResponse([], Http::STATUS_NOT_FOUND); } try { - $mailbox = $this->mailManager->getMailbox($userId, $mailboxId); - $account = $this->accountService->find($userId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMailboxUserId($mailboxId, $userId); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $mailboxId); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new DataResponse([], Http::STATUS_FORBIDDEN); } diff --git a/lib/Controller/MailboxesController.php b/lib/Controller/MailboxesController.php index d391f2dee6..cbcf8fbc81 100644 --- a/lib/Controller/MailboxesController.php +++ b/lib/Controller/MailboxesController.php @@ -21,8 +21,10 @@ use OCA\Mail\Exception\ServiceException; use OCA\Mail\Http\TrapError; use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\Sync\SyncService; use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; @@ -38,6 +40,7 @@ class MailboxesController extends Controller { private IMailManager $mailManager; private SyncService $syncService; private ?string $currentUserId; + private DelegationService $delegationService; public function __construct( string $appName, @@ -48,6 +51,7 @@ public function __construct( SyncService $syncService, private readonly IConfig $config, private readonly ITimeFactory $timeFactory, + DelegationService $delegationService, ) { parent::__construct($appName, $request); @@ -55,6 +59,7 @@ public function __construct( $this->currentUserId = $userId; $this->mailManager = $mailManager; $this->syncService = $syncService; + $this->delegationService = $delegationService; } /** @@ -74,7 +79,12 @@ public function index(int $accountId, bool $forceSync = false): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } - $account = $this->accountService->find($this->currentUserId, $accountId); + try { + $effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->currentUserId); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + $account = $this->accountService->find($effectiveUserId, $accountId); $mailboxes = $this->mailManager->getMailboxes($account, $forceSync); return new JSONResponse([ @@ -102,8 +112,13 @@ public function patch(int $id, return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $id); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + try { + $effectiveUserId = $this->delegationService->resolveMailboxUserId($id, $this->currentUserId); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $id); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); if ($name !== null) { $mailbox = $this->mailManager->renameMailbox( @@ -111,6 +126,7 @@ public function patch(int $id, $mailbox, $name ); + $this->delegationService->logDelegatedAction("$this->currentUserId changed mailbox: id 's name to $name on behalf of $effectiveUserId"); } if ($subscribed !== null) { $mailbox = $this->mailManager->updateSubscription( @@ -118,14 +134,18 @@ public function patch(int $id, $mailbox, $subscribed ); + $subscribedVerb = $subscribed ? 'subscribed' : 'unsubscribed'; + $this->delegationService->logDelegatedAction("$this->currentUserId $subscribedVerb to mailbox: $id on behalf of $effectiveUserId"); + } if ($syncInBackground !== null) { $mailbox = $this->mailManager->enableMailboxBackgroundSync( $mailbox, $syncInBackground ); + $syncVerb = $syncInBackground ? 'enabled' : 'disabled'; + $this->delegationService->logDelegatedAction("$this->currentUserId $syncVerb background sync for mailbox: $id on behalf of $effectiveUserId"); } - return new JSONResponse($mailbox); } @@ -148,8 +168,13 @@ public function sync(int $id, array $ids = [], ?int $lastMessageTimestamp = null return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $id); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + try { + $effectiveUserId = $this->delegationService->resolveMailboxUserId($id, $this->currentUserId); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $id); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); $order = $sortOrder === 'newest' ? IMailSearch::ORDER_NEWEST_FIRST: IMailSearch::ORDER_OLDEST_FIRST; $this->config->setUserValue( @@ -194,8 +219,13 @@ public function clearCache(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $id); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + try { + $effectiveUserId = $this->delegationService->resolveMailboxUserId($id, $this->currentUserId); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $id); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); $this->syncService->clearCache($account, $mailbox); return new JSONResponse([]); @@ -216,11 +246,18 @@ public function markAllAsRead(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $id); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + try { + $effectiveUserId = $this->delegationService->resolveMailboxUserId($id, $this->currentUserId); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $id); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); $this->mailManager->markFolderAsRead($account, $mailbox); + $this->delegationService->logDelegatedAction("$this->currentUserId marked all messages as read in mailbox: $id on behalf of $effectiveUserId"); + return new JSONResponse([]); } @@ -240,7 +277,12 @@ public function stats(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $id); + try { + $effectiveUserId = $this->delegationService->resolveMailboxUserId($id, $this->currentUserId); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $id); return new JSONResponse($mailbox->getStats()); } @@ -280,9 +322,17 @@ public function create(int $accountId, string $name): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } - $account = $this->accountService->find($this->currentUserId, $accountId); + try { + $effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->currentUserId); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + $account = $this->accountService->find($effectiveUserId, $accountId); + $mailbox = $this->mailManager->createMailbox($account, $name); + $id = $mailbox->getId(); + $this->delegationService->logDelegatedAction("$this->currentUserId created mailbox: $id on behalf of $effectiveUserId"); - return new JSONResponse($this->mailManager->createMailbox($account, $name)); + return new JSONResponse($mailbox); } /** @@ -300,10 +350,17 @@ public function destroy(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $id); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + try { + $effectiveUserId = $this->delegationService->resolveMailboxUserId($id, $this->currentUserId); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $id); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); $this->mailManager->deleteMailbox($account, $mailbox); + $this->delegationService->logDelegatedAction("$this->currentUserId deleted mailbox: $id on behalf of $effectiveUserId"); + return new JSONResponse(); } @@ -323,10 +380,16 @@ public function clearMailbox(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $id); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + try { + $effectiveUserId = $this->delegationService->resolveMailboxUserId($id, $this->currentUserId); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $id); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); $this->mailManager->clearMailbox($account, $mailbox); + $this->delegationService->logDelegatedAction("$this->currentUserId cleared mailbox: $id on behalf of $effectiveUserId"); return new JSONResponse(); } @@ -341,10 +404,17 @@ public function repair(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_FORBIDDEN); } - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $id); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + try { + $effectiveUserId = $this->delegationService->resolveMailboxUserId($id, $this->currentUserId); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $id); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); $this->syncService->repairSync($account, $mailbox); + $this->delegationService->logDelegatedAction("$this->currentUserId repaired mailbox: $id on behalf of $effectiveUserId"); + return new JsonResponse(); } } diff --git a/lib/Controller/MessageApiController.php b/lib/Controller/MessageApiController.php index 475deca64f..84142f122f 100644 --- a/lib/Controller/MessageApiController.php +++ b/lib/Controller/MessageApiController.php @@ -21,6 +21,7 @@ use OCA\Mail\Service\AliasesService; use OCA\Mail\Service\Attachment\AttachmentService; use OCA\Mail\Service\Attachment\UploadedFile; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\ItineraryService; use OCA\Mail\Service\MailManager; use OCA\Mail\Service\OutboxService; @@ -66,6 +67,7 @@ public function __construct( private IDkimService $dkimService, private ItineraryService $itineraryService, private TrustedSenderService $trustedSenderService, + private DelegationService $delegationService, ) { parent::__construct($appName, $request); $this->userId = $userId; @@ -120,7 +122,8 @@ public function send( } try { - $mailAccount = $this->accountService->find($this->userId, $accountId); + $effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->userId); + $mailAccount = $this->accountService->find($effectiveUserId, $accountId); } catch (ClientException $e) { $this->logger->error("Mail account #$accountId not found", ['exception' => $e]); return new DataResponse('Account not found.', Http::STATUS_NOT_FOUND); @@ -128,7 +131,7 @@ public function send( if ($fromEmail !== $mailAccount->getEmail()) { try { - $alias = $this->aliasesService->findByAliasAndUserId($fromEmail, $this->userId); + $alias = $this->aliasesService->findByAliasAndUserId($fromEmail, $effectiveUserId); } catch (DoesNotExistException $e) { $this->logger->error("Alias $fromEmail for mail account $accountId not found", ['exception' => $e]); // Cannot send from this email as it is not configured as an alias @@ -203,8 +206,16 @@ public function send( $this->logger->error('SMTP error: could not send message', ['exception' => $e]); return new DataResponse('Fatal SMTP error: could not send message, and no resending is possible. Please check the mail server logs.', Http::STATUS_INTERNAL_SERVER_ERROR); } - - return match ($localMessage->getStatus()) { + $status = $localMessage->getStatus(); + $this->delegationService->logDelegatedAction(match ($status) { + LocalMessage::STATUS_PROCESSED => "$this->userId sent a message on behalf of $effectiveUserId", + LocalMessage::STATUS_NO_SENT_MAILBOX => "$this->userId attempted sending a message on behalf of $effectiveUserId but no sent mailbox is configured", + LocalMessage::STATUS_SMPT_SEND_FAIL => "$this->userId attempted sending a message on behalf of $effectiveUserId but SMTP sending failed", + LocalMessage::STATUS_IMAP_SENT_MAILBOX_FAIL => "$this->userId sent a message on behalf of $effectiveUserId but copying to sent mailbox failed", + default => "$this->userId attempted sending a message on behalf of $effectiveUserId but an unknown error occurred", + }); + + return match ($status) { LocalMessage::STATUS_PROCESSED => new DataResponse('', Http::STATUS_OK), LocalMessage::STATUS_NO_SENT_MAILBOX => new DataResponse('Configuration error: Cannot send message without sent mailbox.', Http::STATUS_FORBIDDEN), LocalMessage::STATUS_SMPT_SEND_FAIL => new DataResponse('SMTP error: could not send message. Message sending will be retried. Please check the logs.', Http::STATUS_INTERNAL_SERVER_ERROR), @@ -234,9 +245,10 @@ public function get(int $id): DataResponse { } try { - $message = $this->mailManager->getMessage($this->userId, $id); - $mailbox = $this->mailManager->getMailbox($this->userId, $message->getMailboxId()); - $account = $this->accountService->find($this->userId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->userId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (ClientException|DoesNotExistException $e) { $this->logger->error('Message, Account or Mailbox not found', ['exception' => $e->getMessage()]); return new DataResponse('Account not found.', Http::STATUS_NOT_FOUND); @@ -322,9 +334,10 @@ public function getRaw(int $id): DataResponse { } try { - $message = $this->mailManager->getMessage($this->userId, $id); - $mailbox = $this->mailManager->getMailbox($this->userId, $message->getMailboxId()); - $account = $this->accountService->find($this->userId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->userId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (ClientException|DoesNotExistException $e) { $this->logger->error('Message, Account or Mailbox not found', ['exception' => $e->getMessage()]); return new DataResponse('Message, Account or Mailbox not found', Http::STATUS_NOT_FOUND); @@ -383,9 +396,10 @@ private function enrichDownloadUrl(int $id, array $attachment): array { #[TrapError] public function getAttachment(int $id, string $attachmentId): DataResponse { try { - $message = $this->mailManager->getMessage($this->userId, $id); - $mailbox = $this->mailManager->getMailbox($this->userId, $message->getMailboxId()); - $account = $this->accountService->find($this->userId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->userId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException|ClientException $e) { return new DataResponse('Message, Account or Mailbox not found', Http::STATUS_NOT_FOUND); } diff --git a/lib/Controller/MessagesController.php b/lib/Controller/MessagesController.php index 84ebc9fab8..5a6c8624f0 100755 --- a/lib/Controller/MessagesController.php +++ b/lib/Controller/MessagesController.php @@ -29,6 +29,7 @@ use OCA\Mail\Model\SmimeData; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AiIntegrations\AiIntegrationsService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\ItineraryService; use OCA\Mail\Service\SmimeService; use OCA\Mail\Service\SnoozeService; @@ -75,6 +76,7 @@ class MessagesController extends Controller { private IUserPreferences $preferences; private SnoozeService $snoozeService; private AiIntegrationsService $aiIntegrationService; + private DelegationService $delegationService; public function __construct( string $appName, @@ -99,6 +101,7 @@ public function __construct( SnoozeService $snoozeService, AiIntegrationsService $aiIntegrationService, private ICacheFactory $cacheFactory, + DelegationService $delegationService, ) { parent::__construct($appName, $request); $this->accountService = $accountService; @@ -120,6 +123,7 @@ public function __construct( $this->preferences = $preferences; $this->snoozeService = $snoozeService; $this->aiIntegrationService = $aiIntegrationService; + $this->delegationService = $delegationService; } /** @@ -150,8 +154,9 @@ public function index(int $mailboxId, $limit = min(100, max(1, $limit)); try { - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $mailboxId); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMailboxUserId($mailboxId, $this->currentUserId); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $mailboxId); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -168,7 +173,7 @@ public function index(int $mailboxId, $filter === '' ? null : $filter, $cursor, $limit, - $this->currentUserId, + $effectiveUserId, $view ); @@ -193,9 +198,10 @@ public function show(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -227,9 +233,10 @@ public function getBody(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -304,9 +311,10 @@ public function getItineraries(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -326,9 +334,10 @@ public function getDkim(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -369,9 +378,10 @@ public function getThread(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -400,11 +410,12 @@ public function move(int $id, int $destFolderId): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $srcMailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $dstMailbox = $this->mailManager->getMailbox($this->currentUserId, $destFolderId); - $srcAccount = $this->accountService->find($this->currentUserId, $srcMailbox->getAccountId()); - $dstAccount = $this->accountService->find($this->currentUserId, $dstMailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $srcMailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $dstMailbox = $this->mailManager->getMailbox($effectiveUserId, $destFolderId); + $srcAccount = $this->accountService->find($effectiveUserId, $srcMailbox->getAccountId()); + $dstAccount = $this->accountService->find($effectiveUserId, $dstMailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -416,6 +427,9 @@ public function move(int $id, int $destFolderId): JSONResponse { $dstAccount, $dstMailbox->getName() ); + + $this->delegationService->logDelegatedAction("$this->currentUserId moved message <$id> to mailbox <$destFolderId> on behalf of $effectiveUserId"); + return new JSONResponse(); } @@ -436,16 +450,18 @@ public function snooze(int $id, int $unixTimestamp, int $destMailboxId): JSONRes return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $srcMailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $dstMailbox = $this->mailManager->getMailbox($this->currentUserId, $destMailboxId); - $srcAccount = $this->accountService->find($this->currentUserId, $srcMailbox->getAccountId()); - $dstAccount = $this->accountService->find($this->currentUserId, $dstMailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $srcMailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $dstMailbox = $this->mailManager->getMailbox($effectiveUserId, $destMailboxId); + $srcAccount = $this->accountService->find($effectiveUserId, $srcMailbox->getAccountId()); + $dstAccount = $this->accountService->find($effectiveUserId, $dstMailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $this->snoozeService->snoozeMessage($message, $unixTimestamp, $srcAccount, $srcMailbox, $dstAccount, $dstMailbox); + $this->delegationService->logDelegatedAction("$this->currentUserId snoozed message <$id> to <$unixTimestamp> on behalf of $effectiveUserId"); return new JSONResponse(); } @@ -465,12 +481,14 @@ public function unSnooze(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } - $this->snoozeService->unSnoozeMessage($message, $this->currentUserId); + $this->snoozeService->unSnoozeMessage($message, $effectiveUserId); + $this->delegationService->logDelegatedAction("$this->currentUserId unsnoozed message <$id> on behalf of $effectiveUserId"); return new JSONResponse(); } @@ -491,9 +509,10 @@ public function mdn(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -525,9 +544,10 @@ public function getSource(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -569,9 +589,10 @@ public function export(int $id): Response { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -619,9 +640,10 @@ public function getHtmlBody(int $id, bool $plain = false): Response { } try { try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException) { return new TemplateResponse( $this->appName, @@ -706,9 +728,10 @@ public function downloadAttachment(int $id, return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -755,9 +778,10 @@ public function downloadAttachments(int $id): Response { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -801,9 +825,10 @@ public function saveAttachment(int $id, return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -862,17 +887,22 @@ public function setFlags(int $id, array $flags): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } + $flagChanges = []; foreach ($flags as $flag => $value) { $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); $this->mailManager->flagMessage($account, $mailbox->getName(), $message->getUid(), $flag, $value); + $flagChanges[] = "$flag=" . ($value ? 'true' : 'false'); } + $flagsSummary = implode(', ', $flagChanges); + $this->delegationService->logDelegatedAction("$this->currentUserId updated flags on message <$id> with [$flagsSummary] on behalf of $effectiveUserId"); return new JSONResponse(); } @@ -893,9 +923,10 @@ public function setTag(int $id, string $imapLabel): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -907,6 +938,7 @@ public function setTag(int $id, string $imapLabel): JSONResponse { } $this->mailManager->tagMessage($account, $mailbox->getName(), $message, $tag, true); + $this->delegationService->logDelegatedAction("$this->currentUserId added tag <$imapLabel> on message <$id> on behalf of $effectiveUserId"); return new JSONResponse($tag); } @@ -927,9 +959,10 @@ public function removeTag(int $id, string $imapLabel): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -941,6 +974,7 @@ public function removeTag(int $id, string $imapLabel): JSONResponse { } $this->mailManager->tagMessage($account, $mailbox->getName(), $message, $tag, false); + $this->delegationService->logDelegatedAction("$this->currentUserId removed tag <$imapLabel> on message <$id> on behalf of $effectiveUserId"); return new JSONResponse($tag); } @@ -958,9 +992,10 @@ public function destroy(int $id): JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -972,6 +1007,7 @@ public function destroy(int $id): JSONResponse { $mailbox->getName(), $message->getUid() ); + $this->delegationService->logDelegatedAction("$this->currentUserId deleted message <$id> on behalf of $effectiveUserId"); return new JSONResponse(); } @@ -988,14 +1024,16 @@ public function smartReply(int $messageId):JSONResponse { return new JSONResponse([], Http::STATUS_UNAUTHORIZED); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $messageId); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($messageId, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $messageId); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } try { - $replies = array_values($this->aiIntegrationService->getSmartReply($account, $mailbox, $message, $this->currentUserId)); + $smartReplies = $this->aiIntegrationService->getSmartReply($account, $mailbox, $message, $effectiveUserId); + $replies = $smartReplies !== null ? array_values($smartReplies) : []; } catch (ServiceException $e) { $this->logger->error('Smart reply failed: ' . $e->getMessage(), [ 'exception' => $e, @@ -1019,9 +1057,10 @@ public function needsTranslation(int $messageId): JSONResponse { return new JSONResponse([], Http::STATUS_FORBIDDEN); } try { - $message = $this->mailManager->getMessage($this->currentUserId, $messageId); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($messageId, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $messageId); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -1037,7 +1076,7 @@ public function needsTranslation(int $messageId): JSONResponse { $account, $mailbox, $message, - $this->currentUserId + $effectiveUserId ); $response = new JSONResponse(['requiresTranslation' => $requiresTranslation === true]); $response->cacheFor(60 * 60 * 24, false, true); diff --git a/lib/Controller/OutboxController.php b/lib/Controller/OutboxController.php index f73c2551f6..21b73c8a3a 100644 --- a/lib/Controller/OutboxController.php +++ b/lib/Controller/OutboxController.php @@ -14,6 +14,7 @@ use OCA\Mail\Http\JsonResponse; use OCA\Mail\Http\TrapError; use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\DraftsService; use OCA\Mail\Service\OutboxService; use OCA\Mail\Service\SmimeService; @@ -29,18 +30,21 @@ class OutboxController extends Controller { private string $userId; private AccountService $accountService; private SmimeService $smimeService; + private DelegationService $delegationService; public function __construct(string $appName, $userId, IRequest $request, OutboxService $service, AccountService $accountService, - SmimeService $smimeService) { + SmimeService $smimeService, + DelegationService $delegationService) { parent::__construct($appName, $request); $this->userId = $userId; $this->service = $service; $this->accountService = $accountService; $this->smimeService = $smimeService; + $this->delegationService = $delegationService; } /** @@ -61,7 +65,8 @@ public function index(): JsonResponse { */ #[TrapError] 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); } @@ -108,7 +113,8 @@ public function create( bool $requestMdn = false, bool $isPgpMime = false, ): JsonResponse { - $account = $this->accountService->find($this->userId, $accountId); + $effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->userId); + $account = $this->accountService->find($effectiveUserId, $accountId); if ($draftId !== null) { $this->service->handleDraft($account, $draftId); @@ -136,6 +142,7 @@ public function create( } $this->service->saveMessage($account, $message, $to, $cc, $bcc, $attachments); + $this->delegationService->logDelegatedAction("$this->userId created an outbox message for account <$accountId> on behalf of $effectiveUserId"); return JsonResponse::success($message, Http::STATUS_CREATED); } @@ -147,11 +154,13 @@ public function create( */ #[TrapError] public function createFromDraft(DraftsService $draftsService, int $id, int $sendAt): JsonResponse { - $draftMessage = $draftsService->getMessage($id, $this->userId); + $effectiveUserId = $this->delegationService->resolveLocalMessageUserId($id, $this->userId); + $draftMessage = $draftsService->getMessage($id, $effectiveUserId); // Locate the account to check authorization - $this->accountService->find($this->userId, $draftMessage->getAccountId()); + $this->accountService->find($effectiveUserId, $draftMessage->getAccountId()); $outboxMessage = $this->service->convertDraft($draftMessage, $sendAt); + $this->delegationService->logDelegatedAction("$this->userId created an outbox message from draft <$id> on behalf of $effectiveUserId"); return JsonResponse::success( $outboxMessage, @@ -201,11 +210,12 @@ public function update( bool $requestMdn = false, bool $isPgpMime = false, ): JsonResponse { - $message = $this->service->getMessage($id, $this->userId); + $effectiveUserId = $this->delegationService->resolveAccountUserId($accountId, $this->userId); + $message = $this->service->getMessage($id, $effectiveUserId); if ($message->getStatus() === LocalMessage::STATUS_PROCESSED) { return JsonResponse::error('Cannot modify already sent message', Http::STATUS_FORBIDDEN, [$message]); } - $account = $this->accountService->find($this->userId, $accountId); + $account = $this->accountService->find($effectiveUserId, $accountId); $message->setAccountId($accountId); $message->setAliasId($aliasId); @@ -227,6 +237,7 @@ public function update( } $message = $this->service->updateMessage($account, $message, $to, $cc, $bcc, $attachments); + $this->delegationService->logDelegatedAction("$this->userId updated outbox message <$id> for account <$accountId> on behalf of $effectiveUserId"); return JsonResponse::success($message, Http::STATUS_ACCEPTED); } @@ -239,12 +250,18 @@ public function update( */ #[TrapError] public function send(int $id): JsonResponse { - $message = $this->service->getMessage($id, $this->userId); - $account = $this->accountService->find($this->userId, $message->getAccountId()); + $effectiveUserId = $this->delegationService->resolveLocalMessageUserId($id, $this->userId); + $message = $this->service->getMessage($id, $effectiveUserId); + $account = $this->accountService->find($effectiveUserId, $message->getAccountId()); $message = $this->service->sendMessage($message, $account); + $status = $message->getStatus(); + $this->delegationService->logDelegatedAction(match ($status) { + LocalMessage::STATUS_PROCESSED => "$this->userId sent outbox message <$id> on behalf of $effectiveUserId", + default => "$this->userId attempted sending outbox message <$id> on behalf of $effectiveUserId but sending failed", + }); - if ($message->getStatus() !== LocalMessage::STATUS_PROCESSED) { + if ($status !== LocalMessage::STATUS_PROCESSED) { return JsonResponse::error('Could not send message', Http::STATUS_INTERNAL_SERVER_ERROR, [$message]); } return JsonResponse::success( @@ -260,8 +277,10 @@ public function send(int $id): JsonResponse { */ #[TrapError] public function destroy(int $id): JsonResponse { - $message = $this->service->getMessage($id, $this->userId); - $this->service->deleteMessage($this->userId, $message); + $effectiveUserId = $this->delegationService->resolveLocalMessageUserId($id, $this->userId); + $message = $this->service->getMessage($id, $effectiveUserId); + $this->service->deleteMessage($effectiveUserId, $message); + $this->delegationService->logDelegatedAction("$this->userId deleted outbox message <$id> on behalf of $effectiveUserId"); return JsonResponse::success('Message deleted', Http::STATUS_ACCEPTED); } } diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 9c1f3ea6f0..0994fa9491 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -158,6 +158,7 @@ public function index(): TemplateResponse { $accountsJson = []; foreach ($mailAccounts as $mailAccount) { $json = $mailAccount->jsonSerialize(); + $json['isDelegated'] = false; $json['aliases'] = $this->aliasesService->findAll($mailAccount->getId(), $this->currentUserId); try { @@ -172,6 +173,26 @@ public function index(): TemplateResponse { } $accountsJson[] = $json; } + + $delegatedAccounts = $this->accountService->findDelegatedAccounts($this->currentUserId); + foreach ($delegatedAccounts as $delegatedAccount) { + $json = $delegatedAccount->jsonSerialize(); + $json['isDelegated'] = true; + $json['aliases'] = $this->aliasesService->findAll($delegatedAccount->getId(), + $delegatedAccount->getUserId()); + try { + $mailboxes = $this->mailManager->getMailboxes($delegatedAccount); + $json['mailboxes'] = $mailboxes; + } catch (Throwable $ex) { + $this->logger->critical('Could not load delegated account mailboxes: ' . $ex->getMessage(), [ + 'exception' => $ex, + ]); + $json['mailboxes'] = []; + $json['error'] = true; + } + $accountsJson[] = $json; + } + $this->initialStateService->provideInitialState( 'accounts', $accountsJson diff --git a/lib/Controller/SieveController.php b/lib/Controller/SieveController.php index e38988cccc..6a4f08139f 100644 --- a/lib/Controller/SieveController.php +++ b/lib/Controller/SieveController.php @@ -16,6 +16,7 @@ use OCA\Mail\Exception\CouldNotConnectException; use OCA\Mail\Http\JsonResponse as MailJsonResponse; use OCA\Mail\Http\TrapError; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\SieveService; use OCA\Mail\Sieve\SieveClientFactory; use OCP\AppFramework\Controller; @@ -46,6 +47,7 @@ public function __construct( IRemoteHostValidator $hostValidator, LoggerInterface $logger, private SieveService $sieveService, + private DelegationService $delegationService, ) { parent::__construct(Application::APP_ID, $request); $this->currentUserId = $userId; @@ -69,7 +71,8 @@ public function __construct( */ #[TrapError] public function getActiveScript(int $id): JSONResponse { - $activeScript = $this->sieveService->getActiveScript($this->currentUserId, $id); + $effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId); + $activeScript = $this->sieveService->getActiveScript($effectiveUserId, $id); return new JSONResponse([ 'scriptName' => $activeScript->getName(), 'script' => $activeScript->getScript(), @@ -89,12 +92,14 @@ public function getActiveScript(int $id): JSONResponse { */ #[TrapError] public function updateActiveScript(int $id, string $script): JSONResponse { + $effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId); try { - $this->sieveService->updateActiveScript($this->currentUserId, $id, $script); + $this->sieveService->updateActiveScript($effectiveUserId, $id, $script); } catch (ManagesieveException $e) { $this->logger->error('Installing sieve script failed: ' . $e->getMessage(), ['app' => 'mail', 'exception' => $e]); return new JSONResponse(data: ['message' => $e->getMessage()], statusCode: Http::STATUS_UNPROCESSABLE_ENTITY); } + $this->delegationService->logDelegatedAction("$this->currentUserId updated the active sieve script for account <$id> on behalf of $effectiveUserId"); return new JSONResponse(); } @@ -135,7 +140,8 @@ public function updateAccount(int $id, Http::STATUS_UNPROCESSABLE_ENTITY ); } - $mailAccount = $this->mailAccountMapper->find($this->currentUserId, $id); + $effectiveUserId = $this->delegationService->resolveAccountUserId($id, $this->currentUserId); + $mailAccount = $this->mailAccountMapper->find($effectiveUserId, $id); if ($sieveEnabled === false) { $mailAccount->setSieveEnabled(false); @@ -177,6 +183,7 @@ public function updateAccount(int $id, } $this->mailAccountMapper->save($mailAccount); + $this->delegationService->logDelegatedAction("$this->currentUserId updated sieve settings for account <$id> on behalf of $effectiveUserId"); return new JSONResponse(['sieveEnabled' => $mailAccount->isSieveEnabled()]); } } diff --git a/lib/Controller/ThreadController.php b/lib/Controller/ThreadController.php index 96aa68977f..64933823c2 100755 --- a/lib/Controller/ThreadController.php +++ b/lib/Controller/ThreadController.php @@ -15,6 +15,7 @@ use OCA\Mail\Http\TrapError; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AiIntegrations\AiIntegrationsService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\SnoozeService; use OCP\AppFramework\Controller; use OCP\AppFramework\Db\DoesNotExistException; @@ -32,6 +33,7 @@ class ThreadController extends Controller { private SnoozeService $snoozeService; private AiIntegrationsService $aiIntergrationsService; private LoggerInterface $logger; + private DelegationService $delegationService; public function __construct(string $appName, @@ -41,7 +43,8 @@ public function __construct(string $appName, IMailManager $mailManager, SnoozeService $snoozeService, AiIntegrationsService $aiIntergrationsService, - LoggerInterface $logger) { + LoggerInterface $logger, + DelegationService $delegationService) { parent::__construct($appName, $request); $this->currentUserId = $userId; $this->accountService = $accountService; @@ -49,6 +52,7 @@ public function __construct(string $appName, $this->snoozeService = $snoozeService; $this->aiIntergrationsService = $aiIntergrationsService; $this->logger = $logger; + $this->delegationService = $delegationService; } /** @@ -64,11 +68,12 @@ public function __construct(string $appName, #[TrapError] public function move(int $id, int $destMailboxId): JSONResponse { try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $srcMailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $srcAccount = $this->accountService->find($this->currentUserId, $srcMailbox->getAccountId()); - $dstMailbox = $this->mailManager->getMailbox($this->currentUserId, $destMailboxId); - $dstAccount = $this->accountService->find($this->currentUserId, $dstMailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $srcMailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $srcAccount = $this->accountService->find($effectiveUserId, $srcMailbox->getAccountId()); + $dstMailbox = $this->mailManager->getMailbox($effectiveUserId, $destMailboxId); + $dstAccount = $this->accountService->find($effectiveUserId, $dstMailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -80,6 +85,7 @@ public function move(int $id, int $destMailboxId): JSONResponse { $dstMailbox, $message->getThreadRootId() ); + $this->delegationService->logDelegatedAction("$this->currentUserId moved thread <$id> to mailbox <$destMailboxId> on behalf of $effectiveUserId"); return new JSONResponse(); } @@ -98,16 +104,18 @@ public function move(int $id, int $destMailboxId): JSONResponse { #[TrapError] public function snooze(int $id, int $unixTimestamp, int $destMailboxId): JSONResponse { try { - $selectedMessage = $this->mailManager->getMessage($this->currentUserId, $id); - $srcMailbox = $this->mailManager->getMailbox($this->currentUserId, $selectedMessage->getMailboxId()); - $srcAccount = $this->accountService->find($this->currentUserId, $srcMailbox->getAccountId()); - $dstMailbox = $this->mailManager->getMailbox($this->currentUserId, $destMailboxId); - $dstAccount = $this->accountService->find($this->currentUserId, $dstMailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $selectedMessage = $this->mailManager->getMessage($effectiveUserId, $id); + $srcMailbox = $this->mailManager->getMailbox($effectiveUserId, $selectedMessage->getMailboxId()); + $srcAccount = $this->accountService->find($effectiveUserId, $srcMailbox->getAccountId()); + $dstMailbox = $this->mailManager->getMailbox($effectiveUserId, $destMailboxId); + $dstAccount = $this->accountService->find($effectiveUserId, $dstMailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } $this->snoozeService->snoozeThread($selectedMessage, $unixTimestamp, $srcAccount, $srcMailbox, $dstAccount, $dstMailbox); + $this->delegationService->logDelegatedAction("$this->currentUserId snoozed thread <$id> until <$unixTimestamp> in mailbox <$destMailboxId> on behalf of $effectiveUserId"); return new JSONResponse(); } @@ -124,12 +132,14 @@ public function snooze(int $id, int $unixTimestamp, int $destMailboxId): JSONRes #[TrapError] public function unSnooze(int $id): JSONResponse { try { - $selectedMessage = $this->mailManager->getMessage($this->currentUserId, $id); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $selectedMessage = $this->mailManager->getMessage($effectiveUserId, $id); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } - $this->snoozeService->unSnoozeThread($selectedMessage, $this->currentUserId); + $this->snoozeService->unSnoozeThread($selectedMessage, $effectiveUserId); + $this->delegationService->logDelegatedAction("$this->currentUserId unsnoozed thread <$id> on behalf of $effectiveUserId"); return new JSONResponse(); } @@ -143,9 +153,10 @@ public function unSnooze(int $id): JSONResponse { */ public function summarize(int $id): JSONResponse { try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -175,9 +186,10 @@ public function summarize(int $id): JSONResponse { */ public function generateEventData(int $id): JSONResponse { try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -207,9 +219,10 @@ public function generateEventData(int $id): JSONResponse { #[TrapError] public function delete(int $id): JSONResponse { try { - $message = $this->mailManager->getMessage($this->currentUserId, $id); - $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); - $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + $effectiveUserId = $this->delegationService->resolveMessageUserId($id, $this->currentUserId); + $message = $this->mailManager->getMessage($effectiveUserId, $id); + $mailbox = $this->mailManager->getMailbox($effectiveUserId, $message->getMailboxId()); + $account = $this->accountService->find($effectiveUserId, $mailbox->getAccountId()); } catch (DoesNotExistException $e) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -219,6 +232,7 @@ public function delete(int $id): JSONResponse { $mailbox, $message->getThreadRootId() ); + $this->delegationService->logDelegatedAction("$this->currentUserId deleted thread <$id> on behalf of $effectiveUserId"); return new JSONResponse(); } diff --git a/lib/Db/AliasMapper.php b/lib/Db/AliasMapper.php index bae9b6d98a..2a3f2791b3 100644 --- a/lib/Db/AliasMapper.php +++ b/lib/Db/AliasMapper.php @@ -141,6 +141,18 @@ public function deleteProvisionedAliasesByUid(string $uid): void { } } + /** + * @throws DoesNotExistException + */ + public function findAccountIdForAlias(int $aliasId): int { + $qb = $this->db->getQueryBuilder(); + $qb->select('account_id') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($aliasId))); + $row = $this->findOneQuery($qb); + return (int)$row['account_id']; + } + public function deleteOrphans(): void { $qb1 = $this->db->getQueryBuilder(); $idsQuery = $qb1->select('a.id') diff --git a/lib/Db/Delegation.php b/lib/Db/Delegation.php new file mode 100644 index 0000000000..d72de2498e --- /dev/null +++ b/lib/Db/Delegation.php @@ -0,0 +1,39 @@ +addType('userId', 'string'); + $this->addType('accountId', 'integer'); + } + + #[ReturnTypeWillChange] + public function jsonSerialize() { + return [ + 'id' => $this->getId(), + 'accountId' => $this->getAccountId(), + 'userId' => $this->getUserId(), + ]; + } +} diff --git a/lib/Db/DelegationMapper.php b/lib/Db/DelegationMapper.php new file mode 100644 index 0000000000..17d507c192 --- /dev/null +++ b/lib/Db/DelegationMapper.php @@ -0,0 +1,83 @@ + + */ +class DelegationMapper extends QBMapper { + public function __construct(IDBConnection $db) { + parent::__construct($db, 'mail_delegations'); + } + + public function findDelegatedAccountsForUser(string $uid): array { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('user_id', $qb->createNamedParameter($uid)) + ); + + return $this->findEntities($select); + } + + public function findDelegatedToUsers(int $accountId): array { + $qb = $this->db->getQueryBuilder(); + + $select = $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('account_id', $qb->createNamedParameter($accountId)) + ); + + return $this->findEntities($select); + } + + /** + * @throws DoesNotExistException + */ + public function find(int $accountId, string $uid): Delegation { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($uid))) + ->andWhere($qb->expr()->eq('account_id', $qb->createNamedParameter($accountId))); + return $this->findEntity($qb); + } + + /** + * @throws DoesNotExistException + */ + 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']; + } +} diff --git a/lib/Db/LocalMessageMapper.php b/lib/Db/LocalMessageMapper.php index 06ef03d023..3196ee1a87 100644 --- a/lib/Db/LocalMessageMapper.php +++ b/lib/Db/LocalMessageMapper.php @@ -104,6 +104,19 @@ public function findById(int $id, string $userId, int $type): LocalMessage { return $entity; } + /** + * @throws DoesNotExistException + */ + public function findAccountIdForLocalMessage(int $localMessageId): int { + $qb = $this->db->getQueryBuilder(); + $qb->select('account_id') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($localMessageId, IQueryBuilder::PARAM_INT))); + + $row = $this->findOneQuery($qb); + return (int)$row['account_id']; + } + /** * Find all messages that should be sent * diff --git a/lib/Db/MailboxMapper.php b/lib/Db/MailboxMapper.php index 36ebdfbbe3..ccae2d48ce 100644 --- a/lib/Db/MailboxMapper.php +++ b/lib/Db/MailboxMapper.php @@ -157,6 +157,19 @@ public function findByUid(int $id, string $uid): Mailbox { } } + /** + * @throws DoesNotExistException + */ + public function findAccountIdForMailbox(int $mailboxId): int { + $qb = $this->db->getQueryBuilder(); + $qb->select('account_id') + ->from($this->getTableName()) + ->where($qb->expr()->eq('id', $qb->createNamedParameter($mailboxId, IQueryBuilder::PARAM_INT))); + + $row = $this->findOneQuery($qb); + return (int)$row['account_id']; + } + /** * @throws MailboxLockedException */ diff --git a/lib/Db/MessageMapper.php b/lib/Db/MessageMapper.php index 63c961bfd3..75fa58011d 100644 --- a/lib/Db/MessageMapper.php +++ b/lib/Db/MessageMapper.php @@ -131,6 +131,21 @@ public function findByUserId(string $userId, int $id): Message { return $results[0]; } + /** + * @throws DoesNotExistException + */ + public function findAccountIdForMessage(int $messageId): int { + $qb = $this->db->getQueryBuilder(); + $qb->select('mb.account_id') + ->from($this->getTableName(), 'm') + ->join('m', 'mail_mailboxes', 'mb', + $qb->expr()->eq('m.mailbox_id', 'mb.id', IQueryBuilder::PARAM_INT)) + ->where($qb->expr()->eq('m.id', $qb->createNamedParameter($messageId, IQueryBuilder::PARAM_INT))); + + $row = $this->findOneQuery($qb); + return (int)$row['account_id']; + } + public function findAllUids(Mailbox $mailbox): array { $query = $this->db->getQueryBuilder(); diff --git a/lib/Exception/DelegationExistsException.php b/lib/Exception/DelegationExistsException.php new file mode 100644 index 0000000000..de14e81a4a --- /dev/null +++ b/lib/Exception/DelegationExistsException.php @@ -0,0 +1,18 @@ +hasTable('mail_delegations')) { + $table = $schema->createTable('mail_delegations'); + $table->addColumn('id', Types::INTEGER, [ + 'autoincrement' => true, + 'notnull' => true, + ]); + $table->addColumn('account_id', Types::INTEGER, [ + 'notnull' => true, + 'length' => 20, + ]); + $table->addColumn('user_id', Types::STRING, [ + 'notnull' => true, + 'length' => 64, + ]); + $table->setPrimaryKey(['id']); + $table->addUniqueIndex(['account_id', 'user_id'], 'mail_deleg_acc_user_uniq'); + if ($schema->hasTable('mail_accounts')) { + $table->addForeignKeyConstraint( + $schema->getTable('mail_accounts'), + ['account_id'], + ['id'], + [ + 'onDelete' => 'CASCADE', + ] + ); + } + } + + return $schema; + } + +} diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index dedd2e7ebd..b3f2d26b41 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -72,8 +72,57 @@ public function prepare(INotification $notification, string $languageCode): INot ] ]); break; + case 'account_delegation': + $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}'), + [ + '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; diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 024ddee9f8..f44206bb21 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -48,6 +48,7 @@ * @psalm-type MailAccountListResponse = array{ * id: int, * email: string, + * isDelegated: bool, * aliases: listmapper = $mapper; $this->aliasesService = $aliasesService; @@ -75,6 +77,23 @@ public function findByUserId(string $currentUserId): array { return $this->accounts[$currentUserId]; } + /** + * @param string $userId + * @return list + */ + public function findDelegatedAccounts(string $userId): array { + $delegations = $this->delegationMapper->findDelegatedAccountsForUser($userId); + $accounts = []; + foreach ($delegations as $delegation) { + try { + $accounts[] = new Account($this->mapper->findById($delegation->getAccountId())); + } catch (DoesNotExistException) { + // Account was deleted but delegation record remains — skip + } + } + return $accounts; + } + /** * @param int $id * diff --git a/lib/Service/DelegationService.php b/lib/Service/DelegationService.php new file mode 100644 index 0000000000..31ee748748 --- /dev/null +++ b/lib/Service/DelegationService.php @@ -0,0 +1,159 @@ +getId(); + 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); + $result = $this->delegationMapper->insert($delegation); + $this->notify($userId, $currentUserId, $account, true); + return $result; + } + + public function findDelegatedToUsersForAccount(int $accountId): array { + return $this->delegationMapper->findDelegatedToUsers($accountId); + } + + /** + * @throws DoesNotExistException + */ + public function unDelegate(Account $account, string $userId, string $currentUserId): void { + $accountId = $account->getId(); + $delegation = $this->delegationMapper->find($accountId, $userId); + $this->delegationMapper->delete($delegation); + $this->notify($userId, $currentUserId, $account, false); + } + + /** + * @throws DoesNotExistException + */ + public function resolveAccountUserId(int $accountId, string $currentUserId): string { + // Check if the current user owns the account + try { + $account = $this->mailAccountMapper->find($currentUserId, $accountId); + return $account->getUserId(); + } catch (DoesNotExistException) { + // Not the owner — check delegation + } + + return $this->delegationMapper->findAccountOwnerForDelegatedUser($accountId, $currentUserId); + } + + /** + * @throws DoesNotExistException + */ + public function resolveMailboxUserId(int $mailboxId, string $currentUserId): string { + $accountId = $this->mailboxMapper->findAccountIdForMailbox($mailboxId); + return $this->resolveAccountUserId($accountId, $currentUserId); + } + + /** + * @throws DoesNotExistException + */ + public function resolveMessageUserId(int $messageId, string $currentUserId): string { + $accountId = $this->messageMapper->findAccountIdForMessage($messageId); + return $this->resolveAccountUserId($accountId, $currentUserId); + } + + /** + * @throws DoesNotExistException + */ + public function resolveAliasUserId(int $aliasId, string $currentUserId): string { + $accountId = $this->aliasMapper->findAccountIdForAlias($aliasId); + return $this->resolveAccountUserId($accountId, $currentUserId); + } + + /** + * @throws DoesNotExistException + */ + public function resolveLocalMessageUserId(int $localMessageId, string $currentUserId): string { + $accountId = $this->localMessageMapper->findAccountIdForLocalMessage($localMessageId); + return $this->resolveAccountUserId($accountId, $currentUserId); + } + + + public function logDelegatedAction(string $logMessage) { + $this->eventDispatcher->dispatchTyped(new CriticalActionPerformedEvent($logMessage)); + } + + /** + * 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); + } +} diff --git a/tests/Integration/MailboxSynchronizationTest.php b/tests/Integration/MailboxSynchronizationTest.php index aa93fdf616..ba7c5b9a24 100644 --- a/tests/Integration/MailboxSynchronizationTest.php +++ b/tests/Integration/MailboxSynchronizationTest.php @@ -16,6 +16,7 @@ use OCA\Mail\Controller\MailboxesController; use OCA\Mail\Db\MessageMapper as DbMessageMapper; use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\Sync\ImapToDbSynchronizer; use OCA\Mail\Service\Sync\SyncService; use OCA\Mail\Tests\Integration\Framework\ImapTest; @@ -51,6 +52,7 @@ protected function setUp(): void { Server::get(SyncService::class), Server::get(IConfig::class), Server::get(ITimeFactory::class), + Server::get(DelegationService::class), ); $this->account = $this->createTestAccount('user12345'); diff --git a/tests/Unit/Controller/AccountApiControllerTest.php b/tests/Unit/Controller/AccountApiControllerTest.php index c560c8e4f5..85808d99f6 100644 --- a/tests/Unit/Controller/AccountApiControllerTest.php +++ b/tests/Unit/Controller/AccountApiControllerTest.php @@ -16,6 +16,7 @@ use OCA\Mail\Db\MailAccount; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AliasesService; +use OCA\Mail\Service\DelegationService; use OCP\AppFramework\Http; use OCP\IRequest; use PHPUnit\Framework\MockObject\MockObject; @@ -28,6 +29,7 @@ class AccountApiControllerTest extends TestCase { private IRequest&MockObject $request; private AccountService&MockObject $accountService; private AliasesService&MockObject $aliasesService; + private DelegationService&MockObject $delegationService; protected function setUp(): void { parent::setUp(); @@ -35,6 +37,7 @@ protected function setUp(): void { $this->request = $this->createMock(IRequest::class); $this->accountService = $this->createMock(AccountService::class); $this->aliasesService = $this->createMock(AliasesService::class); + $this->delegationService = $this->createMock(DelegationService::class); $this->controller = new AccountApiController( 'mail', @@ -42,6 +45,7 @@ protected function setUp(): void { self::USER_ID, $this->accountService, $this->aliasesService, + $this->delegationService, ); } @@ -52,6 +56,7 @@ public function testListWithoutUser() { null, $this->accountService, $this->aliasesService, + $this->delegationService, ); $this->accountService->expects(self::never()) @@ -74,6 +79,10 @@ public function testList() { ->method('findByUserId') ->with(self::USER_ID) ->willReturn([$account]); + $this->accountService->expects(self::once()) + ->method('findDelegatedAccounts') + ->with(self::USER_ID) + ->willReturn([]); $alias = new Alias(); $alias->setId(10); @@ -90,6 +99,7 @@ public function testList() { [ 'id' => 42, 'email' => 'foo@bar.com', + 'isDelegated' => false, 'aliases' => [ [ 'id' => 10, @@ -111,6 +121,10 @@ public function testListWithAliasWithoutName() { ->method('findByUserId') ->with(self::USER_ID) ->willReturn([$account]); + $this->accountService->expects(self::once()) + ->method('findDelegatedAccounts') + ->with(self::USER_ID) + ->willReturn([]); $alias = new Alias(); $alias->setId(10); @@ -127,6 +141,7 @@ public function testListWithAliasWithoutName() { [ 'id' => 42, 'email' => 'foo@bar.com', + 'isDelegated' => false, 'aliases' => [ [ 'id' => 10, @@ -148,6 +163,10 @@ public function testListWithoutAliases() { ->method('findByUserId') ->with(self::USER_ID) ->willReturn([$account]); + $this->accountService->expects(self::once()) + ->method('findDelegatedAccounts') + ->with(self::USER_ID) + ->willReturn([]); $this->aliasesService->expects(self::once()) ->method('findAll') @@ -160,6 +179,7 @@ public function testListWithoutAliases() { [ 'id' => 42, 'email' => 'foo@bar.com', + 'isDelegated' => false, 'aliases' => [], ] ], $actual->getData()); diff --git a/tests/Unit/Controller/AccountsControllerTest.php b/tests/Unit/Controller/AccountsControllerTest.php index b659555d3c..234ca139a6 100644 --- a/tests/Unit/Controller/AccountsControllerTest.php +++ b/tests/Unit/Controller/AccountsControllerTest.php @@ -21,6 +21,7 @@ use OCA\Mail\IMAP\Sync\Response; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AliasesService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\SetupService; use OCA\Mail\Service\Sync\SyncService; use OCP\AppFramework\Db\DoesNotExistException; @@ -85,6 +86,9 @@ class AccountsControllerTest extends TestCase { /** @var IConfig|(IConfig&MockObject)|MockObject */ private IConfig|MockObject $config; + + /** @var DelegationService|MockObject */ + private $delegationService; /** @var IRemoteHostValidator|MockObject */ private $hostValidator; @@ -107,6 +111,9 @@ protected function setUp(): void { $this->hostValidator = $this->createMock(IRemoteHostValidator::class); $this->hostValidator->method('isValid')->willReturn(true); $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->delegationService = $this->createMock(DelegationService::class); + $this->delegationService->method('resolveAccountUserId') + ->willReturn($this->userId); $this->controller = new AccountsController( $this->appName, @@ -124,6 +131,7 @@ protected function setUp(): void { $this->hostValidator, $this->mailboxSync, $this->timeFactory, + $this->delegationService, ); $this->account = $this->createMock(Account::class); $this->accountId = 123; @@ -143,6 +151,10 @@ public function testIndex(): void { ->method('findAll') ->with(self::equalTo($this->accountId), self::equalTo($this->userId)) ->will(self::returnValue(['a1', 'a2'])); + $this->accountService->expects(self::once()) + ->method('findDelegatedAccounts') + ->with(self::equalTo($this->userId)) + ->willReturn([]); $response = $this->controller->index(); @@ -153,6 +165,7 @@ public function testIndex(): void { 'a1', 'a2', ], + 'isDelegated' => false, ] ]); self::assertEquals($expectedResponse, $response); diff --git a/tests/Unit/Controller/AliasesControllerTest.php b/tests/Unit/Controller/AliasesControllerTest.php index 51e36e7933..067cecda14 100644 --- a/tests/Unit/Controller/AliasesControllerTest.php +++ b/tests/Unit/Controller/AliasesControllerTest.php @@ -15,6 +15,7 @@ use OCA\Mail\Exception\ClientException; use OCA\Mail\Exception\NotImplemented; use OCA\Mail\Service\AliasesService; +use OCA\Mail\Service\DelegationService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; @@ -35,6 +36,9 @@ class AliasesControllerTest extends TestCase { /** @var AliasesService */ private $aliasService; + /** @var DelegationService */ + private $delegationService; + public function setUp(): void { parent::setUp(); $this->request = $this->getMockBuilder('OCP\IRequest') @@ -48,7 +52,12 @@ public function setUp(): void { $this->mailAccountMapper = $this->createMock(MailAccountMapper::class); $this->aliasService = new AliasesService($this->aliasMapper, $this->mailAccountMapper); - $this->controller = new AliasesController($this->appName, $this->request, $this->aliasService, $this->userId); + $this->delegationService = $this->createMock(DelegationService::class); + $this->delegationService->method('resolveAccountUserId') + ->willReturn($this->userId); + $this->delegationService->method('resolveAliasUserId') + ->willReturn($this->userId); + $this->controller = new AliasesController($this->appName, $this->request, $this->aliasService, $this->userId, $this->delegationService); } public function testIndex(): void { diff --git a/tests/Unit/Controller/DelegationControllerTest.php b/tests/Unit/Controller/DelegationControllerTest.php new file mode 100644 index 0000000000..16ba90567e --- /dev/null +++ b/tests/Unit/Controller/DelegationControllerTest.php @@ -0,0 +1,239 @@ +request = $this->createMock(IRequest::class); + $this->delegationService = $this->createMock(DelegationService::class); + $this->accountService = $this->createMock(AccountService::class); + $this->userManager = $this->createMock(IUserManager::class); + + $this->controller = new DelegationController( + $this->appName, + $this->request, + $this->delegationService, + $this->accountService, + $this->userManager, + $this->currentUserId, + ); + + $ownMailAccount = new MailAccount(); + $ownMailAccount->setId(1); + $ownMailAccount->setUserId($this->currentUserId); + $ownMailAccount->setEmail('owner@example.com'); + $this->ownAccount = new Account($ownMailAccount); + + $otherMailAccount = new MailAccount(); + $otherMailAccount->setId(2); + $otherMailAccount->setUserId('other'); + $otherMailAccount->setEmail('other@example.com'); + $this->otherAccount = new Account($otherMailAccount); + } + + public function testGetDelegatedUsersSuccess(): void { + $delegation = new Delegation(); + $delegation->setId(10); + $delegation->setAccountId(1); + $delegation->setUserId('delegatee'); + + $this->accountService->expects($this->once()) + ->method('findById') + ->with(1) + ->willReturn($this->ownAccount); + + $this->delegationService->expects($this->once()) + ->method('findDelegatedToUsersForAccount') + ->with(1) + ->willReturn([$delegation]); + + $response = $this->controller->getDelegatedUsers(1); + + $this->assertInstanceOf(JSONResponse::class, $response); + $this->assertEquals(Http::STATUS_OK, $response->getStatus()); + $this->assertEquals([$delegation], $response->getData()); + } + + public function testGetDelegatedUsersUnauthorized(): void { + $this->accountService->expects($this->once()) + ->method('findById') + ->with(2) + ->willReturn($this->otherAccount); + + $this->delegationService->expects($this->never()) + ->method('findDelegatedToUsersForAccount'); + + $response = $this->controller->getDelegatedUsers(2); + + $this->assertInstanceOf(JSONResponse::class, $response); + $this->assertEquals(Http::STATUS_UNAUTHORIZED, $response->getStatus()); + } + + public function testDelegateSuccess(): void { + $delegation = new Delegation(); + $delegation->setId(10); + $delegation->setAccountId(1); + $delegation->setUserId('delegatee'); + + $this->accountService->expects($this->once()) + ->method('findById') + ->with(1) + ->willReturn($this->ownAccount); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with('delegatee') + ->willReturn(true); + + $this->delegationService->expects($this->once()) + ->method('delegate') + ->with($this->ownAccount, 'delegatee', $this->currentUserId) + ->willReturn($delegation); + + $response = $this->controller->delegate(1, 'delegatee'); + + $this->assertInstanceOf(JSONResponse::class, $response); + $this->assertEquals(Http::STATUS_CREATED, $response->getStatus()); + $this->assertEquals($delegation, $response->getData()); + } + + public function testDelegateUnauthorized(): void { + $this->accountService->expects($this->once()) + ->method('findById') + ->with(2) + ->willReturn($this->otherAccount); + + $this->delegationService->expects($this->never()) + ->method('delegate'); + + $response = $this->controller->delegate(2, 'delegatee'); + + $this->assertInstanceOf(JSONResponse::class, $response); + $this->assertEquals(Http::STATUS_UNAUTHORIZED, $response->getStatus()); + } + + public function testDelegateToSelf(): void { + $this->accountService->expects($this->once()) + ->method('findById') + ->with(1) + ->willReturn($this->ownAccount); + + $this->delegationService->expects($this->never()) + ->method('delegate'); + + $response = $this->controller->delegate(1, $this->currentUserId); + + $this->assertInstanceOf(JSONResponse::class, $response); + $this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus()); + $this->assertEquals(['message' => 'Cannot delegate to yourself'], $response->getData()); + } + + public function testDelegateUserNotFound(): void { + $this->accountService->expects($this->once()) + ->method('findById') + ->with(1) + ->willReturn($this->ownAccount); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with('nonexistent') + ->willReturn(false); + + $this->delegationService->expects($this->never()) + ->method('delegate'); + + $response = $this->controller->delegate(1, 'nonexistent'); + + $this->assertInstanceOf(JSONResponse::class, $response); + $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus()); + } + + public function testDelegateAlreadyExists(): void { + $this->accountService->expects($this->once()) + ->method('findById') + ->with(1) + ->willReturn($this->ownAccount); + + $this->userManager->expects($this->once()) + ->method('userExists') + ->with('delegatee') + ->willReturn(true); + + $this->delegationService->expects($this->once()) + ->method('delegate') + ->with($this->ownAccount, 'delegatee', $this->currentUserId) + ->willThrowException(new DelegationExistsException('Delegation already exists')); + + $response = $this->controller->delegate(1, 'delegatee'); + + $this->assertInstanceOf(JSONResponse::class, $response); + $this->assertEquals(Http::STATUS_CONFLICT, $response->getStatus()); + $this->assertEquals(['message' => 'Delegation already exists'], $response->getData()); + } + + public function testUnDelegateSuccess(): void { + $this->accountService->expects($this->once()) + ->method('findById') + ->with(1) + ->willReturn($this->ownAccount); + + $this->delegationService->expects($this->once()) + ->method('unDelegate') + ->with($this->ownAccount, 'delegatee', $this->currentUserId); + + $response = $this->controller->unDelegate(1, 'delegatee'); + + $this->assertInstanceOf(JSONResponse::class, $response); + $this->assertEquals(Http::STATUS_OK, $response->getStatus()); + } + + public function testUnDelegateUnauthorized(): void { + $this->accountService->expects($this->once()) + ->method('findById') + ->with(2) + ->willReturn($this->otherAccount); + + $this->delegationService->expects($this->never()) + ->method('unDelegate'); + + $response = $this->controller->unDelegate(2, 'delegatee'); + + $this->assertInstanceOf(JSONResponse::class, $response); + $this->assertEquals(Http::STATUS_UNAUTHORIZED, $response->getStatus()); + } +} diff --git a/tests/Unit/Controller/DraftsControllerTest.php b/tests/Unit/Controller/DraftsControllerTest.php index e1f672aff1..d9651ff6fa 100644 --- a/tests/Unit/Controller/DraftsControllerTest.php +++ b/tests/Unit/Controller/DraftsControllerTest.php @@ -19,6 +19,7 @@ use OCA\Mail\Exception\ServiceException; use OCA\Mail\Http\JsonResponse; use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\DraftsService; use OCA\Mail\Service\SmimeService; use OCP\AppFramework\Db\DoesNotExistException; @@ -35,6 +36,7 @@ class DraftsControllerTest extends TestCase { private AccountService $accountService; private DraftsController $controller; private SmimeService $smimeService; + private DelegationService $delegationService; protected function setUp(): void { parent::setUp(); @@ -46,6 +48,11 @@ protected function setUp(): void { $this->accountService = $this->createMock(AccountService::class); $this->timeFactory = $this->createMock(ITimeFactory::class); $this->smimeService = $this->createMock(SmimeService::class); + $this->delegationService = $this->createMock(DelegationService::class); + $this->delegationService->method('resolveAccountUserId') + ->willReturn($this->userId); + $this->delegationService->method('resolveLocalMessageUserId') + ->willReturn($this->userId); $this->controller = new DraftsController( $this->appName, @@ -54,7 +61,8 @@ protected function setUp(): void { $this->service, $this->accountService, $this->timeFactory, - $this->smimeService + $this->smimeService, + $this->delegationService, ); } diff --git a/tests/Unit/Controller/ListControllerTest.php b/tests/Unit/Controller/ListControllerTest.php index 259ded1d51..b97b9dd482 100644 --- a/tests/Unit/Controller/ListControllerTest.php +++ b/tests/Unit/Controller/ListControllerTest.php @@ -34,6 +34,9 @@ protected function setUp(): void { $this->serviceMock = $this->createServiceMock(ListController::class, [ 'userId' => 'user123', ]); + $this->serviceMock->getParameter('delegationService') + ->method('resolveMessageUserId') + ->willReturn('user123'); $this->controller = $this->serviceMock->getService(); } diff --git a/tests/Unit/Controller/MailboxesApiControllerTest.php b/tests/Unit/Controller/MailboxesApiControllerTest.php index 221de5de87..f00754af70 100644 --- a/tests/Unit/Controller/MailboxesApiControllerTest.php +++ b/tests/Unit/Controller/MailboxesApiControllerTest.php @@ -19,6 +19,7 @@ use OCA\Mail\Db\Message as DbMessage; use OCA\Mail\Folder; use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DelegationService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\IRequest; @@ -33,6 +34,7 @@ class MailboxesApiControllerTest extends TestCase { private IMailManager|MockObject $mailManager; private AccountService&MockObject $accountService; private MockObject|IMailSearch $mailSearch; + private DelegationService&MockObject $delegationService; protected function setUp(): void { parent::setUp(); @@ -41,6 +43,11 @@ protected function setUp(): void { $this->accountService = $this->createMock(AccountService::class); $this->mailManager = $this->createMock(IMailManager::class); $this->mailSearch = $this->createMock(IMailSearch::class); + $this->delegationService = $this->createMock(DelegationService::class); + $this->delegationService->method('resolveAccountUserId') + ->willReturn(self::USER_ID); + $this->delegationService->method('resolveMailboxUserId') + ->willReturn(self::USER_ID); $this->controller = new MailboxesApiController( 'mail', @@ -49,7 +56,7 @@ protected function setUp(): void { $this->mailManager, $this->accountService, $this->mailSearch, - + $this->delegationService, ); } @@ -61,6 +68,7 @@ public function testListMailboxesWithoutUser() { $this->mailManager, $this->accountService, $this->mailSearch, + $this->delegationService, ); $this->accountService->expects(self::never()) @@ -101,6 +109,7 @@ public function testListMessagesWithoutUser() { $this->mailManager, $this->accountService, $this->mailSearch, + $this->delegationService, ); $this->accountService->expects(self::never()) diff --git a/tests/Unit/Controller/MailboxesControllerTest.php b/tests/Unit/Controller/MailboxesControllerTest.php index 8dddb381d2..51ffc5d3ef 100644 --- a/tests/Unit/Controller/MailboxesControllerTest.php +++ b/tests/Unit/Controller/MailboxesControllerTest.php @@ -18,6 +18,7 @@ use OCA\Mail\Folder; use OCA\Mail\IMAP\MailboxStats; use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\Sync\SyncService; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Utility\ITimeFactory; @@ -49,6 +50,7 @@ class MailboxesControllerTest extends TestCase { private IConfig|MockObject $config; private ITimeFactory|MockObject $timeFactory; + private DelegationService|MockObject $delegationService; public function setUp(): void { parent::setUp(); @@ -59,6 +61,9 @@ public function setUp(): void { $this->syncService = $this->createMock(SyncService::class); $this->config = $this->createMock(IConfig::class); $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->delegationService = $this->createMock(DelegationService::class); + $this->delegationService->method('resolveAccountUserId')->willReturn($this->userId); + $this->delegationService->method('resolveMailboxUserId')->willReturn($this->userId); $this->controller = new MailboxesController( $this->appName, @@ -68,7 +73,8 @@ public function setUp(): void { $this->mailManager, $this->syncService, $this->config, - $this->timeFactory + $this->timeFactory, + $this->delegationService, ); } diff --git a/tests/Unit/Controller/MessageApiControllerTest.php b/tests/Unit/Controller/MessageApiControllerTest.php index 7b06ac01d1..6ab43d2dea 100644 --- a/tests/Unit/Controller/MessageApiControllerTest.php +++ b/tests/Unit/Controller/MessageApiControllerTest.php @@ -27,6 +27,7 @@ use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AliasesService; use OCA\Mail\Service\Attachment\AttachmentService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\DkimService; use OCA\Mail\Service\ItineraryService; use OCA\Mail\Service\MailManager; @@ -58,6 +59,7 @@ class MessageApiControllerTest extends TestCase { private DkimService|MockObject $dkimService; private MockObject|ItineraryService $itineraryService; private TrustedSenderService|MockObject $trustedSenderService; + private DelegationService|MockObject $delegationService; private MessageApiController $controller; private string $fromEmail = 'john@test.com'; private int $accountId = 1; @@ -84,6 +86,9 @@ protected function setUp(): void { $this->dkimService = $this->createMock(DkimService::class); $this->itineraryService = $this->createMock(ItineraryService::class); $this->trustedSenderService = $this->createMock(TrustedSenderService::class); + $this->delegationService = $this->createMock(DelegationService::class); + $this->delegationService->method('resolveAccountUserId')->willReturn($this->userId); + $this->delegationService->method('resolveMessageUserId')->willReturn($this->userId); $this->controller = new MessageApiController($this->appName, $this->userId, @@ -100,6 +105,7 @@ protected function setUp(): void { $this->dkimService, $this->itineraryService, $this->trustedSenderService, + $this->delegationService, ); $mailAccount = new MailAccount(); diff --git a/tests/Unit/Controller/MessagesControllerTest.php b/tests/Unit/Controller/MessagesControllerTest.php index 8577019883..1e443fe160 100644 --- a/tests/Unit/Controller/MessagesControllerTest.php +++ b/tests/Unit/Controller/MessagesControllerTest.php @@ -37,6 +37,7 @@ use OCA\Mail\Model\Message; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AiIntegrations\AiIntegrationsService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\ItineraryService; use OCA\Mail\Service\MailManager; use OCA\Mail\Service\SmimeService; @@ -134,6 +135,8 @@ class MessagesControllerTest extends TestCase { private ICacheFactory&MockObject $cacheFactory; + private DelegationService|MockObject $delegationService; + protected function setUp(): void { parent::setUp(); @@ -164,6 +167,10 @@ protected function setUp(): void { $this->cacheFactory->method('createDistributed') ->willReturn(new NullCache()); + $this->delegationService = $this->createMock(DelegationService::class); + $this->delegationService->method('resolveMessageUserId')->willReturn($this->userId); + $this->delegationService->method('resolveMailboxUserId')->willReturn($this->userId); + $timeFactory = $this->createMocK(ITimeFactory::class); $timeFactory->expects($this->any()) ->method('getTime') @@ -194,6 +201,7 @@ protected function setUp(): void { $this->snoozeService, $this->aiIntegrationsService, $this->cacheFactory, + $this->delegationService, ); $this->account = $this->createMock(Account::class); @@ -1249,6 +1257,7 @@ public function testNeedsTranslationNoUser() { $this->snoozeService, $this->aiIntegrationsService, $this->cacheFactory, + $this->delegationService, ); $actualResponse = $controller->needsTranslation(100); @@ -1421,6 +1430,7 @@ public function testSmartReplyNoUser(): void { $this->snoozeService, $this->aiIntegrationsService, $this->cacheFactory, + $this->delegationService, ); $actualResponse = $controller->smartReply(100); diff --git a/tests/Unit/Controller/OutboxControllerTest.php b/tests/Unit/Controller/OutboxControllerTest.php index 51f25603cf..ff31d91315 100644 --- a/tests/Unit/Controller/OutboxControllerTest.php +++ b/tests/Unit/Controller/OutboxControllerTest.php @@ -19,6 +19,7 @@ use OCA\Mail\Exception\ServiceException; use OCA\Mail\Http\JsonResponse; use OCA\Mail\Service\AccountService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\OutboxService; use OCA\Mail\Service\SmimeService; use OCP\AppFramework\Db\DoesNotExistException; @@ -43,6 +44,8 @@ class OutboxControllerTest extends TestCase { /** @var SmimeService&MockObject */ private $smimeService; + private DelegationService&MockObject $delegationService; + private OutboxController $controller; protected function setUp(): void { parent::setUp(); @@ -53,6 +56,11 @@ protected function setUp(): void { $this->request = $this->createMock(IRequest::class); $this->accountService = $this->createMock(AccountService::class); $this->smimeService = $this->createMock(SmimeService::class); + $this->delegationService = $this->createMock(DelegationService::class); + $this->delegationService->method('resolveAccountUserId') + ->willReturn($this->userId); + $this->delegationService->method('resolveLocalMessageUserId') + ->willReturn($this->userId); $this->controller = new OutboxController( $this->appName, @@ -61,6 +69,7 @@ protected function setUp(): void { $this->service, $this->accountService, $this->smimeService, + $this->delegationService, ); } diff --git a/tests/Unit/Controller/PageControllerTest.php b/tests/Unit/Controller/PageControllerTest.php index 804b9df2ef..9b84226810 100644 --- a/tests/Unit/Controller/PageControllerTest.php +++ b/tests/Unit/Controller/PageControllerTest.php @@ -205,6 +205,10 @@ public function testIndex(): void { $account1, $account2, ])); + $this->accountService->expects($this->once()) + ->method('findDelegatedAccounts') + ->with($this->userId) + ->willReturn([]); $this->mailManager->expects($this->exactly(2)) ->method('getMailboxes') ->withConsecutive( @@ -247,6 +251,7 @@ public function testIndex(): void { 'mailboxes' => [ $mailbox, ], + 'isDelegated' => false, ], [ 'accountId' => 2, @@ -255,6 +260,7 @@ public function testIndex(): void { 'a22', ], 'mailboxes' => [], + 'isDelegated' => false, ], ]; diff --git a/tests/Unit/Controller/SieveControllerTest.php b/tests/Unit/Controller/SieveControllerTest.php index c084c716ab..9a7bde01b9 100644 --- a/tests/Unit/Controller/SieveControllerTest.php +++ b/tests/Unit/Controller/SieveControllerTest.php @@ -43,6 +43,9 @@ protected function setUp(): void { 'userId' => '1', ] ); + $this->serviceMock->getParameter('delegationService') + ->method('resolveAccountUserId') + ->willReturn('1'); $this->sieveController = $this->serviceMock->getService(); } diff --git a/tests/Unit/Controller/ThreadControllerTest.php b/tests/Unit/Controller/ThreadControllerTest.php index bdd8220b7b..1dcf67c695 100644 --- a/tests/Unit/Controller/ThreadControllerTest.php +++ b/tests/Unit/Controller/ThreadControllerTest.php @@ -19,6 +19,7 @@ use OCA\Mail\Model\EventData; use OCA\Mail\Service\AccountService; use OCA\Mail\Service\AiIntegrations\AiIntegrationsService; +use OCA\Mail\Service\DelegationService; use OCA\Mail\Service\SnoozeService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; @@ -54,6 +55,9 @@ class ThreadControllerTest extends TestCase { /** @var LoggerInterface|MockObject */ private $logger; + /** @var DelegationService|MockObject */ + private $delegationService; + protected function setUp(): void { parent::setUp(); @@ -65,6 +69,9 @@ protected function setUp(): void { $this->snoozeService = $this->createMock(SnoozeService::class); $this->aiIntergrationsService = $this->createMock(AiIntegrationsService::class); $this->logger = $this->createMock(LoggerInterface::class); + $this->delegationService = $this->createMock(DelegationService::class); + $this->delegationService->method('resolveMessageUserId')->willReturn($this->userId); + $this->delegationService->method('resolveMailboxUserId')->willReturn($this->userId); $this->controller = new ThreadController( $this->appName, @@ -75,6 +82,7 @@ protected function setUp(): void { $this->snoozeService, $this->aiIntergrationsService, $this->logger, + $this->delegationService, ); } diff --git a/tests/Unit/Service/AccountServiceTest.php b/tests/Unit/Service/AccountServiceTest.php index e218a28fc6..242b2cb427 100644 --- a/tests/Unit/Service/AccountServiceTest.php +++ b/tests/Unit/Service/AccountServiceTest.php @@ -13,6 +13,7 @@ use OCA\Mail\Account; use OCA\Mail\BackgroundJob\QuotaJob; use OCA\Mail\BackgroundJob\SyncJob; +use OCA\Mail\Db\DelegationMapper; use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\MailAccountMapper; use OCA\Mail\Exception\ClientException; @@ -61,6 +62,7 @@ class AccountServiceTest extends TestCase { private IConfig&MockObject $config; private ITimeFactory&MockObject $time; + private DelegationMapper&MockObject $delegationMapper; protected function setUp(): void { parent::setUp(); @@ -72,6 +74,7 @@ protected function setUp(): void { $this->imapClientFactory = $this->createMock(IMAPClientFactory::class); $this->config = $this->createMock(IConfig::class); $this->time = $this->createMock(ITimeFactory::class); + $this->delegationMapper = $this->createMock(DelegationMapper::class); $this->accountService = new AccountService( $this->mapper, $this->aliasesService, @@ -79,6 +82,7 @@ protected function setUp(): void { $this->imapClientFactory, $this->config, $this->time, + $this->delegationMapper, ); $this->account1 = new MailAccount(); diff --git a/tests/Unit/Service/DelegationServiceTest.php b/tests/Unit/Service/DelegationServiceTest.php new file mode 100644 index 0000000000..dd2f1a0ac6 --- /dev/null +++ b/tests/Unit/Service/DelegationServiceTest.php @@ -0,0 +1,333 @@ +delegationMapper = $this->createMock(DelegationMapper::class); + $this->mailAccountMapper = $this->createMock(MailAccountMapper::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->mailAccountMapper, + $this->mailboxMapper, + $this->messageMapper, + $this->aliasMapper, + $this->localMessageMapper, + $this->userManager, + $this->notificationManager, + $this->timeFactory, + $this->eventDispatcher, + ); + + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('owner'); + $mailAccount->setEmail('owner@example.com'); + $this->account = new Account($mailAccount); + } + + private function mockNotification(): void { + $notification = $this->createMock(INotification::class); + $notification->method('setApp')->willReturnSelf(); + $notification->method('setUser')->willReturnSelf(); + $notification->method('setObject')->willReturnSelf(); + $notification->method('setSubject')->willReturnSelf(); + $notification->method('setDateTime')->willReturnSelf(); + $notification->method('setMessage')->willReturnSelf(); + + $this->notificationManager->method('createNotification')->willReturn($notification); + + $user = $this->createMock(IUser::class); + $user->method('getDisplayName')->willReturn('Owner User'); + $this->userManager->method('get')->with('owner')->willReturn($user); + $this->timeFactory->method('getDateTime')->willReturn(new \DateTime()); + } + + public function testDelegateSuccess(): void { + $this->mockNotification(); + + $this->delegationMapper->expects($this->once()) + ->method('find') + ->with(1, 'delegatee') + ->willThrowException(new DoesNotExistException('Not found')); + + $expected = new Delegation(); + $expected->setAccountId(1); + $expected->setUserId('delegatee'); + + $this->delegationMapper->expects($this->once()) + ->method('insert') + ->willReturnCallback(function (Delegation $d) { + $d->setId(10); + return $d; + }); + + $this->notificationManager->expects($this->once()) + ->method('notify'); + + $result = $this->service->delegate($this->account, 'delegatee', 'owner'); + + $this->assertEquals(1, $result->getAccountId()); + $this->assertEquals('delegatee', $result->getUserId()); + } + + public function testDelegateThrowsWhenAlreadyExists(): void { + $existing = new Delegation(); + $existing->setAccountId(1); + $existing->setUserId('delegatee'); + + $this->delegationMapper->expects($this->once()) + ->method('find') + ->with(1, 'delegatee') + ->willReturn($existing); + + $this->delegationMapper->expects($this->never()) + ->method('insert'); + + $this->expectException(DelegationExistsException::class); + + $this->service->delegate($this->account, 'delegatee', 'owner'); + } + + public function testFindDelegatedToUsersForAccount(): void { + $delegation = new Delegation(); + $delegation->setAccountId(1); + $delegation->setUserId('delegatee'); + + $this->delegationMapper->expects($this->once()) + ->method('findDelegatedToUsers') + ->with(1) + ->willReturn([$delegation]); + + $result = $this->service->findDelegatedToUsersForAccount(1); + + $this->assertCount(1, $result); + $this->assertEquals('delegatee', $result[0]->getUserId()); + } + + public function testUnDelegateSuccess(): void { + $this->mockNotification(); + + $delegation = new Delegation(); + $delegation->setId(10); + $delegation->setAccountId(1); + $delegation->setUserId('delegatee'); + + $this->delegationMapper->expects($this->once()) + ->method('find') + ->with(1, 'delegatee') + ->willReturn($delegation); + + $this->delegationMapper->expects($this->once()) + ->method('delete') + ->with($delegation); + + $this->notificationManager->expects($this->once()) + ->method('notify'); + + $this->service->unDelegate($this->account, 'delegatee', 'owner'); + } + + public function testUnDelegateThrowsWhenNotFound(): void { + $this->delegationMapper->expects($this->once()) + ->method('find') + ->with(1, 'delegatee') + ->willThrowException(new DoesNotExistException('Not found')); + + $this->delegationMapper->expects($this->never()) + ->method('delete'); + + $this->expectException(DoesNotExistException::class); + + $this->service->unDelegate($this->account, 'delegatee', 'owner'); + } + + public function testResolveAccountUserIdOwner(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('owner'); + + $this->mailAccountMapper->expects($this->once()) + ->method('find') + ->with('owner', 1) + ->willReturn($mailAccount); + + $result = $this->service->resolveAccountUserId(1, 'owner'); + + $this->assertEquals('owner', $result); + } + + public function testResolveAccountUserIdDelegated(): void { + $this->mailAccountMapper->expects($this->once()) + ->method('find') + ->with('delegatee', 1) + ->willThrowException(new DoesNotExistException('Not found')); + + $this->delegationMapper->expects($this->once()) + ->method('findAccountOwnerForDelegatedUser') + ->with(1, 'delegatee') + ->willReturn('owner'); + + $result = $this->service->resolveAccountUserId(1, 'delegatee'); + + $this->assertEquals('owner', $result); + } + + public function testResolveAccountUserIdNotFound(): void { + $this->mailAccountMapper->expects($this->once()) + ->method('find') + ->with('stranger', 1) + ->willThrowException(new DoesNotExistException('Not found')); + + $this->delegationMapper->expects($this->once()) + ->method('findAccountOwnerForDelegatedUser') + ->with(1, 'stranger') + ->willThrowException(new DoesNotExistException('No delegation found')); + + $this->expectException(DoesNotExistException::class); + + $this->service->resolveAccountUserId(1, 'stranger'); + } + + public function testResolveMailboxUserId(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('owner'); + + $this->mailboxMapper->expects($this->once()) + ->method('findAccountIdForMailbox') + ->with(42) + ->willReturn(1); + + $this->mailAccountMapper->expects($this->once()) + ->method('find') + ->with('owner', 1) + ->willReturn($mailAccount); + + $result = $this->service->resolveMailboxUserId(42, 'owner'); + + $this->assertEquals('owner', $result); + } + + public function testResolveMessageUserId(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('owner'); + + $this->messageMapper->expects($this->once()) + ->method('findAccountIdForMessage') + ->with(99) + ->willReturn(1); + + $this->mailAccountMapper->expects($this->once()) + ->method('find') + ->with('owner', 1) + ->willReturn($mailAccount); + + $result = $this->service->resolveMessageUserId(99, 'owner'); + + $this->assertEquals('owner', $result); + } + + public function testResolveAliasUserId(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('owner'); + + $this->aliasMapper->expects($this->once()) + ->method('findAccountIdForAlias') + ->with(7) + ->willReturn(1); + + $this->mailAccountMapper->expects($this->once()) + ->method('find') + ->with('owner', 1) + ->willReturn($mailAccount); + + $result = $this->service->resolveAliasUserId(7, 'owner'); + + $this->assertEquals('owner', $result); + } + + public function testResolveLocalMessageUserId(): void { + $mailAccount = new MailAccount(); + $mailAccount->setId(1); + $mailAccount->setUserId('owner'); + + $this->localMessageMapper->expects($this->once()) + ->method('findAccountIdForLocalMessage') + ->with(55) + ->willReturn(1); + + $this->mailAccountMapper->expects($this->once()) + ->method('find') + ->with('owner', 1) + ->willReturn($mailAccount); + + $result = $this->service->resolveLocalMessageUserId(55, 'owner'); + + $this->assertEquals('owner', $result); + } + + public function testLogDelegatedAction(): void { + $this->eventDispatcher->expects($this->once()) + ->method('dispatchTyped') + ->with($this->isInstanceOf(CriticalActionPerformedEvent::class)); + + $this->service->logDelegatedAction('User owner read mailbox on behalf of owner'); + } +}