diff --git a/src/Folder.php b/src/Folder.php index 341fc66..d46d745 100644 --- a/src/Folder.php +++ b/src/Folder.php @@ -7,6 +7,7 @@ use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; use DirectoryTree\ImapEngine\Exceptions\Exception; use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException; +use DirectoryTree\ImapEngine\Support\Str; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\ItemNotFoundException; use JsonSerializable; @@ -62,7 +63,9 @@ public function delimiter(): string */ public function name(): string { - return last(explode($this->delimiter, $this->path)); + return Str::decodeUtf7Imap( + last(explode($this->delimiter, $this->path)) + ); } /** diff --git a/src/Support/Str.php b/src/Support/Str.php index 9d09076..b7daa9c 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -130,4 +130,73 @@ public static function escape(string $string): string // Escape backslashes first to avoid double-escaping and then escape double quotes. return str_replace(['\\', '"'], ['\\\\', '\\"'], $string); } + + /** + * Decode a modified UTF-7 string (IMAP specific) to UTF-8. + */ + public static function decodeUtf7Imap(string $string): string + { + // If the string doesn't contain any '&' character, it's not UTF-7 encoded. + if (! str_contains($string, '&')) { + return $string; + } + + // Handle the special case of '&-' which represents '&' in UTF-7. + if ($string === '&-') { + return '&'; + } + + // Direct implementation of IMAP's modified UTF-7 decoding. + return preg_replace_callback('/&([^-]*)-?/', function ($matches) { + // If it's just an ampersand. + if ($matches[1] === '') { + return '&'; + } + + // If it's the special case for ampersand. + if ($matches[1] === '-') { + return '&'; + } + + // Convert modified base64 to standard base64. + $base64 = strtr($matches[1], ',', '/'); + + // Add padding if necessary. + switch (strlen($base64) % 4) { + case 1: $base64 .= '==='; + break; + case 2: $base64 .= '=='; + break; + case 3: $base64 .= '='; + break; + } + + // Decode base64 to binary. + $binary = base64_decode($base64, true); + + if ($binary === false) { + // If decoding fails, return the original string. + return '&'.$matches[1].($matches[2] ?? ''); + } + + $result = ''; + + // Convert binary UTF-16BE to UTF-8. + for ($i = 0; $i < strlen($binary); $i += 2) { + if (isset($binary[$i + 1])) { + $char = (ord($binary[$i]) << 8) | ord($binary[$i + 1]); + + if ($char < 0x80) { + $result .= chr($char); + } elseif ($char < 0x800) { + $result .= chr(0xC0 | ($char >> 6)).chr(0x80 | ($char & 0x3F)); + } else { + $result .= chr(0xE0 | ($char >> 12)).chr(0x80 | (($char >> 6) & 0x3F)).chr(0x80 | ($char & 0x3F)); + } + } + } + + return $result; + }, $string); + } } diff --git a/tests/Unit/FolderTest.php b/tests/Unit/FolderTest.php new file mode 100644 index 0000000..81552bc --- /dev/null +++ b/tests/Unit/FolderTest.php @@ -0,0 +1,52 @@ +name())->toBe('Корзина'); + + // The path should remain as is (UTF-7 encoded). + expect($folder->path())->toBe('[Gmail]/&BBoEPgRABDcEOAQ9BDA-'); +}); + +test('it preserves existing UTF-8 characters in folder names', function () { + $mailbox = Mailbox::make(); + + // Create a folder with a name that already contains UTF-8 characters. + $utf8FolderName = 'Привет'; + + $folder = new Folder( + mailbox: $mailbox, + path: '[Gmail]/'.$utf8FolderName, + flags: ['\\HasNoChildren'], + delimiter: '/' + ); + + // The name should remain unchanged + expect($folder->name())->toBe($utf8FolderName); + + // Test with a mix of UTF-8 characters from different languages. + $mixedUtf8FolderName = 'Привет_你好_こんにちは'; + + $mixedFolder = new Folder( + mailbox: $mailbox, + path: '[Gmail]/'.$mixedUtf8FolderName, + flags: ['\\HasNoChildren'], + delimiter: '/' + ); + + // The name should remain unchanged. + expect($mixedFolder->name())->toBe($mixedUtf8FolderName); +}); diff --git a/tests/Unit/Support/StrTest.php b/tests/Unit/Support/StrTest.php index e4a1786..baa87b0 100644 --- a/tests/Unit/Support/StrTest.php +++ b/tests/Unit/Support/StrTest.php @@ -101,3 +101,45 @@ expect($result)->toEqual($expected); }); + +test('decodeUtf7Imap decodes UTF-7 encoded folder names', function () { + // Russian Cyrillic example from the bug report. + $encoded = '&BBoEPgRABDcEOAQ9BDA-'; + $decoded = 'Корзина'; + + expect(Str::decodeUtf7Imap($encoded))->toBe($decoded); +}); + +test('decodeUtf7Imap handles non-encoded strings', function () { + $plainString = 'INBOX'; + + expect(Str::decodeUtf7Imap($plainString))->toBe($plainString); +}); + +test('decodeUtf7Imap handles special characters', function () { + // Ampersand is represented as &- in UTF-7. + $encoded = '&-'; + $decoded = '&'; + + expect(Str::decodeUtf7Imap($encoded))->toBe($decoded); +}); + +test('decodeUtf7Imap handles mixed content', function () { + // Test that the function doesn't modify the non-encoded part. + $encoded = 'Hello &-'; + $decoded = 'Hello &'; + + expect(Str::decodeUtf7Imap($encoded))->toBe($decoded); +}); + +test('decodeUtf7Imap preserves existing UTF-8 characters', function () { + // Test with various UTF-8 characters that should remain unchanged. + $utf8String = 'Привет мир 你好 こんにちは ñáéíóú'; + + // The function should return the string unchanged since it's already UTF-8. + expect(Str::decodeUtf7Imap($utf8String))->toBe($utf8String); + + // Test with a mix of UTF-8 and regular ASCII. + $mixedString = 'Hello Привет 123'; + expect(Str::decodeUtf7Imap($mixedString))->toBe($mixedString); +});