extractFromCalendarData($calendarData ?? $event->calendardata); $this->syncRows($event, $attendees); } public function syncRows(Event $event, array $attendees): void { $rows = $this->normalizeRows($attendees); $event->attendees()->delete(); if (empty($rows)) { return; } foreach ($rows as &$row) { $email = $row['email'] ?? null; if (!$email) { $row['attendee_user_id'] = null; continue; } $row['attendee_user_id'] = User::query() ->whereRaw('lower(email) = ?', [Str::lower($email)]) ->value('id'); } unset($row); $event->attendees()->createMany($rows); } public function extractFromCalendarData(?string $calendarData): array { if (!$calendarData) { return []; } try { $vcalendar = Reader::read($calendarData); } catch (\Throwable $e) { return []; } $vevent = $vcalendar->select('VEVENT')[0] ?? null; if (!$vevent instanceof VEvent) { return []; } return $this->extractFromVevent($vevent); } public function extractFromVevent(VEvent $vevent): array { $items = []; foreach ($vevent->select('ATTENDEE') as $attendee) { $params = $this->extractParams($attendee); $uri = $this->normalizeAttendeeUri((string) $attendee->getValue()); $email = $this->extractEmail($uri, $params); $items[] = [ 'attendee_uri' => $uri, 'email' => $email, 'name' => $params['CN'] ?? null, 'role' => isset($params['ROLE']) ? strtoupper($params['ROLE']) : null, 'partstat' => isset($params['PARTSTAT']) ? strtoupper($params['PARTSTAT']) : null, 'cutype' => isset($params['CUTYPE']) ? strtoupper($params['CUTYPE']) : null, 'rsvp' => $this->normalizeBooleanParam($params['RSVP'] ?? null), 'is_organizer' => false, 'extra' => $this->extraParams($params, [ 'CN', 'ROLE', 'PARTSTAT', 'CUTYPE', 'RSVP', 'EMAIL', ]), ]; } if (isset($vevent->ORGANIZER)) { $organizer = $vevent->ORGANIZER; $params = $this->extractParams($organizer); $uri = $this->normalizeAttendeeUri((string) $organizer->getValue()); $email = $this->extractEmail($uri, $params); $items[] = [ 'attendee_uri' => $uri, 'email' => $email, 'name' => $params['CN'] ?? null, 'role' => 'CHAIR', 'partstat' => null, 'cutype' => isset($params['CUTYPE']) ? strtoupper($params['CUTYPE']) : null, 'rsvp' => null, 'is_organizer' => true, 'extra' => $this->extraParams($params, ['CN', 'CUTYPE', 'EMAIL']), ]; } return $items; } private function normalizeRows(array $attendees): array { $rows = []; $seen = []; foreach ($attendees as $item) { $email = $this->normalizeEmail(Arr::get($item, 'email')); $uri = $this->normalizeAttendeeUri(Arr::get($item, 'attendee_uri')); if (!$uri && $email) { $uri = 'mailto:' . $email; } if (!$uri && !$email) { continue; } $key = Str::lower($uri ?: $email); if (isset($seen[$key])) { continue; } $seen[$key] = true; $role = Arr::get($item, 'role'); $partstat = Arr::get($item, 'partstat'); $cutype = Arr::get($item, 'cutype'); $rows[] = [ 'attendee_uri' => $uri, 'email' => $email, 'name' => $this->normalizeString(Arr::get($item, 'name')), 'role' => $role ? strtoupper((string) $role) : null, 'partstat' => $partstat ? strtoupper((string) $partstat) : null, 'cutype' => $cutype ? strtoupper((string) $cutype) : null, 'rsvp' => Arr::exists($item, 'rsvp') ? (bool) Arr::get($item, 'rsvp') : null, 'is_organizer' => (bool) Arr::get($item, 'is_organizer', false), 'extra' => is_array(Arr::get($item, 'extra')) ? Arr::get($item, 'extra') : null, ]; } return $rows; } private function extractParams($property): array { $params = []; foreach ($property->parameters() as $parameter) { $params[strtoupper((string) $parameter->name)] = trim((string) $parameter->getValue()); } return $params; } private function extractEmail(?string $uri, array $params): ?string { $fromParam = $this->normalizeEmail($params['EMAIL'] ?? null); if ($fromParam) { return $fromParam; } if (!$uri) { return null; } if (str_starts_with(Str::lower($uri), 'mailto:')) { return $this->normalizeEmail(substr($uri, 7)); } return str_contains($uri, '@') ? $this->normalizeEmail($uri) : null; } private function normalizeAttendeeUri(mixed $uri): ?string { $value = trim((string) $uri); if ($value === '') { return null; } if (str_contains($value, '@') && !str_contains($value, ':')) { return 'mailto:' . Str::lower($value); } if (str_starts_with(Str::lower($value), 'mailto:')) { return 'mailto:' . Str::lower(substr($value, 7)); } return $value; } private function normalizeEmail(mixed $value): ?string { $email = trim((string) $value); if ($email === '') { return null; } return Str::lower($email); } private function normalizeString(mixed $value): ?string { $string = trim((string) $value); return $string === '' ? null : $string; } private function normalizeBooleanParam(?string $value): ?bool { if ($value === null || $value === '') { return null; } return in_array(strtoupper($value), ['1', 'TRUE', 'YES'], true); } private function extraParams(array $params, array $known): ?array { $knownMap = array_fill_keys($known, true); $extra = array_diff_key($params, $knownMap); return empty($extra) ? null : $extra; } }