diff --git a/apps/dav/lib/Listener/CalendarContactInteractionListener.php b/apps/dav/lib/Listener/CalendarContactInteractionListener.php index 62bddd500e..1a04d44f6c 100644 --- a/apps/dav/lib/Listener/CalendarContactInteractionListener.php +++ b/apps/dav/lib/Listener/CalendarContactInteractionListener.php @@ -35,7 +35,13 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\EventDispatcher\IEventListener; use OCP\IUser; use OCP\IUserSession; +use OCP\Mail\IMailer; use Psr\Log\LoggerInterface; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\Parameter; +use Sabre\VObject\Property; +use Sabre\VObject\Reader; +use Throwable; use function strlen; use function strpos; use function substr; @@ -47,39 +53,59 @@ class CalendarContactInteractionListener implements IEventListener { private $dispatcher; /** @var IUserSession */ - private $userManager; + private $userSession; /** @var Principal */ private $principalConnector; + /** @var IMailer */ + private $mailer; + /** @var LoggerInterface */ private $logger; public function __construct(IEventDispatcher $dispatcher, - IUserSession $userManager, + IUserSession $userSession, Principal $principalConnector, + IMailer $mailer, LoggerInterface $logger) { $this->dispatcher = $dispatcher; - $this->userManager = $userManager; + $this->userSession = $userSession; $this->principalConnector = $principalConnector; + $this->mailer = $mailer; $this->logger = $logger; } public function handle(Event $event): void { - if (($user = $this->userManager->getUser()) === null) { + if (($user = $this->userSession->getUser()) === null) { // Without user context we can't do anything return; } if ($event instanceof CalendarObjectCreatedEvent || $event instanceof CalendarObjectUpdatedEvent) { // users: href => principal:principals/users/admin - // TODO: parse (email) attendees from the VCARD foreach ($event->getShares() as $share) { if (!isset($share['href'])) { continue; } $this->emitFromUri($share['href'], $user); } + + // emit interaction for email attendees as well + if (isset($event->getObjectData()['calendardata'])) { + try { + $calendar = Reader::read($event->getObjectData()['calendardata']); + if ($calendar->VEVENT) { + foreach ($calendar->VEVENT as $calendarEvent) { + $this->emitFromObject($calendarEvent, $user); + } + } + } catch (Throwable $e) { + $this->logger->warning('Could not read calendar data for interaction events: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } } if ($event instanceof CalendarShareUpdatedEvent && !empty($event->getAdded())) { @@ -114,4 +140,38 @@ class CalendarContactInteractionListener implements IEventListener { (new ContactInteractedWithEvent($user))->setUid($uid) ); } + + private function emitFromObject(VEvent $vevent, IUser $user): void { + if (!$vevent->ATTENDEE) { + // Nothing left to do + return; + } + + foreach ($vevent->ATTENDEE as $attendee) { + if (!($attendee instanceof Property)) { + continue; + } + + $cuType = $attendee->offsetGet('CUTYPE'); + if ($cuType instanceof Parameter && $cuType->getValue() !== 'INDIVIDUAL') { + // Don't care about those + continue; + } + + $mailTo = $attendee->getValue(); + if (strpos($mailTo, 'mailto:') !== 0) { + // Doesn't look like an email + continue; + } + $email = substr($mailTo, strlen('mailto:')); + if (!$this->mailer->validateMailAddress($email)) { + // This really isn't a valid email + continue; + } + + $this->dispatcher->dispatchTyped( + (new ContactInteractedWithEvent($user))->setEmail($email) + ); + } + } } diff --git a/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php b/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php new file mode 100644 index 0000000000..5a90f5440a --- /dev/null +++ b/apps/dav/tests/unit/Listener/CalendarContactInteractionListenerTest.php @@ -0,0 +1,202 @@ + + * + * @author 2021 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\DAV\Tests\Unit\Listener; + +use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\Events\CalendarObjectCreatedEvent; +use OCA\DAV\Events\CalendarShareUpdatedEvent; +use OCA\DAV\Listener\CalendarContactInteractionListener; +use OCP\Contacts\Events\ContactInteractedWithEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUser; +use OCP\IUserSession; +use OCP\Mail\IMailer; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; +use Test\TestCase; + +class CalendarContactInteractionListenerTest extends TestCase { + + /** @var IEventDispatcher|MockObject */ + private $eventDispatcher; + + /** @var IUserSession|MockObject */ + private $userSession; + + /** @var Principal|MockObject */ + private $principalConnector; + + /** @var LoggerInterface|MockObject */ + private $logger; + + /** @var IMailer|MockObject */ + private $mailer; + + /** @var CalendarContactInteractionListener */ + private $listener; + + protected function setUp(): void { + parent::setUp(); + + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); + $this->userSession = $this->createMock(IUserSession::class); + $this->principalConnector = $this->createMock(Principal::class); + $this->mailer = $this->createMock(IMailer::class); + $this->logger = $this->createMock(LoggerInterface::class); + + $this->listener = new CalendarContactInteractionListener( + $this->eventDispatcher, + $this->userSession, + $this->principalConnector, + $this->mailer, + $this->logger + ); + } + + public function testParseUnrelated(): void { + $event = new Event(); + $this->eventDispatcher->expects(self::never())->method('dispatchTyped'); + + $this->listener->handle($event); + } + + public function testHandleWithoutAnythingInteresting(): void { + $event = new CalendarShareUpdatedEvent(123, [], [], [], []); + $user = $this->createMock(IUser::class); + $this->userSession->expects(self::once())->method('getUser')->willReturn($user); + $this->eventDispatcher->expects(self::never())->method('dispatchTyped'); + + $this->listener->handle($event); + } + + public function testParseInvalidData(): void { + $event = new CalendarObjectCreatedEvent(123, [], [], ['calendardata' => 'BEGIN:FOO']); + $user = $this->createMock(IUser::class); + $this->userSession->expects(self::once())->method('getUser')->willReturn($user); + $this->eventDispatcher->expects(self::never())->method('dispatchTyped'); + $this->logger->expects(self::once())->method('warning'); + + $this->listener->handle($event); + } + + public function testParseCalendarEventWithInvalidEmail(): void { + $event = new CalendarObjectCreatedEvent(123, [], [], ['calendardata' => <<createMock(IUser::class); + $this->userSession->expects(self::once())->method('getUser')->willReturn($user); + $this->eventDispatcher->expects(self::never())->method('dispatchTyped'); + $this->logger->expects(self::never())->method('warning'); + + $this->listener->handle($event); + } + + public function testParseCalendarEvent(): void { + $event = new CalendarObjectCreatedEvent(123, [], [], ['calendardata' => <<createMock(IUser::class); + $this->userSession->expects(self::once())->method('getUser')->willReturn($user); + $this->mailer->expects(self::once())->method('validateMailAddress')->willReturn(true); + $this->eventDispatcher->expects(self::once()) + ->method('dispatchTyped') + ->with(self::equalTo((new ContactInteractedWithEvent($user))->setEmail('user@domain.tld'))); + $this->logger->expects(self::never())->method('warning'); + + $this->listener->handle($event); + } +}