input('q', $request->input('attendee', ''))); if ($q === '' || mb_strlen($q) < 2) { return response()->view('event.partials.attendee-suggestions', ['suggestions' => []]); } $principal = $request->user()?->principal_uri; if (!$principal) { return response()->view('event.partials.attendee-suggestions', ['suggestions' => []]); } $query = Str::lower($q); $contacts = []; $seen = []; $cards = Card::query() ->select('cards.carddata') ->join('addressbooks', 'addressbooks.id', '=', 'cards.addressbookid') ->where('addressbooks.principaluri', $principal) ->whereRaw('LOWER(cards.carddata) LIKE ?', ['%' . $query . '%']) ->orderByDesc('cards.lastmodified') ->limit(200) ->get(); foreach ($cards as $card) { $parsed = $this->parseCardEmails((string) $card->carddata); $name = $parsed['name']; foreach ($parsed['emails'] as $email) { $emailLc = Str::lower($email); $matches = str_contains($emailLc, $query) || ($name !== '' && str_contains(Str::lower($name), $query)); if (!$matches || isset($seen[$emailLc])) { continue; } $seen[$emailLc] = true; $contacts[] = [ 'email' => $emailLc, 'name' => $name !== '' ? $name : null, 'attendee_uri' => 'mailto:' . $emailLc, 'source' => 'contact', ]; } } if (filter_var($q, FILTER_VALIDATE_EMAIL)) { $emailLc = Str::lower($q); if (!isset($seen[$emailLc])) { $contacts[] = [ 'email' => $emailLc, 'name' => null, 'attendee_uri' => 'mailto:' . $emailLc, 'source' => 'email', ]; } } if (empty($contacts)) { return response()->view('event.partials.attendee-suggestions', ['suggestions' => []]); } usort($contacts, function (array $a, array $b) use ($query) { $scoreA = $this->score($a, $query); $scoreB = $this->score($b, $query); if ($scoreA !== $scoreB) { return $scoreA <=> $scoreB; } return strcmp($a['email'], $b['email']); }); $contacts = array_slice($contacts, 0, 8); $emails = array_values(array_unique(array_map(fn ($item) => $item['email'], $contacts))); $knownUsers = User::query() ->whereIn('email', $emails) ->pluck('email') ->map(fn ($email) => Str::lower((string) $email)) ->flip() ->all(); foreach ($contacts as &$item) { $item['verified'] = isset($knownUsers[$item['email']]); } unset($item); return response()->view('event.partials.attendee-suggestions', [ 'suggestions' => $contacts, ]); } private function parseCardEmails(string $carddata): array { $name = ''; $emails = []; try { $vcard = Reader::read($carddata); $name = trim((string) ($vcard->FN ?? '')); foreach ($vcard->select('EMAIL') as $emailProp) { $email = Str::lower(trim((string) $emailProp->getValue())); if ($email !== '') { $emails[] = $email; } } } catch (\Throwable $e) { if (preg_match('/^FN:(.+)$/mi', $carddata, $fn)) { $name = trim($fn[1]); } if (preg_match_all('/^EMAIL[^:]*:(.+)$/mi', $carddata, $matches)) { foreach (($matches[1] ?? []) as $emailRaw) { $email = Str::lower(trim((string) $emailRaw)); if ($email !== '') { $emails[] = $email; } } } } return [ 'name' => $name, 'emails' => array_values(array_unique($emails)), ]; } private function score(array $item, string $query): int { $email = Str::lower((string) ($item['email'] ?? '')); $name = Str::lower((string) ($item['name'] ?? '')); $source = (string) ($item['source'] ?? ''); if ($email === $query) { return 0; } if ($email !== '' && str_starts_with($email, $query)) { return 1; } if ($name !== '' && str_starts_with($name, $query)) { return 2; } if ($source === 'contact') { return 3; } return 4; } }