Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion organizer/src/class/Imap/ImapWrapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,28 @@ function() use ($imap_stream, $msg_number, $options) {
public function utf8(string $text): string {
$textPreview = strlen($text) > 50 ? substr($text, 0, 50) . '...' : $text;
$this->logDebug('utf8', ["text: $textPreview"]);
return \imap_utf8($text);

// Clear any previous errors - imap_errors() returns errors and clears them
\imap_errors();

$result = \imap_utf8($text);

// Check if there were any new errors (e.g., invalid quoted-printable sequence)
// imap_errors() will return errors from imap_utf8() call and clear them
$errors = \imap_errors();
if ($errors !== false && count($errors) > 0) {
// Log the error but don't throw an exception - return the original text
// This handles cases where MIME-encoded headers are malformed
$errorMsg = implode(', ', $errors);
error_log("IMAP utf8 conversion warning for text '$textPreview': $errorMsg");
// If imap_utf8 failed, try mb_decode_mimeheader as a fallback
if (strpos($text, '=?') !== false) {
return mb_decode_mimeheader($text);
}
return $text;
}

return $result;
}

public function fetchstructure(mixed $imap_stream, int $msg_number, int $options = 0): object {
Expand Down
6 changes: 6 additions & 0 deletions organizer/src/class/ThreadFolderManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ public function archiveThreadFolder($entityThreads, $thread): void {
public static function getThreadEmailFolder($entity_id, $thread): string {
$title = $entity_id . ' - ' . $thread->title;

// Decode any MIME-encoded headers (e.g., =?UTF-8?B?...?= or =?iso-8859-1?Q?...?=)
// Only decode if the string contains MIME encoding markers to avoid corrupting UTF-8 text
if (strpos($title, '=?') !== false) {
$title = mb_decode_mimeheader($title);
}
Comment on lines 70 to +76

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MIME decoding happens on the combined string $entity_id . ' - ' . $thread->title. If the entity_id contains the marker =?, it would trigger unnecessary MIME decoding. While entity_ids are unlikely to contain such markers in practice, consider checking for MIME markers specifically in $thread->title before combining, or document this assumption to prevent future issues.

Suggested change
$title = $entity_id . ' - ' . $thread->title;
// Decode any MIME-encoded headers (e.g., =?UTF-8?B?...?= or =?iso-8859-1?Q?...?=)
// Only decode if the string contains MIME encoding markers to avoid corrupting UTF-8 text
if (strpos($title, '=?') !== false) {
$title = mb_decode_mimeheader($title);
}
// Decode MIME-encoded thread title (if any) before combining with entity_id
$decodedTitle = $thread->title;
// Decode any MIME-encoded headers (e.g., =?UTF-8?B?...?= or =?iso-8859-1?Q?...?=)
// Only decode if the string contains MIME encoding markers to avoid corrupting UTF-8 text
if (strpos($decodedTitle, '=?') !== false) {
$decodedTitle = mb_decode_mimeheader($decodedTitle);
}
$title = $entity_id . ' - ' . $decodedTitle;

Copilot uses AI. Check for mistakes.

// Replace Nordic characters
$title = str_replace(
['Æ', 'Ø', 'Å', 'æ', 'ø', 'å'],
Expand Down
75 changes: 75 additions & 0 deletions organizer/src/tests/ImapWrapperUtf8Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

use PHPUnit\Framework\TestCase;
use Imap\ImapWrapper;

require_once __DIR__ . '/../class/Imap/ImapWrapper.php';

class ImapWrapperUtf8Test extends TestCase
{
public function testUtf8WithNormalText()
{
$wrapper = new ImapWrapper();
$text = 'Normal text';
$result = $wrapper->utf8($text);

$this->assertEquals($text, $result);
}

public function testUtf8WithValidMimeEncodedString()
{
$wrapper = new ImapWrapper();
// Valid MIME-encoded string: "Test Title" in UTF-8 base64
$text = '=?UTF-8?B?VGVzdCBUaXRsZQ==?=';
$result = $wrapper->utf8($text);

// imap_utf8() should decode this properly
$this->assertEquals('Test Title', $result);
}

public function testUtf8WithMalformedMimeEncodedString()
{
$wrapper = new ImapWrapper();
// Malformed MIME-encoded string from the error report
$text = '=?iso-8859-1?Q?axz5ZAFym5luZoxqfgeds8xO/E+PtRicCu3CXJTfFFl7/aub8+5SDA59PR? =?iso-';

// This should not throw an exception
$result = $wrapper->utf8($text);

// Should return a string (may be the same as input if imap_utf8 can't decode it)
$this->assertIsString($result);
}
Comment on lines +30 to +41

Copilot AI Jan 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test verifies that the function doesn't throw an exception and returns a string, but it doesn't validate what the decoded output is for the malformed MIME string. Consider adding an assertion to check that MIME markers are removed from the result, similar to the ThreadFolderManagerTest assertions at lines 284-286, to ensure the fallback decoding logic works as expected.

Copilot uses AI. Check for mistakes.

public function testUtf8WithEmptyString()
{
$wrapper = new ImapWrapper();
$text = '';
$result = $wrapper->utf8($text);

$this->assertEquals('', $result);
}

public function testUtf8WithUtf8Text()
{
$wrapper = new ImapWrapper();
// UTF-8 text with special characters
$text = 'Tëst wîth spëcîal çhâracters';
$result = $wrapper->utf8($text);

// Should return the text as-is or decoded
$this->assertIsString($result);
}

public function testUtf8WithNordicCharacters()
{
$wrapper = new ImapWrapper();
// Nordic characters
$text = 'Æble Øre Åre';
$result = $wrapper->utf8($text);

// Should handle Nordic characters properly
$this->assertIsString($result);
$this->assertStringContainsString('ble', $result);
$this->assertStringContainsString('re', $result);
}
}
40 changes: 40 additions & 0 deletions organizer/src/tests/ThreadFolderManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,44 @@ public function testCreateRequiredFoldersWithConcurrentOperations() {

$this->threadFolderManager->createRequiredFolders($threads);
}

public function testGetThreadEmailFolderWithMimeEncodedTitle() {
$entityThreads = (object)[
'entity_id' => 'Test',
'threads' => []
];

// Test with a malformed MIME-encoded string (from the error report)
$thread = (object)[
'title' => '=?iso-8859-1?Q?axz5ZAFym5luZoxqfgeds8xO/E+PtRicCu3CXJTfFFl7/aub8+5SDA59PR? =?iso-',
'archived' => false
];

$folder = $this->threadFolderManager->getThreadEmailFolder($entityThreads->entity_id, $thread);

// Verify MIME-encoded strings are decoded and sanitized
// The malformed MIME header should be decoded as much as possible
$this->assertStringStartsWith('INBOX.Test - ', $folder);
// Should not contain MIME encoding markers
$this->assertStringNotContainsString('=?', $folder);
$this->assertStringNotContainsString('?=', $folder);
}

public function testGetThreadEmailFolderWithValidMimeEncodedTitle() {
$entityThreads = (object)[
'entity_id' => 'Test',
'threads' => []
];

// Test with a valid MIME-encoded string
$thread = (object)[
'title' => '=?UTF-8?B?VGVzdCBUaXRsZQ==?=', // "Test Title" in base64
'archived' => false
];

$folder = $this->threadFolderManager->getThreadEmailFolder($entityThreads->entity_id, $thread);

// Verify MIME-encoded strings are decoded properly
$this->assertEquals('INBOX.Test - Test Title', $folder);
}
}