274 lines
9.7 KiB
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;
|
|
}
|
|
}
|