64 lines
3.0 KiB
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']);
|
|
}
|
|
}
|