aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRichard Steinmetz <richard@steinmetz.cloud>2025-05-21 13:20:32 +0200
committerAndy Scherzinger <info@andy-scherzinger.de>2025-05-25 23:04:30 +0200
commitd45b4c97f0557b59530b3e1b3f086f4368c5aef1 (patch)
tree222ace4548af07b8671336515859b2490fbe367b
parentda30e9d1051d9be992b2799cd3dc0a50de67e025 (diff)
downloadnextcloud-server-backport/53029/stable30.tar.gz
nextcloud-server-backport/53029/stable30.zip
fix(caldav): don't send invitations to circlesbackport/53029/stable30
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipPlugin.php7
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipService.php15
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php110
3 files changed, 129 insertions, 3 deletions
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
index 2e36bda95f1..2f3f101e0ba 100644
--- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
+++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
@@ -170,9 +170,10 @@ class IMipPlugin extends SabreIMipPlugin {
$iTipMessage->scheduleStatus = '5.0; EMail delivery failed';
return;
}
- // Don't send emails to things
- if ($this->imipService->isRoomOrResource($attendee)) {
- $this->logger->debug('No invitation sent as recipient is room or resource', [
+ // Don't send emails to rooms, resources and circles
+ if ($this->imipService->isRoomOrResource($attendee)
+ || $this->imipService->isCircle($attendee)) {
+ $this->logger->debug('No invitation sent as recipient is room, resource or circle', [
'attendee' => $recipient,
]);
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipService.php b/apps/dav/lib/CalDAV/Schedule/IMipService.php
index 137fb3585a3..eb83ebb777f 100644
--- a/apps/dav/lib/CalDAV/Schedule/IMipService.php
+++ b/apps/dav/lib/CalDAV/Schedule/IMipService.php
@@ -1165,6 +1165,21 @@ class IMipService {
return false;
}
+ public function isCircle(Property $attendee): bool {
+ $cuType = $attendee->offsetGet('CUTYPE');
+ if (!$cuType instanceof Parameter) {
+ return false;
+ }
+
+ $uri = $attendee->getValue();
+ if (!$uri) {
+ return false;
+ }
+
+ $cuTypeValue = $cuType->getValue();
+ return $cuTypeValue === 'GROUP' && str_starts_with($uri, 'mailto:circle+');
+ }
+
public function minimizeInterval(\DateInterval $dateInterval): array {
// evaluate if time interval is in the past
if ($dateInterval->invert == 1) {
diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
index 3acbf670d17..4fd62b445e3 100644
--- a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
+++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
@@ -220,6 +220,10 @@ class IMipPluginTest extends TestCase {
->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);
@@ -323,6 +327,88 @@ class IMipPluginTest extends TestCase {
->with($room)
->willReturn(true);
$this->service->expects(self::never())
+ ->method('isCircle');
+ $this->service->expects(self::never())
+ ->method('buildBodyData');
+ $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::never())
+ ->method('getFrom');
+ $this->service->expects(self::never())
+ ->method('addSubjectAndHeading');
+ $this->service->expects(self::never())
+ ->method('addBulletList');
+ $this->service->expects(self::never())
+ ->method('getAttendeeRsvpOrReqForParticipant');
+ $this->config->expects(self::never())
+ ->method('getValueString');
+ $this->service->expects(self::never())
+ ->method('createInvitationToken');
+ $this->service->expects(self::never())
+ ->method('addResponseButtons');
+ $this->service->expects(self::never())
+ ->method('addMoreOptionsButton');
+ $this->mailer->expects(self::never())
+ ->method('send');
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.0', $message->getScheduleStatus());
+ }
+
+ public function testAttendeeIsCircle(): 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 without (!) Boromir',
+ 'DTSTART' => new \DateTime('2016-01-01 00:00:00')
+ ], []));
+ $newVevent->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $newVevent->add('ATTENDEE', 'mailto:' . 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth', ['RSVP' => 'TRUE', 'CN' => 'The Fellowship', 'CUTYPE' => 'GROUP']);
+ $newVevent->add('ATTENDEE', 'mailto:' . 'boromir@tra.it.or', ['RSVP' => 'TRUE', 'MEMBER' => 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth']);
+ $message->message = $newVCalendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth';
+ $attendees = $newVevent->select('ATTENDEE');
+ $circle = '';
+ foreach ($attendees as $attendee) {
+ if (strcasecmp($attendee->getValue(), $message->recipient) === 0) {
+ $circle = $attendee;
+ }
+ }
+ $this->assertNotEmpty($circle, 'Failed to find attendee belonging to the circle');
+ $this->service->expects(self::once())
+ ->method('getLastOccurrence')
+ ->willReturn(1496912700);
+ $this->mailer->expects(self::once())
+ ->method('validateMailAddress')
+ ->with('circle+82utEV1Fle8wvxndZLK5TVAPtxj8IIe@middle.earth')
+ ->willReturn(true);
+ $this->eventComparisonService->expects(self::once())
+ ->method('findModified')
+ ->willReturn(['new' => [$newVevent], 'old' => null]);
+ $this->service->expects(self::once())
+ ->method('getCurrentAttendee')
+ ->with($message)
+ ->willReturn($circle);
+ $this->service->expects(self::once())
+ ->method('isRoomOrResource')
+ ->with($circle)
+ ->willReturn(false);
+ $this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($circle)
+ ->willReturn(true);
+ $this->service->expects(self::never())
->method('buildBodyData');
$this->user->expects(self::any())
->method('getUID')
@@ -424,6 +510,10 @@ class IMipPluginTest extends TestCase {
->with($atnd)
->willReturn(false);
$this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
->method('buildBodyData')
->with($newVevent, null)
->willReturn($data);
@@ -555,6 +645,10 @@ class IMipPluginTest extends TestCase {
->with($atnd)
->willReturn(false);
$this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
->method('buildBodyData')
->with($newVevent, null)
->willReturn($data);
@@ -661,6 +755,10 @@ class IMipPluginTest extends TestCase {
->with($attendee)
->willReturn(false);
$this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($attendee)
+ ->willReturn(false);
+ $this->service->expects(self::once())
->method('buildBodyData')
->with($event, null)
->willReturn($data);
@@ -768,6 +866,10 @@ class IMipPluginTest extends TestCase {
->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);
@@ -863,6 +965,10 @@ class IMipPluginTest extends TestCase {
->with($atnd)
->willReturn(false);
$this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
->method('buildBodyData')
->with($newVevent, null)
->willReturn($data);
@@ -956,6 +1062,10 @@ class IMipPluginTest extends TestCase {
->with($atnd)
->willReturn(false);
$this->service->expects(self::once())
+ ->method('isCircle')
+ ->with($atnd)
+ ->willReturn(false);
+ $this->service->expects(self::once())
->method('buildBodyData')
->with($newVevent, null)
->willReturn($data);