dpl/app/Services/MailFlow/IncomingContent.php

64 lines
3.0 KiB
PHP

<?php
namespace App\Services\MailFlow;
use App\Models\MailMessage;
class IncomingContent
{
public function read($connection, int $uid, object $structure): array
{
$text = ''; $htmlText = ''; $dsn = '';
foreach ($this->parts($structure) as [$part, $number]) {
$subtype = strtolower($part->subtype ?? '');
if (!in_array($subtype, ['plain', 'html', 'delivery-status'], true) || ($part->bytes ?? 0) > 262144) {
continue;
}
$raw = imap_fetchbody($connection, $uid, $number, FT_UID | FT_PEEK);
if ($raw === false || strlen($raw) > 262144) { continue; }
$decoded = $this->decode($raw, (int) ($part->encoding ?? 0), $part->parameters ?? []);
if ($subtype === 'delivery-status') { $dsn .= $decoded; }
elseif ($subtype === 'html' && $htmlText === '') {
$htmlText = html_entity_decode(strip_tags(preg_replace('/<(script|style)\b[^>]*>.*?<\/\1>/is', '', $decoded)), ENT_QUOTES | ENT_HTML5, 'UTF-8');
} elseif ($text === '') { $text = $decoded; }
}
return ['text' => mb_substr($text ?: $htmlText, 0, 65536), 'dsn' => $dsn];
}
private function parts(object $part, string $number = ''): array
{
if (!empty($part->parts)) {
$parts = [];
foreach ($part->parts as $index => $child) {
$parts = array_merge($parts, $this->parts($child, $number === '' ? (string) ($index + 1) : $number . '.' . ($index + 1)));
}
return $parts;
}
return [[$part, $number ?: '1']];
}
public function decode(string $raw, int $encoding, array $parameters): string
{
$text = match ($encoding) { 3 => base64_decode($raw, true) ?: '', 4 => quoted_printable_decode($raw), default => $raw };
foreach ($parameters as $parameter) {
if (strtolower($parameter->attribute ?? '') === 'charset') {
try { $text = mb_convert_encoding($text, 'UTF-8', $parameter->value); } catch (\ValueError $e) { /* keep original */ }
}
}
return mb_convert_encoding($text, 'UTF-8', 'UTF-8');
}
public function recordBounce(string $dsn, array $headers): void
{
if (!preg_match('/^Action:\s*failed\s*$/mi', $dsn)) { return; }
preg_match('/^Original-Message-ID:\s*<?([^<>\r\n]+)>?\s*$/mi', $dsn, $id);
$messageId = trim($id[1] ?? ($headers['in-reply-to'] ?? ''), '<> ');
preg_match('/^Final-Recipient:\s*rfc822;\s*([^\s\r\n]+)/mi', $dsn, $recipient);
if (!$messageId || !isset($recipient[1])) { return; }
// Associate a DSN with both the original message ID and recipient, never by subject.
MailMessage::where('direction', 'out')->where('channel', 'conventional')
->where('provider_id', $messageId)->where('email', $recipient[1])
->whereIn('status', ['submitted', 'unknown'])->update(['status' => 'bounced', 'error_code' => 'conventional_permanent_bounce']);
}
}