Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
MimeDecoderService
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
3 / 3
30
100.00% covered (success)
100.00%
1 / 1
 decodeHeader
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
5
 convertToUtf8
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
21
 decodeTransferEncoding
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Modules\Mail\Application\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Modules\Mail\Domain\Contract\MimeDecoderInterface;
12
13/**
14 * Universal MIME and Character Set Decoder Service.
15 *
16 * Implements robust RFC 2047 decoding, encoding conversions to standard UTF-8 (NFC),
17 * and payload transfer-encoding handling.
18 *
19 * @package App\Modules\Mail\Application\Service
20 */
21final readonly class MimeDecoderService implements MimeDecoderInterface
22{
23    /**
24     * {@inheritdoc}
25     */
26    public function decodeHeader(string $rawHeader): string
27    {
28        if (!str_contains($rawHeader, '=?')) {
29            return $this->convertToUtf8($rawHeader, 'UTF-8');
30        }
31
32        $decoded = iconv_mime_decode($rawHeader, ICONV_MIME_DECODE_CONTINUE_ON_ERROR, 'UTF-8');
33        if ($decoded !== false && !str_contains($decoded, '=?')) {
34            return $decoded;
35        }
36
37        // Regex fallback for folded or non-standard encoded words
38        $pattern = '/=\?([a-zA-Z0-9_\-]+)\?([bBqQ])\?([^?]+)\?=/';
39        return (string) preg_replace_callback($pattern, function (array $m): string {
40            $charset  = strtoupper($m[1]);
41            $encoding = strtoupper($m[2]);
42            $payload  = $m[3];
43
44            $bytes = $encoding === 'B'
45                ? (string) base64_decode($payload, true)
46                : quoted_printable_decode(str_replace('_', ' ', $payload));
47
48            return $this->convertToUtf8($bytes, $charset);
49        }, $rawHeader);
50    }
51
52    /**
53     * {@inheritdoc}
54     */
55    public function convertToUtf8(string $content, string $charset): string
56    {
57        $normalizedCharset = strtoupper(trim($charset));
58        if ($normalizedCharset === '' || $normalizedCharset === 'UTF-8' || $normalizedCharset === 'US-ASCII') {
59            return mb_check_encoding($content, 'UTF-8') ? $content : mb_convert_encoding($content, 'UTF-8');
60        }
61
62        // Map common charset aliases
63        $mappedCharset = match ($normalizedCharset) {
64            'CP1250', 'WINDOWS1250' => 'Windows-1250',
65            'CP1251', 'WINDOWS1251' => 'Windows-1251',
66            'CP1252', 'WINDOWS1252' => 'Windows-1252',
67            'ISO88591', 'ISO-8859-1' => 'ISO-8859-1',
68            'ISO88592', 'ISO-8859-2' => 'ISO-8859-2',
69            default => $normalizedCharset,
70        };
71
72        $result = $content;
73        $mbCharset = match ($mappedCharset) {
74            'Windows-1250' => 'CP1250',
75            'Windows-1251' => 'CP1251',
76            'Windows-1252' => 'CP1252',
77            default        => $mappedCharset,
78        };
79
80        if (function_exists('mb_convert_encoding')) {
81            try {
82                $converted = @mb_convert_encoding($content, 'UTF-8', $mbCharset);
83                if ($converted !== false && mb_check_encoding($converted, 'UTF-8')) {
84                    return $converted;
85                }
86            } catch (\ValueError) {
87                // Ignore unsupported mbstring encodings, proceed to iconv
88            }
89        }
90
91
92        if (function_exists('iconv')) {
93            $converted = @iconv($mappedCharset, 'UTF-8//IGNORE', $content);
94            if ($converted !== false) {
95                $result = $converted;
96            }
97        }
98
99        return $result;
100    }
101
102    /**
103     * {@inheritdoc}
104     */
105    public function decodeTransferEncoding(string $content, string $transferEncoding): string
106    {
107        return match (strtolower(trim($transferEncoding))) {
108            'base64'           => (string) base64_decode(preg_replace('/\s+/', '', $content) ?? '', true),
109            'quoted-printable' => quoted_printable_decode($content),
110            default            => $content,
111        };
112    }
113}