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
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The rating depends on the installed text processing backend. See [the rating ove

Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/).
]]></description>
<version>5.7.0-beta.1</version>
<version>5.7.0-beta.3</version>
<licence>agpl</licence>
<author homepage="https://github.com/ChristophWurst">Christoph Wurst</author>
<author homepage="https://github.com/GretaD">GretaD</author>
Expand Down
4 changes: 4 additions & 0 deletions lib/Account.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ public function getDebug(): bool {
return $this->account->getDebug();
}

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

/**
* Set the quota percentage
* @param Quota $quota
Expand Down
7 changes: 6 additions & 1 deletion lib/Controller/AccountsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,9 @@ public function patchAccount(int $id,
?int $trashRetentionDays = null,
?int $junkMailboxId = null,
?bool $searchBody = null,
?bool $classificationEnabled = null): JSONResponse {
?bool $classificationEnabled = null,
?bool $imipCreate = null,
): JSONResponse {
$account = $this->accountService->find($this->currentUserId, $id);

$dbAccount = $account->getMailAccount();
Expand Down Expand Up @@ -285,6 +287,9 @@ public function patchAccount(int $id,
if ($classificationEnabled !== null) {
$dbAccount->setClassificationEnabled($classificationEnabled);
}
if ($imipCreate !== null) {
$dbAccount->setImipCreate($imipCreate);
}
return new JSONResponse(
new Account($this->accountService->save($dbAccount))
);
Expand Down
9 changes: 9 additions & 0 deletions lib/Db/MailAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
* @method void setDebug(bool $debug)
* @method bool getClassificationEnabled()
* @method void setClassificationEnabled(bool $classificationEnabled)
* @method bool getImipCreate()
* @method void setImipCreate(bool $value)
*/
class MailAccount extends Entity {
public const SIGNATURE_MODE_PLAIN = 0;
Expand Down Expand Up @@ -190,6 +192,8 @@ class MailAccount extends Entity {
protected bool $debug = false;
protected bool $classificationEnabled = true;

protected bool $imipCreate = false;

/**
* @param array $params
*/
Expand Down Expand Up @@ -253,6 +257,9 @@ public function __construct(array $params = []) {
if (isset($params['classificationEnabled'])) {
$this->setClassificationEnabled($params['classificationEnabled']);
}
if (isset($params['imipCreate'])) {
$this->setImipCreate($params['imipCreate']);
}

$this->addType('inboundPort', 'integer');
$this->addType('outboundPort', 'integer');
Expand All @@ -278,6 +285,7 @@ public function __construct(array $params = []) {
$this->addType('oooFollowsSystem', 'boolean');
$this->addType('debug', 'boolean');
$this->addType('classificationEnabled', 'boolean');
$this->addType('imipCreate', 'boolean');
}

public function getOutOfOfficeFollowsSystem(): bool {
Expand Down Expand Up @@ -328,6 +336,7 @@ public function toJson() {
'outOfOfficeFollowsSystem' => $this->getOutOfOfficeFollowsSystem(),
'debug' => $this->getDebug(),
'classificationEnabled' => $this->getClassificationEnabled(),
'imipCreate' => $this->getImipCreate(),
];

if (!is_null($this->getOutboundHost())) {
Expand Down
37 changes: 37 additions & 0 deletions lib/Migration/Version5007Date20251208000000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Mail\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version5007Date20251208000000 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
$schema = $schemaClosure();
$accountsTable = $schema->getTable('mail_accounts');
if (!$accountsTable->hasColumn('imip_create')) {
$accountsTable->addColumn('imip_create', Types::BOOLEAN, [
'default' => false,
'notnull' => false,
]);
}
return $schema;
}
}
49 changes: 35 additions & 14 deletions lib/Service/IMipService.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use OCA\Mail\Db\MessageMapper;
use OCA\Mail\Exception\ServiceException;
use OCA\Mail\Model\IMAPMessage;
use OCA\Mail\Util\ServerVersion;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\Calendar\IManager;
use Psr\Log\LoggerInterface;
Expand All @@ -30,6 +31,7 @@
private MailboxMapper $mailboxMapper;
private MailManager $mailManager;
private MessageMapper $messageMapper;
private ServerVersion $serverVersion;

public function __construct(
AccountService $accountService,
Expand All @@ -38,13 +40,15 @@
MailboxMapper $mailboxMapper,
MailManager $mailManager,
MessageMapper $messageMapper,
ServerVersion $serverVersion,
) {
$this->accountService = $accountService;
$this->calendarManager = $manager;
$this->logger = $logger;
$this->mailboxMapper = $mailboxMapper;
$this->mailManager = $mailManager;
$this->messageMapper = $messageMapper;
$this->serverVersion = $serverVersion;
}

public function process(): void {
Expand Down Expand Up @@ -115,8 +119,10 @@
continue;
}

$principalUri = 'principals/users/' . $account->getUserId();
$userId = $account->getUserId();
$recipient = $account->getEmail();
$imipCreate = $account->getImipCreate();
$systemVersion = $this->serverVersion->getMajorVersion();

foreach ($filteredMessages as $message) {
/** @var IMAPMessage $imapMessage */
Expand All @@ -138,20 +144,35 @@
try {
// an IMAP message could contain more than one iMIP object
foreach ($imapMessage->scheduling as $schedulingInfo) {
if ($schedulingInfo['method'] === 'REQUEST') {
$processed = $this->calendarManager->handleIMipRequest($principalUri, $sender, $recipient, $schedulingInfo['contents']);
$message->setImipProcessed($processed);
$message->setImipError(!$processed);
} elseif ($schedulingInfo['method'] === 'REPLY') {
$processed = $this->calendarManager->handleIMipReply($principalUri, $sender, $recipient, $schedulingInfo['contents']);
$message->setImipProcessed($processed);
$message->setImipError(!$processed);
} elseif ($schedulingInfo['method'] === 'CANCEL') {
$replyTo = $imapMessage->getReplyTo()->first()?->getEmail();
$processed = $this->calendarManager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $schedulingInfo['contents']);
$message->setImipProcessed($processed);
$message->setImipError(!$processed);
$processed = false;
if ($systemVersion < 33) {
$principalUri = 'principals/users/' . $userId;
if ($schedulingInfo['method'] === 'REQUEST') {
$processed = $this->calendarManager->handleIMipRequest($principalUri, $sender, $recipient, $schedulingInfo['contents']);
} elseif ($schedulingInfo['method'] === 'REPLY') {
$processed = $this->calendarManager->handleIMipReply($principalUri, $sender, $recipient, $schedulingInfo['contents']);
} elseif ($schedulingInfo['method'] === 'CANCEL') {
$replyTo = $imapMessage->getReplyTo()->first()?->getEmail();
$processed = $this->calendarManager->handleIMipCancel($principalUri, $sender, $replyTo, $recipient, $schedulingInfo['contents']);
}
} else {
if (!method_exists($this->calendarManager, 'handleIMip')) {
$this->logger->error('iMIP handling is not supported by server version installed.');
continue;
}
$processed = $this->calendarManager->handleIMip(
$userId,
$schedulingInfo['contents'],
[

Check failure on line 166 in lib/Service/IMipService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable32

InvalidArgument

lib/Service/IMipService.php:166:9: InvalidArgument: Argument 3 of OCP\Calendar\IManager::handleIMip expects array{absent?: 'create', recipient?: string}, but array{absent: 'create'|'ignore', absentCreateStatus: 'tentative', recipient: string} with additional array shape fields (absentCreateStatus) was provided (see https://psalm.dev/004)

Check failure on line 166 in lib/Service/IMipService.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

InvalidArgument

lib/Service/IMipService.php:166:9: InvalidArgument: Argument 3 of OCP\Calendar\IManager::handleIMip expects array{absent?: 'create', recipient?: string}, but array{absent: 'create'|'ignore', absentCreateStatus: 'tentative', recipient: string} with additional array shape fields (absentCreateStatus) was provided (see https://psalm.dev/004)
'recipient' => $recipient,
'absent' => $imipCreate ? 'create' : 'ignore',
'absentCreateStatus' => 'tentative',
],
);
}

$message->setImipProcessed($processed);
$message->setImipError(!$processed);
}
} catch (Throwable $e) {
$this->logger->error('iMIP message processing failed', [
Expand Down
25 changes: 25 additions & 0 deletions lib/Util/ServerVersion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Mail\Util;

use OCP\ServerVersion as OCPServerVersion;

class ServerVersion {

public function __construct(
private OCPServerVersion $serverVersion,
) {
}

public function getMajorVersion(): int {
return $this->serverVersion->getMajorVersion();
}

}
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nextcloud-mail",
"version": "5.7.0-beta.1",
"version": "5.7.0-beta.3",
"private": true,
"description": "Nextcloud Mail",
"license": "AGPL-3.0-only",
Expand Down
9 changes: 9 additions & 0 deletions src/components/AccountSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@
</NcButton>
</div>
</AppSettingsSection>
<AppSettingsSection
v-if="account && systemVersion >= 33"
id="calendar-settings"
:name="t('mail', 'Calendar settings')">
<CalendarSettings :account="account" />
</AppSettingsSection>
<AppSettingsSection
v-if="account"
id="classification"
Expand All @@ -79,14 +85,14 @@
id="mail-filters"
:name="t('mail', 'Filters')">
<div id="mail-filters">
<MailFilters :key="account.accountId" ref="mailFilters" :account="account" />

Check warning on line 88 in src/components/AccountSettings.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'mailFilters' is defined as ref, but never used
</div>
</AppSettingsSection>
<AppSettingsSection
v-if="account"
id="quick-actions-settings"
:name="t('mail', 'Quick actions')">
<Settings :key="account.accountId" ref="quickActions" :account="account" />

Check warning on line 95 in src/components/AccountSettings.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'quickActions' is defined as ref, but never used
</AppSettingsSection>
<AppSettingsSection
v-if="account && account.sieveEnabled"
Expand All @@ -95,7 +101,7 @@
<div id="sieve-filter">
<SieveFilterForm
:key="account.accountId"
ref="sieveFilterForm"

Check warning on line 104 in src/components/AccountSettings.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'sieveFilterForm' is defined as ref, but never used
:account="account" />
</div>
</AppSettingsSection>
Expand All @@ -119,7 +125,7 @@
<div id="sieve-settings">
<SieveAccountForm
:key="account.accountId"
ref="sieveAccountForm"

Check warning on line 128 in src/components/AccountSettings.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'sieveAccountForm' is defined as ref, but never used
:account="account" />
</div>
</AppSettingsSection>
Expand All @@ -138,6 +144,7 @@
import AliasSettings from '../components/AliasSettings.vue'
import EditorSettings from '../components/EditorSettings.vue'
import SignatureSettings from '../components/SignatureSettings.vue'
import CalendarSettings from './CalendarSettings.vue'
import CertificateSettings from './CertificateSettings.vue'
import MailFilters from './mailFilter/MailFilters.vue'
import OutOfOfficeForm from './OutOfOfficeForm.vue'
Expand All @@ -161,6 +168,7 @@
AppSettingsDialog,
AppSettingsSection,
AccountDefaultsSettings,
CalendarSettings,
OutOfOfficeForm,
CertificateSettings,
TrashRetentionSettings,
Expand Down Expand Up @@ -188,6 +196,7 @@
trapElements: [],
fetchActiveSieveScript: this.account.sieveEnabled,
loadingClassificationToggle: false,
systemVersion: parseInt(OC.config.version.split('.')[0], 10),
}
},

Expand All @@ -203,7 +212,7 @@
},

watch: {
open(newState, oldState) {

Check warning on line 215 in src/components/AccountSettings.vue

View workflow job for this annotation

GitHub Actions / NPM lint

'oldState' is defined but never used
if (newState === true && this.fetchActiveSieveScript === true) {
logger.debug(`Load active sieve script for account ${this.account.accountId}`)
this.fetchActiveSieveScript = false
Expand Down
76 changes: 76 additions & 0 deletions src/components/CalendarSettings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<template>
<div>
<NcCheckboxRadioSwitch
Copy link
Member

Choose a reason for hiding this comment

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

missing loading state, which is problematic for multiple clicks in a short time

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

id="imip-create"
:checked="imipCreate"
:disabled="saving"
@update:checked="onToggleImipCreate">
{{ t('mail', 'Automatically create tentative appointments in calendar') }}
</NcCheckboxRadioSwitch>
</div>
</template>

<script>
import { NcCheckboxRadioSwitch } from '@nextcloud/vue'
import { mapStores } from 'pinia'
import Logger from '../logger.js'
import useMainStore from '../store/mainStore.js'

export default {
name: 'CalendarSettings',
components: {
NcCheckboxRadioSwitch,
},

props: {
account: {
type: Object,
required: true,
},
},

data() {
return {
imipCreate: this.account.imipCreate,
saving: false,
}
},

computed: {
...mapStores(useMainStore),
},

methods: {
async onToggleImipCreate(val) {
if (this.saving) {
return
}

const oldVal = this.imipCreate
this.imipCreate = val
this.saving = true

try {
await this.mainStore.patchAccount({
account: this.account,
data: {
imipCreate: val,
},
})
Logger.info(`Automatic calendar appointment creation ${val ? 'enabled' : 'disabled'}`)
} catch (error) {
Logger.error(`could not ${val ? 'enable' : 'disable'} automatic calendar appointment creation`, { error })
this.imipCreate = oldVal
throw error
} finally {
this.saving = false
}
},
},
}
</script>
Loading
Loading