]> source.dussan.org Git - nextcloud-server.git/commitdiff
Fix duplicate event email notifications 35019/head
authorRichard Steinmetz <richard@steinmetz.cloud>
Tue, 1 Nov 2022 14:49:54 +0000 (15:49 +0100)
committerRichard Steinmetz <richard@steinmetz.cloud>
Tue, 8 Nov 2022 08:42:56 +0000 (09:42 +0100)
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
apps/dav/lib/CalDAV/Reminder/INotificationProvider.php
apps/dav/lib/CalDAV/Reminder/NotificationProvider/AbstractProvider.php
apps/dav/lib/CalDAV/Reminder/NotificationProvider/EmailProvider.php
apps/dav/lib/CalDAV/Reminder/NotificationProvider/PushProvider.php
apps/dav/lib/CalDAV/Reminder/ReminderService.php
apps/dav/lib/CalDAV/Schedule/Plugin.php
apps/dav/lib/Connector/Sabre/Principal.php
apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/EmailProviderTest.php
apps/dav/tests/unit/CalDAV/Reminder/NotificationProvider/PushProviderTest.php
apps/dav/tests/unit/CalDAV/Reminder/ReminderServiceTest.php
apps/dav/tests/unit/Connector/Sabre/PrincipalTest.php

index a6b439c0b4fe6c944371287eb2840416b1aa072b..505960ed66208d1301cb21ab08e97806863a9911 100644 (file)
@@ -8,6 +8,7 @@ declare(strict_types=1);
  * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  * @author Georg Ehrke <oc.list@georgehrke.com>
  * @author Roeland Jago Douma <roeland@famdouma.nl>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -42,10 +43,12 @@ interface INotificationProvider {
         *
         * @param VEvent $vevent
         * @param string $calendarDisplayName
+        * @param string[] $principalEmailAddresses All email addresses associated to the principal owning the calendar object
         * @param IUser[] $users
         * @return void
         */
        public function send(VEvent $vevent,
                                                 string $calendarDisplayName,
+                                                array  $principalEmailAddresses,
                                                 array $users = []): void;
 }
