netgescon-day0/app/Services/Posta/EmlParser.php

274 lines
9.7 KiB
PHP

<?php
namespace App\Services\Posta;
use Carbon\Carbon;
class EmlParser
{
/**
* @return array{
* message_id: ?string,
* date: ?Carbon,
* from: array{name: string, email: string},
* to: array<int, array{name: string, email: string}>,
* cc: array<int, array{name: string, email: string}>,
* subject: string,
* body_text: string,
* body_html: ?string,
* is_pec: bool,
* attachments: array<int, array{
* filename: string,
* content_type: string,
* size: int,
* content: string,
* is_inline: bool
* }>
* }
*/
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;
}
}