$recipients Empfänger des Jobs — Rückfall, wenn die Meldung keine Adresse nennt und es genau einen gibt * @return array{kind: EmailBounceKind, email: string|null, status: string|null, code: int|null} */ public function classify(string $message, array $recipients = []): array { $code = preg_match('/\b([45]\d{2})\b/', $message, $m) ? (int) $m[1] : null; $status = preg_match('/\b([45]\.\d{1,3}\.\d{1,3})\b/', $message, $m) ? $m[1] : null; $email = $this->recipientIn($message) ?? (count($recipients) === 1 ? EmailSuppressionService::normalise($recipients[0]) : null); $isMailboxFailure = $status !== null && str_starts_with($status, '5.1.') || ($status === null && $code === 550 && preg_match('/user unknown|no such user|unknown user|does not exist|mailbox unavailable|recipient address rejected/i', $message) === 1); if ($isMailboxFailure && $email !== null) { return ['kind' => EmailBounceKind::Hard, 'email' => $email, 'status' => $status, 'code' => $code]; } if (($status !== null && str_starts_with($status, '4.') || ($status === null && $code !== null && $code < 500)) && $email !== null) { return ['kind' => EmailBounceKind::Soft, 'email' => $email, 'status' => $status, 'code' => $code]; } return ['kind' => EmailBounceKind::Relay, 'email' => $email, 'status' => $status, 'code' => $code]; } private function recipientIn(string $message): ?string { if (preg_match('/<([^<>\s]+@[^<>\s]+)>/', $message, $m) === 1) { return EmailSuppressionService::normalise($m[1]); } if (preg_match('/([A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,})/i', $message, $m) === 1) { return EmailSuppressionService::normalise($m[1]); } return null; } }