index 044e5fac4e2378a665511b9f031e89f36cc3013c..68cdc245a481b6c4e42c07731a4a9502b463b16f 100644 (file)
@@ -10,6 +10,7 @@ declare(strict_types=1);
  * @author Georg Ehrke <oc.list@georgehrke.com>
  * @author Joas Schilling <coding@schilljs.com>
  * @author Roeland Jago Douma <roeland@famdouma.nl>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -89,11 +90,13 @@ abstract class AbstractProvider implements INotificationProvider {
         *
         * @param VEvent $vevent
         * @param string $calendarDisplayName
+        * @param string[] $principalEmailAddresses
         * @param IUser[] $users
         * @return void
         */
        abstract public function send(VEvent $vevent,
                                                   string $calendarDisplayName,
+                                                  array $principalEmailAddresses,
                                                   array $users = []): void;
 
        /**
index 85590d506cf15c29f8f71fd44ccbaf2facca09a0..fdd16640d19f4c468c8cbde35afb275eb2b6818c 100644 (file)
@@ -78,16 +78,28 @@ class EmailProvider extends AbstractProvider {
         *
         * @param VEvent $vevent
         * @param string $calendarDisplayName
+        * @param string[] $principalEmailAddresses
         * @param array $users
         * @throws \Exception
         */
        public function send(VEvent $vevent,
                                                 string $calendarDisplayName,
+                            array $principalEmailAddresses,
                                                 array $users = []):void {
                $fallbackLanguage = $this->getFallbackLanguage();
 
+               $organizerEmailAddress = null;
+               if (isset($vevent->ORGANIZER)) {
+                       $organizerEmailAddress = $this->getEMailAddressOfAttendee($vevent->ORGANIZER);
+               }
+
                $emailAddressesOfSharees = $this->getEMailAddressesOfAllUsersWithWriteAccessToCalendar($users);
-               $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
+               $emailAddressesOfAttendees = [];
+               if (count($principalEmailAddresses) === 0
+                       || ($organizerEmailAddress && in_array($organizerEmailAddress, $principalEmailAddresses, true))
+               ) {
+                       $emailAddressesOfAttendees = $this->getAllEMailAddressesFromEvent($vevent);
+               }
 
                // Quote from php.net:
                // If the input arrays have the same string keys, then the later value for that key will overwrite the previous one.
index fb123960df825843b91a234a4b718464c47e5c21..a1336ae96d31d82512e9cfb58db6bcdbffdaa99e 100644 (file)
@@ -10,6 +10,7 @@ declare(strict_types=1);
  * @author Georg Ehrke <oc.list@georgehrke.com>
  * @author Roeland Jago Douma <roeland@famdouma.nl>
  * @author Thomas Citharel <nextcloud@tcit.fr>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -81,11 +82,13 @@ class PushProvider extends AbstractProvider {
         *
         * @param VEvent $vevent
         * @param string $calendarDisplayName
+        * @param string[] $principalEmailAddresses
         * @param IUser[] $users
         * @throws \Exception
         */
        public function send(VEvent $vevent,
-                                                string $calendarDisplayName = null,
+                                                string $calendarDisplayName,
+                                                array $principalEmailAddresses,
                                                 array $users = []):void {
                if ($this->config->getAppValue('dav', 'sendEventRemindersPush', 'no') !== 'yes') {
                        return;
index d6901cc4fb03f814564b26344076e8ddce7085bd..2911e502049c6dda5172833498a7d0abd17c34b3 100644 (file)
@@ -11,6 +11,7 @@ declare(strict_types=1);
  * @author Joas Schilling <coding@schilljs.com>
  * @author Roeland Jago Douma <roeland@famdouma.nl>
  * @author Thomas Citharel <nextcloud@tcit.fr>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -32,6 +33,7 @@ namespace OCA\DAV\CalDAV\Reminder;
 
 use DateTimeImmutable;
 use OCA\DAV\CalDAV\CalDavBackend;
+use OCA\DAV\Connector\Sabre\Principal;
 use OCP\AppFramework\Utility\ITimeFactory;
 use OCP\IConfig;
 use OCP\IGroup;
@@ -70,6 +72,9 @@ class ReminderService {
        /** @var IConfig */
        private $config;
 
+       /** @var Principal */
+       private $principalConnector;
+
        public const REMINDER_TYPE_EMAIL = 'EMAIL';
        public const REMINDER_TYPE_DISPLAY = 'DISPLAY';
        public const REMINDER_TYPE_AUDIO = 'AUDIO';
@@ -102,7 +107,8 @@ class ReminderService {
                                                                IGroupManager $groupManager,
                                                                CalDavBackend $caldavBackend,
                                                                ITimeFactory $timeFactory,
-                                                               IConfig $config) {
+                                                               IConfig $config,
+                                                               Principal $principalConnector) {
                $this->backend = $backend;
                $this->notificationProviderManager = $notificationProviderManager;
                $this->userManager = $userManager;
@@ -110,6 +116,7 @@ class ReminderService {
                $this->caldavBackend = $caldavBackend;
                $this->timeFactory = $timeFactory;
                $this->config = $config;
+               $this->principalConnector = $principalConnector;
        }
 
        /**
@@ -163,8 +170,14 @@ class ReminderService {
                                $users[] = $user;
                        }
 
+                       $userPrincipalEmailAddresses = [];
+                       $userPrincipal = $this->principalConnector->getPrincipalByPath($reminder['principaluri']);
+                       if ($userPrincipal) {
+                               $userPrincipalEmailAddresses = $this->principalConnector->getEmailAddressesOfPrincipal($userPrincipal);
+                       }
+
                        $notificationProvider = $this->notificationProviderManager->getProvider($reminder['type']);
-                       $notificationProvider->send($vevent, $reminder['displayname'], $users);
+                       $notificationProvider->send($vevent, $reminder['displayname'], $userPrincipalEmailAddresses, $users);
 
                        $this->deleteOrProcessNext($reminder, $vevent);
                }
index 96bacce4454b6b6e8962bef8ef63125bf2253b43..21334724b6620c98e7e5f7279ee9e93b64bf88f7 100644 (file)
@@ -9,6 +9,7 @@
  * @author Joas Schilling <coding@schilljs.com>
  * @author Roeland Jago Douma <roeland@famdouma.nl>
  * @author Thomas Citharel <nextcloud@tcit.fr>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -45,6 +46,7 @@ use Sabre\VObject\Component;
 use Sabre\VObject\Component\VCalendar;
 use Sabre\VObject\Component\VEvent;
 use Sabre\VObject\DateTimeParser;
+use Sabre\VObject\Document;
 use Sabre\VObject\FreeBusyGenerator;
 use Sabre\VObject\ITip;
 use Sabre\VObject\Parameter;
@@ -163,6 +165,14 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
         * @inheritDoc
         */
        public function scheduleLocalDelivery(ITip\Message $iTipMessage):void {
+               /** @var Component|null $vevent */
+               $vevent = $iTipMessage->message->VEVENT ?? null;
+
+               // Strip VALARMs from incoming VEVENT
+               if ($vevent && isset($vevent->VALARM)) {
+                       $vevent->remove('VALARM');
+               }
+
                parent::scheduleLocalDelivery($iTipMessage);
 
                // We only care when the message was successfully delivered locally
@@ -199,18 +209,10 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
                        return;
                }
 
-               if (!isset($iTipMessage->message)) {
+               if (!$vevent) {
                        return;
                }
 
-               $vcalendar = $iTipMessage->message;
-               if (!isset($vcalendar->VEVENT)) {
-                       return;
-               }
-
-               /** @var Component $vevent */
-               $vevent = $vcalendar->VEVENT;
-
                // We don't support autoresponses for recurrencing events for now
                if (isset($vevent->RRULE) || isset($vevent->RDATE)) {
                        return;
index 94e3978e67d1a6b5f694145fd18151fa0b2c6a3e..c1ad2535936252339a97abde2eb132f7d06033c4 100644 (file)
@@ -591,4 +591,44 @@ class Principal implements BackendInterface {
 
                return [];
        }
+
+       /**
+        * Get all email addresses associated to a principal.
+        *
+        * @param array $principal Data from getPrincipal*()
+        * @return string[] All email addresses without the mailto: prefix
+        */
+       public function getEmailAddressesOfPrincipal(array $principal): array {
+               $emailAddresses = [];
+
+               if (($primaryAddress = $principal['{http://sabredav.org/ns}email-address'])) {
+                       $emailAddresses[] = $primaryAddress;
+               }
+
+               if (isset($principal['{DAV:}alternate-URI-set'])) {
+                       foreach ($principal['{DAV:}alternate-URI-set'] as $address) {
+                               if (str_starts_with($address, 'mailto:')) {
+                                       $emailAddresses[] = substr($address, 7);
+                               }
+                       }
+               }
+
+               if (isset($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'])) {
+                       foreach ($principal['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'] as $address) {
+                               if (str_starts_with($address, 'mailto:')) {
+                                       $emailAddresses[] = substr($address, 7);
+                               }
+                       }
+               }
+
+               if (isset($principal['{http://calendarserver.org/ns/}email-address-set'])) {
+                       foreach ($principal['{http://calendarserver.org/ns/}email-address-set'] as $address) {
+                               if (str_starts_with($address, 'mailto:')) {
+                                       $emailAddresses[] = substr($address, 7);
+                               }
+                       }
+               }
+
+               return array_values(array_unique($emailAddresses));
+       }
 }
index b5cbf94016ccc5d33ed22407be323cf2d387429a..67d93d94103a1203c86e40ef7ff323177d8de263 100644 (file)
@@ -81,6 +81,7 @@ class EmailProviderTest extends AbstractNotificationProviderTest {
 
        public function testSendWithoutAttendees():void {
                [$user1, $user2, $user3, , $user5] = $users = $this->getUsers();
+               $principalEmailAddresses = [$user1->getEmailAddress()];
 
                $enL10N = $this->createMock(IL10N::class);
                $enL10N->method('t')
@@ -186,11 +187,12 @@ class EmailProviderTest extends AbstractNotificationProviderTest {
                $this->setupURLGeneratorMock(2);
 
                $vcalendar = $this->getNoAttendeeVCalendar();
-               $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $users);
+               $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $principalEmailAddresses, $users);
        }
 
-       public function testSendWithAttendees(): void {
+       public function testSendWithAttendeesWhenOwnerIsOrganizer(): void {
                [$user1, $user2, $user3, , $user5] = $users = $this->getUsers();
+               $principalEmailAddresses = [$user1->getEmailAddress()];
 
                $enL10N = $this->createMock(IL10N::class);
                $enL10N->method('t')
@@ -282,7 +284,81 @@ class EmailProviderTest extends AbstractNotificationProviderTest {
                $this->setupURLGeneratorMock(2);
 
                $vcalendar = $this->getAttendeeVCalendar();
-               $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $users);
+               $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $principalEmailAddresses, $users);
+       }
+
+       public function testSendWithAttendeesWhenOwnerIsAttendee(): void {
+               [$user1, $user2, $user3] = $this->getUsers();
+               $users = [$user2, $user3];
+               $principalEmailAddresses = [$user2->getEmailAddress()];
+
+               $deL10N = $this->createMock(IL10N::class);
+               $deL10N->method('t')
+                       ->willReturnArgument(0);
+               $deL10N->method('l')
+                       ->willReturnArgument(0);
+
+               $this->l10nFactory
+                       ->method('getUserLanguage')
+                       ->willReturnMap([
+                               [$user2, 'de'],
+                               [$user3, 'de'],
+                       ]);
+
+               $this->l10nFactory
+                       ->method('findGenericLanguage')
+                       ->willReturn('en');
+
+               $this->l10nFactory
+                       ->method('languageExists')
+                       ->willReturnMap([
+                               ['dav', 'de', true],
+                       ]);
+
+               $this->l10nFactory
+                       ->method('get')
+                       ->willReturnMap([
+                               ['dav', 'de', null, $deL10N],
+                       ]);
+
+               $template1 = $this->getTemplateMock();
+               $message12 = $this->getMessageMock('uid2@example.com', $template1);
+               $message13 = $this->getMessageMock('uid3@example.com', $template1);
+
+               $this->mailer->expects(self::once())
+                       ->method('createEMailTemplate')
+                       ->with('dav.calendarReminder')
+                       ->willReturnOnConsecutiveCalls(
+                               $template1,
+                       );
+               $this->mailer->expects($this->atLeastOnce())
+                       ->method('validateMailAddress')
+                       ->willReturnMap([
+                               ['foo1@example.org', true],
+                               ['foo3@example.org', true],
+                               ['foo4@example.org', true],
+                               ['uid1@example.com', true],
+                               ['uid2@example.com', true],
+                               ['uid3@example.com', true],
+                               ['invalid', false],
+                       ]);
+               $this->mailer->expects($this->exactly(2))
+                       ->method('createMessage')
+                       ->with()
+                       ->willReturnOnConsecutiveCalls(
+                               $message12,
+                               $message13,
+                       );
+               $this->mailer->expects($this->exactly(2))
+                       ->method('send')
+                       ->withConsecutive(
+                               [$message12],
+                               [$message13],
+                       )->willReturn([]);
+               $this->setupURLGeneratorMock(1);
+
+               $vcalendar = $this->getAttendeeVCalendar();
+               $this->provider->send($vcalendar->VEVENT, $this->calendarDisplayName, $principalEmailAddresses, $users);
        }
 
        /**
@@ -392,6 +468,14 @@ class EmailProviderTest extends AbstractNotificationProviderTest {
                        'DESCRIPTION' => 'DESCRIPTION 456',
                ]);
 
+               $vcalendar->VEVENT->add(
+                       'ORGANIZER',
+                       'mailto:uid1@example.com',
+                       [
+                               'LANG' => 'en'
+                       ]
+               );
+
                $vcalendar->VEVENT->add(
                        'ATTENDEE',
                        'mailto:foo1@example.org',
index 9e9759f5eb884e5b8c482a71eae66a23a14bc42e..62571e855ca8b4df2785f5134d33520ed6a0311a 100644 (file)
@@ -10,6 +10,7 @@ declare(strict_types=1);
  * @author Georg Ehrke <oc.list@georgehrke.com>
  * @author Roeland Jago Douma <roeland@famdouma.nl>
  * @author Thomas Citharel <nextcloud@tcit.fr>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -107,7 +108,7 @@ class PushProviderTest extends AbstractNotificationProviderTest {
 
                $users = [$user1, $user2, $user3];
 
-               $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, $users);
+               $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, [], $users);
        }
 
        public function testSend(): void {
@@ -160,7 +161,7 @@ class PushProviderTest extends AbstractNotificationProviderTest {
                        ->method('notify')
                        ->with($notification3);
 
-               $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, $users);
+               $this->provider->send($this->vcalendar->VEVENT, $this->calendarDisplayName, [], $users);
        }
 
        /**
index 39fbf1c79ffc264db470dd452901ce4bec0843b4..7bf246ac0ebb73b664c4e8f79582d4fbab2e921c 100644 (file)
@@ -9,6 +9,7 @@ declare(strict_types=1);
  * @author Georg Ehrke <oc.list@georgehrke.com>
  * @author Roeland Jago Douma <roeland@famdouma.nl>
  * @author Thomas Citharel <nextcloud@tcit.fr>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license GNU AGPL version 3 or any later version
  *
@@ -33,6 +34,7 @@ use OCA\DAV\CalDAV\Reminder\Backend;
 use OCA\DAV\CalDAV\Reminder\INotificationProvider;
 use OCA\DAV\CalDAV\Reminder\NotificationProviderManager;
 use OCA\DAV\CalDAV\Reminder\ReminderService;
+use OCA\DAV\Connector\Sabre\Principal;
 use OCP\AppFramework\Utility\ITimeFactory;
 use OCP\IConfig;
 use OCP\IGroupManager;
@@ -70,6 +72,9 @@ class ReminderServiceTest extends TestCase {
        /** @var ReminderService */
        private $reminderService;
 
+       /** @var Principal|\PHPUnit\Framework\MockObject\MockObject  */
+       private $principalConnector;
+
        public const CALENDAR_DATA = <<<EOD
 BEGIN:VCALENDAR
 PRODID:-//Nextcloud calendar v1.6.4
@@ -199,6 +204,7 @@ EOD;
                $this->caldavBackend = $this->createMock(CalDavBackend::class);
                $this->timeFactory = $this->createMock(ITimeFactory::class);
                $this->config = $this->createMock(IConfig::class);
+               $this->principalConnector = $this->createMock(Principal::class);
 
                $this->caldavBackend->method('getShares')->willReturn([]);
 
@@ -208,7 +214,8 @@ EOD;
                        $this->groupManager,
                        $this->caldavBackend,
                        $this->timeFactory,
-                       $this->config);
+                       $this->config,
+                       $this->principalConnector);
        }
 
        public function testOnCalendarObjectDelete():void {
index 86413e4a366a5a6c9008e74ff43d7e08d66ef1e4..3ffbe7c405821f156011e0d5867a817a90add4f4 100644 (file)
@@ -11,6 +11,7 @@
  * @author Morris Jobke <hey@morrisjobke.de>
  * @author Roeland Jago Douma <roeland@famdouma.nl>
  * @author Thomas Müller <thomas.mueller@tmit.eu>
+ * @author Richard Steinmetz <richard@steinmetz.cloud>
  *
  * @license AGPL-3.0
  *
@@ -925,4 +926,34 @@ class PrincipalTest extends TestCase {
                        ['mailto:user3@foo.bar', 'user3@foo.bar', 'principals/users/user3'],
                ];
        }
+
+       public function testGetEmailAddressesOfPrincipal(): void {
+               $principal = [
+                       '{http://sabredav.org/ns}email-address' => 'bar@company.org',
+                       '{DAV:}alternate-URI-set' => [
+                               '/some/url',
+                               'mailto:foo@bar.com',
+                               'mailto:duplicate@example.com',
+                       ],
+                       '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => [
+                               'mailto:bernard@example.com',
+                               'mailto:bernard.desruisseaux@example.com',
+                       ],
+                       '{http://calendarserver.org/ns/}email-address-set' => [
+                               'mailto:duplicate@example.com',
+                               'mailto:user@some.org',
+                       ],
+               ];
+
+               $expected = [
+                       'bar@company.org',
+                       'foo@bar.com',
+                       'duplicate@example.com',
+                       'bernard@example.com',
+                       'bernard.desruisseaux@example.com',
+                       'user@some.org',
+               ];
+               $actual = $this->connector->getEmailAddressesOfPrincipal($principal);
+               $this->assertEquals($expected, $actual);
+       }
 }