From 261d3e57ab0134dc4d080c58c54f849cc2eaf327 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Thu, 12 Feb 2026 09:16:36 +0100 Subject: [PATCH 1/8] IONOS(ionos-mail): update IONOS Mail API client reference to 2.0.0-20251208083401 composer update ionos-productivity/ionos-mail-configuration-api-client https://github.com/IONOS-Productivity/ionos-mail-configuration-api-client/releases/tag/2.0.0-20251208083401 Signed-off-by: Misha M.-Kupriyanov --- composer.json | 2 +- composer.lock | 4 ++-- .../Service/Core/IonosAccountMutationService.php | 10 +++++----- .../Ionos/Service/IonosMailService.php | 10 +++++----- .../Ionos/Service/IonosMailServiceTest.php | 12 ++++++------ 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index 03c8fabdc5..1235d8e2d8 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,7 @@ "source": { "type": "git", "url": "https://github.com/ionos-productivity/ionos-mail-configuration-api-client.git", - "reference": "2.0.0-20251208083401" + "reference": "2.0.0-20260210132735" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index d99348d985..5822e59de5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fb553591efe3fd5dbaed693076de60c6", + "content-hash": "be0c6c53c9b00d2b30f96797ecdc80e2", "packages": [ { "name": "amphp/amp", @@ -1860,7 +1860,7 @@ "source": { "type": "git", "url": "https://github.com/ionos-productivity/ionos-mail-configuration-api-client.git", - "reference": "2.0.0-20251208083401" + "reference": "2.0.0-20260210132735" }, "type": "library", "autoload": { diff --git a/lib/Provider/MailAccountProvider/Implementations/Ionos/Service/Core/IonosAccountMutationService.php b/lib/Provider/MailAccountProvider/Implementations/Ionos/Service/Core/IonosAccountMutationService.php index 475efca764..0f7d8d1204 100644 --- a/lib/Provider/MailAccountProvider/Implementations/Ionos/Service/Core/IonosAccountMutationService.php +++ b/lib/Provider/MailAccountProvider/Implementations/Ionos/Service/Core/IonosAccountMutationService.php @@ -10,11 +10,11 @@ namespace OCA\Mail\Provider\MailAccountProvider\Implementations\Ionos\Service\Core; use IONOS\MailConfigurationAPI\Client\ApiException; -use IONOS\MailConfigurationAPI\Client\Model\Imap; +use IONOS\MailConfigurationAPI\Client\Model\ImapConfig; use IONOS\MailConfigurationAPI\Client\Model\MailAccountCreatedResponse; use IONOS\MailConfigurationAPI\Client\Model\MailAddonErrorMessage; use IONOS\MailConfigurationAPI\Client\Model\MailCreateData; -use IONOS\MailConfigurationAPI\Client\Model\Smtp; +use IONOS\MailConfigurationAPI\Client\Model\SmtpConfig; use OCA\Mail\Exception\ServiceException; use OCA\Mail\Provider\MailAccountProvider\Common\Dto\MailAccountConfig; use OCA\Mail\Provider\MailAccountProvider\Common\Dto\MailServerConfig; @@ -356,13 +356,13 @@ private function buildSuccessResponse(MailAccountCreatedResponse $response): Mai * Creates a complete MailAccountConfig object by combining IMAP and SMTP server * configurations with email credentials. SSL modes are normalized to standard format. * - * @param Imap $imapServer IMAP server configuration object - * @param Smtp $smtpServer SMTP server configuration object + * @param ImapConfig $imapServer IMAP server configuration object + * @param SmtpConfig $smtpServer SMTP server configuration object * @param string $email Email address * @param string $password Account password * @return MailAccountConfig Complete mail account configuration */ - private function buildMailAccountConfig(Imap $imapServer, Smtp $smtpServer, string $email, string $password): MailAccountConfig { + private function buildMailAccountConfig(ImapConfig $imapServer, SmtpConfig $smtpServer, string $email, string $password): MailAccountConfig { $imapConfig = new MailServerConfig( host: $imapServer->getHost(), port: $imapServer->getPort(), diff --git a/lib/Provider/MailAccountProvider/Implementations/Ionos/Service/IonosMailService.php b/lib/Provider/MailAccountProvider/Implementations/Ionos/Service/IonosMailService.php index 69f31cd278..1a5d7a37d8 100644 --- a/lib/Provider/MailAccountProvider/Implementations/Ionos/Service/IonosMailService.php +++ b/lib/Provider/MailAccountProvider/Implementations/Ionos/Service/IonosMailService.php @@ -11,12 +11,12 @@ use IONOS\MailConfigurationAPI\Client\Api\MailConfigurationAPIApi; use IONOS\MailConfigurationAPI\Client\ApiException; -use IONOS\MailConfigurationAPI\Client\Model\Imap; +use IONOS\MailConfigurationAPI\Client\Model\ImapConfig; use IONOS\MailConfigurationAPI\Client\Model\MailAccountCreatedResponse; use IONOS\MailConfigurationAPI\Client\Model\MailAccountResponse; use IONOS\MailConfigurationAPI\Client\Model\MailAddonErrorMessage; use IONOS\MailConfigurationAPI\Client\Model\MailCreateData; -use IONOS\MailConfigurationAPI\Client\Model\Smtp; +use IONOS\MailConfigurationAPI\Client\Model\SmtpConfig; use OCA\Mail\Exception\ServiceException; use OCA\Mail\Provider\MailAccountProvider\Common\Dto\MailAccountConfig; use OCA\Mail\Provider\MailAccountProvider\Common\Dto\MailServerConfig; @@ -333,13 +333,13 @@ private function buildSuccessResponse(MailAccountCreatedResponse $response): Mai /** * Build mail account configuration from server details * - * @param Imap $imapServer IMAP server configuration object - * @param Smtp $smtpServer SMTP server configuration object + * @param ImapConfig $imapServer IMAP server configuration object + * @param SmtpConfig $smtpServer SMTP server configuration object * @param string $email Email address * @param string $password Account password * @return MailAccountConfig Complete mail account configuration */ - private function buildMailAccountConfig(Imap $imapServer, Smtp $smtpServer, string $email, string $password): MailAccountConfig { + private function buildMailAccountConfig(ImapConfig $imapServer, SmtpConfig $smtpServer, string $email, string $password): MailAccountConfig { $imapConfig = new MailServerConfig( host: $imapServer->getHost(), port: $imapServer->getPort(), diff --git a/tests/Unit/Provider/MailAccountProvider/Implementations/Ionos/Service/IonosMailServiceTest.php b/tests/Unit/Provider/MailAccountProvider/Implementations/Ionos/Service/IonosMailServiceTest.php index 667010a520..a2c6f093a4 100644 --- a/tests/Unit/Provider/MailAccountProvider/Implementations/Ionos/Service/IonosMailServiceTest.php +++ b/tests/Unit/Provider/MailAccountProvider/Implementations/Ionos/Service/IonosMailServiceTest.php @@ -12,12 +12,12 @@ use ChristophWurst\Nextcloud\Testing\TestCase; use GuzzleHttp\ClientInterface; use IONOS\MailConfigurationAPI\Client\Api\MailConfigurationAPIApi; -use IONOS\MailConfigurationAPI\Client\Model\Imap; +use IONOS\MailConfigurationAPI\Client\Model\ImapConfig; use IONOS\MailConfigurationAPI\Client\Model\MailAccountCreatedResponse; use IONOS\MailConfigurationAPI\Client\Model\MailAccountResponse; use IONOS\MailConfigurationAPI\Client\Model\MailAddonErrorMessage; use IONOS\MailConfigurationAPI\Client\Model\MailServer; -use IONOS\MailConfigurationAPI\Client\Model\Smtp; +use IONOS\MailConfigurationAPI\Client\Model\SmtpConfig; use OCA\Mail\Exception\ServiceException; use OCA\Mail\Provider\MailAccountProvider\Common\Dto\MailAccountConfig; use OCA\Mail\Provider\MailAccountProvider\Implementations\Ionos\Service\ApiMailConfigClientService; @@ -121,8 +121,8 @@ private function createMockImapServer( string $host = self::IMAP_HOST, int $port = self::IMAP_PORT, string $sslMode = 'ssl', - ): Imap&MockObject { - $imapServer = $this->getMockBuilder(Imap::class) + ): ImapConfig&MockObject { + $imapServer = $this->getMockBuilder(ImapConfig::class) ->disableOriginalConstructor() ->onlyMethods(['getHost', 'getPort', 'getSslMode']) ->getMock(); @@ -139,8 +139,8 @@ private function createMockSmtpServer( string $host = self::SMTP_HOST, int $port = self::SMTP_PORT, string $sslMode = 'tls', - ): Smtp&MockObject { - $smtpServer = $this->getMockBuilder(Smtp::class) + ): SmtpConfig&MockObject { + $smtpServer = $this->getMockBuilder(SmtpConfig::class) ->disableOriginalConstructor() ->onlyMethods(['getHost', 'getPort', 'getSslMode']) ->getMock(); From 5cac64c55f6ddfe5e49eb6b9c08dc85da3b47279 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:41:51 +0000 Subject: [PATCH 2/8] feat: Add backend API for mailbox management - Add AccountAlreadyExistsException for conflict handling - Extend IMailAccountProvider interface with mailbox methods (getMailboxes, updateMailbox, deleteMailbox) - Implement mailbox methods in IonosProvider and IonosProviderFacade - Add controller methods in ExternalAccountsController (indexMailboxes, updateMailbox, destroyMailbox) - Add new routes for mailbox operations - Add localpart validation in updateMailbox with regex check Co-authored-by: tanyaka <1401715+tanyaka@users.noreply.github.com> --- appinfo/routes.php | 15 ++ lib/Controller/ExternalAccountsController.php | 184 ++++++++++++++++++ .../AccountAlreadyExistsException.php | 17 ++ .../IMailAccountProvider.php | 31 +++ .../Ionos/IonosProviderFacade.php | 60 ++++++ .../Implementations/IonosProvider.php | 12 ++ 6 files changed, 319 insertions(+) create mode 100644 lib/Exception/AccountAlreadyExistsException.php diff --git a/appinfo/routes.php b/appinfo/routes.php index addac33608..c54fee7967 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -135,6 +135,21 @@ 'url' => '/api/providers/{providerId}/password', 'verb' => 'POST' ], + [ + 'name' => 'externalAccounts#indexMailboxes', + 'url' => '/api/providers/{providerId}/mailboxes', + 'verb' => 'GET' + ], + [ + 'name' => 'externalAccounts#updateMailbox', + 'url' => '/api/providers/{providerId}/mailboxes/{userId}', + 'verb' => 'PUT' + ], + [ + 'name' => 'externalAccounts#destroyMailbox', + 'url' => '/api/providers/{providerId}/mailboxes/{userId}', + 'verb' => 'DELETE' + ], [ 'name' => 'tags#update', 'url' => '/api/tags/{id}', diff --git a/lib/Controller/ExternalAccountsController.php b/lib/Controller/ExternalAccountsController.php index b2e968ee50..4782106979 100644 --- a/lib/Controller/ExternalAccountsController.php +++ b/lib/Controller/ExternalAccountsController.php @@ -248,6 +248,190 @@ public function generatePassword(string $providerId): JSONResponse { } } + /** + * List all mailboxes for a specific provider + * + * @NoAdminRequired + * + * @param string $providerId The provider ID + * @return JSONResponse + */ + #[TrapError] + public function indexMailboxes(string $providerId): JSONResponse { + try { + $userId = $this->getUserIdOrFail(); + + $this->logger->debug('Listing mailboxes for provider', [ + 'providerId' => $providerId, + 'userId' => $userId, + ]); + + $provider = $this->providerRegistry->getProvider($providerId); + if ($provider === null) { + return MailJsonResponse::fail([ + 'error' => self::ERR_PROVIDER_NOT_FOUND, + 'message' => 'Provider not found: ' . $providerId, + ], Http::STATUS_NOT_FOUND); + } + + $mailboxes = $provider->getMailboxes(); + + return MailJsonResponse::success(['mailboxes' => $mailboxes]); + } catch (\Exception $e) { + $this->logger->error('Error listing mailboxes', [ + 'providerId' => $providerId, + 'exception' => $e, + ]); + return MailJsonResponse::error('Could not list mailboxes'); + } + } + + /** + * Update a mailbox (e.g., change localpart) + * + * @NoAdminRequired + * + * @param string $providerId The provider ID + * @param string $userId The user ID whose mailbox to update + * @return JSONResponse + */ + #[TrapError] + public function updateMailbox(string $providerId, string $userId): JSONResponse { + try { + $currentUserId = $this->getUserIdOrFail(); + + // Get update data from request + $data = $this->request->getParams(); + unset($data['providerId']); + unset($data['userId']); + unset($data['_route']); + + // Validate localpart if provided + if (isset($data['localpart'])) { + $localpart = trim($data['localpart']); + if (empty($localpart)) { + return MailJsonResponse::fail([ + 'error' => self::ERR_INVALID_PARAMETERS, + 'message' => 'Localpart cannot be empty', + ], Http::STATUS_BAD_REQUEST); + } + // Basic validation: alphanumeric, dots, hyphens, underscores + if (!preg_match('/^[a-zA-Z0-9._-]+$/', $localpart)) { + return MailJsonResponse::fail([ + 'error' => self::ERR_INVALID_PARAMETERS, + 'message' => 'Localpart contains invalid characters', + ], Http::STATUS_BAD_REQUEST); + } + $data['localpart'] = $localpart; + } + + $this->logger->info('Updating mailbox', [ + 'providerId' => $providerId, + 'userId' => $userId, + 'currentUserId' => $currentUserId, + 'data' => array_keys($data), + ]); + + $provider = $this->providerRegistry->getProvider($providerId); + if ($provider === null) { + return MailJsonResponse::fail([ + 'error' => self::ERR_PROVIDER_NOT_FOUND, + 'message' => 'Provider not found: ' . $providerId, + ], Http::STATUS_NOT_FOUND); + } + + $mailbox = $provider->updateMailbox($userId, $data); + + $this->logger->info('Mailbox updated successfully', [ + 'userId' => $userId, + 'email' => $mailbox['email'] ?? null, + ]); + + return MailJsonResponse::success($mailbox); + } catch (\OCA\Mail\Exception\AccountAlreadyExistsException $e) { + $this->logger->warning('Email address already taken', [ + 'providerId' => $providerId, + 'userId' => $userId, + ]); + return MailJsonResponse::fail([ + 'error' => 'EMAIL_ALREADY_TAKEN', + 'message' => 'Email is already taken', + ], Http::STATUS_CONFLICT); + } catch (ServiceException $e) { + return $this->buildServiceErrorResponse($e, $providerId); + } catch (\InvalidArgumentException $e) { + $this->logger->error('Invalid parameters for mailbox update', [ + 'providerId' => $providerId, + 'userId' => $userId, + 'exception' => $e, + ]); + return MailJsonResponse::fail([ + 'error' => self::ERR_INVALID_PARAMETERS, + 'message' => $e->getMessage(), + ], Http::STATUS_BAD_REQUEST); + } catch (\Exception $e) { + $this->logger->error('Unexpected error updating mailbox', [ + 'providerId' => $providerId, + 'userId' => $userId, + 'exception' => $e, + ]); + return MailJsonResponse::error('Could not update mailbox'); + } + } + + /** + * Delete a mailbox + * + * @NoAdminRequired + * + * @param string $providerId The provider ID + * @param string $userId The user ID whose mailbox to delete + * @return JSONResponse + */ + #[TrapError] + public function destroyMailbox(string $providerId, string $userId): JSONResponse { + try { + $currentUserId = $this->getUserIdOrFail(); + + $this->logger->info('Deleting mailbox', [ + 'providerId' => $providerId, + 'userId' => $userId, + 'currentUserId' => $currentUserId, + ]); + + $provider = $this->providerRegistry->getProvider($providerId); + if ($provider === null) { + return MailJsonResponse::fail([ + 'error' => self::ERR_PROVIDER_NOT_FOUND, + 'message' => 'Provider not found: ' . $providerId, + ], Http::STATUS_NOT_FOUND); + } + + $success = $provider->deleteMailbox($userId); + + if ($success) { + $this->logger->info('Mailbox deleted successfully', [ + 'userId' => $userId, + ]); + return MailJsonResponse::success(['deleted' => true]); + } else { + return MailJsonResponse::fail([ + 'error' => self::ERR_SERVICE_ERROR, + 'message' => 'Failed to delete mailbox', + ], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } catch (ServiceException $e) { + return $this->buildServiceErrorResponse($e, $providerId); + } catch (\Exception $e) { + $this->logger->error('Unexpected error deleting mailbox', [ + 'providerId' => $providerId, + 'userId' => $userId, + 'exception' => $e, + ]); + return MailJsonResponse::error('Could not delete mailbox'); + } + } + /** * Get the current user ID * diff --git a/lib/Exception/AccountAlreadyExistsException.php b/lib/Exception/AccountAlreadyExistsException.php new file mode 100644 index 0000000000..b3f45a81af --- /dev/null +++ b/lib/Exception/AccountAlreadyExistsException.php @@ -0,0 +1,17 @@ + List of mailbox information + * @throws \OCA\Mail\Exception\ServiceException If fetching mailboxes fails + */ + public function getMailboxes(): array; + + /** + * Update a mailbox (e.g., change localpart/username) + * + * @param string $userId The Nextcloud user ID + * @param array $data Update data (e.g., ['localpart' => 'newusername']) + * @return array{userId: string, email: string, name: string} Updated mailbox information + * @throws \OCA\Mail\Exception\AccountAlreadyExistsException If email is already taken + * @throws \OCA\Mail\Exception\ServiceException If update fails + */ + public function updateMailbox(string $userId, array $data): array; + + /** + * Delete a mailbox + * + * @param string $userId The Nextcloud user ID + * @return bool True if deletion was successful + * @throws \OCA\Mail\Exception\ServiceException If deletion fails + */ + public function deleteMailbox(string $userId): bool; } diff --git a/lib/Provider/MailAccountProvider/Implementations/Ionos/IonosProviderFacade.php b/lib/Provider/MailAccountProvider/Implementations/Ionos/IonosProviderFacade.php index 56023407a8..5af7fdec3b 100644 --- a/lib/Provider/MailAccountProvider/Implementations/Ionos/IonosProviderFacade.php +++ b/lib/Provider/MailAccountProvider/Implementations/Ionos/IonosProviderFacade.php @@ -222,4 +222,64 @@ public function generateAppPassword(string $userId): string { return $this->mutationService->resetAppPassword($userId, IonosConfigService::APP_PASSWORD_NAME_USER); } + + /** + * Get all mailboxes managed by this provider + * + * Note: The IONOS API doesn't provide a direct "list all mailboxes" endpoint. + * This implementation returns an empty array as mailbox listing requires + * admin-level functionality that would need to be implemented separately. + * + * @return array List of mailbox information + */ + public function getMailboxes(): array { + $this->logger->debug('Getting all IONOS mailboxes'); + + // TODO: Implement when IONOS API provides admin-level mailbox listing + // For now, return empty array as this requires iterating over all users + // which is not practical without dedicated API support + return []; + } + + /** + * Update a mailbox (e.g., change localpart) + * + * @param string $userId The Nextcloud user ID + * @param array $data Update data + * @return array{userId: string, email: string, name: string} Updated mailbox information + * @throws \OCA\Mail\Exception\ServiceException If update fails + */ + public function updateMailbox(string $userId, array $data): array { + $this->logger->info('Updating IONOS mailbox via facade', [ + 'userId' => $userId, + 'data' => array_keys($data), + ]); + + $localpart = $data['localpart'] ?? null; + $name = $data['name'] ?? ''; + + if ($localpart === null || $localpart === '') { + throw new \InvalidArgumentException('localpart is required for mailbox update'); + } + + // Update the account using the creation service (which handles updates) + $account = $this->creationService->createOrUpdateAccount($userId, $localpart, $name); + + return [ + 'userId' => $userId, + 'email' => $account->getEmail(), + 'name' => $account->getName(), + ]; + } + + /** + * Delete a mailbox + * + * @param string $userId The Nextcloud user ID + * @return bool True if deletion was successful + * @throws \OCA\Mail\Exception\ServiceException If deletion fails + */ + public function deleteMailbox(string $userId): bool { + return $this->deleteAccount($userId); + } } diff --git a/lib/Provider/MailAccountProvider/Implementations/IonosProvider.php b/lib/Provider/MailAccountProvider/Implementations/IonosProvider.php index 46e2b6f6b2..f2f9a839c7 100644 --- a/lib/Provider/MailAccountProvider/Implementations/IonosProvider.php +++ b/lib/Provider/MailAccountProvider/Implementations/IonosProvider.php @@ -156,4 +156,16 @@ public function generateAppPassword(string $userId): string { ); } } + + public function getMailboxes(): array { + return $this->facade->getMailboxes(); + } + + public function updateMailbox(string $userId, array $data): array { + return $this->facade->updateMailbox($userId, $data); + } + + public function deleteMailbox(string $userId): bool { + return $this->facade->deleteMailbox($userId); + } } From 86f227cd7de74cdad228a30d49cc5a9b1dcbedb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:43:20 +0000 Subject: [PATCH 3/8] test: Add unit tests for mailbox management endpoints - Add tests for indexMailboxes endpoint - Add tests for updateMailbox with validation scenarios - Add tests for destroyMailbox endpoint - Cover error cases including invalid localpart, account conflicts, and provider not found Co-authored-by: tanyaka <1401715+tanyaka@users.noreply.github.com> --- .../ExternalAccountsControllerTest.php | 243 ++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/tests/Unit/Controller/ExternalAccountsControllerTest.php b/tests/Unit/Controller/ExternalAccountsControllerTest.php index 3c29745ba4..72feea8299 100644 --- a/tests/Unit/Controller/ExternalAccountsControllerTest.php +++ b/tests/Unit/Controller/ExternalAccountsControllerTest.php @@ -599,4 +599,247 @@ public function testGeneratePasswordWithServiceException(): void { $this->assertEquals('fail', $data['status']); $this->assertEquals('SERVICE_ERROR', $data['data']['error']); } + + // Mailbox management tests + + public function testIndexMailboxesSuccess(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('testuser'); + + $this->userSession->method('getUser') + ->willReturn($user); + + $mailboxes = [ + ['userId' => 'user1', 'email' => 'user1@example.com', 'name' => 'User One'], + ['userId' => 'user2', 'email' => 'user2@example.com', 'name' => 'User Two'], + ]; + + $provider = $this->createMock(IMailAccountProvider::class); + $provider->method('getMailboxes') + ->willReturn($mailboxes); + + $this->providerRegistry->method('getProvider') + ->with('test-provider') + ->willReturn($provider); + + $response = $this->controller->indexMailboxes('test-provider'); + + $this->assertEquals(Http::STATUS_OK, $response->getStatus()); + $data = $response->getData(); + $this->assertEquals('success', $data['status']); + $this->assertEquals($mailboxes, $data['data']['mailboxes']); + } + + public function testIndexMailboxesWithProviderNotFound(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('testuser'); + + $this->userSession->method('getUser') + ->willReturn($user); + + $this->providerRegistry->method('getProvider') + ->with('nonexistent') + ->willReturn(null); + + $response = $this->controller->indexMailboxes('nonexistent'); + + $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus()); + $data = $response->getData(); + $this->assertEquals('fail', $data['status']); + $this->assertEquals('PROVIDER_NOT_FOUND', $data['data']['error']); + } + + public function testUpdateMailboxSuccess(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('testuser'); + + $this->userSession->method('getUser') + ->willReturn($user); + + $this->request->method('getParams') + ->willReturn([ + 'providerId' => 'test-provider', + 'userId' => 'targetuser', + '_route' => 'some-route', + 'localpart' => 'newusername', + 'name' => 'New Name', + ]); + + $updatedMailbox = [ + 'userId' => 'targetuser', + 'email' => 'newusername@example.com', + 'name' => 'New Name', + ]; + + $provider = $this->createMock(IMailAccountProvider::class); + $provider->method('updateMailbox') + ->with('targetuser', ['localpart' => 'newusername', 'name' => 'New Name']) + ->willReturn($updatedMailbox); + + $this->providerRegistry->method('getProvider') + ->with('test-provider') + ->willReturn($provider); + + $response = $this->controller->updateMailbox('test-provider', 'targetuser'); + + $this->assertEquals(Http::STATUS_OK, $response->getStatus()); + $data = $response->getData(); + $this->assertEquals('success', $data['status']); + $this->assertEquals($updatedMailbox, $data['data']); + } + + public function testUpdateMailboxWithInvalidLocalpart(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('testuser'); + + $this->userSession->method('getUser') + ->willReturn($user); + + $this->request->method('getParams') + ->willReturn([ + 'providerId' => 'test-provider', + 'userId' => 'targetuser', + '_route' => 'some-route', + 'localpart' => 'invalid@user', // Contains invalid character @ + ]); + + $this->providerRegistry->method('getProvider') + ->with('test-provider') + ->willReturn($this->createMock(IMailAccountProvider::class)); + + $response = $this->controller->updateMailbox('test-provider', 'targetuser'); + + $this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus()); + $data = $response->getData(); + $this->assertEquals('fail', $data['status']); + $this->assertEquals('INVALID_PARAMETERS', $data['data']['error']); + $this->assertStringContainsString('invalid characters', $data['data']['message']); + } + + public function testUpdateMailboxWithEmptyLocalpart(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('testuser'); + + $this->userSession->method('getUser') + ->willReturn($user); + + $this->request->method('getParams') + ->willReturn([ + 'providerId' => 'test-provider', + 'userId' => 'targetuser', + '_route' => 'some-route', + 'localpart' => ' ', // Empty after trim + ]); + + $this->providerRegistry->method('getProvider') + ->with('test-provider') + ->willReturn($this->createMock(IMailAccountProvider::class)); + + $response = $this->controller->updateMailbox('test-provider', 'targetuser'); + + $this->assertEquals(Http::STATUS_BAD_REQUEST, $response->getStatus()); + $data = $response->getData(); + $this->assertEquals('fail', $data['status']); + $this->assertEquals('INVALID_PARAMETERS', $data['data']['error']); + $this->assertStringContainsString('cannot be empty', $data['data']['message']); + } + + public function testUpdateMailboxWithAccountAlreadyExistsException(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('testuser'); + + $this->userSession->method('getUser') + ->willReturn($user); + + $this->request->method('getParams') + ->willReturn([ + 'providerId' => 'test-provider', + 'userId' => 'targetuser', + '_route' => 'some-route', + 'localpart' => 'existinguser', + ]); + + $provider = $this->createMock(IMailAccountProvider::class); + $provider->method('updateMailbox') + ->willThrowException(new \OCA\Mail\Exception\AccountAlreadyExistsException('Email already taken')); + + $this->providerRegistry->method('getProvider') + ->with('test-provider') + ->willReturn($provider); + + $response = $this->controller->updateMailbox('test-provider', 'targetuser'); + + $this->assertEquals(Http::STATUS_CONFLICT, $response->getStatus()); + $data = $response->getData(); + $this->assertEquals('fail', $data['status']); + $this->assertEquals('EMAIL_ALREADY_TAKEN', $data['data']['error']); + $this->assertEquals('Email is already taken', $data['data']['message']); + } + + public function testDestroyMailboxSuccess(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('testuser'); + + $this->userSession->method('getUser') + ->willReturn($user); + + $provider = $this->createMock(IMailAccountProvider::class); + $provider->method('deleteMailbox') + ->with('targetuser') + ->willReturn(true); + + $this->providerRegistry->method('getProvider') + ->with('test-provider') + ->willReturn($provider); + + $response = $this->controller->destroyMailbox('test-provider', 'targetuser'); + + $this->assertEquals(Http::STATUS_OK, $response->getStatus()); + $data = $response->getData(); + $this->assertEquals('success', $data['status']); + $this->assertTrue($data['data']['deleted']); + } + + public function testDestroyMailboxWithProviderNotFound(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('testuser'); + + $this->userSession->method('getUser') + ->willReturn($user); + + $this->providerRegistry->method('getProvider') + ->with('nonexistent') + ->willReturn(null); + + $response = $this->controller->destroyMailbox('nonexistent', 'targetuser'); + + $this->assertEquals(Http::STATUS_NOT_FOUND, $response->getStatus()); + $data = $response->getData(); + $this->assertEquals('fail', $data['status']); + $this->assertEquals('PROVIDER_NOT_FOUND', $data['data']['error']); + } + + public function testDestroyMailboxFailed(): void { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn('testuser'); + + $this->userSession->method('getUser') + ->willReturn($user); + + $provider = $this->createMock(IMailAccountProvider::class); + $provider->method('deleteMailbox') + ->with('targetuser') + ->willReturn(false); + + $this->providerRegistry->method('getProvider') + ->with('test-provider') + ->willReturn($provider); + + $response = $this->controller->destroyMailbox('test-provider', 'targetuser'); + + $this->assertEquals(Http::STATUS_INTERNAL_SERVER_ERROR, $response->getStatus()); + $data = $response->getData(); + $this->assertEquals('fail', $data['status']); + $this->assertEquals('SERVICE_ERROR', $data['data']['error']); + } } From 0f4539a41a05da8f0e303cd73232d4c22dc3cbc2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:46:07 +0000 Subject: [PATCH 4/8] feat: Add frontend components for mailbox administration - Create MailboxAdminService for API integration - Create MailboxAdministration main component with list view - Create MailboxListItem for individual mailbox rows with edit/delete actions - Create MailboxDeletionModal for delete confirmation - Implement loading, error, and empty states - Add user avatar and details display Co-authored-by: tanyaka <1401715+tanyaka@users.noreply.github.com> --- .../mailboxAdmin/MailboxAdministration.vue | 225 ++++++++++++++++++ .../mailboxAdmin/MailboxDeletionModal.vue | 118 +++++++++ .../mailboxAdmin/MailboxListItem.vue | 114 +++++++++ src/service/MailboxAdminService.js | 61 +++++ 4 files changed, 518 insertions(+) create mode 100644 src/components/mailboxAdmin/MailboxAdministration.vue create mode 100644 src/components/mailboxAdmin/MailboxDeletionModal.vue create mode 100644 src/components/mailboxAdmin/MailboxListItem.vue create mode 100644 src/service/MailboxAdminService.js diff --git a/src/components/mailboxAdmin/MailboxAdministration.vue b/src/components/mailboxAdmin/MailboxAdministration.vue new file mode 100644 index 0000000000..5f9ec99fa8 --- /dev/null +++ b/src/components/mailboxAdmin/MailboxAdministration.vue @@ -0,0 +1,225 @@ + + + + + + + diff --git a/src/components/mailboxAdmin/MailboxDeletionModal.vue b/src/components/mailboxAdmin/MailboxDeletionModal.vue new file mode 100644 index 0000000000..f3ae402990 --- /dev/null +++ b/src/components/mailboxAdmin/MailboxDeletionModal.vue @@ -0,0 +1,118 @@ + + + + + + + diff --git a/src/components/mailboxAdmin/MailboxListItem.vue b/src/components/mailboxAdmin/MailboxListItem.vue new file mode 100644 index 0000000000..f4b48b39ac --- /dev/null +++ b/src/components/mailboxAdmin/MailboxListItem.vue @@ -0,0 +1,114 @@ + + + + + + + diff --git a/src/service/MailboxAdminService.js b/src/service/MailboxAdminService.js new file mode 100644 index 0000000000..ce8d097e7b --- /dev/null +++ b/src/service/MailboxAdminService.js @@ -0,0 +1,61 @@ +/** + * SPDX-FileCopyrightText: 2025 STRATO GmbH + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import axios from '@nextcloud/axios' +import { generateUrl } from '@nextcloud/router' + +/** + * Get all mailboxes for a provider + * + * @param {string} providerId The provider ID + * @return {Promise} + */ +export const getMailboxes = (providerId) => { + const url = generateUrl('/apps/mail/api/providers/{providerId}/mailboxes', { + providerId, + }) + return axios.get(url).then((resp) => resp.data) +} + +/** + * Update a mailbox + * + * @param {string} providerId The provider ID + * @param {string} userId The user ID + * @param {object} data Update data (localpart, name, etc.) + * @return {Promise} + */ +export const updateMailbox = (providerId, userId, data) => { + const url = generateUrl('/apps/mail/api/providers/{providerId}/mailboxes/{userId}', { + providerId, + userId, + }) + return axios.put(url, data).then((resp) => resp.data) +} + +/** + * Delete a mailbox + * + * @param {string} providerId The provider ID + * @param {string} userId The user ID + * @return {Promise} + */ +export const deleteMailbox = (providerId, userId) => { + const url = generateUrl('/apps/mail/api/providers/{providerId}/mailboxes/{userId}', { + providerId, + userId, + }) + return axios.delete(url).then((resp) => resp.data) +} + +/** + * Get available providers + * + * @return {Promise} + */ +export const getProviders = () => { + const url = generateUrl('/apps/mail/api/providers') + return axios.get(url).then((resp) => resp.data) +} From 709a36a2162f4e76e38becacb0b2e07d8843fad8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:46:45 +0000 Subject: [PATCH 5/8] feat: Integrate MailboxAdministration into admin settings - Add MailboxAdministration component to AdminSettings.vue - Component appears as separate settings section - Displays after existing mail app settings Co-authored-by: tanyaka <1401715+tanyaka@users.noreply.github.com> --- src/components/settings/AdminSettings.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/settings/AdminSettings.vue b/src/components/settings/AdminSettings.vue index bfd88096fa..1826757e59 100644 --- a/src/components/settings/AdminSettings.vue +++ b/src/components/settings/AdminSettings.vue @@ -267,6 +267,7 @@ +