Skip to content
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

Fix duplicate notification emails #34872

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions apps/dav/lib/BackgroundJob/BuildReminderIndexBackgroundJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @author Christoph Wurst <[email protected]>
* @author Georg Ehrke <[email protected]>
* @author Roeland Jago Douma <[email protected]>
* @author Richard Steinmetz <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
Expand All @@ -27,6 +28,7 @@
*/
namespace OCA\DAV\BackgroundJob;

use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Reminder\ReminderService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
Expand Down Expand Up @@ -55,20 +57,25 @@ class BuildReminderIndexBackgroundJob extends QueuedJob {
/** @var ITimeFactory */
private $timeFactory;

/** @var CalDavBackend */
private $calDavBackend;

/**
* BuildReminderIndexBackgroundJob constructor.
*/
public function __construct(IDBConnection $db,
ReminderService $reminderService,
LoggerInterface $logger,
IJobList $jobList,
ITimeFactory $timeFactory) {
ITimeFactory $timeFactory,
CalDavBackend $calDavBackend) {
parent::__construct($timeFactory);
$this->db = $db;
$this->reminderService = $reminderService;
$this->logger = $logger;
$this->jobList = $jobList;
$this->timeFactory = $timeFactory;
$this->calDavBackend = $calDavBackend;
}

public function run($argument) {
Expand Down Expand Up @@ -105,6 +112,8 @@ private function buildIndex(int $offset, int $stopAt):int {
->andWhere($query->expr()->gt('id', $query->createNamedParameter($offset)))
->orderBy('id', 'ASC');

$calendars = [];

$stmt = $query->execute();
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$offset = $row['id'];
Expand All @@ -114,7 +123,22 @@ private function buildIndex(int $offset, int $stopAt):int {
$row['component'] = $row['componenttype'];

try {
$this->reminderService->onCalendarObjectCreate($row);
// Fetch each calendar at most once
$calendarId = $row['calendarid'];
if (!isset($calendars[$calendarId])) {
$calendars[$calendarId] = $this->calDavBackend->getCalendarById($calendarId);
}

$calendar = $calendars[$calendarId];
if ($calendar === null) {
$this->logger->warning('Failed to fetch calendar of calendar object', [
'id' => $row['id'],
'uid' => $row['uid'],
'calendarid' => $calendarId
]);
} else {
$this->reminderService->onCalendarObjectCreate($row, $calendar);
}
} catch (\Exception $ex) {
$this->logger->error($ex->getMessage(), ['exception' => $ex]);
}
Expand Down
64 changes: 58 additions & 6 deletions apps/dav/lib/CalDAV/Reminder/ReminderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* @author Joas Schilling <[email protected]>
* @author Roeland Jago Douma <[email protected]>
* @author Thomas Citharel <[email protected]>
* @author Richard Steinmetz <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
Expand Down Expand Up @@ -187,10 +188,11 @@ public function processReminders() :void {
}

/**
* @param array $objectData
* @param array $objectData Calendar object data
* @param array $collectionData Calendar collection data
* @throws VObject\InvalidDataException
*/
public function onCalendarObjectCreate(array $objectData):void {
public function onCalendarObjectCreate(array $objectData, array $collectionData):void {
// We only support VEvents for now
if (strcasecmp($objectData['component'], 'vevent') !== 0) {
return;
Expand All @@ -215,6 +217,12 @@ public function onCalendarObjectCreate(array $objectData):void {
return;
}

$userEmail = null;
$user = $this->getUserFromPrincipalURI($collectionData['principaluri']);
if ($user) {
$userEmail = $user->getEMailAddress();
}

$uid = (string) $vevents[0]->UID;
$recurrenceExceptions = $this->getRecurrenceExceptionFromListOfVEvents($vevents);
$masterItem = $this->getMasterItemFromListOfVEvents($vevents);
Expand All @@ -237,6 +245,13 @@ public function onCalendarObjectCreate(array $objectData):void {
continue;
}

// Skip email reminders if the principal is not the organizer to prevent
// multiple email notifications (one per event copy).
if ((string) $valarm->ACTION === self::REMINDER_TYPE_EMAIL
&& !$this->userIsOrganizer($userEmail, $recurrenceException)) {
continue;
}

$alarms = $this->getRemindersForVAlarm($valarm, $objectData,
$eventHash, $alarmHash, true, true);
$this->writeRemindersToDatabase($alarms);
Expand Down Expand Up @@ -285,13 +300,22 @@ public function onCalendarObjectCreate(array $objectData):void {
continue;
}

if (!\in_array((string) $valarm->ACTION, self::REMINDER_TYPES, true)) {
$action = (string) $valarm->ACTION;
if (!\in_array($action, self::REMINDER_TYPES, true)) {
// Action allows x-name, we don't insert reminders
// into the database if they are not standard
$processedAlarms[] = $alarmHash;
continue;
}

// Skip email reminders if the principal is not the organizer to prevent
// multiple email notifications (one per event copy).
if ($action === self::REMINDER_TYPE_EMAIL
&& !$this->userIsOrganizer($userEmail, $event)) {
$processedAlarms[] = $alarmHash;
continue;
}

try {
$triggerTime = $valarm->getEffectiveTriggerTime();
} catch (InvalidDataException $e) {
Expand Down Expand Up @@ -323,16 +347,17 @@ public function onCalendarObjectCreate(array $objectData):void {
}

/**
* @param array $objectData
* @param array $objectData Calendar object data
* @param array $collectionData Calendar collection data
* @throws VObject\InvalidDataException
*/
public function onCalendarObjectEdit(array $objectData):void {
public function onCalendarObjectEdit(array $objectData, array $collectionData):void {
// TODO - this can be vastly improved
// - get cached reminders
// - ...

$this->onCalendarObjectDelete($objectData);
$this->onCalendarObjectCreate($objectData);
$this->onCalendarObjectCreate($objectData, $collectionData);
}

/**
Expand Down Expand Up @@ -808,4 +833,31 @@ private function getEffectiveRecurrenceIdOfVEvent(VEvent $vevent):int {
private function isRecurring(VEvent $vevent):bool {
return isset($vevent->RRULE) || isset($vevent->RDATE);
}

/**
* Check whether the given email address is the organizer of an event.
* Always returns true if the event has no organizer or the email address is null.
*
* @param string|null $userEmail
* @param VEvent $vevent
* @return bool
*/
private function userIsOrganizer(?string $userEmail, VEvent $vevent): bool {
if ($userEmail === null) {
// Notification won't be sent anyway if user has no email address
return true;
}

// No organizer => this is my private event (with no attendees)
if (!isset($vevent->ORGANIZER)) {
return true;
}

$organizerEmail = $vevent->ORGANIZER->getValue();
if (str_starts_with($organizerEmail, 'mailto:')) {
$organizerEmail = substr($organizerEmail, 7);
}

return $userEmail === $organizerEmail;
}
}
15 changes: 11 additions & 4 deletions apps/dav/lib/Listener/CalendarObjectReminderUpdaterListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* @copyright 2021 Christoph Wurst <[email protected]>
*
* @author Christoph Wurst <[email protected]>
* @author Richard Steinmetz <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
Expand Down Expand Up @@ -106,7 +107,10 @@ public function handle(Event $event): void {
$event->getCalendarId(),
$object['uri']
);
$this->reminderService->onCalendarObjectCreate($fullObject);
$this->reminderService->onCalendarObjectCreate(
$fullObject,
$event->getCalendarData()
);
}

$this->logger->debug(
Expand All @@ -121,7 +125,8 @@ public function handle(Event $event): void {
} elseif ($event instanceof CalendarObjectCreatedEvent) {
try {
$this->reminderService->onCalendarObjectCreate(
$event->getObjectData()
$event->getObjectData(),
$event->getCalendarData(),
);

$this->logger->debug(
Expand All @@ -136,7 +141,8 @@ public function handle(Event $event): void {
} elseif ($event instanceof CalendarObjectUpdatedEvent) {
try {
$this->reminderService->onCalendarObjectEdit(
$event->getObjectData()
$event->getObjectData(),
$event->getCalendarData()
);

$this->logger->debug(
Expand Down Expand Up @@ -166,7 +172,8 @@ public function handle(Event $event): void {
} elseif ($event instanceof CalendarObjectRestoredEvent) {
try {
$this->reminderService->onCalendarObjectCreate(
$event->getObjectData()
$event->getObjectData(),
$event->getCalendarData()
);

$this->logger->debug(
Expand Down
Loading