mailwolt/app/Services/SandboxMailParser.php

146 lines
4.8 KiB
PHP

<?php
namespace App\Services;
use App\Models\SandboxMail;
class SandboxMailParser
{
public function parseAndStore(string $raw, array $recipients = []): SandboxMail
{
[$headerBlock, $body] = $this->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);
}
}