diff --git a/appinfo/info.xml b/appinfo/info.xml index 9a9c7833d4..c8cf685c20 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -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/). ]]> - 5.7.0-beta.1 + 5.7.0-beta.3 agpl Christoph Wurst GretaD diff --git a/lib/Account.php b/lib/Account.php index 15aecc58ac..fa68bd0d21 100644 --- a/lib/Account.php +++ b/lib/Account.php @@ -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 diff --git a/lib/Controller/AccountsController.php b/lib/Controller/AccountsController.php index f64f1ac6be..ab6f2da070 100644 --- a/lib/Controller/AccountsController.php +++ b/lib/Controller/AccountsController.php @@ -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(); @@ -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)) ); diff --git a/lib/Db/MailAccount.php b/lib/Db/MailAccount.php index 8c26ef1137..f3e8b900cb 100644 --- a/lib/Db/MailAccount.php +++ b/lib/Db/MailAccount.php @@ -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; @@ -190,6 +192,8 @@ class MailAccount extends Entity { protected bool $debug = false; protected bool $classificationEnabled = true; + protected bool $imipCreate = false; + /** * @param array $params */ @@ -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'); @@ -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 { @@ -328,6 +336,7 @@ public function toJson() { 'outOfOfficeFollowsSystem' => $this->getOutOfOfficeFollowsSystem(), 'debug' => $this->getDebug(), 'classificationEnabled' => $this->getClassificationEnabled(), + 'imipCreate' => $this->getImipCreate(), ]; if (!is_null($this->getOutboundHost())) { diff --git a/lib/Migration/Version5007Date20251208000000.php b/lib/Migration/Version5007Date20251208000000.php new file mode 100644 index 0000000000..9bae7b18e1 --- /dev/null +++ b/lib/Migration/Version5007Date20251208000000.php @@ -0,0 +1,37 @@ +getTable('mail_accounts'); + if (!$accountsTable->hasColumn('imip_create')) { + $accountsTable->addColumn('imip_create', Types::BOOLEAN, [ + 'default' => false, + 'notnull' => false, + ]); + } + return $schema; + } +} diff --git a/lib/Service/IMipService.php b/lib/Service/IMipService.php index 2b976afea4..1c8d3f991c 100644 --- a/lib/Service/IMipService.php +++ b/lib/Service/IMipService.php @@ -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 @@ class IMipService { private MailboxMapper $mailboxMapper; private MailManager $mailManager; private MessageMapper $messageMapper; + private ServerVersion $serverVersion; public function __construct( AccountService $accountService, @@ -38,6 +40,7 @@ public function __construct( MailboxMapper $mailboxMapper, MailManager $mailManager, MessageMapper $messageMapper, + ServerVersion $serverVersion, ) { $this->accountService = $accountService; $this->calendarManager = $manager; @@ -45,6 +48,7 @@ public function __construct( $this->mailboxMapper = $mailboxMapper; $this->mailManager = $mailManager; $this->messageMapper = $messageMapper; + $this->serverVersion = $serverVersion; } public function process(): void { @@ -115,8 +119,10 @@ public function process(): void { 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 @@ public function process(): void { 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'], + [ + 'recipient' => $recipient, + 'absent' => $imipCreate ? 'create' : 'ignore', + 'absentCreateStatus' => 'tentative', + ], + ); } + + $message->setImipProcessed($processed); + $message->setImipError(!$processed); } } catch (Throwable $e) { $this->logger->error('iMIP message processing failed', [ diff --git a/lib/Util/ServerVersion.php b/lib/Util/ServerVersion.php new file mode 100644 index 0000000000..a4e93dfd5d --- /dev/null +++ b/lib/Util/ServerVersion.php @@ -0,0 +1,25 @@ +serverVersion->getMajorVersion(); + } + +} diff --git a/package-lock.json b/package-lock.json index 091ca103b0..7de01230ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "nextcloud-mail", - "version": "5.7.0-beta.1", + "version": "5.7.0-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "nextcloud-mail", - "version": "5.7.0-beta.1", + "version": "5.7.0-beta.3", "hasInstallScript": true, "license": "AGPL-3.0-only", "dependencies": { diff --git a/package.json b/package.json index b835374e81..87b90b84b0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/AccountSettings.vue b/src/components/AccountSettings.vue index 0764872b25..6c5c21e95a 100644 --- a/src/components/AccountSettings.vue +++ b/src/components/AccountSettings.vue @@ -62,6 +62,12 @@ + + + + + + + diff --git a/tests/Unit/Service/IMipServiceTest.php b/tests/Unit/Service/IMipServiceTest.php index 0b2bd7a265..fa31224c30 100644 --- a/tests/Unit/Service/IMipServiceTest.php +++ b/tests/Unit/Service/IMipServiceTest.php @@ -23,7 +23,9 @@ use OCA\Mail\Service\AccountService; use OCA\Mail\Service\IMipService; use OCA\Mail\Service\MailManager; +use OCA\Mail\Util\ServerVersion; use OCP\Calendar\IManager; +use OCP\ServerVersion as OCPServerVersion; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; @@ -36,6 +38,7 @@ class IMipServiceTest extends TestCase { /** @var AccountService|MockObject */ private $accountService; + private IManager $calendarManager; /** @var MailManager|MockObject */ @@ -46,6 +49,10 @@ class IMipServiceTest extends TestCase { private IMipService $service; + private ServerVersion|MockObject $serverVersion; + + private OCPServerVersion $OCPServerVersion; + protected function setUp(): void { parent::setUp(); @@ -55,6 +62,8 @@ protected function setUp(): void { $this->logger = $this->createMock(LoggerInterface::class); $this->mailManager = $this->createMock(MailManager::class); $this->messageMapper = $this->createMock(MessageMapper::class); + $this->serverVersion = $this->createMock(ServerVersion::class); + $this->OCPServerVersion = new OCPServerVersion(); $this->service = new IMipService( $this->accountService, @@ -62,7 +71,8 @@ protected function setUp(): void { $this->logger, $this->mailboxMapper, $this->mailManager, - $this->messageMapper + $this->messageMapper, + $this->serverVersion ); } @@ -76,6 +86,8 @@ public function testNoSchedulingInformation(): void { ->method('findById'); $this->accountService->expects(self::never()) ->method('findById'); + $this->calendarManager->expects(self::never()) + ->method('handleIMipRequest'); $this->calendarManager->expects(self::never()) ->method('handleIMipReply'); $this->calendarManager->expects(self::never()) @@ -112,6 +124,8 @@ public function testIsSpecialUse(): void { ->willReturn($account); $this->messageMapper->expects(self::once()) ->method('updateImipData'); + $this->calendarManager->expects(self::never()) + ->method('handleIMipRequest'); $this->calendarManager->expects(self::never()) ->method('handleIMipReply'); $this->calendarManager->expects(self::never()) @@ -145,6 +159,8 @@ public function testIsArchive(): void { ->willReturn($account); $this->messageMapper->expects(self::once()) ->method('updateImipData'); + $this->calendarManager->expects(self::never()) + ->method('handleIMipRequest'); $this->calendarManager->expects(self::never()) ->method('handleIMipReply'); $this->calendarManager->expects(self::never()) @@ -183,6 +199,8 @@ public function testNoSchedulingInfo(): void { ->method('getImapMessagesForScheduleProcessing') ->with($account, $mailbox, [$message->getUid()]) ->willReturn([$imapMessage]); + $this->calendarManager->expects(self::never()) + ->method('handleIMipRequest'); $this->calendarManager->expects(self::never()) ->method('handleIMipReply'); $this->calendarManager->expects(self::never()) @@ -225,6 +243,8 @@ public function testImapConnectionServiceException(): void { ->willThrowException(new ServiceException()); $this->logger->expects(self::once()) ->method('error'); + $this->calendarManager->expects(self::never()) + ->method('handleIMipRequest'); $this->calendarManager->expects(self::never()) ->method('handleIMipReply'); $this->calendarManager->expects(self::never()) @@ -266,6 +286,9 @@ public function testIsRequest(): void { ->method('getImapMessagesForScheduleProcessing') ->with($account, $mailbox, [$message->getUid()]) ->willReturn([$imapMessage]); + $this->serverVersion->expects(self::once()) + ->method('getMajorVersion') + ->willReturn(32); $imapMessage->expects(self::once()) ->method('getUid') ->willReturn(1); @@ -323,11 +346,12 @@ public function testIsReply(): void { ->method('getImapMessagesForScheduleProcessing') ->with($account, $mailbox, [$message->getUid()]) ->willReturn([$imapMessage]); + $this->serverVersion->expects(self::once()) + ->method('getMajorVersion') + ->willReturn(32); $imapMessage->expects(self::once()) ->method('getUid') ->willReturn(1); - $this->logger->expects(self::never()) - ->method('info'); $imapMessage->expects(self::once()) ->method('getFrom') ->willReturn($addressList); @@ -337,9 +361,6 @@ public function testIsReply(): void { $address->expects(self::once()) ->method('getEmail') ->willReturn('pam@stardew-bus-service.com'); - $imapMessage->expects(self::never()) - ->method('getInReplyTo') - ->willReturn($addressList); $this->calendarManager->expects(self::once()) ->method('handleIMipReply') ->with('principals/users/vincent', @@ -383,6 +404,207 @@ public function testIsCancel(): void { ->method('getImapMessagesForScheduleProcessing') ->with($account, $mailbox, [$message->getUid()]) ->willReturn([$imapMessage]); + $this->serverVersion->expects(self::once()) + ->method('getMajorVersion') + ->willReturn(32); + $imapMessage->expects(self::once()) + ->method('getUid') + ->willReturn(1); + $this->logger->expects(self::never()) + ->method('info'); + $imapMessage->expects(self::once()) + ->method('getFrom') + ->willReturn($addressList); + $addressList->expects(self::once()) + ->method('first') + ->willReturn($address); + $address->expects(self::once()) + ->method('getEmail') + ->willReturn('pam@stardew-bus-service.com'); + $this->calendarManager->expects(self::once()) + ->method('handleIMipCancel') + ->with('principals/users/vincent', + 'pam@stardew-bus-service.com', + null, + $account->getEmail(), + $imapMessage->scheduling[0]['contents'] + ); + $this->messageMapper->expects(self::once()) + ->method('updateImipData'); + + $this->service->process(); + } + + public function testIsRequestServerVersion33(): void { + if ($this->OCPServerVersion->getMajorVersion() < 33) { + $this->markTestSkipped('Requires Nextcloud 33 or higher'); + } + + $message = new Message(); + $message->setImipMessage(true); + $message->setUid(1); + $message->setMailboxId(100); + $mailbox = new Mailbox(); + $mailbox->setId(100); + $mailbox->setAccountId(200); + $mailAccount = new MailAccount(); + $mailAccount->setId(200); + $mailAccount->setEmail('vincent@stardew-valley.edu'); + $mailAccount->setUserId('vincent'); + $account = new Account($mailAccount); + $imapMessage = $this->createMock(IMAPMessage::class); + $imapMessage->scheduling[] = ['method' => 'REQUEST', 'contents' => 'VCALENDAR']; + $addressList = $this->createMock(AddressList::class); + $address = $this->createMock(Address::class); + + $this->messageMapper->expects(self::once()) + ->method('findIMipMessagesAscending') + ->willReturn([$message]); + $this->mailboxMapper->expects(self::once()) + ->method('findById') + ->willReturn($mailbox); + $this->accountService->expects(self::once()) + ->method('findById') + ->willReturn($account); + $this->mailManager->expects(self::once()) + ->method('getImapMessagesForScheduleProcessing') + ->with($account, $mailbox, [$message->getUid()]) + ->willReturn([$imapMessage]); + $this->serverVersion->expects(self::once()) + ->method('getMajorVersion') + ->willReturn(33); + $imapMessage->expects(self::once()) + ->method('getUid') + ->willReturn(1); + $imapMessage->expects(self::once()) + ->method('getFrom') + ->willReturn($addressList); + $addressList->expects(self::once()) + ->method('first') + ->willReturn($address); + $address->expects(self::once()) + ->method('getEmail') + ->willReturn('pam@stardew-bus-service.com'); + $this->logger->expects(self::never()) + ->method('info'); + $this->calendarManager->expects(self::once()) + ->method('handleIMip') + ->with('vincent', 'VCALENDAR', [ + 'recipient' => 'vincent@stardew-valley.edu', + 'absent' => 'ignore', + 'absentCreateStatus' => 'tentative', + ]) + ->willReturn(true); + $this->messageMapper->expects(self::once()) + ->method('updateImipData'); + + $this->service->process(); + } + + public function testIsReplyServerVersion33(): void { + if ($this->OCPServerVersion->getMajorVersion() < 33) { + $this->markTestSkipped('Requires Nextcloud 33 or higher'); + } + + $message = new Message(); + $message->setImipMessage(true); + $message->setUid(1); + $message->setMailboxId(100); + $mailbox = new Mailbox(); + $mailbox->setId(100); + $mailbox->setAccountId(200); + $mailAccount = new MailAccount(); + $mailAccount->setId(200); + $mailAccount->setEmail('vincent@stardew-valley.edu'); + $mailAccount->setUserId('vincent'); + $account = new Account($mailAccount); + $imapMessage = $this->createMock(IMAPMessage::class); + $imapMessage->scheduling[] = ['method' => 'REPLY', 'contents' => 'VCARD']; + $addressList = $this->createMock(AddressList::class); + $address = $this->createMock(Address::class); + + $this->messageMapper->expects(self::once()) + ->method('findIMipMessagesAscending') + ->willReturn([$message]); + $this->mailboxMapper->expects(self::once()) + ->method('findById') + ->willReturn($mailbox); + $this->accountService->expects(self::once()) + ->method('findById') + ->willReturn($account); + $this->mailManager->expects(self::once()) + ->method('getImapMessagesForScheduleProcessing') + ->with($account, $mailbox, [$message->getUid()]) + ->willReturn([$imapMessage]); + $this->serverVersion->expects(self::once()) + ->method('getMajorVersion') + ->willReturn(33); + $imapMessage->expects(self::once()) + ->method('getUid') + ->willReturn(1); + $this->logger->expects(self::never()) + ->method('info'); + $imapMessage->expects(self::once()) + ->method('getFrom') + ->willReturn($addressList); + $addressList->expects(self::once()) + ->method('first') + ->willReturn($address); + $address->expects(self::once()) + ->method('getEmail') + ->willReturn('pam@stardew-bus-service.com'); + $this->calendarManager->expects(self::once()) + ->method('handleIMip') + ->with('vincent', 'VCARD', [ + 'recipient' => 'vincent@stardew-valley.edu', + 'absent' => 'ignore', + 'absentCreateStatus' => 'tentative', + ]) + ->willReturn(true); + $this->messageMapper->expects(self::once()) + ->method('updateImipData'); + + $this->service->process(); + } + + public function testIsCancelServerVersion33(): void { + if ($this->OCPServerVersion->getMajorVersion() < 33) { + $this->markTestSkipped('Requires Nextcloud 33 or higher'); + } + + $message = new Message(); + $message->setImipMessage(true); + $message->setUid(1); + $message->setMailboxId(100); + $mailbox = new Mailbox(); + $mailbox->setId(100); + $mailbox->setAccountId(200); + $mailAccount = new MailAccount(); + $mailAccount->setId(200); + $mailAccount->setEmail('vincent@stardew-valley.edu'); + $mailAccount->setUserId('vincent'); + $account = new Account($mailAccount); + $imapMessage = $this->createMock(IMAPMessage::class); + $imapMessage->scheduling[] = ['method' => 'CANCEL', 'contents' => 'VCARD']; + $addressList = $this->createMock(AddressList::class); + $address = $this->createMock(Address::class); + + $this->messageMapper->expects(self::once()) + ->method('findIMipMessagesAscending') + ->willReturn([$message]); + $this->mailboxMapper->expects(self::once()) + ->method('findById') + ->willReturn($mailbox); + $this->accountService->expects(self::once()) + ->method('findById') + ->willReturn($account); + $this->mailManager->expects(self::once()) + ->method('getImapMessagesForScheduleProcessing') + ->with($account, $mailbox, [$message->getUid()]) + ->willReturn([$imapMessage]); + $this->serverVersion->expects(self::once()) + ->method('getMajorVersion') + ->willReturn(33); $imapMessage->expects(self::once()) ->method('getUid') ->willReturn(1); @@ -407,7 +629,8 @@ public function testIsCancel(): void { null, $account->getEmail(), $imapMessage->scheduling[0]['contents'] - ); + ) + ->willReturn(true); $this->messageMapper->expects(self::once()) ->method('updateImipData');