VOXMail · Sync Policy

Sync Policy Technical Report

mbsync Orphan File Resolution

00 · Overview

This report documents a defect in the VOXMail offline mailbox sync pipeline. Under the current mbsync configuration, deletions performed on the remote IMAP server are not reconciled against the local Maildir, which leaves orphaned files behind and inflates unread counts. It describes the observed behavior, the intended behavior, and the minimal configuration and indexer changes required to resolve it.

01

observed

Current Behavior

  1. 01

    IMAP synchronization runs on mbsync with Sync All, Remove None and Expunge None policies.

  2. 02

    When a message is moved or deleted on the remote IMAP server (for example Gmail), the remote UID mapping is pruned from .mbsyncstate, but the local Maildir file stays on disk carrying a trashed flag (:2,T).

  3. 03

    Because mbsync no longer tracks that UID, it treats the local file as untracked rather than deleted, leaving ghost files in the local Inbox.

  4. 04

    The mail indexer reads local Maildir flags; files flagged T (trashed) without the S (Seen) flag are counted as unread, producing persistent phantom unread counts.

02

wanted

Desired Behavior

  1. 01

    When a message is removed from the remote Inbox (archived, moved to Trash, or deleted), the local copy in Inbox/cur is expunged automatically on the next sync pass.

  2. 02

    Local Inbox unread counts reflect the remote inbox accurately, with no accumulation of ghost files.

  3. 03

    Flag sync operates bidirectionally without preserving orphaned local files.

03

Changes Required to Fix

Inbox channel

internal/mailconfig/mbsync.go

118
R-Remove NoneR+Remove Near
120
E-Expunge NoneE+Expunge Near

Folders channel

internal/mailconfig/mbsync.go

134
R-Remove NoneR+Remove Near
136
E-Expunge NoneE+Expunge Near

Sync policy unit test

internal/mailconfig/mbsync_test.go · line 21

21
E-Expunge NoneE+Expunge Near

Indexer logic

internal/store/mail.go

Exclude messages flagged T (trashed) from the Inbox unread count calculation, so ghost files no longer surface as unread.

// unreadCount counts local messages missing the Seen (S) flag.
// Trashed (T) files are excluded - they are orphaned and never read.
func unreadCount(messages []*MaildirMessage) int {
    n := 0
    for _, m := range messages {
        if m.flags.Has("S") || m.flags.Has("T") {
            continue
        }
        n++
    }
    return n
}

04 · Verification

How this is confirmed

  1. 01

    Update the unit test assertion in internal/mailconfig/mbsync_test.go to expect Expunge Near in place of Expunge None.

  2. 02

    Run the full test suite. All config generation tests must still pass against the updated assertion.

  3. 03

    Expected result: with both channel channels set to Remove Near and Expunge Near, mbsync expunges local Inbox files whose remote UID has been removed, and unread counts no longer include orphaned trashed files.