aboutsummaryrefslogtreecommitdiffstats
path: root/src/maildirname.h
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-25 11:44:05 +0200
committerDanilo M. <danix@danix.xyz>2026-08-25 11:44:05 +0200
commit82588d51c4ad0c9e46bbd41658785eb0ba77f48b (patch)
treea4c92bf1a050db260f01897a945b94de22bd6f58 /src/maildirname.h
parentcd56144ba0304247812cf61c8b6e435779df29d0 (diff)
downloadqtmaildir-82588d51c4ad0c9e46bbd41658785eb0ba77f48b.tar.gz
qtmaildir-82588d51c4ad0c9e46bbd41658785eb0ba77f48b.zip
fix(compose): resolve a path a sync renamed, at all three read sites
Item 163. mbsync renames an uploaded file to add its `,U=<uid>` infix, and the model's `MessageRef::filePath` was captured when the query ran, so a row loaded before that sync names a file that no longer exists. MimeParser then honestly reports a message it cannot open. MaildirName::resolveRenamed() answers the filesystem question: returns the path unchanged when it still exists, otherwise looks in that one directory for the file whose unique stem matches. mbsync preserves the stem (`<stem>:2,D` becomes `<stem>,U=5:2,D`), which is what makes this safe to do by filename at all. It never recurses, never crosses a folder boundary, and refuses an ambiguous match rather than guessing, since opening or moving the wrong message is worse than reporting none. It lives in MaildirName because that namespace already owns the `,U=` infix and is a pure-value unit testable without a widget. A file that changed FOLDERS is a different question that only the message id can answer, and NotmuchWorker::moveMessages() re-resolves that way already. Three call sites, all of which held a stale path: - The message pane, which reported "(unreadable message)" over a file that was on disk and readable. Cosmetic and self-repairing. - Reply and Forward, refused outright, so the user could not answer a message that was sitting there. - The draft reopen, and this is the half that costs data. The refusal happens BEFORE any composer exists, so the user composes again into a fresh window whose autosave has no previous path to unlink. The old revision survives, each save mints a new Message-ID, and both files reach the server. The unlink machinery was correct throughout and never ran. forDraft() seeds draftPath from the RESOLVED path, never the caller's: seeding the stale one would let the reopen succeed and the unlink still miss, which is the same fork arriving one step later. Covered by five unit tests on the resolver, including the two that keep it honest (a genuinely missing file yields nothing, and a neighbouring message is never matched), and by an integration test that renames the draft the way mbsync does and asserts the file COUNT, which is the shape the fork actually takes. Both mutation-checked; the integration test fails with the reported symptom when the resolution is removed. The stable-Message-ID question is deliberately untouched: it is what turns a stale path into two server-side messages rather than one replaced file, and it wants its own item. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UUQS6n3cmsFrsjCNmwNtf8
Diffstat (limited to 'src/maildirname.h')
-rw-r--r--src/maildirname.h24
1 files changed, 24 insertions, 0 deletions
diff --git a/src/maildirname.h b/src/maildirname.h
index f24bc71..255517d 100644
--- a/src/maildirname.h
+++ b/src/maildirname.h
@@ -38,4 +38,28 @@ namespace MaildirName {
/// what a newly composed draft is.
QString fresh(const QString &oldName);
+/// The file \p path names, or the renamed file that replaced it.
+///
+/// Item 163. mbsync renames an uploaded file to add its `,U=<uid>` infix, and
+/// anything holding the previous name (the model's `MessageRef::filePath`, a
+/// draft's `ComposeContext::draftPath`) then points at a path that no longer
+/// exists. Returns \p path unchanged when it is still there, so the ordinary
+/// case costs one stat and nothing else.
+///
+/// Matched on the UNIQUE STEM, the part before the first `,` or `:`, which
+/// mbsync preserves: `<stem>:2,D` becomes `<stem>,U=5:2,D`. That is what makes
+/// this safe to do by filename at all. The search is confined to the file's
+/// own directory and never recurses, and an ambiguous match (more than one
+/// candidate, which a correct Maildir cannot produce) yields nothing rather
+/// than guessing.
+///
+/// Empty when there is no such file, which every caller must treat as the
+/// genuine "it is gone" it is: recovering silently from a real deletion would
+/// turn a reportable defect into a wrong answer.
+///
+/// This resolves a RENAME, not a MOVE. A file that changed folders is a
+/// different question and belongs to whoever knows the message id;
+/// `NotmuchWorker::moveMessages()` re-resolves that way for item 162.
+QString resolveRenamed(const QString &path);
+
} // namespace MaildirName