From c68d03b2115ec366d2ef96f31686514c3e0074c5 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Fri, 29 May 2026 21:12:58 +0200 Subject: [PATCH] Fix stale mailbox entries after deleted or moved Refresh stale Maildir and sorted mailbox indexes when messages disappear, avoid rendering missing messages, and keep IMAP sort indexes in sync with mailbox count changes. --- mailboxes/boxes-lib.pl | 27 +++++++++++++++++++++++---- mailboxes/folders-lib.pl | 26 ++++++++++++++++++++++++-- mailboxes/mailboxes-lib.pl | 1 + 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/mailboxes/boxes-lib.pl b/mailboxes/boxes-lib.pl index ac413b75e..a558db107 100755 --- a/mailboxes/boxes-lib.pl +++ b/mailboxes/boxes-lib.pl @@ -2087,10 +2087,20 @@ foreach $f (@files) { $i++; next; } + local $idx = $i++; local $mail = &read_mail_file($f, $_[3]); - $mail->{'idx'} = $i++; - $mail->{'id'} = $f; # ID is relative path, like cur/4535534 - $mail->{'id'} = substr($mail->{'id'}, length($_[0])+1); + if (!$mail && !$_[4]) { + # The cached Maildir file list can be stale if another client + # deleted or moved a message. Re-read it once before returning + # blank entries to the caller. + &flush_maildir_cachefile($_[0]); + return &list_maildir($_[0], $_[1], $_[2], $_[3], 1); + } + if ($mail) { + $mail->{'idx'} = $idx; + $mail->{'id'} = $f; # ID is relative path, like cur/4535534 + $mail->{'id'} = substr($mail->{'id'}, length($_[0])+1); + } push(@rv, $mail); } return @rv; @@ -2110,9 +2120,11 @@ return map { substr($_, length($file)+1) } &get_maildir_files($file); sub select_maildir { local ($file, $ids, $headersonly) = @_; +local $retried = $_[3]; &mark_read_maildir($file); local @files = &get_maildir_files($file); local @rv; +local $missing; foreach my $i (@$ids) { local $path = "$file/$i"; local $mail = &read_mail_file($path, $headersonly); @@ -2139,8 +2151,15 @@ foreach my $i (@$ids) { # Get index in directory $mail->{'idx'} = &indexof($path, @files); } + else { + $missing = 1; + } push(@rv, $mail); } +if ($missing && !$retried) { + &flush_maildir_cachefile($file); + return &select_maildir($file, $ids, $headersonly, 1); + } return @rv; } @@ -2167,7 +2186,7 @@ else { # Check the on-disk cache file local $cachefile = &get_maildir_cachefile($_[0]); local @cst = $cachefile ? stat($cachefile) : ( ); - if ($cst[9] >= $newest) { + if ($cst[9] > $newest) { # Can read the cache open(CACHE, "<", $cachefile); while() { diff --git a/mailboxes/folders-lib.pl b/mailboxes/folders-lib.pl index a699ce1fa..0e35e2b6e 100755 --- a/mailboxes/folders-lib.pl +++ b/mailboxes/folders-lib.pl @@ -174,6 +174,7 @@ elsif ($_[2]->{'type'} == 4) { local $count = $rv[2]; return () if (!$count); $_[2]->{'lastchange'} = $rv[3] if ($rv[3]); + $_[2]->{'mailcount'} = $count; # Work out what range we want local ($start, $end) = &compute_start_end($_[0], $_[1], $count); @@ -458,6 +459,7 @@ elsif ($folder->{'type'} == 4) { } local $h = $irv[1]; local $count = $irv[2]; + $folder->{'mailcount'} = $count; return () if (!$count); $folder->{'lastchange'} = $irv[3] if ($irv[3]); @@ -637,8 +639,9 @@ elsif ($folder->{'type'} == 4) { } local $h = $rv[1]; local $count = $rv[2]; + $folder->{'mailcount'} = $count; return () if (!$count); - $folder->{'lastchange'} = $irv[3] if ($irv[3]); + $folder->{'lastchange'} = $rv[3] if ($rv[3]); @rv = &imap_command($h, "FETCH 1:$count UID"); foreach my $uid (@{$rv[1]}) { @@ -708,6 +711,8 @@ else { sub mailbox_list_mails_sorted { local ($start, $end, $folder, $headersonly, $error, $field, $dir) = @_; +local ($requested_start, $requested_end) = ($start, $end); +local $retried = $_[7]; print DEBUG "mailbox_list_mails_sorted from $start to $end\n"; if (!$field) { # Default to current ordering @@ -738,11 +743,25 @@ local @rv = map { undef } (0 .. scalar(@sorter)-1); local @wantids = map { $sorter[$_] } ($start .. $end); print DEBUG "wantids = ",scalar(@wantids),"\n"; local @mails = &mailbox_select_mails($folder, \@wantids, $headersonly); +local @missing; for(my $i=0; $i<@mails; $i++) { + if (!$mails[$i]) { + push(@missing, $wantids[$i]); + next; + } $rv[$start+$i] = $mails[$i]; print DEBUG "setting $start+$i to ",$mails[$i]," id ",$wantids[$i],"\n"; $mails[$i]->{'sortidx'} = $start+$i; } +if (@missing && !$retried) { + # A sorted IMAP list can contain UIDs for messages that were + # expunged or moved by another client. Force one rebuild so stale + # entries don't render as blank 1969/no-subject rows. + &force_new_index_recheck($folder); + return &mailbox_list_mails_sorted($requested_start, $requested_end, + $folder, $headersonly, $error, + $field, $dir, 1); + } print DEBUG "rv = ",scalar(@rv),"\n"; return @rv; } @@ -808,7 +827,9 @@ local $ifile = &folder_new_sort_index_file($folder); &open_dbm_db($index, $ifile, 0600); print DEBUG "indexchange=$index->{'lastchange'} folderchange=$folder->{'lastchange'}\n"; if ($index->{'lastchange'} != $folder->{'lastchange'} || - !$folder->{'lastchange'}) { + !$folder->{'lastchange'} || + (defined($folder->{'mailcount'}) && + $index->{'mailcount'} != $folder->{'mailcount'})) { # The mail file has changed .. get IDs and update the index with any # that are missing local @ids = &mailbox_idlist($folder); @@ -823,6 +844,7 @@ if ($index->{'lastchange'} != $folder->{'lastchange'} || local @mails = scalar(@newids) ? &mailbox_select_mails($folder, \@newids, 1) : ( ); foreach my $mail (@mails) { + next if (!$mail || !defined($mail->{'id'})); foreach my $f (@index_fields) { if ($f eq "date") { # Convert date to Unix time diff --git a/mailboxes/mailboxes-lib.pl b/mailboxes/mailboxes-lib.pl index 98cebc09b..8a1f2389e 100755 --- a/mailboxes/mailboxes-lib.pl +++ b/mailboxes/mailboxes-lib.pl @@ -1209,6 +1209,7 @@ print &ui_columns_start(\@hcols, 100, 0, \@tds); # Show rows for actual mail messages my $i = 0; foreach my $mail (@mail) { + next if (!$mail); local $idx = $mail->{'idx'}; local $cols = 0; local @cols;