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
1 change: 1 addition & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud
<command>OCA\Mail\Command\DiagnoseAccount</command>
<command>OCA\Mail\Command\ExportAccount</command>
<command>OCA\Mail\Command\ExportAccountThreads</command>
<command>OCA\Mail\Command\IonosCreateAccount</command>
<command>OCA\Mail\Command\PredictImportance</command>
<command>OCA\Mail\Command\SyncAccount</command>
<command>OCA\Mail\Command\Thread</command>
Expand Down
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.

212 changes: 212 additions & 0 deletions lib/Command/IonosCreateAccount.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<?php

declare(strict_types=1);

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

namespace OCA\Mail\Command;

use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Service\IONOS\IonosAccountCreationService;
use OCA\Mail\Service\IONOS\IonosConfigService;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

final class IonosCreateAccount extends Command {
public const ARGUMENT_USER_ID = 'user-id';
public const ARGUMENT_EMAIL_USER = 'email-user';
public const OPTION_NAME = 'name';
public const OPTION_OUTPUT = 'output';

public function __construct(
private IonosAccountCreationService $accountCreationService,
private IUserManager $userManager,
private IonosConfigService $configService,
private LoggerInterface $logger,
) {
parent::__construct();
}

/**
* @return void
*/
protected function configure() {
$this->setName('mail:ionos:create');
$this->setDescription('Creates IONOS mail account and configure it in Nextcloud');
$this->addArgument(self::ARGUMENT_USER_ID, InputArgument::REQUIRED, 'User ID');
$this->addArgument(self::ARGUMENT_EMAIL_USER, InputArgument::REQUIRED, 'IONOS Email user. (The local part of the email address before @domain)');
$this->addOption(self::OPTION_NAME, '', InputOption::VALUE_REQUIRED, 'Account name');
$this->addOption(self::OPTION_OUTPUT, '', InputOption::VALUE_OPTIONAL, 'Output format (json, json_pretty)');
}

protected function execute(InputInterface $input, OutputInterface $output): int {
$userId = $input->getArgument(self::ARGUMENT_USER_ID);
$emailUser = $input->getArgument(self::ARGUMENT_EMAIL_USER);
$name = $input->getOption(self::OPTION_NAME);
$outputFormat = $input->getOption(self::OPTION_OUTPUT);
$isJsonOutput = in_array($outputFormat, ['json', 'json_pretty'], true);

// Preflight checks
if (!$isJsonOutput) {
$output->writeln('<info>Running preflight checks...</info>');
}

// Check if IONOS integration is enabled and configured
if (!$this->configService->isIonosIntegrationEnabled()) {
if ($isJsonOutput) {
$this->outputJson($output, [
'success' => false,
'error' => 'IONOS integration is not enabled or not properly configured',
'details' => [
'ionos-mailconfig-enabled' => 'must be set to "yes"',
'ionos_mailconfig_api_base_url' => 'must be configured',
'ionos_mailconfig_api_auth_user' => 'must be configured',
'ionos_mailconfig_api_auth_pass' => 'must be configured',
'ncw.ext_ref' => 'must be configured in system config',
]
], $outputFormat);
} else {
$output->writeln('<error>IONOS integration is not enabled or not properly configured</error>');
$output->writeln('<comment>Please verify the following configuration:</comment>');
$output->writeln(' - ionos-mailconfig-enabled is set to "yes"');
$output->writeln(' - ionos_mailconfig_api_base_url is configured');
$output->writeln(' - ionos_mailconfig_api_auth_user is configured');
$output->writeln(' - ionos_mailconfig_api_auth_pass is configured');
$output->writeln(' - ncw.ext_ref is configured in system config');
}
return 1;
}

// Get and display the mail domain
$mailDomain = $this->configService->getMailDomain();
if (empty($mailDomain)) {
if ($isJsonOutput) {
$this->outputJson($output, [
'success' => false,
'error' => 'Mail domain could not be determined',
'details' => 'Please verify ncw.customerDomain is configured in system config'
], $outputFormat);
} else {
$output->writeln('<error>Mail domain could not be determined</error>');
$output->writeln('<comment>Please verify ncw.customerDomain is configured in system config</comment>');
}
return 1;
}

if (!$isJsonOutput) {
$output->writeln('<info>✓ IONOS API is properly configured</info>');
$output->writeln('<info>✓ Mail domain: ' . $mailDomain . '</info>');
$output->writeln('');
}

if (!$this->userManager->userExists($userId)) {
if ($isJsonOutput) {
$this->outputJson($output, [
'success' => false,
'error' => 'User does not exist',
'userId' => $userId
], $outputFormat);
} else {
$output->writeln("<error>User $userId does not exist</error>");
}
return 1;
}

if (!$isJsonOutput) {
$output->writeln('Creating IONOS mail account...');
$output->writeln(' user-id: ' . $userId);
$output->writeln(' name: ' . $name);
$output->writeln(' email-user: ' . $emailUser);
$output->writeln(' full-email: ' . $emailUser . '@' . $mailDomain);
}

try {
$this->logger->info('Starting IONOS email account creation from CLI', [
'userId' => $userId,
'emailUser' => $emailUser,
'accountName' => $name
]);

// Use the shared account creation service
$account = $this->accountCreationService->createOrUpdateAccount($userId, $emailUser, $name);

if ($isJsonOutput) {
$this->outputJson($output, [
'success' => true,
'account' => [
'id' => $account->getId(),
'userId' => $account->getUserId(),
'name' => $account->getName(),
'email' => $account->getEmail(),
'inbound' => [
'host' => $account->getInboundHost(),
'port' => $account->getInboundPort(),
'sslMode' => $account->getInboundSslMode(),
'user' => $account->getInboundUser(),
],
'outbound' => [
'host' => $account->getOutboundHost(),
'port' => $account->getOutboundPort(),
'sslMode' => $account->getOutboundSslMode(),
'user' => $account->getOutboundUser(),
]
]
], $outputFormat);
} else {
$output->writeln('<info>Account created successfully!</info>');
$output->writeln(' Account ID: ' . $account->getId());
$output->writeln(' Email: ' . $account->getEmail());
}

return 0;
} catch (ServiceException $e) {
$this->logger->error('IONOS service error: ' . $e->getMessage(), [
'statusCode' => $e->getCode()
]);

if ($isJsonOutput) {
$this->outputJson($output, [
'success' => false,
'error' => 'IONOS service error',
'message' => $e->getMessage(),
'statusCode' => $e->getCode()
], $outputFormat);
} else {
$output->writeln('<error>Failed to create IONOS account: ' . $e->getMessage() . '</error>');
}
return 1;
} catch (\Exception $e) {
$this->logger->error('Unexpected error creating account: ' . $e->getMessage());

if ($isJsonOutput) {
$this->outputJson($output, [
'success' => false,
'error' => 'Unexpected error',
'message' => $e->getMessage()
], $outputFormat);
} else {
$output->writeln('<error>Could not create account: ' . $e->getMessage() . '</error>');
}
return 1;
}
}

/**
* Output data as JSON based on the specified format
*/
private function outputJson(OutputInterface $output, array $data, ?string $format): void {
if ($format === 'json_pretty') {
$output->writeln(json_encode($data, JSON_PRETTY_PRINT));
} else {
$output->writeln(json_encode($data));
}
}
}
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
92 changes: 61 additions & 31 deletions lib/Controller/IonosAccountsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
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\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 +29,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 @@ -53,42 +53,72 @@ public function create(string $accountName, string $emailUser): JSONResponse {
return $error;
}

