Skip to content
Open
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
5 changes: 5 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@
'url' => '/api/tags',
'verb' => 'POST'
],
[
'name' => 'ionosAccounts#create',
'url' => '/api/ionos/accounts',
'verb' => 'POST'
],
[
'name' => 'tags#update',
'url' => '/api/tags/{id}',
Expand Down
20 changes: 20 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"glenscott/url-normalizer": "^1.4",
"gravatarphp/gravatar": "dev-master#6b9f6a45477ce48285738d9d0c3f0dbf97abe263",
"hamza221/html2text": "^1.0",
"ionos-productivity/ionos-mail-configuration-api-client": "2.0.0",
"jeremykendall/php-domain-parser": "^6.4.0",
"nextcloud/horde-managesieve": "^1.0",
"nextcloud/horde-smtp": "^1.0.2",
Expand All @@ -44,6 +45,25 @@
"wamania/php-stemmer": "4.0 as 3.0",
"youthweb/urllinker": "^2.1.0"
},
"repositories": [
{
"type": "package",
"package": {
"name": "ionos-productivity/ionos-mail-configuration-api-client",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/ionos-productivity/ionos-mail-configuration-api-client.git",
"reference": "2.0.0-e22cc02"
},
"autoload": {
"psr-4": {
"IONOS\\MailConfigurationAPI\\Client\\" : "lib/"
}
}
}
}
],
"provide": {
"psr/log": "^1.0.4|^2|^3"
},
Expand Down
19 changes: 17 additions & 2 deletions composer.lock

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

163 changes: 163 additions & 0 deletions lib/Controller/IonosAccountsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

declare(strict_types=1);

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

use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Http\TrapError;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;
use Psr\Log\LoggerInterface;

