|
|
|
|
@ -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 {
|
|
|
|
|
|