$userId = $this->getUserIdOrFail();
if ($userId instanceof JSONResponse) {
return $userId;
}

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

// Use the shared account creation service
$account = $this->accountCreationService->createOrUpdateAccount($userId, $emailUser, $accountName);

$this->logger->info('IONOS email account created successfully', [ 'emailAddress' => $ionosResponse->getEmail() ]);
return $this->createNextcloudMailAccount($accountName, $ionosResponse);
$this->logger->info('Account creation completed successfully', [
'accountId' => $account->getId(),
'emailAddress' => $account->getEmail(),
'userId' => $userId,
]);

return new JSONResponse([
'id' => $account->getId(),
'accountName' => $account->getName(),
'emailAddress' => $account->getEmail(),
]);
} catch (ServiceException $e) {
return $this->buildServiceErrorResponse($e, 'account creation');
} catch (\Exception $e) {
$this->logger->error('Unexpected error during account creation: ' . $e->getMessage(), [
'exception' => $e,
'userId' => $userId,
]);
return MailJsonResponse::error('Could not create account');
}
}

/**
* Get the current user ID or return error response
*
* @return string|JSONResponse User ID string or error response
*/
private function getUserIdOrFail(): string|JSONResponse {
$user = $this->userSession->getUser();
if ($user === null) {
$data = [
'error' => self::ERR_IONOS_API_ERROR,
'statusCode' => $e->getCode(),
'statusCode' => 401,
'message' => 'No user session found',
];
$this->logger->error('IONOS service error: ' . $e->getMessage(), $data);

$this->logger->error('No user session found during account creation', $data);
return MailJsonResponse::fail($data);
} catch (\Exception $e) {
return MailJsonResponse::error('Could not create account');
}
return $user->getUID();
}

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(),
);
/**
* 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(),
];
$this->logger->error('IONOS service error during ' . $context . ': ' . $e->getMessage(), $data);
return MailJsonResponse::fail($data);
}
}
Loading
Loading