fix: aliases and capitalization of emails

Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com>
pull/54771/head
SebastianKrupinski 2025-05-04 17:35:32 +07:00 committed by Andy Scherzinger
parent 4a259f9a4a
commit e25d0dbf2d
5 changed files with 376 additions and 1504 deletions

@ -20,9 +20,9 @@ use OCP\Constants;
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
use Sabre\DAV\Exception\Conflict;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Component\VTimeZone;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\ParseException;
use Sabre\VObject\Property;
use Sabre\VObject\Reader;
use function Sabre\Uri\split as uriSplit;
@ -36,6 +36,9 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIs
) {
}
private const DAV_PROPERTY_USER_ADDRESS = '{http://sabredav.org/ns}email-address';
private const DAV_PROPERTY_USER_ADDRESSES = '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set';
/**
* @return string defining the technical unique key
* @since 13.0.0
@ -210,58 +213,93 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage, ICalendarIs
* @throws CalendarException
*/
public function handleIMipMessage(string $name, string $calendarData): void {
$server = $this->getInvitationResponseServer();
/** @var CustomPrincipalPlugin $plugin */
$plugin = $server->getServer()->getPlugin('auth');
// we're working around the previous implementation
// that only allowed the public system principal to be used
// so set the custom principal here
$plugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
if (empty($this->calendarInfo['uri'])) {
throw new CalendarException('Could not write to calendar as URI parameter is missing');
try {
/** @var VCalendar $vObject|null */
$vObject = Reader::read($calendarData);
} catch (ParseException $e) {
throw new CalendarException('iMip message could not be processed because an error occurred while parsing the iMip message', 0, $e);
}
// Force calendar change URI
/** @var Schedule\Plugin $schedulingPlugin */
$schedulingPlugin = $server->getServer()->getPlugin('caldav-schedule');
// Let sabre handle the rest
$iTipMessage = new Message();
/** @var VCalendar $vObject */
$vObject = Reader::read($calendarData);
/** @var VEvent $vEvent */
$vEvent = $vObject->{'VEVENT'};
if ($vObject->{'METHOD'} === null) {
throw new CalendarException('No Method provided for scheduling data. Could not process message');
// validate the iMip message
if (!isset($vObject->METHOD)) {
throw new CalendarException('iMip message contains no valid method');
}
if (!isset($vEvent->{'ORGANIZER'}) || !isset($vEvent->{'ATTENDEE'})) {
throw new CalendarException('Could not process scheduling data, neccessary data missing from ICAL');
if (!isset($vObject->VEVENT)) {
throw new CalendarException('iMip message contains no event');
}
$organizer = $vEvent->{'ORGANIZER'}->getValue();
$attendee = $vEvent->{'ATTENDEE'}->getValue();
$iTipMessage->method = $vObject->{'METHOD'}->getValue();
if ($iTipMessage->method === 'REQUEST') {
$iTipMessage->sender = $organizer;
$iTipMessage->recipient = $attendee;
} elseif ($iTipMessage->method === 'REPLY') {
if ($server->isExternalAttendee($vEvent->{'ATTENDEE'}->getValue())) {
$iTipMessage->recipient = $organizer;
} else {
$iTipMessage->recipient = $attendee;
if (!isset($vObject->VEVENT->UID)) {
throw new CalendarException('iMip message event dose not contain a UID');
}
if (!isset($vObject->VEVENT->ORGANIZER)) {
throw new CalendarException('iMip message event dose not contain an organizer');
}
if (!isset($vObject->VEVENT->ATTENDEE)) {
throw new CalendarException('iMip message event dose not contain an attendee');
}
if (empty($this->calendarInfo['uri'])) {
throw new CalendarException('Could not write to calendar as URI parameter is missing');
}
// construct dav server
$server = $this->getInvitationResponseServer();
/** @var CustomPrincipalPlugin $authPlugin */
$authPlugin = $server->getServer()->getPlugin('auth');
// we're working around the previous implementation
// that only allowed the public system principal to be used
// so set the custom principal here
$authPlugin->setCurrentPrincipal($this->calendar->getPrincipalURI());
// retrieve all users addresses
$userProperties = $server->getServer()->getProperties($this->calendar->getPrincipalURI(), [ self::DAV_PROPERTY_USER_ADDRESS, self::DAV_PROPERTY_USER_ADDRESSES ]);
$userAddress = 'mailto:' . ($userProperties[self::DAV_PROPERTY_USER_ADDRESS] ?? null);
$userAddresses = $userProperties[self::DAV_PROPERTY_USER_ADDRESSES]->getHrefs() ?? [];
$userAddresses = array_map('strtolower', array_map('urldecode', $userAddresses));
// validate the method, recipient and sender
$imipMethod = strtoupper($vObject->METHOD->getValue());
if (in_array($imipMethod, ['REPLY', 'REFRESH'], true)) {
// extract sender (REPLY and REFRESH method should only have one attendee)
$sender = strtolower($vObject->VEVENT->ATTENDEE->getValue());
// extract and verify the recipient
$recipient = strtolower($vObject->VEVENT->ORGANIZER->getValue());
if (!in_array($recipient, $userAddresses, true)) {
throw new CalendarException('iMip message dose not contain an organizer that matches the user');
}
// if the recipient address is not the same as the user address this means an alias was used
// the iTip broker uses the users primary email address during processing
if ($userAddress !== $recipient) {
$recipient = $userAddress;
}
} elseif (in_array($imipMethod, ['PUBLISH', 'REQUEST', 'ADD', 'CANCEL'], true)) {
// extract sender
$sender = strtolower($vObject->VEVENT->ORGANIZER->getValue());
// extract and verify the recipient
foreach ($vObject->VEVENT->ATTENDEE as $attendee) {
$recipient = strtolower($attendee->getValue());
if (in_array($recipient, $userAddresses, true)) {
break;
}
$recipient = null;
}
if ($recipient === null) {
throw new CalendarException('iMip message dose not contain an attendee that matches the user');
}
$iTipMessage->sender = $attendee;
} elseif ($iTipMessage->method === 'CANCEL') {
$iTipMessage->recipient = $attendee;
$iTipMessage->sender = $organizer;
// if the recipient address is not the same as the user address this means an alias was used
// the iTip broker uses the users primary email address during processing
if ($userAddress !== $recipient) {
$recipient = $userAddress;
}
} else {
throw new CalendarException('iMip message contains a method that is not supported: ' . $imipMethod);
}
$iTipMessage->uid = isset($vEvent->{'UID'}) ? $vEvent->{'UID'}->getValue() : '';
$iTipMessage->component = 'VEVENT';
$iTipMessage->sequence = isset($vEvent->{'SEQUENCE'}) ? (int)$vEvent->{'SEQUENCE'}->getValue() : 0;
$iTipMessage->message = $vObject;
$server->server->emit('schedule', [$iTipMessage]);
// generate the iTip message
$iTip = new Message();
$iTip->method = $imipMethod;
$iTip->sender = $sender;
$iTip->recipient = $recipient;
$iTip->component = 'VEVENT';
$iTip->uid = $vObject->VEVENT->UID->getValue();
$iTip->sequence = isset($vObject->VEVENT->SEQUENCE) ? (int)$vObject->VEVENT->SEQUENCE->getValue() : 1;
$iTip->message = $vObject;
$server->server->emit('schedule', [$iTip]);
}
public function getInvitationResponseServer(): InvitationResponseServer {

@ -132,7 +132,7 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
* @param string $principal
* @return array
*/
protected function getAddressesForPrincipal($principal) {
public function getAddressesForPrincipal($principal) {
$result = parent::getAddressesForPrincipal($principal);
if ($result === null) {

@ -10,14 +10,12 @@ use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarImpl;
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
use OCA\DAV\CalDAV\Schedule\Plugin;
use OCA\DAV\Connector\Sabre\Server;
use OCP\Calendar\Exceptions\CalendarException;
use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\ITip\Message;
use Sabre\VObject\Reader;
class CalendarImplTest extends \Test\TestCase {
/** @var CalendarImpl */
@ -32,9 +30,13 @@ class CalendarImplTest extends \Test\TestCase {
/** @var CalDavBackend | \PHPUnit\Framework\MockObject\MockObject */
private $backend;
/** @var VCalendar */
private $vCalendar1a;
protected function setUp(): void {
parent::setUp();
$this->backend = $this->createMock(CalDavBackend::class);
$this->calendar = $this->createMock(Calendar::class);
$this->calendarInfo = [
'id' => 'fancy_id_123',
@ -43,10 +45,28 @@ class CalendarImplTest extends \Test\TestCase {
'uri' => '/this/is/a/uri',
'principaluri' => 'principal/users/foobar'
];
$this->backend = $this->createMock(CalDavBackend::class);
$this->calendarImpl = new CalendarImpl($this->calendar, $this->calendarInfo, $this->backend);
$this->calendarImpl = new CalendarImpl($this->calendar,
$this->calendarInfo, $this->backend);
// construct calendar with a 1 hour event and same start/end time zones
$this->vCalendar1a = new VCalendar();
/** @var VEvent $vEvent */
$vEvent = $this->vCalendar1a->add('VEVENT', []);
$vEvent->UID->setValue('96a0e6b1-d886-4a55-a60d-152b31401dcc');
$vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
$vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
$vEvent->add('SEQUENCE', 1);
$vEvent->add('SUMMARY', 'Test Event');
$vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
$vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
'CN' => 'Attendee One',
'CUTYPE' => 'INDIVIDUAL',
'PARTSTAT' => 'NEEDS-ACTION',
'ROLE' => 'REQ-PARTICIPANT',
'RSVP' => 'TRUE'
]);
}
@ -123,99 +143,128 @@ class CalendarImplTest extends \Test\TestCase {
$this->assertEquals(31, $this->calendarImpl->getPermissions());
}
public function testHandleImipMessage(): void {
$message = <<<EOF
BEGIN:VCALENDAR
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
METHOD:REPLY
VERSION:2.0
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED:mailto:lewis@stardew-tent-living.com
ORGANIZER:mailto:pierre@generalstore.com
UID:aUniqueUid
SEQUENCE:2
REQUEST-STATUS:2.0;Success
END:VEVENT
END:VCALENDAR
EOF;
public function testHandleImipNoMethod(): void {
// Arrange
$vObject = $this->vCalendar1a;
/** @var CustomPrincipalPlugin|MockObject $authPlugin */
$authPlugin = $this->createMock(CustomPrincipalPlugin::class);
$authPlugin->expects(self::once())
->method('setCurrentPrincipal')
->with($this->calendar->getPrincipalURI());
$this->expectException(CalendarException::class);
$this->expectExceptionMessage('iMip message contains no valid method');
/** @var \Sabre\DAVACL\Plugin|MockObject $aclPlugin */
$aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class);
// Act
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
}
/** @var Plugin|MockObject $schedulingPlugin */
$schedulingPlugin = $this->createMock(Plugin::class);
$iTipMessage = $this->getITipMessage($message);
$iTipMessage->recipient = 'mailto:lewis@stardew-tent-living.com';
public function testHandleImipNoEvent(): void {
// Arrange
$vObject = $this->vCalendar1a;
$vObject->add('METHOD', 'REQUEST');
$vObject->remove('VEVENT');
$server = $this->createMock(Server::class);
$server->expects($this->any())
->method('getPlugin')
->willReturnMap([
['auth', $authPlugin],
['acl', $aclPlugin],
['caldav-schedule', $schedulingPlugin]
]);
$server->expects(self::once())
->method('emit');
$this->expectException(CalendarException::class);
$this->expectExceptionMessage('iMip message contains no event');
$invitationResponseServer = $this->createPartialMock(InvitationResponseServer::class, ['getServer', 'isExternalAttendee']);
$invitationResponseServer->server = $server;
$invitationResponseServer->expects($this->any())
->method('getServer')
->willReturn($server);
$invitationResponseServer->expects(self::once())
->method('isExternalAttendee')
->willReturn(false);
// Act
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
}
$calendarImpl = $this->getMockBuilder(CalendarImpl::class)
->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend])
->onlyMethods(['getInvitationResponseServer'])
->getMock();
$calendarImpl->expects($this->once())
->method('getInvitationResponseServer')
->willReturn($invitationResponseServer);
public function testHandleImipNoUid(): void {
// Arrange
$vObject = $this->vCalendar1a;
$vObject->add('METHOD', 'REQUEST');
$vObject->VEVENT->remove('UID');
$this->expectException(CalendarException::class);
$this->expectExceptionMessage('iMip message event dose not contain a UID');
// Act
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
}
public function testHandleImipNoOrganizer(): void {
// Arrange
$vObject = $this->vCalendar1a;
$vObject->add('METHOD', 'REQUEST');
$vObject->VEVENT->remove('ORGANIZER');
$calendarImpl->handleIMipMessage('filename.ics', $message);
$this->expectException(CalendarException::class);
$this->expectExceptionMessage('iMip message event dose not contain an organizer');
// Act
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
}
public function testHandleImipNoAttendee(): void {
// Arrange
$vObject = $this->vCalendar1a;
$vObject->add('METHOD', 'REQUEST');
$vObject->VEVENT->remove('ATTENDEE');
$this->expectException(CalendarException::class);
$this->expectExceptionMessage('iMip message event dose not contain an attendee');
// Act
$this->calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
}
public function testHandleImipMessageNoCalendarUri(): void {
public function testHandleImipRequest(): void {
$userAddressSet = new class([ 'mailto:attendee1@testing.com', '/remote.php/dav/principals/users/attendee1/', ]) {
public function __construct(
private array $hrefs,
) {
}
public function getHrefs(): array {
return $this->hrefs;
}
};
$vObject = $this->vCalendar1a;
$vObject->add('METHOD', 'REQUEST');
$iTip = new Message();
$iTip->method = 'REQUEST';
$iTip->sender = $vObject->VEVENT->ORGANIZER->getValue();
$iTip->recipient = $vObject->VEVENT->ATTENDEE->getValue();
$iTip->component = 'VEVENT';
$iTip->uid = $vObject->VEVENT->UID->getValue();
$iTip->sequence = (int)$vObject->VEVENT->SEQUENCE->getValue() ?? 0;
$iTip->message = $vObject;
/** @var CustomPrincipalPlugin|MockObject $authPlugin */
$authPlugin = $this->createMock(CustomPrincipalPlugin::class);
$authPlugin->expects(self::once())
->method('setCurrentPrincipal')
->with($this->calendar->getPrincipalURI());
unset($this->calendarInfo['uri']);
/** @var Plugin|MockObject $schedulingPlugin */
$schedulingPlugin = $this->createMock(Plugin::class);
/** @var \Sabre\DAVACL\Plugin|MockObject $schedulingPlugin */
/** @var \Sabre\DAVACL\Plugin|MockObject $aclPlugin */
$aclPlugin = $this->createMock(\Sabre\DAVACL\Plugin::class);
$server =
$this->createMock(Server::class);
$server = $this->createMock(Server::class);
$server->expects($this->any())
->method('getPlugin')
->willReturnMap([
['auth', $authPlugin],
['acl', $aclPlugin],
['caldav-schedule', $schedulingPlugin]
]);
$server->expects(self::never())
->method('emit');
$invitationResponseServer = $this->createPartialMock(InvitationResponseServer::class, ['getServer']);
$server->expects(self::once())
->method('getProperties')
->with(
$this->calendar->getPrincipalURI(),
[
'{http://sabredav.org/ns}email-address',
'{urn:ietf:params:xml:ns:caldav}calendar-user-address-set'
]
)
->willReturn([
'{http://sabredav.org/ns}email-address' => 'attendee1@testing.com',
'{urn:ietf:params:xml:ns:caldav}calendar-user-address-set' => $userAddressSet,
]);
$server->expects(self::once())
->method('emit');
$invitationResponseServer = $this->createMock(InvitationResponseServer::class, ['getServer']);
$invitationResponseServer->server = $server;
$invitationResponseServer->expects($this->any())
->method('getServer')
->willReturn($server);
$calendarImpl = $this->getMockBuilder(CalendarImpl::class)
->setConstructorArgs([$this->calendar, $this->calendarInfo, $this->backend])
->onlyMethods(['getInvitationResponseServer'])
@ -224,41 +273,6 @@ EOF;
->method('getInvitationResponseServer')
->willReturn($invitationResponseServer);
$message = <<<EOF
BEGIN:VCALENDAR
PRODID:-//Nextcloud/Nextcloud CalDAV Server//EN
METHOD:REPLY
VERSION:2.0
BEGIN:VEVENT
ATTENDEE;PARTSTAT=ACCEPTED:mailto:lewis@stardew-tent-living.com
ORGANIZER:mailto:pierre@generalstore.com
UID:aUniqueUid
SEQUENCE:2
REQUEST-STATUS:2.0;Success
END:VEVENT
END:VCALENDAR
EOF;
$this->expectException(CalendarException::class);
$calendarImpl->handleIMipMessage('filename.ics', $message);
}
private function getITipMessage($calendarData): Message {
$iTipMessage = new Message();
/** @var VCalendar $vObject */
$vObject = Reader::read($calendarData);
/** @var VEvent $vEvent */
$vEvent = $vObject->{'VEVENT'};
$orgaizer = $vEvent->{'ORGANIZER'}->getValue();
$attendee = $vEvent->{'ATTENDEE'}->getValue();
$iTipMessage->method = $vObject->{'METHOD'}->getValue();
$iTipMessage->recipient = $orgaizer;
$iTipMessage->sender = $attendee;
$iTipMessage->uid = isset($vEvent->{'UID'}) ? $vEvent->{'UID'}->getValue() : '';
$iTipMessage->component = 'VEVENT';
$iTipMessage->sequence = isset($vEvent->{'SEQUENCE'}) ? (int)$vEvent->{'SEQUENCE'}->getValue() : 0;
$iTipMessage->message = $vObject;
return $iTipMessage;
$calendarImpl->handleIMipMessage('fakeUser', $vObject->serialize());
}
}

@ -16,7 +16,6 @@ use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\Exceptions\CalendarException;
use OCP\Calendar\ICalendar;
use OCP\Calendar\ICalendarEventBuilder;
use OCP\Calendar\ICalendarIsShared;
use OCP\Calendar\ICalendarIsWritable;
use OCP\Calendar\ICalendarProvider;
use OCP\Calendar\ICalendarQuery;
@ -221,17 +220,18 @@ class Manager implements IManager {
}
/**
* @since 31.0.0
* @since 31.0.9
*
* @throws \OCP\DB\Exception
*/
public function handleIMipRequest(
string $principalUri,
string $sender,
string $recipient,
string $calendarData,
protected function handleIMip(
string $userId,
string $message,
): bool {
$userCalendars = $this->getCalendarsForPrincipal($principalUri);
$userUri = 'principals/users/' . $userId;
$userCalendars = $this->getCalendarsForPrincipal($userUri);
if (empty($userCalendars)) {
$this->logger->warning('iMip message could not be processed because user has no calendars');
return false;
@ -239,188 +239,100 @@ class Manager implements IManager {
try {
/** @var VCalendar $vObject|null */
$calendarObject = Reader::read($calendarData);
$vObject = Reader::read($message);
} catch (ParseException $e) {
$this->logger->error('iMip message could not be processed because an error occurred while parsing the iMip message', ['exception' => $e]);
return false;
}
if (!isset($calendarObject->METHOD) || $calendarObject->METHOD->getValue() !== 'REQUEST') {
$this->logger->warning('iMip message contains an incorrect or invalid method');
return false;
}
if (!isset($calendarObject->VEVENT)) {
$this->logger->warning('iMip message contains no event');
if (!isset($vObject->VEVENT)) {
$this->logger->warning('iMip message does not contain any event(s)');
return false;
}
/** @var VEvent $vEvent */
$vEvent = $vObject->VEVENT;
/** @var VEvent|null $vEvent */
$eventObject = $calendarObject->VEVENT;
if (!isset($eventObject->UID)) {
if (!isset($vEvent->UID)) {
$this->logger->warning('iMip message event dose not contains a UID');
return false;
}
if (!isset($eventObject->ORGANIZER)) {
if (!isset($vEvent->ORGANIZER)) {
$this->logger->warning('iMip message event dose not contains an organizer');
return false;
}
if (!isset($eventObject->ATTENDEE)) {
if (!isset($vEvent->ATTENDEE)) {
$this->logger->warning('iMip message event dose not contains any attendees');
return false;
}
foreach ($eventObject->ATTENDEE as $entry) {
$address = trim(str_replace('mailto:', '', $entry->getValue()));
if ($address === $recipient) {
$attendee = $address;
break;
}
}
if (!isset($attendee)) {
$this->logger->warning('iMip message event does not contain a attendee that matches the recipient');
return false;
}
foreach ($userCalendars as $calendar) {
if (!$calendar instanceof ICalendarIsWritable && !$calendar instanceof ICalendarIsShared) {
if (!$calendar instanceof ICalendarIsWritable) {
continue;
}
if ($calendar->isDeleted() || !$calendar->isWritable() || $calendar->isShared()) {
if ($calendar->isDeleted() || !$calendar->isWritable()) {
continue;
}
if (!empty($calendar->search($recipient, ['ATTENDEE'], ['uid' => $eventObject->UID->getValue()]))) {
if (!empty($calendar->search('', [], ['uid' => $vEvent->UID->getValue()]))) {
try {
if ($calendar instanceof IHandleImipMessage) {
$calendar->handleIMipMessage('', $calendarData);
$calendar->handleIMipMessage($userId, $vObject->serialize());
}
return true;
} catch (CalendarException $e) {
$this->logger->error('An error occurred while processing the iMip message event', ['exception' => $e]);
$this->logger->error('iMip message could not be processed because an error occurred', ['exception' => $e]);
return false;
}
}
}
$this->logger->warning('iMip message event could not be processed because no corresponding event was found in any calendar');
$this->logger->warning('iMip message could not be processed because no corresponding event was found in any calendar');
return false;
}
/**
* @since 31.0.0
*
* @throws \OCP\DB\Exception
*/
public function handleIMipReply(
public function handleIMipRequest(
string $principalUri,
string $sender,
string $recipient,
string $calendarData,
): bool {
$calendars = $this->getCalendarsForPrincipal($principalUri);
if (empty($calendars)) {
$this->logger->warning('iMip message could not be processed because user has no calendars');
return false;
}
try {
/** @var VCalendar $vObject|null */
$vObject = Reader::read($calendarData);
} catch (ParseException $e) {
$this->logger->error('iMip message could not be processed because an error occurred while parsing the iMip message', ['exception' => $e]);
return false;
}
if ($vObject === null) {
$this->logger->warning('iMip message contains an invalid calendar object');
return false;
}
if (!isset($vObject->METHOD) || $vObject->METHOD->getValue() !== 'REPLY') {
$this->logger->warning('iMip message contains an incorrect or invalid method');
return false;
}
if (!isset($vObject->VEVENT)) {
$this->logger->warning('iMip message contains no event');
return false;
}
/** @var VEvent|null $vEvent */
$vEvent = $vObject->VEVENT;
if (!isset($vEvent->UID)) {
$this->logger->warning('iMip message event dose not contains a UID');
return false;
}
if (!isset($vEvent->ORGANIZER)) {
$this->logger->warning('iMip message event dose not contains an organizer');
return false;
}
if (!isset($vEvent->ATTENDEE)) {
$this->logger->warning('iMip message event dose not contains any attendees');
return false;
}
// check if mail recipient and organizer are one and the same
$organizer = substr($vEvent->{'ORGANIZER'}->getValue(), 7);
if (strcasecmp($recipient, $organizer) !== 0) {
$this->logger->warning('iMip message event could not be processed because recipient and ORGANIZER must be identical');
return false;
}
//check if the event is in the future
/** @var DateTime $eventTime */
$eventTime = $vEvent->{'DTSTART'};
if ($eventTime->getDateTime()->getTimeStamp() < $this->timeFactory->getTime()) { // this might cause issues with recurrences
$this->logger->warning('iMip message event could not be processed because the event is in the past');
return false;
}
$found = null;
// if the attendee has been found in at least one calendar event with the UID of the iMIP event
// we process it.
// Benefit: no attendee lost
// Drawback: attendees that have been deleted will still be able to update their partstat
foreach ($calendars as $calendar) {
// We should not search in writable calendars
if ($calendar instanceof IHandleImipMessage) {
$o = $calendar->search($sender, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]);
if (!empty($o)) {
$found = $calendar;
$name = $o[0]['uri'];
break;
}
}
}
if (empty($found)) {
$this->logger->warning('iMip message event could not be processed because no corresponding event was found in any calendar', [
'principalUri' => $principalUri,
'eventUid' => $vEvent->{'UID'}->getValue(),
]);
if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) {
$this->logger->error('Invalid principal URI provided for iMip request');
return false;
}
$userId = substr($principalUri, 17);
return $this->handleIMip($userId, $calendarData);
}
try {
$found->handleIMipMessage($name, $calendarData); // sabre will handle the scheduling behind the scenes
} catch (CalendarException $e) {
$this->logger->error('An error occurred while processing the iMip message event', ['exception' => $e]);
/**
* @since 25.0.0
*
* @throws \OCP\DB\Exception
*/
public function handleIMipReply(
string $principalUri,
string $sender,
string $recipient,
string $calendarData,
): bool {
if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) {
$this->logger->error('Invalid principal URI provided for iMip reply');
return false;
}
return true;
$userId = substr($principalUri, 17);
return $this->handleIMip($userId, $calendarData);
}
/**
* @since 25.0.0
*
* @throws \OCP\DB\Exception
*/
public function handleIMipCancel(
@ -430,111 +342,12 @@ class Manager implements IManager {
string $recipient,
string $calendarData,
): bool {
$calendars = $this->getCalendarsForPrincipal($principalUri);
if (empty($calendars)) {
$this->logger->warning('iMip message could not be processed because user has no calendars');
return false;
}
try {
/** @var VCalendar $vObject|null */
$vObject = Reader::read($calendarData);
} catch (ParseException $e) {
$this->logger->error('iMip message could not be processed because an error occurred while parsing the iMip message', ['exception' => $e]);
return false;
}
if ($vObject === null) {
$this->logger->warning('iMip message contains an invalid calendar object');
return false;
}
if (!isset($vObject->METHOD) || $vObject->METHOD->getValue() !== 'CANCEL') {
$this->logger->warning('iMip message contains an incorrect or invalid method');
return false;
}
if (!isset($vObject->VEVENT)) {
$this->logger->warning('iMip message contains no event');
return false;
}
/** @var VEvent|null $vEvent */
$vEvent = $vObject->{'VEVENT'};
if (!isset($vEvent->UID)) {
$this->logger->warning('iMip message event dose not contains a UID');
return false;
}
if (!isset($vEvent->ORGANIZER)) {
$this->logger->warning('iMip message event dose not contains an organizer');
return false;
}
if (!isset($vEvent->ATTENDEE)) {
$this->logger->warning('iMip message event dose not contains any attendees');
return false;
}
$attendee = substr($vEvent->{'ATTENDEE'}->getValue(), 7);
if (strcasecmp($recipient, $attendee) !== 0) {
$this->logger->warning('iMip message event could not be processed because recipient must be an ATTENDEE of this event');
return false;
}
// Thirdly, we need to compare the email address the CANCEL is coming from (in Mail)
// or the Reply- To Address submitted with the CANCEL email
// to the email address in the ORGANIZER.
// We don't want to accept a CANCEL request from just anyone
$organizer = substr($vEvent->{'ORGANIZER'}->getValue(), 7);
$isNotOrganizer = ($replyTo !== null) ? (strcasecmp($sender, $organizer) !== 0 && strcasecmp($replyTo, $organizer) !== 0) : (strcasecmp($sender, $organizer) !== 0);
if ($isNotOrganizer) {
$this->logger->warning('iMip message event could not be processed because sender must be the ORGANIZER of this event');
return false;
}
//check if the event is in the future
/** @var DateTime $eventTime */
$eventTime = $vEvent->{'DTSTART'};
if ($eventTime->getDateTime()->getTimeStamp() < $this->timeFactory->getTime()) { // this might cause issues with recurrences
$this->logger->warning('iMip message event could not be processed because the event is in the past');
return false;
}
$found = null;
// if the attendee has been found in at least one calendar event with the UID of the iMIP event
// we process it.
// Benefit: no attendee lost
// Drawback: attendees that have been deleted will still be able to update their partstat
foreach ($calendars as $calendar) {
// We should not search in writable calendars
if ($calendar instanceof IHandleImipMessage) {
$o = $calendar->search($recipient, ['ATTENDEE'], ['uid' => $vEvent->{'UID'}->getValue()]);
if (!empty($o)) {
$found = $calendar;
$name = $o[0]['uri'];
break;
}
}
}
if (empty($found)) {
$this->logger->warning('iMip message event could not be processed because no corresponding event was found in any calendar', [
'principalUri' => $principalUri,
'eventUid' => $vEvent->{'UID'}->getValue(),
]);
return false;
}
try {
$found->handleIMipMessage($name, $calendarData); // sabre will handle the scheduling behind the scenes
return true;
} catch (CalendarException $e) {
$this->logger->error('An error occurred while processing the iMip message event', ['exception' => $e]);
if (empty($principalUri) || !str_starts_with($principalUri, 'principals/users/')) {
$this->logger->error('Invalid principal URI provided for iMip cancel');
return false;
}
$userId = substr($principalUri, 17);
return $this->handleIMip($userId, $calendarData);
}
public function createEventBuilder(): ICalendarEventBuilder {

File diff suppressed because it is too large Load Diff