#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
class IonosAccountsController extends Controller {

// Error message constants
private const ERR_ALL_FIELDS_REQUIRED = 'All fields are required';
private const ERR_CREATE_EMAIL_FAILED = 'Failed to create email account';
private const ERR_IONOS_API_ERROR = 'IONOS_API_ERROR';
private const ERR_UNKNOWN_ERROR = 'UNKNOWN_ERROR';
private const ERR_GENERIC_SETUP = 'There was an error while setting up your account';

public function __construct(
string $appName,
IRequest $request,
private AccountsController $accountsController,
private LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}

// Helper: input validation
private function validateInput(string $accountName, string $emailAddress): ?JSONResponse {
if ($accountName === '' || $emailAddress === '') {
return new JSONResponse(['success' => false, 'message' => self::ERR_ALL_FIELDS_REQUIRED, 'error' => self::ERR_IONOS_API_ERROR], 400);
}
if (!filter_var($emailAddress, FILTER_VALIDATE_EMAIL)) {
return new JSONResponse(['success' => false, 'message' => 'Invalid email address format', 'error' => self::ERR_IONOS_API_ERROR], 400);
}
return null;
}

/**
* @NoAdminRequired
*/
#[TrapError]
public function create(string $accountName, string $emailAddress): JSONResponse {
if ($error = $this->validateInput($accountName, $emailAddress)) {
return $error;
}

try {
$this->logger->info('Starting IONOS email account creation', [ 'emailAddress' => $emailAddress, 'accountName' => $accountName ]);
$mailConfig = $this->createIonosEmailAccount($accountName, $emailAddress);
$accountResponse = $this->createNextcloudMailAccount($accountName, $emailAddress, $mailConfig);
$this->logger->info('IONOS email account created successfully', [ 'emailAddress' => $emailAddress ]);
return new JSONResponse([
'success' => true,
'message' => 'Email account created successfully via IONOS',
'account' => $accountResponse->getData(),
], 201);
} catch (ServiceException $e) {
return $this->handleServiceException($e, $emailAddress);
} catch (\Exception $e) {
return $this->handleGenericException($e, $emailAddress);
}
}

/**
* @throws ServiceException
*/
private function createIonosEmailAccount(string $accountName, string $emailAddress): array {
$ionosResponse = $this->callIonosCreateEmailAPI($accountName, $emailAddress);
if ($ionosResponse === null || !($ionosResponse['success'] ?? false)) {
$this->logger->error('Failed to create IONOS email account', [ 'emailAddress' => $emailAddress, 'response' => $ionosResponse ]);
throw new ServiceException(self::ERR_CREATE_EMAIL_FAILED);
}
$mailConfig = $ionosResponse['mailConfig'] ?? null;
if (!is_array($mailConfig)) {
$this->logger->error('IONOS API response missing mailConfig', [ 'emailAddress' => $emailAddress, 'response' => $ionosResponse ]);
throw new ServiceException('Invalid IONOS API response: missing mail configuration');
}
return $mailConfig;
}

/**
* @throws ServiceException
*/
private function createNextcloudMailAccount(string $accountName, string $emailAddress, array $mailConfig): JSONResponse {
if (!isset($mailConfig['imap'], $mailConfig['smtp'])) {
throw new ServiceException('Invalid mail configuration: missing IMAP or SMTP configuration');
}
$imap = $mailConfig['imap'];
$smtp = $mailConfig['smtp'];
if (!is_array($imap) || !is_array($smtp)) {
throw new ServiceException('Invalid mail configuration: IMAP or SMTP configuration must be arrays');
}
return $this->accountsController->create(
$accountName,
$emailAddress,
(string)$imap['host'],
(int)$imap['port'],
(string)$imap['security'],
(string)($imap['username'] ?? $emailAddress),
(string)($imap['password'] ?? ''),
(string)$smtp['host'],
(int)$smtp['port'],
(string)$smtp['security'],
(string)($smtp['username'] ?? $emailAddress),
(string)($smtp['password'] ?? ''),
);
}

private function handleServiceException(ServiceException $e, string $emailAddress): JSONResponse {
$this->logger->error('IONOS service error', [ 'exception' => $e, 'emailAddress' => $emailAddress ]);
return new JSONResponse(['success' => false, 'message' => $e->getMessage(), 'error' => self::ERR_IONOS_API_ERROR], 400);
}

/**
* @throws ServiceException
*/
protected function callIonosCreateEmailAPI(string $accountName, string $emailAddress): ?array {
$atPosition = strrchr($emailAddress, '@');
if ($atPosition === false) {
throw new ServiceException('Invalid email address: unable to extract domain');
}
$domain = substr($atPosition, 1);
if ($domain === '') {
throw new ServiceException('Invalid email address: unable to extract domain');
}
return [
'success' => true,
'message' => 'Email account created successfully via IONOS (mock)',
'mailConfig' => [
'imap' => [
'host' => 'mail.localhost', // 'imap.' . $domain,
'password' => 'tmp',
'port' => 1143, // 993,
'security' => 'none',
'username' => '[email protected]' // $emailAddress,
],
'smtp' => [
'host' => 'mail.localhost', // 'smtp.' . $domain,
'password' => 'tmp',
'port' => 1587, // 465,
'security' => 'none',
'username' => '[email protected]' // $emailAddress,
]
]
];
}

private function handleGenericException(\Exception $e, string $emailAddress): JSONResponse {
$this->logger->error('Unexpected error during IONOS account creation', [ 'exception' => $e, 'emailAddress' => $emailAddress ]);
return new JSONResponse(['success' => false, 'message' => self::ERR_GENERIC_SETUP, 'error' => self::ERR_UNKNOWN_ERROR], 500);
}
}
7 changes: 7 additions & 0 deletions src/components/AccountForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,11 @@
required
@change="clearFeedback" />
</Tab>
<Tab id="create" key="create" :name="t('mail', 'New Email Address')">
<NewEmailAddressTab :loading="loading"
:clear-feedback="clearFeedback"
:is-valid-email="isValidEmail" />
</Tab>
</Tabs>
<div v-if="isGoogleAccount && !googleOauthUrl" class="account-form__google-sso">
{{ t('mail', 'For the Google account to work with this app you need to enable two-factor authentication for Google and generate an app password.') }}
Expand Down Expand Up @@ -252,6 +257,7 @@ import {
import { CONSENT_ABORTED, getUserConsent } from '../integration/oauth.js'
import useMainStore from '../store/mainStore.js'
import { mapStores, mapState } from 'pinia'
import NewEmailAddressTab from './ionos/NewEmailAddressTab.vue'

export default {
name: 'AccountForm',
Expand All @@ -264,6 +270,7 @@ export default {
ButtonVue,
IconLoading,
IconCheck,
NewEmailAddressTab,
},
props: {
displayName: {
Expand Down
Loading
Loading