fix(imap): a message that fails to build is retried instead of lost

The catch around buildMessage() logged and moved on without holding the
high-water mark, so lastSeenUid passed the failed UID and it was never fetched
again. The clamp that did exist for empty fetches was per batch and got
overwritten by the next batch's higher UIDs. Now the lowest skipped UID is
tracked for the whole run, both failure paths hold the mark below it, and a
per-mailbox counter (mailbox.failed_uid / failed_uid_attempts, new migration)
lets one message that always fails go after five syncs so it cannot block the
folder.

A present but unparseable Date header made webklex throw while building the
message, failing the fetch for the whole folder. The client now configures a
fallback date (the epoch), and MessageSyncer::dateFromHeader() reads that, or
any throw, as "no date" and uses the arrival time.

Attachment parts are persisted only after the message is fully built; before,
a second attachment that threw left the first part managed and pointing at an
unpersisted message, and the batch flush failed on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Paul LütznerandClaude Opus 5.5 committed 2026-09-23 19:14:14 +02:00
1 parent 63b236b134
commit 07b6724e8c
5 files changed
+207 -19

No files matched your search

+36
View File
@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* A retry counter for the one message in a folder that will not import.
*
* A message that failed to build used to be skipped past for good: the
* high-water mark moved over it and it was never fetched again. It is now held
* inside the next sync's range, and these two columns cap how often, so a
* message that always fails cannot stop the folder importing newer mail.
*/
final class Version20260923101200 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add mailbox.failed_uid and mailbox.failed_uid_attempts.';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE mailbox ADD failed_uid INT DEFAULT NULL');
$this->addSql('ALTER TABLE mailbox ADD failed_uid_attempts INT DEFAULT 0 NOT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE mailbox DROP failed_uid');
$this->addSql('ALTER TABLE mailbox DROP failed_uid_attempts');
}
}