-
Notifications
You must be signed in to change notification settings - Fork 295
feat: automated appointment creation #12144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -30,6 +31,7 @@ | |
| private MailboxMapper $mailboxMapper; | ||
| private MailManager $mailManager; | ||
| private MessageMapper $messageMapper; | ||
| private ServerVersion $serverVersion; | ||
|
|
||
| public function __construct( | ||
| AccountService $accountService, | ||
|
|
@@ -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 { | ||
|
|
@@ -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 */ | ||
|
|
@@ -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
|
||
| 'recipient' => $recipient, | ||
| 'absent' => $imipCreate ? 'create' : 'ignore', | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Server PR is merged... will rebase in a couple hours will see what psalm says
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We could change
To avoid the warning about "ignore" and make psalm more relax about the unknown "absentCreateStatus" with the "...". That isn't ideal, but at least we don't have to suppress some errors or bump the baseline only for one supported version.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah... I agree we need to adjust stable32 cause psalm will wine again
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm skeptical about this approach. That means you have the public API (interface) changes partially backported without the implementation.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding "ignore" as value to absent is syntactical sugar. It's already possible as of today by just omitting absent. There's no implementation to be done, the code only runs if you set it to "create" on 32 or newer. absentCreateStatus is not documented for stable32 and will not be. We are just telling psalm, by adding
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, that makes sense. Thanks! |
||
| 'absentCreateStatus' => 'tentative', | ||
| ], | ||
| ); | ||
| } | ||
|
|
||
| $message->setImipProcessed($processed); | ||
| $message->setImipError(!$processed); | ||
| } | ||
| } catch (Throwable $e) { | ||
| $this->logger->error('iMIP message processing failed', [ | ||
|
|
||
| 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(); | ||
| } | ||
|
|
||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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 | ||
DerDreschner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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> | ||
Uh oh!
There was an error while loading. Please reload this page.