parts($structure) as [$part, $number]) { $subtype = strtolower($part->subtype ?? ''); if (!in_array($subtype, ['plain', 'html', 'delivery-status'], true) || ($part->bytes ?? 0) > 262144) { continue; } $raw = imap_fetchbody($connection, $uid, $number, FT_UID | FT_PEEK); if ($raw === false || strlen($raw) > 262144) { continue; } $decoded = $this->decode($raw, (int) ($part->encoding ?? 0), $part->parameters ?? []); if ($subtype === 'delivery-status') { $dsn .= $decoded; } elseif ($subtype === 'html' && $htmlText === '') { $htmlText = html_entity_decode(strip_tags(preg_replace('/<(script|style)\b[^>]*>.*?<\/\1>/is', '', $decoded)), ENT_QUOTES | ENT_HTML5, 'UTF-8'); } elseif ($text === '') { $text = $decoded; } } return ['text' => mb_substr($text ?: $htmlText, 0, 65536), 'dsn' => $dsn]; } private function parts(object $part, string $number = ''): array { if (!empty($part->parts)) { $parts = []; foreach ($part->parts as $index => $child) { $parts = array_merge($parts, $this->parts($child, $number === '' ? (string) ($index + 1) : $number . '.' . ($index + 1))); } return $parts; } return [[$part, $number ?: '1']]; } public function decode(string $raw, int $encoding, array $parameters): string { $text = match ($encoding) { 3 => base64_decode($raw, true) ?: '', 4 => quoted_printable_decode($raw), default => $raw }; foreach ($parameters as $parameter) { if (strtolower($parameter->attribute ?? '') === 'charset') { try { $text = mb_convert_encoding($text, 'UTF-8', $parameter->value); } catch (\ValueError $e) { /* keep original */ } } } return mb_convert_encoding($text, 'UTF-8', 'UTF-8'); } public function recordBounce(string $dsn, array $headers): void { if (!preg_match('/^Action:\s*failed\s*$/mi', $dsn)) { return; } preg_match('/^Original-Message-ID:\s*\r\n]+)>?\s*$/mi', $dsn, $id); $messageId = trim($id[1] ?? ($headers['in-reply-to'] ?? ''), '<> '); preg_match('/^Final-Recipient:\s*rfc822;\s*([^\s\r\n]+)/mi', $dsn, $recipient); if (!$messageId || !isset($recipient[1])) { return; } // Associate a DSN with both the original message ID and recipient, never by subject. MailMessage::where('direction', 'out')->where('channel', 'conventional') ->where('provider_id', $messageId)->where('email', $recipient[1]) ->whereIn('status', ['submitted', 'unknown'])->update(['status' => 'bounced', 'error_code' => 'conventional_permanent_bounce']); } }