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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,21 @@
'url' => '/api/action-step/{actionId}/steps',
'verb' => 'GET'
],
[
'name' => 'mailboxAdmin#listMailboxes',
'url' => '/api/admin/mailboxes',
'verb' => 'GET',
],
[
'name' => 'mailboxAdmin#updateMailbox',
'url' => '/api/admin/mailboxes/{userId}',
'verb' => 'PATCH',
],
[
'name' => 'mailboxAdmin#deleteMailbox',
'url' => '/api/admin/mailboxes/{userId}',
'verb' => 'DELETE',
],
],
'resources' => [
'accounts' => ['url' => '/api/accounts'],
Expand Down
38 changes: 38 additions & 0 deletions doc/admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,41 @@ If you can not access your Outlook.com account try to enable the 'Two-Factor Ver
If autoconfiguration for your domain fails, you can create an autoconfig file and place it as https://autoconfig.yourdomain.tld/mail/config-v1.1.xml
For more information please refer to Mozilla's documentation:
https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration/FileFormat/HowTo

## IONOS Mailbox Management

If you have IONOS mail integration enabled, administrators can manage IONOS mailboxes from the admin settings page.

### Features

- **View all mailboxes**: Lists all IONOS mailboxes linked to Nextcloud users
- **Edit email addresses**: Change the local part (before @) of a user's email address
- **Delete mailboxes**: Remove IONOS mailboxes with confirmation

### Access

1. Navigate to Administration Settings > Mail
2. Scroll down to the "E-Mail Verwaltung" (Email Administration) section

### Edit a Mailbox

To change a user's email address:

1. Click the three-dot menu next to the mailbox
2. Select "Edit"
3. Enter the new local part (username before @)
4. Click "Save"

The system will validate that:
- The new email address is not already in use
- The local part is valid (alphanumeric, dots, hyphens, underscores)

### Delete a Mailbox

To delete a user's IONOS mailbox:

1. Click the three-dot menu next to the mailbox
2. Select "Delete"
3. Confirm the deletion in the dialog

**Warning**: This action cannot be undone and will remove all email data for that user.
124 changes: 124 additions & 0 deletions lib/Controller/MailboxAdminController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 IONOS SE
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Mail\Controller;

use OCA\Mail\AppInfo\Application;
use OCA\Mail\Service\MailboxAdminService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use Psr\Log\LoggerInterface;

/**
* Controller for mailbox administration endpoints
*/
class MailboxAdminController extends Controller {
public function __construct(
IRequest $request,
private MailboxAdminService $mailboxAdminService,
private LoggerInterface $logger,
) {
parent::__construct(Application::APP_ID, $request);
}

/**
* List all IONOS mailboxes with linked users
*
* @return JSONResponse
* @NoCSRFRequired
* @AuthorizedAdminSetting(settings=OCA\Mail\Settings\AdminSettings)
*/
public function listMailboxes(): JSONResponse {
try {
$mailboxes = $this->mailboxAdminService->listAllMailboxes();
return new JSONResponse([
'mailboxes' => $mailboxes,
]);
} catch (\Exception $e) {
$this->logger->error('Failed to list mailboxes', [
'exception' => $e,
]);
return new JSONResponse([
'error' => 'Failed to list mailboxes: ' . $e->getMessage(),
], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* Update a mailbox email address (change localpart)
*
* @param string $userId The Nextcloud user ID
* @param string $newLocalpart The new local part of the email (before @)
* @return JSONResponse
* @NoCSRFRequired
* @AuthorizedAdminSetting(settings=OCA\Mail\Settings\AdminSettings)
*/
public function updateMailbox(string $userId, string $newLocalpart): JSONResponse {
try {
$result = $this->mailboxAdminService->updateMailboxEmail($userId, $newLocalpart);

if ($result['success']) {
return new JSONResponse([
'success' => true,
'email' => $result['email'],
'message' => 'Mailbox updated successfully',
]);
} else {
return new JSONResponse([
'success' => false,
'error' => $result['error'],
], Http::STATUS_BAD_REQUEST);
}
} catch (\Exception $e) {
$this->logger->error('Failed to update mailbox', [
'userId' => $userId,
'exception' => $e,
]);
return new JSONResponse([
'error' => 'Failed to update mailbox: ' . $e->getMessage(),
], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}

/**
* Delete a mailbox
*
* @param string $userId The Nextcloud user ID
* @return JSONResponse
* @NoCSRFRequired
* @AuthorizedAdminSetting(settings=OCA\Mail\Settings\AdminSettings)
*/
public function deleteMailbox(string $userId): JSONResponse {
try {
$success = $this->mailboxAdminService->deleteMailbox($userId);

if ($success) {
return new JSONResponse([
'success' => true,
'message' => 'Mailbox deleted successfully',
]);
} else {
return new JSONResponse([
'success' => false,
'error' => 'Failed to delete mailbox',
], Http::STATUS_INTERNAL_SERVER_ERROR);
}
} catch (\Exception $e) {
$this->logger->error('Failed to delete mailbox', [
'userId' => $userId,
'exception' => $e,
]);
return new JSONResponse([
'error' => 'Failed to delete mailbox: ' . $e->getMessage(),
], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
}
201 changes: 201 additions & 0 deletions lib/Service/MailboxAdminService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 IONOS SE
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Mail\Service;

use OCA\Mail\Db\MailAccountMapper;
use OCA\Mail\Provider\MailAccountProvider\Implementations\Ionos\IonosProviderFacade;
use OCA\Mail\Provider\MailAccountProvider\Implementations\Ionos\Service\IonosConfigService;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;

/**
* Service for managing mailboxes in the admin interface
*/
class MailboxAdminService {
public function __construct(
private IonosProviderFacade $ionosFacade,
private IonosConfigService $configService,
private IUserManager $userManager,
private MailAccountMapper $mailAccountMapper,
private LoggerInterface $logger,
) {
}

/**
* List all IONOS mailboxes with their linked users
*
* @return array<array{email: string, userId: string, displayName: string, username: string}>
*/
public function listAllMailboxes(): array {
$mailboxes = [];

// Check if IONOS integration is enabled
if (!$this->ionosFacade->isEnabled()) {
$this->logger->debug('IONOS integration is not enabled');
return $mailboxes;
}

// Get all users from Nextcloud
$this->userManager->callForSeenUsers(function ($user) use (&$mailboxes) {
$userId = $user->getUID();

// Try to get IONOS email for this user
$email = $this->ionosFacade->getProvisionedEmail($userId);

if ($email !== null) {
$mailboxes[] = [
'email' => $email,
'userId' => $userId,
'displayName' => $user->getDisplayName(),
'username' => $userId,
];
}
});

$this->logger->info('Listed IONOS mailboxes', [
'count' => count($mailboxes),
]);

return $mailboxes;
}

/**
* Update a mailbox email address by changing the localpart
*
* @param string $userId The Nextcloud user ID
* @param string $newLocalpart The new local part (before @)
* @return array{success: bool, email?: string, error?: string}
*/
public function updateMailboxEmail(string $userId, string $newLocalpart): array {
// Validate localpart
if (empty($newLocalpart) || !$this->isValidLocalpart($newLocalpart)) {
return [
'success' => false,
'error' => 'Invalid email localpart',
];
}

// Get the domain
$domain = $this->configService->getMailDomain();
$newEmail = $newLocalpart . '@' . $domain;

// Check if the new email is already taken
if ($this->isEmailTaken($newEmail, $userId)) {
return [
'success' => false,
'error' => 'Email is already taken. Please use some other username.',
];
}

try {
// Get user display name for account name
$user = $this->userManager->get($userId);
if ($user === null) {
return [
'success' => false,
'error' => 'User not found',
];
}

$accountName = $user->getDisplayName();

// Update the account via IONOS facade
// The createAccount method in the facade handles both create and update
$account = $this->ionosFacade->createAccount($userId, $newLocalpart, $accountName);

$this->logger->info('Successfully updated mailbox', [
'userId' => $userId,
'newEmail' => $newEmail,
]);

return [
'success' => true,
'email' => $newEmail,
];
} catch (\Exception $e) {
$this->logger->error('Failed to update mailbox email', [
'userId' => $userId,
'newLocalpart' => $newLocalpart,
'exception' => $e,
]);

return [
'success' => false,
'error' => $e->getMessage(),
];
}
}

/**
* Delete a mailbox
*
* @param string $userId The Nextcloud user ID
* @return bool True if successful
*/
public function deleteMailbox(string $userId): bool {
try {
$success = $this->ionosFacade->deleteAccount($userId);

$this->logger->info('Mailbox deletion result', [
'userId' => $userId,
'success' => $success,
]);

return $success;
} catch (\Exception $e) {
$this->logger->error('Failed to delete mailbox', [
'userId' => $userId,
'exception' => $e,
]);
return false;
}
}

/**
* Check if an email is already taken by another user
*
* @param string $email The email address to check
* @param string $excludeUserId User ID to exclude from the check
* @return bool True if email is taken
*/
private function isEmailTaken(string $email, string $excludeUserId): bool {
// Check all users for this email
$allUsers = $this->userManager->search('');

foreach ($allUsers as $user) {
$userId = $user->getUID();

// Skip the user we're updating
if ($userId === $excludeUserId) {
continue;
}

// Check if this user has this email
$userEmail = $this->ionosFacade->getProvisionedEmail($userId);
if ($userEmail !== null && strcasecmp($userEmail, $email) === 0) {
return true;
}
}

return false;
}

/**
* Validate email localpart
*
* @param string $localpart The localpart to validate
* @return bool True if valid
*/
private function isValidLocalpart(string $localpart): bool {
// Basic validation for email localpart
// Allow alphanumeric, dot, hyphen, underscore
return preg_match('/^[a-zA-Z0-9._-]+$/', $localpart) === 1;
}
}
Loading