Skip to content
Merged
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
24 changes: 10 additions & 14 deletions lib/Controller/IonosAccountsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,34 +38,30 @@ public function __construct(
}

// Helper: input validation
private function validateInput(string $accountName, string $emailAddress): ?JSONResponse {
if ($accountName === '' || $emailAddress === '') {
private function validateInput(string $accountName, string $emailUser): ?JSONResponse {
if ($accountName === '' || $emailUser === '') {
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)) {
public function create(string $accountName, string $emailUser): JSONResponse {
if ($error = $this->validateInput($accountName, $emailUser)) {
return $error;
}

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

$this->logger->info('IONOS email account created successfully', [ 'emailAddress' => $emailAddress ]);
return $this->createNextcloudMailAccount($accountName, $emailAddress, $ionosResponse);
$this->logger->info('IONOS email account created successfully', [ 'emailAddress' => $ionosResponse->getEmail() ]);
return $this->createNextcloudMailAccount($accountName, $ionosResponse);
} catch (ServiceException $e) {
$data = [
'emailAddress' => $emailAddress,
'error' => self::ERR_IONOS_API_ERROR,
'statusCode' => $e->getCode(),
];
Expand All @@ -77,13 +73,13 @@ public function create(string $accountName, string $emailAddress): JSONResponse
}
}

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

return $this->accountsController->create(
$accountName,
$emailAddress,
$mailConfig->getEmail(),
$imap->getHost(),
$imap->getPort(),
$imap->getSecurity(),
Expand Down
8 changes: 7 additions & 1 deletion lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
use OCA\Mail\Service\AliasesService;
use OCA\Mail\Service\Classification\ClassificationSettingsService;
use OCA\Mail\Service\InternalAddressService;
use OCA\Mail\Service\IONOS\IonosConfigService;
use OCA\Mail\Service\OutboxService;
use OCA\Mail\Service\QuickActionsService;
use OCA\Mail\Service\SmimeService;
Expand Down Expand Up @@ -74,7 +75,8 @@ class PageController extends Controller {
private InternalAddressService $internalAddressService;
private QuickActionsService $quickActionsService;

public function __construct(string $appName,
public function __construct(
string $appName,
IRequest $request,
IURLGenerator $urlGenerator,
IConfig $config,
Expand All @@ -97,6 +99,7 @@ public function __construct(string $appName,
InternalAddressService $internalAddressService,
IAvailabilityCoordinator $availabilityCoordinator,
QuickActionsService $quickActionsService,
private IonosConfigService $ionosConfigService,
) {
parent::__construct($appName, $request);

Expand Down Expand Up @@ -208,9 +211,12 @@ public function index(): TemplateResponse {

$user = $this->userSession->getUser();
$response = new TemplateResponse($this->appName, 'index');


Copy link

Copilot AI Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary blank lines added. Remove extra blank line to maintain consistent code formatting.

Suggested change

Copilot uses AI. Check for mistakes.
$this->initialStateService->provideInitialState('preferences', [
'attachment-size-limit' => $this->config->getSystemValue('app.mail.attachment-size-limit', 0),
'ionos-mailconfig-enabled' => $this->config->getAppValue('mail', 'ionos-mailconfig-enabled', 'no') === 'yes',
'ionos-mailconfig-domain' => $this->ionosConfigService->getMailDomain(),
'app-version' => $this->config->getAppValue('mail', 'installed_version'),
'external-avatars' => $this->preferences->getPreference($this->currentUserId, 'external-avatars', 'true'),
'layout-mode' => $this->preferences->getPreference($this->currentUserId, 'layout-mode', 'vertical-split'),
Expand Down
48 changes: 48 additions & 0 deletions lib/Service/IONOS/IonosConfigService.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
use OCP\Exceptions\AppConfigException;
use OCP\IAppConfig;
use OCP\IConfig;
use Pdp\Domain;
use Pdp\Rules;
use Psr\Log\LoggerInterface;
use Throwable;

/**
* Service for managing IONOS API configuration
Expand Down Expand Up @@ -125,4 +128,49 @@ public function getApiConfig(): array {
'basicAuthPass' => $this->getBasicAuthPassword(),
];
}

/**
* Get the mail domain from customer domain
*
* Extracts the registrable domain (mail domain) from the customer domain
* configured in system settings.
*/
public function getMailDomain(): string {
$customerDomain = $this->config->getSystemValue('ncw.customerDomain', '');
return $this->extractMailDomain($customerDomain);
}

/**
* Extract the registrable domain (mail domain) from a customer domain.
*
* Uses the Public Suffix List via Pdp library to properly extract the
* registrable domain, handling multi-level TLDs like .co.uk correctly.
*
* Examples:
* - foo.bar.lol -> bar.lol
* - mail.test.co.uk -> test.co.uk
* - sub.domain.example.com -> example.com
*
* @param string $customerDomain The full customer domain
* @return string The extracted mail domain, or empty string if input is empty
*/
private function extractMailDomain(string $customerDomain): string {
if (empty($customerDomain)) {
return '';
}

try {
$publicSuffixList = Rules::fromPath(__DIR__ . '/../../../resources/public_suffix_list.dat');
$domain = Domain::fromIDNA2008($customerDomain);
$result = $publicSuffixList->resolve($domain);
return $result->registrableDomain()->toString();
} catch (Throwable $e) {
// Fallback to simple extraction if Pdp fails
$parts = explode('.', $customerDomain);
if (count($parts) >= 2) {
return $parts[count($parts) - 2] . '.' . $parts[count($parts) - 1];
}
return $customerDomain;
}
}
}
62 changes: 13 additions & 49 deletions lib/Service/IONOS/IonosMailService.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,23 @@ public function __construct(
* @throws ServiceException
* @throws AppConfigException
*/
public function createEmailAccount(string $emailAddress): MailAccountConfig {
$config = $this->configService->getApiConfig();
public function createEmailAccount(string $userName): MailAccountConfig {
$userId = $this->getCurrentUserId();
$userName = $this->extractUsername($emailAddress);
$domain = $this->extractDomain($emailAddress);
$domain = $this->configService->getMailDomain();

$this->logger->debug('Sending request to mailconfig service', [
'extRef' => $config['extRef'],
'emailAddress' => $emailAddress,
'apiBaseUrl' => $config['apiBaseUrl']
'extRef' => $this->configService->getExternalReference(),
'userName' => $userName,
'domain' => $domain,
'apiBaseUrl' => $this->configService->getApiBaseUrl()
]);

$client = $this->apiClientService->newClient([
'auth' => [$config['basicAuthUser'], $config['basicAuthPass']],
'verify' => !$config['allowInsecure'],
'auth' => [$this->configService->getBasicAuthUser(), $this->configService->getBasicAuthPassword()],
'verify' => !$this->configService->getAllowInsecure(),
]);

$apiInstance = $this->apiClientService->newEventAPIApi($client, $config['apiBaseUrl']);
$apiInstance = $this->apiClientService->newEventAPIApi($client, $this->configService->getApiBaseUrl());

$mailCreateData = new MailCreateData();
$mailCreateData->setNextcloudUserId($userId);
Expand All @@ -72,14 +71,14 @@ public function createEmailAccount(string $emailAddress): MailAccountConfig {

try {
$this->logger->debug('Send message to mailconfig service', ['data' => $mailCreateData]);
$result = $apiInstance->createMailbox(self::BRAND, $config['extRef'], $mailCreateData);
$result = $apiInstance->createMailbox(self::BRAND, $this->configService->getExternalReference(), $mailCreateData);

if ($result instanceof ErrorMessage) {
$this->logger->error('Failed to create ionos mail', ['status code' => $result->getStatus(), 'message' => $result->getMessage()]);
throw new ServiceException('Failed to create ionos mail', $result->getStatus());
}
if ($result instanceof MailAccountResponse) {
return $this->buildSuccessResponse($emailAddress, $result);
return $this->buildSuccessResponse($result);
}

$this->logger->debug('Failed to create ionos mail: Unknown response type', ['data' => $result ]);
Expand All @@ -98,40 +97,6 @@ public function createEmailAccount(string $emailAddress): MailAccountConfig {
}
}

/**
* Extract domain from email address
*
* @throws ServiceException
*/
public function extractDomain(string $emailAddress): string {
$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 $domain;
}

/**
* Extract username from email address
*
* @throws ServiceException
*/
public function extractUsername(string $emailAddress): string {
$atPosition = strrpos($emailAddress, '@');
if ($atPosition === false) {
throw new ServiceException('Invalid email address: unable to extract username');
}
$userName = substr($emailAddress, 0, $atPosition);
if ($userName === '') {
throw new ServiceException('Invalid email address: unable to extract username');
}
return $userName;
}

/**
* Get the current user ID
*
Expand Down Expand Up @@ -170,11 +135,10 @@ private function normalizeSslMode(string $apiSslMode): string {
/**
* Build success response with mail configuration
*
* @param string $emailAddress
* @param MailAccountResponse $response
* @return MailAccountConfig
*/
private function buildSuccessResponse(string $emailAddress, MailAccountResponse $response): MailAccountConfig {
private function buildSuccessResponse(MailAccountResponse $response): MailAccountConfig {
$smtpServer = $response->getServer()->getSmtp();
$imapServer = $response->getServer()->getImap();

Expand All @@ -195,7 +159,7 @@ private function buildSuccessResponse(string $emailAddress, MailAccountResponse
);

return new MailAccountConfig(
email: $emailAddress,
email: $response->getEmail(),
imap: $imapConfig,
smtp: $smtpConfig,
);
Expand Down
35 changes: 22 additions & 13 deletions src/components/ionos/NewEmailAddressTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@
@change="clearAllFeedback"
autofocus />
<NcInputField id="ionos-email-address"
v-model="emailAddress"
:label="t('mail', 'Mail address')"
type="email"
:placeholder="t('mail', 'Mail address')"
v-model="emailUser"
:label="t('mail', 'User')"
type="text"
:placeholder="t('mail', 'User')"
:disabled="loading || localLoading"
required
@change="clearAllFeedback" />
<p v-if="emailAddress && !isValidEmail(emailAddress)" class="account-form--error">
{{ t('mail', 'Please enter an email of the format name@example.com') }}
<p v-if="emailUser && !isValidEmail(fullEmailAddress)" class="account-form--error">
{{ t('mail', 'Please enter a valid email user name') }}
</p>
<span class="email-domain-hint">@myworkspace.com</span>
<span class="email-domain-hint">@{{ emailDomain }}</span>
<div class="account-form__submit-buttons">
<NcButton class="account-form__submit-button"
type="primary"
Expand Down Expand Up @@ -72,29 +72,38 @@ export default {
},
isValidEmail: {
type: Function,
required: true,
default: () => {},
},
},
data() {
return {
accountName: '',
emailAddress: '',
emailUser: '',
localLoading: false,
feedback: null,
}
},
computed: {
...mapStores(useMainStore),
fullEmailAddress() {
return this.emailUser ? `${this.emailUser}@${this.emailDomain}` : ''
},

isFormValid() {
return this.accountName
&& this.isValidEmail(this.emailAddress)
&& this.emailUser
&& this.isValidEmail(this.fullEmailAddress)
},

buttonText() {
return this.localLoading
? t('mail', 'Creating account...')
: t('mail', 'Create & Connect')
},

emailDomain() {
return this.mainStore.getPreference('ionos-mailconfig-domain', 'myworkspace.com')
},
},
methods: {
async submitForm() {
Expand All @@ -104,7 +113,7 @@ export default {
try {
const account = await this.callIonosAPI({
accountName: this.accountName,
emailAddress: this.emailAddress,
emailUser: this.emailUser,
})

logger.debug(`account ${account.id} created`, { account })
Expand Down Expand Up @@ -147,11 +156,11 @@ export default {
}
},

async callIonosAPI({ accountName, emailAddress }) {
async callIonosAPI({ accountName, emailUser }) {
const url = generateUrl('/apps/mail/api/ionos/accounts')

return axios
.post(url, { accountName, emailAddress })
.post(url, { accountName, emailUser })
.then((resp) => resp.data.data)
.then(fixAccountId)
.catch((e) => {
Expand Down
4 changes: 4 additions & 0 deletions src/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export default function initAfterAppCreation() {
key: 'ionos-mailconfig-enabled',
value: preferences['ionos-mailconfig-enabled'],
})
mainStore.savePreferenceMutation({
key: 'ionos-mailconfig-domain',
value: preferences['ionos-mailconfig-domain'],
})
mainStore.savePreferenceMutation({
key: 'external-avatars',
value: preferences['external-avatars'],
Expand Down
Loading
Loading