header('auto-submitted')); if ($autoSubmitted !== '' && $autoSubmitted !== 'no') { return "Auto-Submitted: {$autoSubmitted}"; } if ($email->header('x-autoreply') !== null || $email->header('x-autorespond') !== null) { return 'X-Autoreply/X-Autorespond header present'; } $precedence = strtolower((string) $email->header('precedence')); if (in_array($precedence, ['bulk', 'junk', 'list'], true)) { return "Precedence: {$precedence}"; } $senderLocalPart = strtolower(explode('@', $email->fromEmail)[0] ?? ''); $blocked = array_map('strtolower', $extraBlocklist); if ($senderLocalPart !== '' && in_array($senderLocalPart, $blocked, true)) { return "Blocked sender: {$email->fromEmail}"; } if (in_array(strtolower($email->fromEmail), $blocked, true)) { return "Blocked sender: {$email->fromEmail}"; } foreach (self::AUTO_REPLY_SUBJECT_PATTERNS as $pattern) { if (preg_match($pattern, $email->subject) === 1) { return "Subject matched auto-reply pattern ({$pattern})"; } } return null; } /** * Same gate Landing::submit() applies to web/guest ticket creation * (Settings::bool('restrict_tickets_to_ldap')) — must apply identically * to mail-originated tickets/replies, or the restriction has a hole. */ public function isSenderAllowed(string $email): bool { if (! Settings::bool('restrict_tickets_to_ldap')) { return true; } return User::query()->where('email', $email)->exists() || app(LdapUserProvisioner::class)->existsInLdap($email); } /** * Existing local user, or an LDAP-provisioned one if enabled — mirrors * TicketService::create()'s own guest-resolution branch. Returns null * for a genuine, unprovisionable guest. */ public function resolveSender(string $email): ?User { if ($user = User::query()->where('email', $email)->first()) { return $user; } if (Settings::bool('ldap_auto_provision_guests')) { return app(LdapUserProvisioner::class)->findOrCreateByEmail($email); } return null; } /** * Strips common reply/forward prefixes, then tries every digit run of * length >= 4 (longest first) against Ticket::resolveRouteBinding() — * covers both the plain sequential number and the obfuscated checksum, * since both are plain digit strings and every outbound notification * subject already carries one (see database/seeders/DatabaseSeeder.php). * Prefix-aware matching was considered and rejected: {numer} email * templates hardcode their own literal '#', independent of the * admin-configurable ticket_number_prefix setting, and templates are * themselves admin-editable. */ public function matchTicket(string $subject): ?Ticket { $cleaned = preg_replace('/^\s*(re|odp|fwd|fw|aw)\s*:\s*/i', '', $subject) ?? $subject; $cleaned = preg_replace('/^\s*(re|odp|fwd|fw|aw)\s*:\s*/i', '', $cleaned) ?? $cleaned; preg_match_all('/\d{4,}/', $cleaned, $matches); $tokens = $matches[0] ?? []; usort($tokens, fn ($a, $b) => strlen($b) <=> strlen($a)); foreach ($tokens as $token) { $ticket = (new Ticket)->resolveRouteBinding($token); if ($ticket) { return $ticket; } } return null; } }