From c68d03b2115ec366d2ef96f31686514c3e0074c5 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Fri, 29 May 2026 21:12:58 +0200 Subject: [PATCH 1/3] 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; From 034d0a09ce4dc37de6af445a95d2a01c2800b4df Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 30 May 2026 16:13:55 +0200 Subject: [PATCH 2/3] Fix to skip unusable Maildir entries * Note: Ignore zero-byte or unreadable Maildir files when listing messages, log skipped entries, and treat cached zero-byte reads as missing to avoid blank rows and inflated counts. --- mailboxes/boxes-lib.pl | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/mailboxes/boxes-lib.pl b/mailboxes/boxes-lib.pl index a558db107..6514561f6 100755 --- a/mailboxes/boxes-lib.pl +++ b/mailboxes/boxes-lib.pl @@ -2207,6 +2207,32 @@ else { # Flagged as deleted by IMAP .. skip next; } + # Skip entries that cannot be read as messages. + local $path = "$_[0]/$d/$f"; + local @fst = stat($path); + if (!@fst) { + &error_stderr("Skipping Maildir file ". + "$path : stat failed : ". + "$!"); + next; + } + if (!$fst[7]) { + &error_stderr("Skipping Maildir file ". + "$path : file is zero ". + "bytes"); + next; + } + if (!-r _) { + my $m = sprintf("%04o",$fst[2] & 07777); + my $o = getpwuid($fst[4]) || $fst[4]; + my $g = getgrgid($fst[5]) || $fst[5]; + &error_stderr("Skipping Maildir file ". + "$path : not readable by". + " current user, owner=". + "$o($fst[4]):$g($fst[5])". + " mode=$m"); + next; + } push(@shorts, "$d/$f") } closedir(DIR); @@ -2682,9 +2708,11 @@ my ($file, $headersonly) = @_; # Open and read the mail file &open_as_mail_user(MAIL, $file) || return undef; my $mail = &read_mail_fh(MAIL, 0, $headersonly); -$mail->{'file'} = $file; close(MAIL); + local @st = stat($file); +return undef if (@st && !$st[7]); +$mail->{'file'} = $file; $mail->{'size'} = $st[7]; $mail->{'time'} = $st[9]; $mail->{'ctime'} = $st[10]; From 1eb4eb85a7435b7b6221f36bb010cfd26ffcec4b Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 30 May 2026 21:22:38 +0200 Subject: [PATCH 3/3] Fix to check empty mail files before opening --- mailboxes/boxes-lib.pl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mailboxes/boxes-lib.pl b/mailboxes/boxes-lib.pl index 6514561f6..5e5b8447e 100755 --- a/mailboxes/boxes-lib.pl +++ b/mailboxes/boxes-lib.pl @@ -2706,12 +2706,12 @@ sub read_mail_file my ($file, $headersonly) = @_; # Open and read the mail file +local @st = stat($file); +return undef if (@st && !$st[7]); &open_as_mail_user(MAIL, $file) || return undef; my $mail = &read_mail_fh(MAIL, 0, $headersonly); close(MAIL); -local @st = stat($file); -return undef if (@st && !$st[7]); $mail->{'file'} = $file; $mail->{'size'} = $st[7]; $mail->{'time'} = $st[9];