146 lines
4.8 KiB
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);
|
|
}
|
|
}
|