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
50 changes: 17 additions & 33 deletions lib/Controller/IonosAccountsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace OCA\Mail\Controller;

use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Http\JsonResponse as MailJsonResponse;
use OCA\Mail\Http\TrapError;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\OpenAPI;
Expand All @@ -23,8 +24,6 @@ class IonosAccountsController extends Controller {
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,
Expand Down Expand Up @@ -58,25 +57,28 @@ public function create(string $accountName, string $emailAddress): JSONResponse
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);
return $this->createNextcloudMailAccount($accountName, $emailAddress, $mailConfig);
} catch (ServiceException $e) {
return $this->handleServiceException($e, $emailAddress);

$data = [
'emailAddress' => $emailAddress,
'error' => self::ERR_IONOS_API_ERROR,
];
$this->logger->error('IONOS service error: ' . $e->getMessage(), $data);

return MailJsonResponse::fail($data);
} catch (\Exception $e) {
return $this->handleGenericException($e, $emailAddress);
return MailJsonResponse::error('Could not create account');
}
}

/**
* @throws ServiceException
*/
private function createIonosEmailAccount(string $accountName, string $emailAddress): array {
$ionosResponse = $this->callIonosCreateEmailAPI($accountName, $emailAddress);
$ionosResponse = $this->callIonosCreateEmailAPI($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);
Expand All @@ -89,18 +91,10 @@ private function createIonosEmailAccount(string $accountName, string $emailAddre
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,
Expand All @@ -117,15 +111,10 @@ private function createNextcloudMailAccount(string $accountName, string $emailAd
);
}

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 {
protected function callIonosCreateEmailAPI(string $emailAddress): ?array {
$atPosition = strrchr($emailAddress, '@');
if ($atPosition === false) {
throw new ServiceException('Invalid email address: unable to extract domain');
Expand All @@ -143,21 +132,16 @@ protected function callIonosCreateEmailAPI(string $accountName, string $emailAdd
'password' => 'tmp',
'port' => 1143, // 993,
'security' => 'none',
'username' => '[email protected]' // $emailAddress,
'username' => $emailAddress,
],
'smtp' => [
'host' => 'mail.localhost', // 'smtp.' . $domain,
'password' => 'tmp',
'port' => 1587, // 465,
'security' => 'none',
'username' => '[email protected]' // $emailAddress,
'username' => $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);
}
}
1 change: 1 addition & 0 deletions lib/Controller/PageController.php
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ public function index(): TemplateResponse {
$response = new TemplateResponse($this->appName, 'index');
$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',
'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
18 changes: 16 additions & 2 deletions src/components/AccountForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,14 @@
required
@change="clearFeedback" />
</Tab>
<Tab id="create" key="create" :name="t('mail', 'New Email Address')">
<Tab v-if="useIonosMailconfig"
id="create"
key="create"
:name="t('mail', 'New Email Address')">
<NewEmailAddressTab :loading="loading"
:clear-feedback="clearFeedback"
:is-valid-email="isValidEmail" />
:is-valid-email="isValidEmail"
@account-created="(account) => $emit('account-created', account)" />
</Tab>
</Tabs>
<div v-if="isGoogleAccount && !googleOauthUrl" class="account-form__google-sso">
Expand Down Expand Up @@ -328,6 +332,10 @@ export default {
'microsoftOauthUrl',
]),

useIonosMailconfig() {
return this.mainStore.getPreference('ionos-mailconfig-enabled', null)
},

settingsPage() {
return this.account !== undefined
},
Expand Down Expand Up @@ -416,6 +424,12 @@ export default {
this.manualConfig.smtpPassword = this.autoConfig.password
}
}
if (this.mode === 'create') {
// cleanup host info in order to remove isGoogleAccount message from interface
this.manualConfig.imapHost = undefined
this.manualConfig.smtpHost = undefined
this.clearFeedback()
}
},
onImapSslModeChange(value) {
this.clearFeedback()
Expand Down
69 changes: 42 additions & 27 deletions src/components/ionos/NewEmailAddressTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
type="text"
:placeholder="t('mail', 'Name')"
:disabled="loading || localLoading"
@change="clearAllFeedback"
autofocus />
<NcInputField id="ionos-email-address"
v-model="emailAddress"
Expand All @@ -18,18 +19,11 @@
:placeholder="t('mail', 'Mail address')"
:disabled="loading || localLoading"
required
@change="clearFeedback" />
@change="clearAllFeedback" />
<p v-if="emailAddress && !isValidEmail(emailAddress)" class="account-form--error">
{{ t('mail', 'Please enter an email of the format [email protected]') }}
</p>
<span class="email-domain-hint">@myworkspace.com</span>
<NcPasswordField id="ionos-password"
v-model="password"
:disabled="loading || localLoading"
type="password"
:label="t('mail', 'Password')"
:placeholder="t('mail', 'Password')"
required />
<div class="account-form__submit-buttons">
<NcButton class="account-form__submit-button"
type="primary"
Expand All @@ -49,16 +43,20 @@
</template>

<script>
import { NcInputField, NcPasswordField, NcButton, NcLoadingIcon as IconLoading } from '@nextcloud/vue'
import { NcInputField, NcButton, NcLoadingIcon as IconLoading } from '@nextcloud/vue'
import IconCheck from 'vue-material-design-icons/Check.vue'
import { generateUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
import { translate as t } from '@nextcloud/l10n'
import logger from '../../logger.js'
import { fixAccountId } from '../../service/AccountService.js'
import { mapStores } from 'pinia'
import useMainStore from '../../store/mainStore.js'

export default {
name: 'NewEmailAddressTab',
components: {
NcInputField,
NcPasswordField,
NcButton,
IconLoading,
IconCheck,
Expand All @@ -81,61 +79,78 @@ export default {
return {
accountName: '',
emailAddress: '',
password: '',
localLoading: false,
feedback: null,
}
},
computed: {
...mapStores(useMainStore),
isFormValid() {
return this.accountName
&& this.isValidEmail(this.emailAddress)
&& this.password
},

buttonText() {
return this.localLoading
? this.t('mail', 'Creating account...')
: this.t('mail', 'Create & Connect')
? t('mail', 'Creating account...')
: t('mail', 'Create & Connect')
},
},
methods: {
async submitForm() {
this.clearLocalFeedback()
this.clearFeedback()
this.clearAllFeedback()
this.localLoading = true

try {
const response = await this.callIonosAPI({
const account = await this.callIonosAPI({
accountName: this.accountName,
emailAddress: this.emailAddress,
password: this.password,
})

this.feedback = response.data.message || this.t('mail', 'Account created successfully')
logger.debug(`account ${account.id} created`, { account })

this.feedback = t('mail', 'Account created successfully')

this.loadingMessage = t('mail', 'Loading account')
await this.mainStore.finishAccountSetup({ account })
this.$emit('account-created', account)
} catch (error) {
console.error('Account creation failed:', error)

this.feedback = error.response?.data?.message
|| this.t('mail', 'There was an error while setting up your account')
if (error.data?.error === 'IONOS_API_ERROR') {
this.feedback = t('mail', 'There was an error while setting up your account')
} else {
this.feedback = t('mail', 'There was an error while setting up your account')
}
} finally {
this.localLoading = false
}
},

async callIonosAPI({ accountName, emailAddress, password }) {
async callIonosAPI({ accountName, emailAddress }) {
const url = generateUrl('/apps/mail/api/ionos/accounts')
return await axios.post(url, {
accountName,
emailAddress,
password,
})

return axios
.post(url, { accountName, emailAddress })
.then((resp) => resp.data.data)
.then(fixAccountId)
.catch((e) => {
if (e.response && e.response.status === 400) {
throw e.response.data
}

throw e
})
},

clearLocalFeedback() {
this.feedback = null
},

clearAllFeedback() {
this.clearLocalFeedback()
this.clearFeedback()
},
},
}
</script>
Expand Down
4 changes: 4 additions & 0 deletions src/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export default function initAfterAppCreation() {
key: 'version',
value: preferences['config-installed-version'],
})
mainStore.savePreferenceMutation({
key: 'ionos-mailconfig-enabled',
value: preferences['ionos-mailconfig-enabled'],
})
mainStore.savePreferenceMutation({
key: 'external-avatars',
value: preferences['external-avatars'],
Expand Down
Loading
Loading