splitHeadersBody($raw); $headers = $this->parseHeaders($headerBlock); [$textBody, $htmlBody] = $this->extractBodies($headers, $body); $from = $this->parseAddress($headers['from'] ?? ''); $toHeader = $this->parseAddressList($headers['to'] ?? ''); $toAddresses = !empty($recipients) ? $recipients : $toHeader; return SandboxMail::create([ 'message_id' => trim($headers['message-id'] ?? '', '<>') ?: null, 'from_address' => $from['address'], 'from_name' => $from['name'] ?: null, 'to_addresses' => $toAddresses, 'subject' => $this->decodeMimeHeader($headers['subject'] ?? '(kein Betreff)'), 'body_text' => $textBody, 'body_html' => $htmlBody, 'raw_headers' => $headerBlock, 'received_at' => now(), ]); } private function splitHeadersBody(string $raw): array { $raw = str_replace("\r\n", "\n", $raw); $pos = strpos($raw, "\n\n"); if ($pos === false) { return [$raw, '']; } return [substr($raw, 0, $pos), substr($raw, $pos + 2)]; } private function parseHeaders(string $block): array { $headers = []; // Unfold multi-line headers $block = preg_replace("/\n[ \t]+/", ' ', $block); foreach (explode("\n", $block) as $line) { if (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $headers[strtolower(trim($name))] = trim($value); } } return $headers; } private function extractBodies(array $headers, string $body): array { $contentType = $headers['content-type'] ?? 'text/plain'; if (str_starts_with($contentType, 'multipart/')) { preg_match('/boundary="?([^";\s]+)"?/i', $contentType, $m); $boundary = $m[1] ?? null; if ($boundary) { return $this->parseMultipart($body, $boundary); } } if (str_contains($contentType, 'text/html')) { return [null, $this->decodeBody($body, $headers['content-transfer-encoding'] ?? '')]; } return [$this->decodeBody($body, $headers['content-transfer-encoding'] ?? ''), null]; } private function parseMultipart(string $body, string $boundary): array { $text = null; $html = null; $parts = preg_split('/--' . preg_quote($boundary, '/') . '(--)?\r?\n?/', $body); foreach ($parts as $part) { $part = ltrim($part); if (empty($part) || $part === '--') continue; [$ph, $pb] = $this->splitHeadersBody($part); $ph = $this->parseHeaders($ph); $ct = $ph['content-type'] ?? ''; $enc = $ph['content-transfer-encoding'] ?? ''; if (str_contains($ct, 'multipart/')) { preg_match('/boundary="?([^";\s]+)"?/i', $ct, $m); if (!empty($m[1])) { [$t, $h] = $this->parseMultipart($pb, $m[1]); $text ??= $t; $html ??= $h; } } elseif (str_contains($ct, 'text/html')) { $html ??= $this->decodeBody($pb, $enc); } elseif (str_contains($ct, 'text/plain')) { $text ??= $this->decodeBody($pb, $enc); } } return [$text, $html]; } private function decodeBody(string $body, string $encoding): string { $enc = strtolower(trim($encoding)); if ($enc === 'base64') { return base64_decode(str_replace(["\r", "\n"], '', $body)); } if ($enc === 'quoted-printable') { return quoted_printable_decode($body); } return $body; } private function parseAddress(string $addr): array { $addr = $this->decodeMimeHeader($addr); if (preg_match('/^(.+?)\s*<([^>]+)>$/', trim($addr), $m)) { return ['name' => trim($m[1], '"'), 'address' => strtolower(trim($m[2]))]; } return ['name' => '', 'address' => strtolower(trim($addr, '<>'))]; } private function parseAddressList(string $list): array { $addresses = []; foreach (preg_split('/,(?![^<>]*>)/', $list) as $addr) { $parsed = $this->parseAddress(trim($addr)); if ($parsed['address']) { $addresses[] = $parsed['address']; } } return $addresses; } private function decodeMimeHeader(string $value): string { if (!str_contains($value, '=?')) return $value; return mb_decode_mimeheader($value); } }