Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117778143
D5511.1775244555.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
48 KB
Referenced Files
None
Subscribers
None
D5511.1775244555.diff
View Options
diff --git a/src/app/Policy/Mailfilter/Modules/ItipModule.php b/src/app/Policy/Mailfilter/Modules/ItipModule.php
--- a/src/app/Policy/Mailfilter/Modules/ItipModule.php
+++ b/src/app/Policy/Mailfilter/Modules/ItipModule.php
@@ -271,4 +271,79 @@
return $object;
}
+
+ /**
+ * Check iTip message origin. Spoofing detection
+ *
+ * @param Component $request iTip content
+ * @param ?Component $existing Existing event object
+ */
+ protected function checkOrigin(Component $request, ?Component $existing): bool
+ {
+ $method = strtoupper(str_replace('Handler', '', class_basename(static::class)));
+
+ // First we check if the envelope sender matches the ORGANIZER/ATTENDEE in the iTip
+ switch ($method) {
+ case 'REQUEST':
+ case 'CANCEL':
+ $property = 'ORGANIZER';
+ $email = str_ireplace('mailto:', '', (string) $request->ORGANIZER);
+ break;
+ case 'REPLY':
+ $property = 'ATTENDEE';
+ // Per RFC 5546 there can be only one ATTENDEE in REPLY
+ if (count($request->ATTENDEE) == 1) {
+ $email = str_ireplace('mailto:', '', (string) $request->ATTENDEE);
+ }
+ break;
+ default:
+ throw new \Exception("Unexpected iTip method: {$method}");
+ }
+
+ if (empty($email)) {
+ $this->parser->debug("Missing {$property} in iTip {$method}. Ignored.");
+ return false;
+ }
+
+ $email = strtolower($email);
+ $sender = strtolower($this->parser->getSender());
+
+ $result = $email === $sender;
+
+ // TODO: Check if it is a local user using one of his aliases?
+
+ // Check if this ORGANIZER/ATTENDEE matches the one in the existing object
+ if ($result && $existing) {
+ $rid = (string) $request->{'RECURRENCE-ID'};
+ $master = $this->extractMainComponent($existing);
+ $occurence = $rid ? $this->extractRecurrenceInstanceComponent($existing, $rid) : null;
+
+ switch ($method) {
+ case 'REQUEST':
+ case 'CANCEL':
+ $source = $occurence && !empty($occurence->ORGANIZER) ? $occurence : $master;
+ $organizer = str_ireplace('mailto:', '', (string) $source->ORGANIZER);
+
+ $result = strtolower($organizer) === $email;
+ break;
+ case 'REPLY':
+ $source = $occurence && !empty($occurence->ATTENDEE) ? $occurence : $master;
+ foreach ($source->ATTENDEE ?? [] as $attendee) {
+ $attendee = str_ireplace('mailto:', '', (string) $attendee);
+ if (strtolower($attendee) === $email) {
+ break 2;
+ }
+ }
+
+ $result = false;
+ break;
+ }
+ }
+
+ if (!$result) {
+ \Log::warning("Itip {$method} origin mismatch for {$property}.");
+ }
+
+ return $result;
+ }
}
diff --git a/src/app/Policy/Mailfilter/Modules/ItipModule/CancelHandler.php b/src/app/Policy/Mailfilter/Modules/ItipModule/CancelHandler.php
--- a/src/app/Policy/Mailfilter/Modules/ItipModule/CancelHandler.php
+++ b/src/app/Policy/Mailfilter/Modules/ItipModule/CancelHandler.php
@@ -33,27 +33,19 @@
$existing = $this->findObject($user, $this->uid, $this->type);
if (!$existing) {
- // FIXME: Should we stop message delivery?
return null;
}
- // FIXME: what to do if CANCEL attendees do not match with the recipient email(s)?
- // FIXME: what to do if CANCEL does not come from the organizer's email?
- // stop processing here and pass the message to the inbox?
-
$existingMaster = $this->extractMainComponent($existing);
$cancelMaster = $this->extractMainComponent($this->itip);
if (!$existingMaster || !$cancelMaster) {
- // FIXME: Should we stop message delivery?
$parser->debug("Failed to get the main component. Ignored.");
return null;
}
- // SEQUENCE does not match, deliver the message, let the MUAs to deal with this
- // FIXME: Is this even a valid aproach regarding recurrence?
- if ((string) $existingMaster->SEQUENCE != (string) $cancelMaster->SEQUENCE) {
- $parser->debug("Sequence mismatch. Ignored.");
+ // Spoofing protection
+ if (!$this->checkOrigin($cancelMaster, $existing)) {
return null;
}
@@ -65,6 +57,12 @@
// First find and remove the exception object, if exists
if ($existingInstance = $this->extractRecurrenceInstanceComponent($existing, $recurrence_id)) {
+ // SEQUENCE does not match, deliver the message, let the MUAs to deal with this
+ if ((string) $existingInstance->SEQUENCE > (string) $cancelMaster->SEQUENCE) {
+ $parser->debug("Sequence mismatch. Ignored.");
+ return null;
+ }
+
$existing->remove($existingInstance);
}
@@ -79,6 +77,12 @@
$dav = $this->getDAVClient($user);
$dav->update($this->toOpaqueObject($existing, $this->davLocation));
} else {
+ // SEQUENCE does not match, deliver the message, let the MUAs to deal with this
+ if ((string) $existingMaster->SEQUENCE > (string) $cancelMaster->SEQUENCE) {
+ $parser->debug("Sequence mismatch. Ignored.");
+ return null;
+ }
+
$existingInstance = $existingMaster;
$parser->debug("Deleting object at {$this->davLocation}");
diff --git a/src/app/Policy/Mailfilter/Modules/ItipModule/ReplyHandler.php b/src/app/Policy/Mailfilter/Modules/ItipModule/ReplyHandler.php
--- a/src/app/Policy/Mailfilter/Modules/ItipModule/ReplyHandler.php
+++ b/src/app/Policy/Mailfilter/Modules/ItipModule/ReplyHandler.php
@@ -40,14 +40,9 @@
$existing = $this->findObject($user, $this->uid, $this->type);
if (!$existing) {
- // FIXME: Should we stop message delivery?
return null;
}
- // FIXME: what to do if the REPLY comes from an address not mentioned in the event?
- // FIXME: Should we check if the recipient is an organizer?
- // stop processing here and pass the message to the inbox, or drop it?
-
$existingMaster = $this->extractMainComponent($existing);
$replyMaster = $this->extractMainComponent($this->itip);
@@ -56,21 +51,20 @@
return null;
}
- // SEQUENCE does not match, deliver the message, let the MUAs to deal with this
- // FIXME: Is this even a valid aproach regarding recurrence?
- if ((string) $existingMaster->SEQUENCE != (string) $replyMaster->SEQUENCE) {
- $parser->debug("Sequence mismatch. Ignored.");
+ // Spoofing protection
+ if (!$this->checkOrigin($replyMaster, $existing)) {
return null;
}
+ // TODO: Check if the recipient is an organizer,
+ // stop processing here and pass the message to the inbox, or drop it?
+
// Per RFC 5546 there can be only one ATTENDEE in REPLY
if (count($replyMaster->ATTENDEE) != 1) {
$parser->debug("Too many attendees in REPLY. Ignored.");
return null;
}
- // TODO: Delegation
-
$sender = $replyMaster->ATTENDEE;
$partstat = $sender['PARTSTAT'];
$email = strtolower(preg_replace('!^mailto:!i', '', (string) $sender));
@@ -82,7 +76,6 @@
}
// Invalid/useless reply, let the MUA deal with it
- // FIXME: Or should we stop delivery?
if (empty($partstat) || $partstat == 'NEEDS-ACTION') {
$parser->debug("Unexpected PARTSTAT in REPLY. Ignored.");
return null;
@@ -92,13 +85,25 @@
if ($recurrence_id) {
$existingInstance = $this->extractRecurrenceInstanceComponent($existing, $recurrence_id);
+
// No such recurrence exception, let the MUA deal with it
- // FIXME: Or should we stop delivery?
if (!$existingInstance) {
$parser->debug("No existing recurrence instance. Ignored.");
return null;
}
+
+ // SEQUENCE does not match, deliver the message, let the MUAs deal with this
+ if ((string) $existingInstance->SEQUENCE > (string) $replyMaster->SEQUENCE) {
+ $parser->debug("Sequence mismatch. Ignored.");
+ return null;
+ }
} else {
+ // SEQUENCE does not match, deliver the message, let the MUAs deal with this
+ if ((string) $existingMaster->SEQUENCE > (string) $replyMaster->SEQUENCE) {
+ $parser->debug("Sequence mismatch. Ignored.");
+ return null;
+ }
+
$existingInstance = $existingMaster;
}
@@ -119,6 +124,8 @@
if ($updated) {
$parser->debug("Updating object at {$this->davLocation}");
+ // TODO: We should probably bump LAST-MODIFIED and/or DTSTAMP property
+
$dav = $this->getDAVClient($user);
$dav->update($this->toOpaqueObject($existing, $this->davLocation));
diff --git a/src/app/Policy/Mailfilter/Modules/ItipModule/RequestHandler.php b/src/app/Policy/Mailfilter/Modules/ItipModule/RequestHandler.php
--- a/src/app/Policy/Mailfilter/Modules/ItipModule/RequestHandler.php
+++ b/src/app/Policy/Mailfilter/Modules/ItipModule/RequestHandler.php
@@ -6,7 +6,6 @@
use App\Policy\Mailfilter\Modules\ItipModule;
use App\Policy\Mailfilter\Result;
use Sabre\VObject\Component;
-use Sabre\VObject\Document;
class RequestHandler extends ItipModule
{
@@ -37,10 +36,6 @@
// - For an existing "VEVENT" calendar component, delegate the role of "Attendee" to another user.
// - For an existing "VEVENT" calendar component, change the role of "Organizer" to another user.
- // FIXME: This whole method could be async, if we wanted to be more responsive on mail delivery,
- // but CANCEL and REPLY could not, because we're potentially stopping mail delivery there,
- // so I suppose we'll do all of them synchronously for now. Still some parts of it can be async.
-
$this->parser = $parser;
// Check whether the object already exists in the recipient's calendar
@@ -52,12 +47,17 @@
return null;
}
- // FIXME: what to do if REQUEST attendees do not match with the recipient email(s)?
- // stop processing here and pass the message to the inbox?
-
$requestMaster = $this->extractMainComponent($this->itip);
$recurrence_id = (string) $requestMaster->{'RECURRENCE-ID'};
+ // Spoofing protection
+ if (!$this->checkOrigin($requestMaster, $existing)) {
+ return null;
+ }
+
+ // TODO: if REQUEST attendees do not match with the recipient email(s)
+ // stop processing here and pass the message to the Inbox.
+
// The event does not exist yet in the recipient's calendar, create it
if (!$existing) {
if (!empty($recurrence_id)) {
@@ -76,47 +76,37 @@
return null;
}
- // TODO: Cover all cases mentioned above
-
- // FIXME: For updates that don't create a new exception should we replace the iTip with a notification?
- // Or maybe we should not even bother with auto-updating and leave it to MUAs?
-
if ($recurrence_id) {
// Recurrence instance
$existingInstance = $this->extractRecurrenceInstanceComponent($existing, $recurrence_id);
- // A new recurrence instance, just add it to the existing event
- if (!$existingInstance) {
- // TODO: Bump LAST-MODIFIED on the master object
- $existing->add($requestMaster);
- } else {
- // SEQUENCE does not match, deliver the message, let the MUAs deal with this
- // TODO: A higher SEQUENCE indicates a re-scheduled object, we should update the existing event.
- if ((int) ((string) $existingInstance->SEQUENCE) != (int) ((string) $requestMaster->SEQUENCE)) {
- return null;
- }
-
- $this->mergeComponents($existingInstance, $requestMaster);
- // TODO: Bump LAST-MODIFIED on the master object
+ // Old SEQUENCE in the request, let the MUA deal with this
+ if ($existingInstance && (int) ((string) $existingInstance->SEQUENCE) > (int) ((string) $requestMaster->SEQUENCE)) {
+ return null;
+ }
+
+ // Organizer is the event owner, always replace the whole exception with the new one
+ if ($existingInstance) {
+ $existing->remove($existingInstance);
}
+
+ $existing->add($requestMaster);
} else {
// Master event
$existingMaster = $this->extractMainComponent($existing);
- if (!$existingMaster) {
- return null;
- }
-
- // SEQUENCE does not match, deliver the message, let the MUAs deal with this
- // TODO: A higher SEQUENCE indicates a re-scheduled object, we should update the existing event.
- if ((int) ((string) $existingMaster->SEQUENCE) != (int) ((string) $requestMaster->SEQUENCE)) {
+ // Old SEQUENCE in the request, let the MUA deal with this
+ if ($existingMaster && (int) ((string) $existingMaster->SEQUENCE) > (int) ((string) $requestMaster->SEQUENCE)) {
return null;
}
- // FIXME: Merge all components included in the request?
- $this->mergeComponents($existingMaster, $requestMaster);
+ // Organizer is the event owner, always replace the whole event with the new one
+ $existing = $this->itip;
}
+ // TODO: If the recipient status is not NEEDS-ACTION (the request does not require attention)
+ // we should replace the message with a notification
+
$parser->debug("Updating object at {$this->davLocation}");
$dav = $this->getDAVClient($user);
@@ -124,42 +114,4 @@
return null;
}
-
- /**
- * Merge VOBJECT component properties into another component
- */
- protected function mergeComponents(Component $to, Component $from): void
- {
- // TODO: Every property? What other properties? EXDATE/RDATE? ORGANIZER? ATTACH?
- // TODO: Removal of RRULE from the master event
- $props = ['SEQUENCE', 'RRULE'];
- foreach ($props as $prop) {
- if (isset($from->{$prop})) {
- $to->{$prop} = $from->{$prop};
- }
- }
-
- // Replace the list of ATTENDEEs
- $to->remove('ATTENDEE');
- foreach ($from->ATTENDEE ?? [] as $attendee) {
- $class = $attendee::class;
- $to->add(new $class($to->parent, 'ATTENDEE', $attendee->getValue(), $attendee->parameters()));
- }
-
- // If RRULE contains UNTIL remove exceptions from the timestamp forward
- if (isset($to->RRULE) && ($parts = $to->RRULE->getParts()) && !empty($parts['UNTIL'])) {
- // TODO: Timezone? Also test that with UNTIL using a date format
- $until = new \DateTime($parts['UNTIL']);
- /** @var Document $doc */
- $doc = $to->parent;
-
- foreach ($doc->getComponents() as $component) {
- if ($component->name == $to->name && !empty($component->{'RECURRENCE-ID'})) {
- if ($component->{'RECURRENCE-ID'}->getDateTime() >= $until) {
- $doc->remove($component);
- }
- }
- }
- }
- }
}
diff --git a/src/tests/Feature/Policy/Mailfilter/Modules/ItipModule/CancelHandlerTest.php b/src/tests/Feature/Policy/Mailfilter/Modules/ItipModule/CancelHandlerTest.php
--- a/src/tests/Feature/Policy/Mailfilter/Modules/ItipModule/CancelHandlerTest.php
+++ b/src/tests/Feature/Policy/Mailfilter/Modules/ItipModule/CancelHandlerTest.php
@@ -34,29 +34,24 @@
$this->davEmptyFolder($account, 'Calendar', 'event');
// Jack cancelled the meeting, but there's no event in John's calendar
- $parser = MailParserTest::getParserForFile('mailfilter/itip1_cancel.eml', 'john@kolab.org');
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_cancel.eml', 'john@kolab.org', 'jack@kolab.org');
$module = new ItipModule();
$result = $module->handle($parser);
$this->assertNull($result);
+ $this->assertCount(0, $this->davList($account, 'Calendar', 'event'));
Notification::assertNothingSent();
// Jack cancelled the meeting, and now the event exists in John's calendar
$this->davAppend($account, 'Calendar', ['mailfilter/event2.ics'], 'event');
- $list = $this->davList($account, 'Calendar', 'event');
- $this->assertCount(1, $list);
- $this->assertSame('5463F1DDF6DA264A3FC70E7924B729A5-D9F1889254B163F5', $list[0]->uid);
-
- $parser = MailParserTest::getParserForFile('mailfilter/itip1_cancel.eml', 'john@kolab.org');
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_cancel.eml', 'john@kolab.org', 'jack@kolab.org');
$module = new ItipModule();
$result = $module->handle($parser);
$this->assertSame(Result::STATUS_DISCARD, $result->getStatus());
-
- $list = $this->davList($account, 'Calendar', 'event');
- $this->assertCount(0, $list);
+ $this->assertCount(0, $this->davList($account, 'Calendar', 'event'));
Notification::assertCount(1);
Notification::assertSentTo(
@@ -72,6 +67,63 @@
&& empty($notification->params->recurrenceId);
}
);
+
+ Notification::fake();
+
+ // Jack cancelled the meeting, but the iTIP has outdated SEQUENCE
+ $replaces = [
+ '/SEQUENCE:0/' => 'SEQUENCE:1',
+ ];
+ $this->davEmptyFolder($account, 'Calendar', 'event');
+ $this->davAppend($account, 'Calendar', ['mailfilter/event2.ics'], 'event', $replaces);
+
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_cancel.eml', 'john@kolab.org', 'jack@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $this->davList($account, 'Calendar', 'event'));
+
+ Notification::assertNothingSent();
+ }
+
+ /**
+ * Test spoofing protection on CANCEL
+ *
+ * @group @dav
+ */
+ public function testItipCancelSpoofing(): void
+ {
+ Notification::fake();
+
+ $user = $this->getTestUser('john@kolab.org');
+ $uri = preg_replace('|^http|', 'dav', \config('services.dav.uri'));
+ $account = new Account(preg_replace('|://|', '://john%40kolab.org:simple123@', $uri));
+
+ $this->davEmptyFolder($account, 'Calendar', 'event');
+
+ // Ned impersonates Jack cancelling the meeting, but there's no event in John's calendar
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_cancel.eml', 'john@kolab.org', 'ned@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(0, $this->davList($account, 'Calendar', 'event'));
+
+ $this->davAppend($account, 'Calendar', ['mailfilter/event2.ics'], 'event');
+
+ // Ned impersonates Jack cancelling the meeting, but now the event exists in John's calendar
+ $replaces = [
+ 'mailto:jack@kolab.org' => 'mailto:ned@kolab.org',
+ ];
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_cancel.eml', 'john@kolab.org', 'ned@kolab.org', $replaces);
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $this->davList($account, 'Calendar', 'event'));
+
+ Notification::assertNothingSent();
}
/**
@@ -90,8 +142,8 @@
$this->davEmptyFolder($account, 'Calendar', 'event');
$this->davAppend($account, 'Calendar', ['mailfilter/event4.ics'], 'event');
- // Jack cancelled the meeting, and the event exists in John's calendar
- $parser = MailParserTest::getParserForFile('mailfilter/itip2_cancel.eml', 'john@kolab.org');
+ // Jack cancelled the meeting occurence, and the event exists in John's calendar
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_cancel.eml', 'john@kolab.org', 'jack@kolab.org');
$module = new ItipModule();
$result = $module->handle($parser);
@@ -119,5 +171,56 @@
&& $notification->params->recurrenceId == '20240717T123000';
}
);
+
+ Notification::fake();
+
+ // Jack cancelled the meeting occurence, but the iTip CANCEL contains outdated SEQUENCE
+ $replaces = [
+ '/SEQUENCE:0\nTRANSP/' => 'SEQUENCE:1\nTRANSP',
+ ];
+ $this->davEmptyFolder($account, 'Calendar', 'event');
+ $this->davAppend($account, 'Calendar', ['mailfilter/event4.ics'], 'event', $replaces);
+
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_cancel.eml', 'john@kolab.org', 'jack@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $list = $this->davList($account, 'Calendar', 'event');
+ $this->assertCount(1, $list[0]->exceptions);
+
+ Notification::assertNothingSent();
+ }
+
+ /**
+ * Test spoofing protection on CANCEL with recurrence
+ *
+ * @group @dav
+ */
+ public function testItipCancelRecurrenceSpoofing(): void
+ {
+ Notification::fake();
+
+ $user = $this->getTestUser('john@kolab.org');
+ $uri = preg_replace('|^http|', 'dav', \config('services.dav.uri'));
+ $account = new Account(preg_replace('|://|', '://john%40kolab.org:simple123@', $uri));
+
+ $this->davEmptyFolder($account, 'Calendar', 'event');
+ $this->davAppend($account, 'Calendar', ['mailfilter/event4.ics'], 'event');
+
+ // Ned impersonates Jack cancelling the meeting occurrence, the event exists in John's calendar
+ $replaces = [
+ 'mailto:jack@kolab.org' => 'mailto:ned@kolab.org',
+ ];
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_cancel.eml', 'john@kolab.org', 'ned@kolab.org', $replaces);
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertCount(1, $list[0]->exceptions);
+ $this->assertCount(0, $list[0]->exdate);
+
+ Notification::assertNothingSent();
}
}
diff --git a/src/tests/Feature/Policy/Mailfilter/Modules/ItipModule/ReplyHandlerTest.php b/src/tests/Feature/Policy/Mailfilter/Modules/ItipModule/ReplyHandlerTest.php
--- a/src/tests/Feature/Policy/Mailfilter/Modules/ItipModule/ReplyHandlerTest.php
+++ b/src/tests/Feature/Policy/Mailfilter/Modules/ItipModule/ReplyHandlerTest.php
@@ -34,7 +34,7 @@
$this->davEmptyFolder($account, 'Calendar', 'event');
// John's response to the invitation, but there's no event in Jack's (organizer's) calendar
- $parser = MailParserTest::getParserForFile('mailfilter/itip1_reply.eml', $user->email);
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_reply.eml', $user->email, 'john@kolab.org');
$module = new ItipModule();
$result = $module->handle($parser);
@@ -45,14 +45,12 @@
// John's response to the invitation, the Jack's event exists now
$this->davAppend($account, 'Calendar', ['mailfilter/event1.ics'], 'event');
- $parser = MailParserTest::getParserForFile('mailfilter/itip1_reply.eml', $user->email);
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_reply.eml', $user->email, 'john@kolab.org');
$module = new ItipModule();
$result = $module->handle($parser);
$this->assertSame(Result::STATUS_DISCARD, $result->getStatus());
-
- $list = $this->davList($account, 'Calendar', 'event');
- $this->assertCount(1, $list);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
$this->assertSame('5463F1DDF6DA264A3FC70E7924B729A5-D9F1889254B163F5', $list[0]->uid);
$this->assertCount(2, $attendees = $list[0]->attendees);
$this->assertSame('john@kolab.org', $attendees[0]['email']);
@@ -77,7 +75,71 @@
}
);
- // TODO: Test corner cases, spoofing case, etc.
+ Notification::fake();
+
+ // John's response to the invitation, but with outdated SEQUENCE
+ $replaces = [
+ '/SEQUENCE:0/' => 'SEQUENCE:1',
+ ];
+ $this->davEmptyFolder($account, 'Calendar', 'event');
+ $this->davAppend($account, 'Calendar', ['mailfilter/event1.ics'], 'event', $replaces);
+
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_reply.eml', $user->email, 'john@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertCount(2, $attendees = $list[0]->attendees);
+ $this->assertSame('john@kolab.org', $attendees[0]['email']);
+ $this->assertSame('NEEDS-ACTION', $attendees[0]['partstat']);
+
+ Notification::assertNothingSent();
+ }
+
+ /**
+ * Test spoofing protection for REPLY
+ *
+ * @group @dav
+ */
+ public function testItipReplySpoofing(): void
+ {
+ Notification::fake();
+
+ $user = $this->getTestUser('jack@kolab.org');
+ $uri = preg_replace('|^http|', 'dav', \config('services.dav.uri'));
+ $account = new Account(preg_replace('|://|', '://jack%40kolab.org:simple123@', $uri));
+
+ $this->davEmptyFolder($account, 'Calendar', 'event');
+
+ // Unknown impersonates John's response to the invitation, but there's no event in Jack's (organizer's) calendar
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_reply.eml', $user->email, 'unknown@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(0, $this->davList($account, 'Calendar', 'event'));
+
+ $this->davAppend($account, 'Calendar', ['mailfilter/event1.ics'], 'event');
+
+ // Unknown user impersonates John's response to the invitation, the Jack's event exists now
+ $replaces = [
+ 'mailto:john@kolab.org' => 'mailto:unknown@kolab.org',
+ ];
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_reply.eml', $user->email, 'unknown@kolab.org', $replaces);
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertSame('5463F1DDF6DA264A3FC70E7924B729A5-D9F1889254B163F5', $list[0]->uid);
+ $this->assertCount(2, $attendees = $list[0]->attendees);
+ $this->assertSame('john@kolab.org', $attendees[0]['email']);
+ $this->assertSame('NEEDS-ACTION', $attendees[0]['partstat']);
+ $this->assertSame('ned@kolab.org', $attendees[1]['email']);
+ $this->assertSame('NEEDS-ACTION', $attendees[1]['partstat']);
+
+ Notification::assertNothingSent();
}
/**
@@ -97,16 +159,12 @@
$this->davAppend($account, 'Calendar', ['mailfilter/event3.ics'], 'event');
// John's response to the invitation, but there's no exception in Jack's event
- $parser = MailParserTest::getParserForFile('mailfilter/itip2_reply.eml', 'jack@kolab.org');
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_reply.eml', 'jack@kolab.org', 'john@kolab.org');
$module = new ItipModule();
$result = $module->handle($parser);
$this->assertNull($result);
-
- Notification::assertNothingSent();
-
- $list = $this->davList($account, 'Calendar', 'event');
- $this->assertCount(1, $list);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
$this->assertSame('5463F1DDF6DA264A3FC70E7924B729A5-222222', $list[0]->uid);
$this->assertCount(2, $attendees = $list[0]->attendees);
$this->assertSame('john@kolab.org', $attendees[0]['email']);
@@ -116,18 +174,18 @@
$this->assertSame('jack@kolab.org', $list[0]->organizer['email']);
$this->assertCount(0, $list[0]->exceptions);
+ Notification::assertNothingSent();
+
$this->davEmptyFolder($account, 'Calendar', 'event');
$this->davAppend($account, 'Calendar', ['mailfilter/event4.ics'], 'event');
// John's response to the invitation, the Jack's event exists now
- $parser = MailParserTest::getParserForFile('mailfilter/itip2_reply.eml', 'jack@kolab.org');
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_reply.eml', 'jack@kolab.org', 'john@kolab.org');
$module = new ItipModule();
$result = $module->handle($parser);
$this->assertSame(Result::STATUS_DISCARD, $result->getStatus());
-
- $list = $this->davList($account, 'Calendar', 'event');
- $this->assertCount(1, $list);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
$this->assertSame('5463F1DDF6DA264A3FC70E7924B729A5-222222', $list[0]->uid);
$this->assertCount(2, $attendees = $list[0]->attendees);
$this->assertSame('john@kolab.org', $attendees[0]['email']);
@@ -158,6 +216,82 @@
}
);
- // TODO: Test corner cases, etc.
+ Notification::fake();
+
+ // John's response to the invitation, but with the outdated SEQUENCE
+ $replaces = [
+ '/SEQUENCE:0\nTRANSP/' => 'SEQUENCE:1\nTRANSP',
+ ];
+ $this->davEmptyFolder($account, 'Calendar', 'event');
+ $this->davAppend($account, 'Calendar', ['mailfilter/event4.ics'], 'event', $replaces);
+
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_reply.eml', 'jack@kolab.org', 'john@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertCount(1, $list[0]->exceptions);
+ $this->assertCount(2, $attendees = $list[0]->exceptions[0]->attendees);
+ $this->assertSame('john@kolab.org', $attendees[0]['email']);
+ $this->assertSame('NEEDS-ACTION', $attendees[0]['partstat']);
+ }
+
+ /**
+ * Test spoofing protection on REPLY with recurrence
+ *
+ * @group @dav
+ */
+ public function testItipReplyRecurrenceSpoofing(): void
+ {
+ Notification::fake();
+
+ $user = $this->getTestUser('jack@kolab.org');
+ $uri = preg_replace('|^http|', 'dav', \config('services.dav.uri'));
+ $account = new Account(preg_replace('|://|', '://jack%40kolab.org:simple123@', $uri));
+
+ $this->davEmptyFolder($account, 'Calendar', 'event');
+ $this->davAppend($account, 'Calendar', ['mailfilter/event3.ics'], 'event');
+
+ // Unknown user impersonates John's response to the invitation, but there's no exception in Jack's event
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_reply.eml', 'jack@kolab.org', 'unknown@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertCount(0, $list[0]->exceptions);
+
+ $this->davEmptyFolder($account, 'Calendar', 'event');
+ $this->davAppend($account, 'Calendar', ['mailfilter/event4.ics'], 'event');
+
+ // Unknown impersonates John's response to the invitation, the Jack's event exists now with the exception
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_reply.eml', 'jack@kolab.org', 'unknown@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertCount(1, $list[0]->exceptions);
+ $this->assertCount(2, $attendees = $list[0]->exceptions[0]->attendees);
+ $this->assertSame('john@kolab.org', $attendees[0]['email']);
+ $this->assertSame('NEEDS-ACTION', $attendees[0]['partstat']);
+
+ // Unknown impersonates John's response to the invitation, the Jack's event exists now with the exception
+ $replaces = [
+ 'mailto:john@kolab.org' => 'mailto:unknown@kolab.org',
+ ];
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_reply.eml', 'jack@kolab.org', 'unknown@kolab.org', $replaces);
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertCount(1, $list[0]->exceptions);
+ $this->assertCount(2, $attendees = $list[0]->exceptions[0]->attendees);
+ $this->assertSame('john@kolab.org', $attendees[0]['email']);
+ $this->assertSame('NEEDS-ACTION', $attendees[0]['partstat']);
+
+ Notification::assertNothingSent();
}
}
diff --git a/src/tests/Feature/Policy/Mailfilter/Modules/ItipModule/RequestHandlerTest.php b/src/tests/Feature/Policy/Mailfilter/Modules/ItipModule/RequestHandlerTest.php
--- a/src/tests/Feature/Policy/Mailfilter/Modules/ItipModule/RequestHandlerTest.php
+++ b/src/tests/Feature/Policy/Mailfilter/Modules/ItipModule/RequestHandlerTest.php
@@ -31,7 +31,7 @@
$this->davEmptyFolder($account, 'Calendar', 'event');
// Jack invites John (and Ned) to a new meeting
- $parser = MailParserTest::getParserForFile('mailfilter/itip1_request.eml', 'john@kolab.org');
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_request.eml', 'john@kolab.org', 'jack@kolab.org');
$module = new ItipModule();
$result = $module->handle($parser);
@@ -49,7 +49,7 @@
Notification::assertNothingSent();
- // Test REQUEST to an existing event, and other corner cases
+ // Test REQUEST to an existing event
$replaces = [
'CN=Ned;PARTSTAT=NEEDS-ACTION' => 'CN=Ned;PARTSTAT=ACCEPTED',
];
@@ -62,6 +62,7 @@
$list = $this->davList($account, 'Calendar', 'event');
$this->assertCount(1, $list);
$this->assertSame('5463F1DDF6DA264A3FC70E7924B729A5-D9F1889254B163F5', $list[0]->uid);
+ $this->assertSame('Test Meeting 1', $list[0]->summary);
$this->assertCount(2, $list[0]->attendees);
$this->assertSame('john@kolab.org', $list[0]->attendees[0]['email']);
$this->assertSame('NEEDS-ACTION', $list[0]->attendees[0]['partstat']);
@@ -71,7 +72,85 @@
Notification::assertNothingSent();
- // TODO: Test various supported message structures (ItipModule::getItip())
+ // Jack sends a REQUEST with old SEQUENCE (master event, not exception)
+ $this->davEmptyFolder($account, 'Calendar', 'event');
+ $this->davAppend($account, 'Calendar', ['mailfilter/event5.ics'], 'event');
+
+ $parser = MailParserTest::getParserForFile('mailfilter/itip3_request_rrule_update.eml', 'john@kolab.org', 'jack@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertCount(2, $list[0]->exceptions);
+
+ // Jack sends a REQUEST with updated SEQUENCE (master event, not exception)
+ $replace = [
+ 'SEQUENCE:0' => 'SEQUENCE:1',
+ ];
+ $parser = MailParserTest::getParserForFile('mailfilter/itip3_request_rrule_update.eml', 'john@kolab.org', 'jack@kolab.org', $replace);
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertCount(0, $list[0]->exceptions);
+
+ Notification::assertNothingSent();
+ }
+
+ /**
+ * Test spoofing protections on REQUEST
+ *
+ * @group dav
+ */
+ public function testItipRequestSpoofing(): void
+ {
+ Notification::fake();
+
+ $uri = preg_replace('|^http|', 'dav', \config('services.dav.uri'));
+ $account = new Account(preg_replace('|://|', '://john%40kolab.org:simple123@', $uri));
+
+ $this->davEmptyFolder($account, 'Calendar', 'event');
+
+ // A new meeting, Ned impersonates Jack
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_request.eml', 'john@kolab.org', 'ned@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(0, $this->davList($account, 'Calendar', 'event'));
+
+ Notification::assertNothingSent();
+
+ $this->davAppend($account, 'Calendar', ['mailfilter/event1.ics'], 'event');
+
+ // An existing meeting, Ned impersonates Jack
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_request.eml', 'john@kolab.org', 'ned@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertSame('Test Meeting', $list[0]->summary);
+ $this->assertSame('12:43:04', $list[0]->lastModified->getDateTime()->format('H:i:s'));
+
+ Notification::assertNothingSent();
+
+ // An existing meeting, Ned as himself, but the organizer is already Jack
+ $replaces = [
+ 'CN=Jack:mailto:jack@kolab.org' => 'CN=Ned:mailto:ned@kolab.org',
+ ];
+ $parser = MailParserTest::getParserForFile('mailfilter/itip1_request.eml', 'john@kolab.org', 'ned@kolab.org', $replaces);
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertSame('Test Meeting', $list[0]->summary);
+ $this->assertSame('12:43:04', $list[0]->lastModified->getDateTime()->format('H:i:s'));
+
+ Notification::assertNothingSent();
}
/**
@@ -87,18 +166,25 @@
$account = new Account(preg_replace('|://|', '://john%40kolab.org:simple123@', $uri));
$this->davEmptyFolder($account, 'Calendar', 'event');
+
+ // A recurrence exception in the iTip, but the event does not exist yet
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_request.eml', 'john@kolab.org', 'jack@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(0, $this->davList($account, 'Calendar', 'event'));
+
$this->davAppend($account, 'Calendar', ['mailfilter/event3.ics'], 'event');
// Jack invites John (and Ned) to a new meeting occurrence, the event
- // is already in John's calendar, but has no recurrence exceptions yet
- $parser = MailParserTest::getParserForFile('mailfilter/itip2_request.eml', 'john@kolab.org');
+ // is already in John's calendar and has no recurrence exceptions yet
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_request.eml', 'john@kolab.org', 'jack@kolab.org');
$module = new ItipModule();
$result = $module->handle($parser);
$this->assertNull($result);
-
- $list = $this->davList($account, 'Calendar', 'event');
- $this->assertCount(1, $list);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
$this->assertSame('5463F1DDF6DA264A3FC70E7924B729A5-222222', $list[0]->uid);
$this->assertCount(2, $attendees = $list[0]->attendees);
$this->assertSame('john@kolab.org', $attendees[0]['email']);
@@ -113,18 +199,32 @@
$this->assertSame('ned@kolab.org', $attendees[1]['email']);
$this->assertSame('NEEDS-ACTION', $attendees[1]['partstat']);
- // Test updating an existing occurence
+ // Test updating an existing occurence using old SEQUENCE - expect no changes to the event
$replaces = [
'CN=Ned;PARTSTAT=NEEDS-ACTION' => 'CN=Ned;PARTSTAT=ACCEPTED',
+ 'SEQUENCE:1' => 'SEQUENCE:0',
];
$parser = MailParserTest::getParserForFile('mailfilter/itip2_request.eml', 'john@kolab.org', 'jack@kolab.org', $replaces);
$module = new ItipModule();
$result = $module->handle($parser);
$this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertCount(1, $list[0]->exceptions);
+ $this->assertCount(2, $attendees = $list[0]->exceptions[0]->attendees);
+ $this->assertSame('ned@kolab.org', $attendees[1]['email']);
+ $this->assertSame('NEEDS-ACTION', $attendees[1]['partstat']);
- $list = $this->davList($account, 'Calendar', 'event');
- $this->assertCount(1, $list);
+ // Test updating an existing occurence using same SEQUENCE as in the existing exception
+ $replaces = [
+ 'CN=Ned;PARTSTAT=NEEDS-ACTION' => 'CN=Ned;PARTSTAT=ACCEPTED',
+ ];
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_request.eml', 'john@kolab.org', 'jack@kolab.org', $replaces);
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
$this->assertSame('5463F1DDF6DA264A3FC70E7924B729A5-222222', $list[0]->uid);
$this->assertCount(2, $attendees = $list[0]->attendees);
$this->assertSame('john@kolab.org', $attendees[0]['email']);
@@ -139,28 +239,66 @@
$this->assertSame('ned@kolab.org', $attendees[1]['email']);
$this->assertSame('ACCEPTED', $attendees[1]['partstat']);
- // Jack sends REQUEST with RRULE containing UNTIL parameter, which is a case when
- // an organizer deletes "this and future" event occurence
- $this->davAppend($account, 'Calendar', ['mailfilter/event5.ics'], 'event');
+ Notification::assertNothingSent();
+ }
+
+ /**
+ * Test spoofing protection on REQUEST with recurrence
+ *
+ * @group dav
+ */
+ public function testItipRequestRecurrenceSpoofing(): void
+ {
+ Notification::fake();
+
+ $uri = preg_replace('|^http|', 'dav', \config('services.dav.uri'));
+ $account = new Account(preg_replace('|://|', '://john%40kolab.org:simple123@', $uri));
+
+ $this->davEmptyFolder($account, 'Calendar', 'event');
- $parser = MailParserTest::getParserForFile('mailfilter/itip3_request_rrule_update.eml', 'john@kolab.org');
+ // A recurrence exception in the iTip, but the event does not exist yet, Ned impersonates Jack
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_request.eml', 'john@kolab.org', 'ned@kolab.org');
$module = new ItipModule();
$result = $module->handle($parser);
$this->assertNull($result);
+ $this->assertCount(0, $this->davList($account, 'Calendar', 'event'));
- $list = $this->davList($account, 'Calendar', 'event');
- $list = array_filter($list, static fn ($event) => $event->uid == '5464F1DDF6DA264A3FC70E7924B729A5-333333');
- $event = $list[array_key_first($list)];
+ $this->davAppend($account, 'Calendar', ['mailfilter/event3.ics'], 'event');
- $this->assertCount(1, $list);
- $this->assertCount(2, $attendees = $event->attendees);
- $this->assertSame('john@kolab.org', $attendees[0]['email']);
- $this->assertSame('ACCEPTED', $attendees[0]['partstat']);
- $this->assertSame('ned@kolab.org', $attendees[1]['email']);
- $this->assertSame('NEEDS-ACTION', $attendees[1]['partstat']);
- $this->assertCount(1, $event->exceptions);
- $this->assertSame('20240717T123000', $event->exceptions[0]->recurrenceId);
+ // The event is already in John's calendar, but has no recurrence exceptions yet, Ned impersonates Jack
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_request.eml', 'john@kolab.org', 'ned@kolab.org');
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertCount(0, $list[0]->exceptions);
+
+ // The event is already in John's calendar, but has no recurrence exceptions yet, Ned impersonates Jack
+ $replaces = [
+ 'CN=Jack:mailto:jack@kolab.org' => 'CN=Ned:mailto:ned@kolab.org',
+ ];
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_request.eml', 'john@kolab.org', 'ned@kolab.org', $replaces);
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertCount(0, $list[0]->exceptions);
+
+ $this->davEmptyFolder($account, 'Calendar', 'event');
+ $this->davAppend($account, 'Calendar', ['mailfilter/event4.ics'], 'event');
+
+ // Test updating an existing occurence, Ned impersonates Jack
+ $parser = MailParserTest::getParserForFile('mailfilter/itip2_request.eml', 'john@kolab.org', 'ned@kolab.org', $replaces);
+ $module = new ItipModule();
+ $result = $module->handle($parser);
+
+ $this->assertNull($result);
+ $this->assertCount(1, $list = $this->davList($account, 'Calendar', 'event'));
+ $this->assertCount(1, $list[0]->exceptions);
+ $this->assertSame('Test Meeting', $list[0]->exceptions[0]->summary);
Notification::assertNothingSent();
}
diff --git a/src/tests/data/mailfilter/event4.ics b/src/tests/data/mailfilter/event4.ics
--- a/src/tests/data/mailfilter/event4.ics
+++ b/src/tests/data/mailfilter/event4.ics
@@ -34,12 +34,12 @@
LOCATION:Berlin
RRULE:FREQ=WEEKLY;INTERVAL=1
SEQUENCE:0
-TRANSP:OPAQUE
ATTENDEE;CN=John;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT;CUTYPE=INDI
VIDUAL:mailto:john@kolab.org
ATTENDEE;CN=Ned;PARTSTAT=TENTATIVE;ROLE=REQ-PARTICIPANT;CUTYPE=IND
IVIDUAL:mailto:ned@kolab.org
ORGANIZER;CN=Jack:mailto:jack@kolab.org
+TRANSP:OPAQUE
END:VEVENT
BEGIN:VEVENT
UID:5463F1DDF6DA264A3FC70E7924B729A5-222222
diff --git a/src/tests/data/mailfilter/event5.ics b/src/tests/data/mailfilter/event5.ics
--- a/src/tests/data/mailfilter/event5.ics
+++ b/src/tests/data/mailfilter/event5.ics
@@ -33,7 +33,7 @@
SUMMARY:Test Meeting
LOCATION:Berlin
RRULE:FREQ=WEEKLY;INTERVAL=1
-SEQUENCE:0
+SEQUENCE:1
TRANSP:OPAQUE
ATTENDEE;CN=John;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT;CUTYPE=INDI
VIDUAL:mailto:john@kolab.org
diff --git a/src/tests/data/mailfilter/itip1_request.eml b/src/tests/data/mailfilter/itip1_request.eml
--- a/src/tests/data/mailfilter/itip1_request.eml
+++ b/src/tests/data/mailfilter/itip1_request.eml
@@ -56,10 +56,10 @@
UID:5463F1DDF6DA264A3FC70E7924B729A5-D9F1889254B163F5
DTSTAMP:20240709T124304Z
CREATED:20240709T124304Z
-LAST-MODIFIED:20240709T124304Z
+LAST-MODIFIED:20240709T124305Z
DTSTART;TZID=Europe/Berlin:20240710T103000
DTEND;TZID=Europe/Berlin:20240710T113000
-SUMMARY:Test Meeting
+SUMMARY:Test Meeting 1
LOCATION:Berlin
SEQUENCE:0
TRANSP:OPAQUE
diff --git a/src/tests/data/mailfilter/itip2_request.eml b/src/tests/data/mailfilter/itip2_request.eml
--- a/src/tests/data/mailfilter/itip2_request.eml
+++ b/src/tests/data/mailfilter/itip2_request.eml
@@ -53,9 +53,9 @@
LAST-MODIFIED:20240709T124304Z
DTSTART;TZID=Europe/Berlin:20240717T123000
DTEND;TZID=Europe/Berlin:20240717T133000
-SUMMARY:Test Meeting
+SUMMARY:Test Meeting Ex
LOCATION:Berlin
-SEQUENCE:0
+SEQUENCE:1
TRANSP:OPAQUE
ATTENDEE;CN=John;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYPE=INDI
VIDUAL;RSVP=TRUE:mailto:john@kolab.org
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 7:29 PM (4 h, 5 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18826008
Default Alt Text
D5511.1775244555.diff (48 KB)
Attached To
Mode
D5511: iTip spoofing protection
Attached
Detach File
Event Timeline