diff --git a/application/Espo/Classes/FieldProcessing/Email/IcsDataLoader.php b/application/Espo/Classes/FieldProcessing/Email/IcsDataLoader.php index dfe2420281..29088e0f08 100644 --- a/application/Espo/Classes/FieldProcessing/Email/IcsDataLoader.php +++ b/application/Espo/Classes/FieldProcessing/Email/IcsDataLoader.php @@ -39,6 +39,7 @@ use Espo\Core\{ FieldProcessing\Loader, FieldProcessing\LoaderParams, Mail\Event\Event as EspoEvent, + Mail\Event\EventFactory, Utils\Log, }; @@ -84,18 +85,13 @@ class IcsDataLoader implements Loader return; } - $valueMap = (object) []; + if ($event->status === 'CANCELLED') { + return; + } - $espoEvent = EspoEvent::create() - ->withUid($event->uid ?? null) - ->withDateStart($event->dtstart_tz ?? null) - ->withDateEnd($event->dtend_tz ?? null) - ->withName($event->summary ?? null) - ->withLocation($event->location ?? null) - ->withDescription($event->description ?? null) - ->withTimezone($ical->calendarTimeZone() ?? null) - ->withOrganizer($event->organizer ?? null) - ->withAttendees($event->attendee ?? null); + $espoEvent = EventFactory::createFromU01jmg3Ical($ical); + + $valueMap = (object) []; try { $valueMap->name = $espoEvent->getName(); @@ -103,6 +99,12 @@ class IcsDataLoader implements Loader $valueMap->dateStart = $espoEvent->getDateStart(); $valueMap->dateEnd = $espoEvent->getDateEnd(); $valueMap->location = $espoEvent->getLocation(); + $valueMap->isAllDay = $espoEvent->isAllDay(); + + if ($espoEvent->isAllDay()) { + $valueMap->dateStartDate = $espoEvent->getDateStart(); + $valueMap->dateEndDate = $espoEvent->getDateEnd(); + } } catch (Throwable $e) { $this->log->warning("Error while converting ICS event '" . $entity->getId() . "': " . $e->getMessage()); @@ -158,6 +160,10 @@ class IcsDataLoader implements Loader $entity->set('icsEventData', $eventData); $entity->set('icsEventDateStart', $espoEvent->getDateStart()); + + if ($espoEvent->isAllDay()) { + $entity->set('icsEventDateStartDate', $espoEvent->getDateStart()); + } } private function loadCreatedEvent(Entity $entity, EspoEvent $espoEvent, object $eventData): void diff --git a/application/Espo/Core/Mail/Event/Event.php b/application/Espo/Core/Mail/Event/Event.php index e89bb665b4..509b3770a8 100644 --- a/application/Espo/Core/Mail/Event/Event.php +++ b/application/Espo/Core/Mail/Event/Event.php @@ -55,10 +55,11 @@ class Event private $uid = null; + private $isAllDay = false; + public function withAttendees(?string $attendees): self { $obj = clone $this; - $obj->attendees = $attendees; return $obj; @@ -67,7 +68,6 @@ class Event public function withOrganizer(?string $organizer): self { $obj = clone $this; - $obj->organizer = $organizer; return $obj; @@ -76,7 +76,6 @@ class Event public function withDateStart(?string $dateStart): self { $obj = clone $this; - $obj->dateStart = $dateStart; return $obj; @@ -85,7 +84,6 @@ class Event public function withDateEnd(?string $dateEnd): self { $obj = clone $this; - $obj->dateEnd = $dateEnd; return $obj; @@ -94,7 +92,6 @@ class Event public function withLocation(?string $location): self { $obj = clone $this; - $obj->location = $location; return $obj; @@ -103,7 +100,6 @@ class Event public function withName(?string $name): self { $obj = clone $this; - $obj->name = $name; return $obj; @@ -112,7 +108,6 @@ class Event public function withDescription(?string $description): self { $obj = clone $this; - $obj->description = $description; return $obj; @@ -121,7 +116,6 @@ class Event public function withTimezone(?string $timezone): self { $obj = clone $this; - $obj->timezone = $timezone; return $obj; @@ -130,17 +124,29 @@ class Event public function withUid(?string $uid): self { $obj = clone $this; - $obj->uid = $uid; return $obj; } + public function withIsAllDay(bool $isAllDay): self + { + $obj = clone $this; + $obj->isAllDay = $isAllDay; + + return $obj; + } + public function getUid(): ?string { return $this->uid; } + public function isAllDay(): bool + { + return $this->isAllDay; + } + public function getName(): ?string { return $this->name; @@ -153,7 +159,7 @@ class Event public function getDateEnd(): ?string { - return $this->convertDate($this->dateEnd); + return $this->convertDate($this->dateEnd, true); } public function getLocation(): ?string @@ -171,12 +177,26 @@ class Event return new self(); } - private function convertDate(?string $value): ?string + private function convertDate(?string $value, bool $isEnd = false): ?string { if ($value === null) { return null; } + if ($this->isAllDay) { + $dt = DateTime::createFromFormat('Ymd', $value); + + if ($dt === false) { + throw new RuntimeException("Could not parse '{$value}'."); + } + + if ($isEnd) { + $dt->modify('-1 day'); + } + + return $dt->format(DateTimeUtil::SYSTEM_DATE_FORMAT); + } + $timezone = $this->timezone ?? 'UTC'; $dt = DateTime::createFromFormat('Ymd\THis', $value, new DateTimeZone($timezone)); diff --git a/application/Espo/Core/Mail/Event/EventFactory.php b/application/Espo/Core/Mail/Event/EventFactory.php new file mode 100644 index 0000000000..0d40e9c355 --- /dev/null +++ b/application/Espo/Core/Mail/Event/EventFactory.php @@ -0,0 +1,72 @@ +events()[0] ?? null; + + if (!$event) { + throw new RuntimeException(); + } + + $dateStart = $event->dtstart_tz ?? null; + $dateEnd = $event->dtend_tz ?? null; + + $isAllDay = strlen($event->dtstart) === 8; + + if ($isAllDay) { + $dateStart = $event->dtstart ?? null; + $dateEnd = $event->dtend ?? null; + } + + $espoEvent = Event::create() + ->withUid($event->uid ?? null) + ->withIsAllDay($isAllDay) + ->withDateStart($dateStart) + ->withDateEnd($dateEnd) + ->withName($event->summary ?? null) + ->withLocation($event->location ?? null) + ->withDescription($event->description ?? null) + ->withTimezone($ical->calendarTimeZone() ?? null) + ->withOrganizer($event->organizer ?? null) + ->withAttendees($event->attendee ?? null); + + return $espoEvent; + } +} diff --git a/application/Espo/Resources/metadata/entityDefs/Email.json b/application/Espo/Resources/metadata/entityDefs/Email.json index b48441b0b3..64048289c9 100644 --- a/application/Espo/Resources/metadata/entityDefs/Email.json +++ b/application/Espo/Resources/metadata/entityDefs/Email.json @@ -349,7 +349,7 @@ "index": true }, "icsEventDateStart": { - "type": "datetime", + "type": "datetimeOptional", "readOnly": true, "notStorable": true }, diff --git a/tests/unit/Espo/Core/Mail/Event/EventTest.php b/tests/unit/Espo/Core/Mail/Event/EventTest.php index 502bc14ecd..ccde538d84 100644 --- a/tests/unit/Espo/Core/Mail/Event/EventTest.php +++ b/tests/unit/Espo/Core/Mail/Event/EventTest.php @@ -32,6 +32,7 @@ namespace tests\unit\Espo\Core\Mail\Event; use ICal\ICal; use ICal\Event; +use Espo\Core\Mail\Event\EventFactory; use Espo\Core\Mail\Event\Event as MailEvent; class EventTest extends \PHPUnit\Framework\TestCase @@ -113,6 +114,32 @@ END:VEVENT END:VCALENDAR "; + private $icsContents3 = +"BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +DTSTART;VALUE=DATE:20210810 +DTEND;VALUE=DATE:20210811 +DTSTAMP:20210810T091857Z +ORGANIZER;CN=test:mailto:test@group.calendar.google.c + om +UID:4r15namb5v2h4dou58gkfajjbe@google.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= + TRUE;CN=test.com;X-NUM-GUESTS=0:mailto:test@test.com +X-MICROSOFT-CDO-OWNERAPPTID:1443094082 +CREATED:20210810T091748Z +LAST-MODIFIED:20210810T091856Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:test ics 4 +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR"; + public function testEvent1(): void { $ical = new ICal(); @@ -181,5 +208,20 @@ END:VCALENDAR "1 Broadway Ave., Brooklyn", $espoEvent->getLocation() ); + + $this->assertFalse($espoEvent->isAllDay()); + } + + public function testEvent3(): void + { + $ical = new ICal(); + + $ical->initString($this->icsContents3); + + $event = EventFactory::createFromU01jmg3Ical($ical); + + $this->assertTrue($event->isAllDay()); + $this->assertEquals('2021-08-10', $event->getDateStart()); + $this->assertEquals('2021-08-10', $event->getDateEnd()); } }