Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
805ef72
IONOS(ionos-mail): update IONOS Mail API client reference to 2.0.0-20…
printminion-co Dec 8, 2025
3055744
test(service): add unit tests for SetupService
printminion-co Dec 5, 2025
7b628e6
IONOS(ionos-mail): SetupService: add skipConnectivityTest parameter t…
printminion-co Dec 4, 2025
0b28d3e
IONOS(ionos-mail): AccountsController: add skipConnectivityTest param…
printminion-co Dec 4, 2025
7ded9da
IONOS(ionos-mail): refactor IonosMailServiceTest with consistent test…
printminion-co Dec 5, 2025
8e92767
IONOS(ionos-mail): IonosMailService: add method to create email accou…
printminion-co Dec 4, 2025
4b0f456
IONOS(ionos-mail): feat(dto): add withPassword method to MailServerCo…
printminion-co Dec 16, 2025
98c13e2
IONOS(ionos-mail): feat(dto): add withPassword method to MailAccountC…
printminion-co Dec 16, 2025
2792fa8
IONOS(ionos-mail): feat(config): add APP_NAME constant to IonosConfig…
printminion-co Dec 16, 2025
4f32400
IONOS(ionos-mail): feat(service): add ConflictResolutionResult value …
printminion-co Dec 16, 2025
3fa5d80
IONOS(ionos-mail): feat(service): add account config retrieval and pa…
printminion-co Dec 16, 2025
5a2877b
IONOS(ionos-mail): feat(service): add IonosAccountConflictResolver
printminion-co Dec 16, 2025
eca8bb5
IONOS(ionos-mail): feat(exception): add IonosServiceException for enh…
printminion-co Dec 17, 2025
de62be3
IONOS(ionos-mail): feat(service): add IonosAccountCreationService
printminion-co Dec 16, 2025
af10036
IONOS(ionos-mail): refactor(service): enhance IonosMailConfigService …
printminion-co Dec 16, 2025
9f1ede6
IONOS(ionos-mail): refactor(controller): improve error handling and l…
printminion-co Dec 17, 2025
da32d37
IONOS(ionos-mail): feat(controller): add user session handling for ac…
printminion-co Dec 17, 2025
efdbb6c
IONOS(ionos-mail): refactor(controller): use IonosAccountCreationServ…
printminion-co Dec 16, 2025
d4022b9
IONOS(ionos-mail): fix(NewEmailAddressTab): improve error message for…
printminion-co Dec 17, 2025
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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"source": {
"type": "git",
"url": "https://github.com/ionos-productivity/ionos-mail-configuration-api-client.git",
"reference": "2.0.0-20251110130214"
"reference": "2.0.0-20251208083401"
},
"autoload": {
"psr-4": {
Expand Down
4 changes: 2 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions lib/Controller/AccountsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,8 @@ public function create(string $accountName,
?string $smtpSslMode = null,
?string $smtpUser = null,
?string $smtpPassword = null,
string $authMethod = 'password'): JSONResponse {
string $authMethod = 'password',
bool $skipConnectivityTest = false): JSONResponse {
if ($this->config->getAppValue(Application::APP_ID, 'allow_new_mail_accounts', 'yes') === 'no') {
$this->logger->info('Creating account disabled by admin.');
return MailJsonResponse::error('Could not create account');
Expand Down Expand Up @@ -378,7 +379,7 @@ public function create(string $accountName,
);
}
try {
$account = $this->setup->createNewAccount($accountName, $emailAddress, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->currentUserId, $authMethod);
$account = $this->setup->createNewAccount($accountName, $emailAddress, $imapHost, $imapPort, $imapSslMode, $imapUser, $imapPassword, $smtpHost, $smtpPort, $smtpSslMode, $smtpUser, $smtpPassword, $this->currentUserId, $authMethod, null, $skipConnectivityTest);
} catch (CouldNotConnectException $e) {
$data = [
'error' => $e->getReason(),
Expand Down
91 changes: 58 additions & 33 deletions lib/Controller/IonosAccountsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
*/
namespace OCA\Mail\Controller;

use OCA\Mail\Exception\IonosServiceException;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Http\JsonResponse as MailJsonResponse;
use OCA\Mail\Http\TrapError;
use OCA\Mail\Service\IONOS\Dto\MailAccountConfig;
use OCA\Mail\Service\IONOS\IonosMailService;
use OCA\Mail\Service\IONOS\IonosAccountCreationService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;

#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
Expand All @@ -29,8 +31,8 @@ class IonosAccountsController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private IonosMailService $ionosMailService,
private AccountsController $accountsController,
private IonosAccountCreationService $accountCreationService,
private IUserSession $userSession,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
Expand All @@ -54,41 +56,64 @@ public function create(string $accountName, string $emailUser): JSONResponse {
}

try {
$this->logger->info('Starting IONOS email account creation', [ 'emailAddress' => $emailUser, 'accountName' => $accountName ]);
$ionosResponse = $this->ionosMailService->createEmailAccount($emailUser);
$userId = $this->getUserIdOrFail();

$this->logger->info('IONOS email account created successfully', [ 'emailAddress' => $ionosResponse->getEmail() ]);
return $this->createNextcloudMailAccount($accountName, $ionosResponse);
} catch (ServiceException $e) {
$data = [
'error' => self::ERR_IONOS_API_ERROR,
'statusCode' => $e->getCode(),
];
$this->logger->error('IONOS service error: ' . $e->getMessage(), $data);
$this->logger->info('Starting IONOS email account creation from web', [
'userId' => $userId,
'emailAddress' => $emailUser,
'accountName' => $accountName,
]);

$account = $this->accountCreationService->createOrUpdateAccount($userId, $emailUser, $accountName);

return MailJsonResponse::fail($data);
$this->logger->info('Account creation completed successfully', [
'emailAddress' => $account->getEmail(),
'accountName' => $accountName,
'accountId' => $account->getId(),
'userId' => $userId,
]);

return MailJsonResponse::success($account, Http::STATUS_CREATED);
} catch (ServiceException $e) {
return $this->buildServiceErrorResponse($e, 'account creation');
} catch (\Exception $e) {
$this->logger->error('Unexpected error during account creation: ' . $e->getMessage(), [
'exception' => $e,
]);
return MailJsonResponse::error('Could not create account');
}
}

private function createNextcloudMailAccount(string $accountName, MailAccountConfig $mailConfig): JSONResponse {
$imap = $mailConfig->getImap();
$smtp = $mailConfig->getSmtp();

return $this->accountsController->create(
$accountName,
$mailConfig->getEmail(),
$imap->getHost(),
$imap->getPort(),
$imap->getSecurity(),
$imap->getUsername(),
$imap->getPassword(),
$smtp->getHost(),
$smtp->getPort(),
$smtp->getSecurity(),
$smtp->getUsername(),
$smtp->getPassword(),
);
/**
* Get the current user ID
*
* @return string User ID string
* @throws ServiceException
*/
private function getUserIdOrFail(): string {
$user = $this->userSession->getUser();
if ($user === null) {
throw new ServiceException('No user session found during account creation', 401);
}
return $user->getUID();
}

/**
* Build service error response
*/
private function buildServiceErrorResponse(ServiceException $e, string $context): JSONResponse {
$data = [
'error' => self::ERR_IONOS_API_ERROR,
'statusCode' => $e->getCode(),
'message' => $e->getMessage(),
];

// If it's an IonosServiceException, merge in the additional data
if ($e instanceof IonosServiceException) {
$data = array_merge($data, $e->getData());
}

$this->logger->error('IONOS service error during ' . $context . ': ' . $e->getMessage(), $data);
return MailJsonResponse::fail($data);
}
}
38 changes: 38 additions & 0 deletions lib/Exception/IonosServiceException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 STRATO GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Mail\Exception;

use Throwable;

class IonosServiceException extends ServiceException {
/**
* @param string $message [optional] The Exception message to throw.
* @param mixed $code [optional] The Exception code.
* @param null|Throwable $previous [optional] The previous throwable used for the exception chaining.
* @param array<string, mixed> $data [optional] Additional data to pass with the exception.
*/
public function __construct(
$message = '',
$code = 0,
?Throwable $previous = null,
private readonly array $data = [],
) {
parent::__construct($message, $code, $previous);
}

/**
* Get additional data associated with the exception
*
* @return array<string, mixed>
*/
public function getData(): array {
return $this->data;
}
}
81 changes: 81 additions & 0 deletions lib/Service/IONOS/ConflictResolutionResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 STRATO GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Mail\Service\IONOS;

use OCA\Mail\Service\IONOS\Dto\MailAccountConfig;

/**
* Result of conflict resolution when IONOS account creation fails
*/
class ConflictResolutionResult {

private function __construct(
private readonly bool $canRetry,
private readonly ?MailAccountConfig $accountConfig,
private readonly ?string $expectedEmail,
private readonly ?string $existingEmail,
) {
}

/**
* Create result indicating account creation can be retried with existing config
*/
public static function retry(MailAccountConfig $config): self {
return new self(
canRetry: true,
accountConfig: $config,
expectedEmail: null,
existingEmail: null,
);
}

/**
* Create result indicating no existing account was found
*/
public static function noExistingAccount(): self {
return new self(
canRetry: false,
accountConfig: null,
expectedEmail: null,
existingEmail: null,
);
}

/**
* Create result indicating email mismatch between expected and existing account
*/
public static function emailMismatch(string $expectedEmail, string $existingEmail): self {
return new self(
canRetry: false,
accountConfig: null,
expectedEmail: $expectedEmail,
existingEmail: $existingEmail,
);
}

public function canRetry(): bool {
return $this->canRetry;
}

public function getAccountConfig(): ?MailAccountConfig {
return $this->accountConfig;
}

public function hasEmailMismatch(): bool {
return $this->expectedEmail !== null && $this->existingEmail !== null;
}

public function getExpectedEmail(): ?string {
return $this->expectedEmail;
}

public function getExistingEmail(): ?string {
return $this->existingEmail;
}
}
14 changes: 14 additions & 0 deletions lib/Service/IONOS/Dto/MailAccountConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ public function getSmtp(): MailServerConfig {
return $this->smtp;
}

/**
* Create a new instance with updated passwords for both IMAP and SMTP
*
* @param string $newPassword The new password to use
* @return self New instance with updated passwords
*/
public function withPassword(string $newPassword): self {
return new self(
email: $this->email,
imap: $this->imap->withPassword($newPassword),
smtp: $this->smtp->withPassword($newPassword),
);
}

/**
* Convert to array format for backwards compatibility
*
Expand Down
16 changes: 16 additions & 0 deletions lib/Service/IONOS/Dto/MailServerConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ public function getPassword(): string {
return $this->password;
}

/**
* Create a new instance with a different password
*
* @param string $newPassword The new password to use
* @return self New instance with updated password
*/
public function withPassword(string $newPassword): self {
return new self(
host: $this->host,
port: $this->port,
security: $this->security,
username: $this->username,
password: $newPassword,
);
}

/**
* Convert to array format for backwards compatibility
*
Expand Down
Loading
Loading