, * cc: array, * subject: string, * body_text: string, * body_html: ?string, * is_pec: bool, * attachments: array * } */ public function parse(string $rawEml): array { $rawEml = str_replace(["\r\n", "\r"], "\n", $rawEml); $headerBodySplit = explode("\n\n", $rawEml, 2); $rawHeaders = $headerBodySplit[0] ?? ''; $rawBody = $headerBodySplit[1] ?? ''; $headers = $this->parseHeaders($rawHeaders); $fromStr = $headers['from'] ?? ''; $from = $this->parseAddress($fromStr); $toStr = $headers['to'] ?? ''; $to = $this->parseAddressList($toStr); $ccStr = $headers['cc'] ?? ''; $cc = $this->parseAddressList($ccStr); $subject = $this->decodeMimeHeader($headers['subject'] ?? '(Nessun oggetto)'); $dateStr = $headers['date'] ?? null; $date = null; if ($dateStr) { try { $date = Carbon::parse($dateStr); } catch (\Throwable) { $date = now(); } } else { $date = now(); } $messageId = trim($headers['message-id'] ?? ''); $messageId = trim($messageId, '<>'); $contentType = $headers['content-type'] ?? 'text/plain'; $contentTransferEncoding = $headers['content-transfer-encoding'] ?? '7bit'; $isPec = false; if ( str_contains(strtolower($headers['x-trasporto'] ?? ''), 'pec') || str_contains(strtolower($headers['x-ricevuta'] ?? ''), 'accettazione') || str_contains(strtolower($headers['x-ricevuta'] ?? ''), 'avvenuta-consegna') || str_contains(strtolower($headers['x-tipo-ricevuta'] ?? ''), 'pec') || str_contains(strtolower($fromStr), 'pec') || str_contains(strtolower($fromStr), 'legalmail') || str_contains(strtolower($fromStr), 'postecert') || str_contains(strtolower($fromStr), 'arubapec') ) { $isPec = true; } $parsedParts = $this->parseBodyParts($rawBody, $contentType, $contentTransferEncoding); return [ 'message_id' => $messageId ?: null, 'date' => $date, 'from' => $from, 'to' => $to, 'cc' => $cc, 'subject' => $subject, 'body_text' => $parsedParts['text'], 'body_html' => $parsedParts['html'], 'is_pec' => $isPec, 'attachments' => $parsedParts['attachments'], ]; } private function parseHeaders(string $rawHeaders): array { $headers = []; $lines = explode("\n", $rawHeaders); $currentHeader = null; foreach ($lines as $line) { if ($line === '') { continue; } if (preg_match('/^[ \t]+/', $line)) { if ($currentHeader !== null) { $headers[$currentHeader] .= ' ' . trim($line); } } elseif (preg_match('/^([^:]+):(.*)$/', $line, $matches)) { $currentHeader = strtolower(trim($matches[1])); $headers[$currentHeader] = trim($matches[2]); } } return $headers; } private function parseAddress(string $addr): array { $addr = trim($addr); if (preg_match('/^(.*?)\s*<([^>]+)>$/', $addr, $m)) { return [ 'name' => $this->decodeMimeHeader(trim($m[1], " \t\n\r\0\x0B\"'")), 'email' => trim($m[2]), ]; } return [ 'name' => '', 'email' => trim($addr, " \t\n\r\0\x0B\"'<>"), ]; } private function parseAddressList(string $addrList): array { if (trim($addrList) === '') { return []; } $items = explode(',', $addrList); $result = []; foreach ($items as $item) { $parsed = $this->parseAddress($item); if ($parsed['email'] !== '') { $result[] = $parsed; } } return $result; } private function decodeMimeHeader(string $value): string { if (function_exists('mb_decode_mimeheader')) { return mb_decode_mimeheader($value); } if (function_exists('iconv_mime_decode')) { return iconv_mime_decode($value, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, 'UTF-8'); } return $value; } private function parseBodyParts(string $body, string $contentType, string $transferEncoding): array { $result = [ 'text' => '', 'html' => null, 'attachments' => [], ]; if (preg_match('/boundary=["\']?([^"\';]+)["\']?/i', $contentType, $matches)) { $boundary = $matches[1]; $parts = explode('--' . $boundary, $body); foreach ($parts as $part) { $part = trim($part); if ($part === '' || $part === '--') { continue; } $subSplit = explode("\n\n", $part, 2); $subHeadersRaw = $subSplit[0] ?? ''; $subBodyRaw = $subSplit[1] ?? ''; $subHeaders = $this->parseHeaders($subHeadersRaw); $subContentType = $subHeaders['content-type'] ?? 'text/plain'; $subTransferEncoding = $subHeaders['content-transfer-encoding'] ?? '7bit'; $subContentDisposition = $subHeaders['content-disposition'] ?? ''; $filename = null; if (preg_match('/filename=["\']?([^"\';]+)["\']?/i', $subContentDisposition . ';' . $subContentType, $fnMatches)) { $filename = $this->decodeMimeHeader($fnMatches[1]); } elseif (preg_match('/name=["\']?([^"\';]+)["\']?/i', $subContentType, $fnMatches)) { $filename = $this->decodeMimeHeader($fnMatches[1]); } if (str_contains(strtolower($subContentType), 'multipart/')) { $nested = $this->parseBodyParts($subBodyRaw, $subContentType, $subTransferEncoding); if ($nested['text'] !== '' && $result['text'] === '') { $result['text'] = $nested['text']; } if ($nested['html'] !== null && $result['html'] === null) { $result['html'] = $nested['html']; } foreach ($nested['attachments'] as $att) { $result['attachments'][] = $att; } continue; } $decodedBody = $this->decodeContent($subBodyRaw, $subTransferEncoding); $isAttachment = (bool) $filename || str_contains(strtolower($subContentDisposition), 'attachment'); if ($isAttachment) { $fn = $filename ?: ('allegato_' . (count($result['attachments']) + 1)); $cleanType = trim(explode(';', $subContentType)[0]); $result['attachments'][] = [ 'filename' => $fn, 'content_type' => $cleanType ?: 'application/octet-stream', 'size' => strlen($decodedBody), 'content' => $decodedBody, 'is_inline' => str_contains(strtolower($subContentDisposition), 'inline'), ]; } elseif (str_contains(strtolower($subContentType), 'text/html')) { $result['html'] = $decodedBody; if ($result['text'] === '') { $result['text'] = trim(strip_tags($decodedBody)); } } elseif (str_contains(strtolower($subContentType), 'text/plain')) { $result['text'] = $decodedBody; } else { $fn = $filename ?: ('file_' . (count($result['attachments']) + 1)); $cleanType = trim(explode(';', $subContentType)[0]); $result['attachments'][] = [ 'filename' => $fn, 'content_type' => $cleanType ?: 'application/octet-stream', 'size' => strlen($decodedBody), 'content' => $decodedBody, 'is_inline' => false, ]; } } } else { $decodedBody = $this->decodeContent($body, $transferEncoding); if (str_contains(strtolower($contentType), 'text/html')) { $result['html'] = $decodedBody; $result['text'] = trim(strip_tags($decodedBody)); } else { $result['text'] = $decodedBody; } } return $result; } private function decodeContent(string $data, string $encoding): string { $encoding = strtolower(trim($encoding)); if ($encoding === 'base64') { return (string) base64_decode($data); } if ($encoding === 'quoted-printable') { return (string) quoted_printable_decode($data); } return $data; } }