diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php index 202689db68c03..3b35cd59146a3 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php @@ -138,6 +138,18 @@ public function schedule(Message $iTipMessage) { $iTipMessage->scheduleStatus = '5.0; EMail delivery failed'; return; } + + // Check if external attendees are disabled + $externalAttendeesDisabled = $this->config->getValueBool('dav', 'caldav_external_attendees_disabled', false); + if ($externalAttendeesDisabled && !$this->imipService->isSystemUser($recipient)) { + $this->logger->debug('Invitation not sent to external attendee (external attendees disabled)', [ + 'uid' => $iTipMessage->uid, + 'attendee' => $recipient, + ]); + $iTipMessage->scheduleStatus = '5.0; External attendees are disabled'; + return; + } + $recipientName = $iTipMessage->recipientName ? (string) $iTipMessage->recipientName : null; $newEvents = $iTipMessage->message; diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php index cc68cfec6574b..925821b5b9bbf 100644 --- a/apps/dav/lib/CalDAV/Schedule/IMipService.php +++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php @@ -14,6 +14,7 @@ use OCP\IConfig; use OCP\IDBConnection; use OCP\IL10N; +use OCP\IUserManager; use OCP\L10N\IFactory as L10NFactory; use OCP\Mail\IEMailTemplate; use OCP\Security\ISecureRandom; @@ -35,7 +36,8 @@ class IMipService { private L10NFactory $l10nFactory; private IL10N $l10n; private ITimeFactory $timeFactory; - + private IUserManager $userManager; + /** @var string[] */ private const STRING_DIFF = [ 'meeting_title' => 'SUMMARY', @@ -49,13 +51,16 @@ public function __construct(URLGenerator $urlGenerator, IDBConnection $db, ISecureRandom $random, L10NFactory $l10nFactory, - ITimeFactory $timeFactory) { + ITimeFactory $timeFactory, + IUserManager $userManager, + ) { $this->urlGenerator = $urlGenerator; $this->config = $config; $this->db = $db; $this->random = $random; $this->l10nFactory = $l10nFactory; $this->timeFactory = $timeFactory; + $this->userManager = $userManager; $language = $this->l10nFactory->findGenericLanguage(); $locale = $this->l10nFactory->findLocale($language); $this->l10n = $this->l10nFactory->get('dav', $language, $locale); @@ -1182,6 +1187,16 @@ public function getReplyingAttendee(Message $iTipMessage): ?Property { return null; } + /** + * Check if an email address belongs to a system user + * + * @param string $email + * @return bool True if the email belongs to a system user, false otherwise + */ + public function isSystemUser(string $email): bool { + return !empty($this->userManager->getByEmail($email)); + } + public function isRoomOrResource(Property $attendee): bool { $cuType = $attendee->offsetGet('CUTYPE'); if (!$cuType instanceof Parameter) { diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php index 4fd62b445e360..e286975fc5c5d 100644 --- a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php @@ -157,6 +157,10 @@ public function testDeliveryNoSignificantChange(): void { $message->senderName = 'Mr. Wizard'; $message->recipient = 'mailto:' . 'frodo@hobb.it'; $message->significantChange = false; + + $this->config->expects(self::never()) + ->method('getValueBool'); + $this->plugin->schedule($message); $this->assertEquals('1.0', $message->getScheduleStatus()); } @@ -204,6 +208,17 @@ public function testParsingSingle(): void { $this->service->expects(self::once()) ->method('getLastOccurrence') ->willReturn(1496912700); + $this->config->expects(self::exactly(2)) + ->method('getValueBool') + ->willReturnCallback(function ($app, $key, $default) { + if ($app === 'dav' && $key === 'caldav_external_attendees_disabled') { + return false; + } + if ($app === 'core' && $key === 'mail_providers_enabled') { + return false; + } + return $default; + }); $this->mailer->expects(self::once()) ->method('validateMailAddress') ->with('frodo@hobb.it') @@ -311,6 +326,10 @@ public function testAttendeeIsResource(): void { $this->service->expects(self::once()) ->method('getLastOccurrence') ->willReturn(1496912700); + $this->config->expects(self::once()) + ->method('getValueBool') + ->with('dav', 'caldav_external_attendees_disabled', false) + ->willReturn(false); $this->mailer->expects(self::once()) ->method('validateMailAddress') ->with('the-shire@hobb.it') @@ -389,6 +408,10 @@ public function testAttendeeIsCircle(): void { $this->service->expects(self::once()) ->method('getLastOccurrence') ->willReturn(1496912700); + $this->config->expects(self::once()) + ->method('getValueBool') + ->with('dav', 'caldav_external_attendees_disabled', false) + ->willReturn(false); $this->mailer->expects(self::once()) ->method('validateMailAddress') ->with('circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth') @@ -494,6 +517,17 @@ public function testParsingRecurrence(): void { $this->service->expects(self::once()) ->method('getLastOccurrence') ->willReturn(1496912700); + $this->config->expects(self::exactly(2)) + ->method('getValueBool') + ->willReturnCallback(function ($app, $key, $default) { + if ($app === 'dav' && $key === 'caldav_external_attendees_disabled') { + return false; + } + if ($app === 'core' && $key === 'mail_providers_enabled') { + return false; + } + return $default; + }); $this->mailer->expects(self::once()) ->method('validateMailAddress') ->with('frodo@hobb.it') @@ -746,6 +780,17 @@ public function testMailProviderSend(): void { $this->service->expects(self::once()) ->method('getLastOccurrence') ->willReturn(1496912700); + $this->config->expects(self::exactly(2)) + ->method('getValueBool') + ->willReturnCallback(function ($app, $key, $default) { + if ($app === 'dav' && $key === 'caldav_external_attendees_disabled') { + return false; + } + if ($app === 'core' && $key === 'mail_providers_enabled') { + return true; + } + return $default; + }); $this->service->expects(self::once()) ->method('getCurrentAttendee') ->with($message) @@ -897,10 +942,17 @@ public function testMailProviderDisabled(): void { ->method('getValueString') ->with('dav', 'invitation_link_recipients', 'yes') ->willReturn('yes'); - $this->config->expects(self::once()) + $this->config->expects(self::exactly(2)) ->method('getValueBool') - ->with('core', 'mail_providers_enabled', true) - ->willReturn(false); + ->willReturnCallback(function ($app, $key, $default) { + if ($app === 'dav' && $key === 'caldav_external_attendees_disabled') { + return false; + } + if ($app === 'core' && $key === 'mail_providers_enabled') { + return false; + } + return $default; + }); $this->service->expects(self::once()) ->method('createInvitationToken') ->with($message, $newVevent, 1496912700) @@ -948,6 +1000,17 @@ public function testNoOldEvent(): void { $this->service->expects(self::once()) ->method('getLastOccurrence') ->willReturn(1496912700); + $this->config->expects(self::exactly(2)) + ->method('getValueBool') + ->willReturnCallback(function ($app, $key, $default) { + if ($app === 'dav' && $key === 'caldav_external_attendees_disabled') { + return false; + } + if ($app === 'core' && $key === 'mail_providers_enabled') { + return false; + } + return $default; + }); $this->mailer->expects(self::once()) ->method('validateMailAddress') ->with('frodo@hobb.it') @@ -1045,6 +1108,17 @@ public function testNoButtons(): void { $this->service->expects(self::once()) ->method('getLastOccurrence') ->willReturn(1496912700); + $this->config->expects(self::exactly(2)) + ->method('getValueBool') + ->willReturnCallback(function ($app, $key, $default) { + if ($app === 'dav' && $key === 'caldav_external_attendees_disabled') { + return false; + } + if ($app === 'core' && $key === 'mail_providers_enabled') { + return false; + } + return $default; + }); $this->mailer->expects(self::once()) ->method('validateMailAddress') ->with('frodo@hobb.it') @@ -1108,4 +1182,167 @@ public function testNoButtons(): void { $this->plugin->schedule($message); $this->assertEquals('1.1', $message->getScheduleStatus()); } + + public function testExternalAttendeesDisabledForExternalUser(): void { + $message = new Message(); + $message->method = 'REQUEST'; + $newVCalendar = new VCalendar(); + $newVevent = new VEvent($newVCalendar, 'one', array_merge([ + 'UID' => 'uid-1234', + 'SEQUENCE' => 1, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ], [])); + $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $newVevent->add('ATTENDEE', 'mailto:external@example.com', ['RSVP' => 'TRUE', 'CN' => 'External User']); + $message->message = $newVCalendar; + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->senderName = 'Mr. Wizard'; + $message->recipient = 'mailto:external@example.com'; + + $this->service->expects(self::once()) + ->method('getLastOccurrence') + ->willReturn(1496912700); + $this->config->expects(self::once()) + ->method('getValueBool') + ->with('dav', 'caldav_external_attendees_disabled', false) + ->willReturn(true); + $this->service->expects(self::once()) + ->method('isSystemUser') + ->with('external@example.com') + ->willReturn(false); + $this->eventComparisonService->expects(self::never()) + ->method('findModified'); + $this->service->expects(self::never()) + ->method('getCurrentAttendee'); + $this->mailer->expects(self::once()) + ->method('validateMailAddress') + ->willReturn(true); + $this->mailer->expects(self::never()) + ->method('send'); + + $this->plugin->schedule($message); + $this->assertEquals('5.0', $message->getScheduleStatus()); + } + + public function testExternalAttendeesDisabledForSystemUser(): void { + $message = new Message(); + $message->method = 'REQUEST'; + $newVCalendar = new VCalendar(); + $newVevent = new VEvent($newVCalendar, 'one', array_merge([ + 'UID' => 'uid-1234', + 'SEQUENCE' => 1, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ], [])); + $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $newVevent->add('ATTENDEE', 'mailto:frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $message->message = $newVCalendar; + $message->sender = 'mailto:gandalf@wiz.ard'; + $message->senderName = 'Mr. Wizard'; + $message->recipient = 'mailto:frodo@hobb.it'; + + $oldVCalendar = new VCalendar(); + $oldVEvent = new VEvent($oldVCalendar, 'one', [ + 'UID' => 'uid-1234', + 'SEQUENCE' => 0, + 'SUMMARY' => 'Fellowship meeting', + 'DTSTART' => new \DateTime('2016-01-01 00:00:00') + ]); + $oldVEvent->add('ORGANIZER', 'mailto:gandalf@wiz.ard'); + $oldVEvent->add('ATTENDEE', 'mailto:frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']); + $oldVCalendar->add($oldVEvent); + + $data = ['invitee_name' => 'Mr. Wizard', + 'meeting_title' => 'Fellowship meeting', + 'attendee_name' => 'frodo@hobb.it' + ]; + $attendees = $newVevent->select('ATTENDEE'); + $atnd = ''; + foreach ($attendees as $attendee) { + if (strcasecmp($attendee->getValue(), $message->recipient) === 0) { + $atnd = $attendee; + } + } + $this->plugin->setVCalendar($oldVCalendar); + $this->service->expects(self::once()) + ->method('getLastOccurrence') + ->willReturn(1496912700); + $this->config->expects(self::exactly(2)) + ->method('getValueBool') + ->willReturnCallback(function ($app, $key, $default) { + if ($app === 'dav' && $key === 'caldav_external_attendees_disabled') { + return true; + } + if ($app === 'core' && $key === 'mail_providers_enabled') { + return false; + } + return $default; + }); + $this->service->expects(self::once()) + ->method('isSystemUser') + ->with('frodo@hobb.it') + ->willReturn(true); + $this->eventComparisonService->expects(self::once()) + ->method('findModified') + ->willReturn(['new' => [$newVevent], 'old' => [$oldVEvent]]); + $this->service->expects(self::once()) + ->method('getCurrentAttendee') + ->with($message) + ->willReturn($atnd); + $this->service->expects(self::once()) + ->method('isRoomOrResource') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('isCircle') + ->with($atnd) + ->willReturn(false); + $this->service->expects(self::once()) + ->method('buildBodyData') + ->with($newVevent, $oldVEvent) + ->willReturn($data); + $this->user->expects(self::any()) + ->method('getUID') + ->willReturn('user1'); + $this->user->expects(self::any()) + ->method('getDisplayName') + ->willReturn('Mr. Wizard'); + $this->userSession->expects(self::any()) + ->method('getUser') + ->willReturn($this->user); + $this->service->expects(self::once()) + ->method('getFrom'); + $this->service->expects(self::once()) + ->method('addSubjectAndHeading') + ->with($this->emailTemplate, 'request', 'Mr. Wizard', 'Fellowship meeting', true); + $this->service->expects(self::once()) + ->method('addBulletList') + ->with($this->emailTemplate, $newVevent, $data); + $this->service->expects(self::once()) + ->method('getAttendeeRsvpOrReqForParticipant') + ->willReturn(true); + $this->config->expects(self::once()) + ->method('getValueString') + ->with('dav', 'invitation_link_recipients', 'yes') + ->willReturn('yes'); + $this->service->expects(self::once()) + ->method('createInvitationToken') + ->with($message, $newVevent, 1496912700) + ->willReturn('token'); + $this->service->expects(self::once()) + ->method('addResponseButtons') + ->with($this->emailTemplate, 'token'); + $this->service->expects(self::once()) + ->method('addMoreOptionsButton') + ->with($this->emailTemplate, 'token'); + $this->mailer->expects(self::once()) + ->method('validateMailAddress') + ->willReturn(true); + $this->mailer->expects(self::once()) + ->method('send') + ->willReturn([]); + $this->plugin->schedule($message); + $this->assertEquals('1.1', $message->getScheduleStatus()); + } } diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php index abf8cfe3177f6..0241c867e2de6 100644 --- a/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php +++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipServiceTest.php @@ -16,6 +16,7 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; use OCP\IDBConnection; +use OCP\IUserManager; use OCP\L10N\IFactory as L10NFactory; use OCP\Security\ISecureRandom; use PHPUnit\Framework\MockObject\MockObject; @@ -45,6 +46,9 @@ class IMipServiceTest extends TestCase { /** @var ITimeFactory|MockObject */ private $timeFactory; + /** @var IUserManager|MockObject */ + private $userManager; + /** @var IMipService */ private $service; @@ -67,6 +71,7 @@ protected function setUp(): void { $this->l10nFactory = $this->createMock(L10NFactory::class); $this->l10n = $this->createMock(LazyL10N::class); $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->userManager = $this->createMock(IUserManager::class); $this->l10nFactory->expects(self::once()) ->method('findGenericLanguage') ->willReturn('en'); @@ -80,7 +85,8 @@ protected function setUp(): void { $this->db, $this->random, $this->l10nFactory, - $this->timeFactory + $this->timeFactory, + $this->userManager, ); // construct calendar with a 1 hour event and same start/end time zones @@ -169,6 +175,31 @@ public function testGetFrom(): void { $this->assertEquals($expected, $actual); } + public function testIsSystemUserWhenUserExists(): void { + $email = 'user@example.com'; + $user = $this->createMock(\OCP\IUser::class); + + $this->userManager->expects(self::once()) + ->method('getByEmail') + ->with($email) + ->willReturn([$user]); + + $result = $this->service->isSystemUser($email); + $this->assertTrue($result); + } + + public function testIsSystemUserWhenUserDoesNotExist(): void { + $email = 'external@example.com'; + + $this->userManager->expects(self::once()) + ->method('getByEmail') + ->with($email) + ->willReturn([]); + + $result = $this->service->isSystemUser($email); + $this->assertFalse($result); + } + public function testBuildBodyDataCreated(): void { // construct l10n return(s)