diff --git a/apps/api/internal/app/app_test.go b/apps/api/internal/app/app_test.go index 40fc6be..e5c391f 100644 --- a/apps/api/internal/app/app_test.go +++ b/apps/api/internal/app/app_test.go @@ -2418,6 +2418,9 @@ func TestWebmailSentWritesMaildirSent(t *testing.T) { if !strings.Contains(filepath.ToSlash(rawPath), "/.Sent/cur/") { t.Fatalf("raw_path=%q, want .Sent/cur", rawPath) } + if !strings.Contains(filepath.Base(rawPath), maildirFlagSeparator()+"S") { + t.Fatalf("sent raw_path missing seen flag: %s", rawPath) + } raw, err := os.ReadFile(rawPath) if err != nil { t.Fatal(err) @@ -2506,6 +2509,9 @@ func TestDraftWritesAndUpdatesMaildirDrafts(t *testing.T) { if !strings.Contains(filepath.ToSlash(rawPath), "/.Drafts/cur/") { t.Fatalf("raw_path=%q, want .Drafts/cur", rawPath) } + if !strings.Contains(filepath.Base(rawPath), maildirFlagSeparator()+"S") { + t.Fatalf("draft raw_path missing seen flag: %s", rawPath) + } oldRawPath := rawPath payload["subject"] = "draft two" diff --git a/apps/api/internal/app/mail_handlers.go b/apps/api/internal/app/mail_handlers.go index 069b063..f7f5a88 100644 --- a/apps/api/internal/app/mail_handlers.go +++ b/apps/api/internal/app/mail_handlers.go @@ -328,6 +328,10 @@ func (a *App) handleMailMessage(w http.ResponseWriter, r *http.Request) { return } if r.URL.Query().Get("markRead") != "0" && !msg.IsRead && userHasPermission(currentUser(r), PermissionMailOrganize) { + read := true + if err := a.updateMessageMaildirFlags(r.Context(), msg.ID, &read, nil); err != nil { + a.log.Warn("failed to update maildir read flag", "message_id", msg.ID, "error", err) + } _, _ = a.db.ExecContext(r.Context(), `UPDATE messages SET is_read=1, updated_at=? WHERE id=?`, a.now().UTC().Format(time.RFC3339Nano), msg.ID) msg.IsRead = true } diff --git a/apps/api/internal/app/maildir_write.go b/apps/api/internal/app/maildir_write.go index c695e11..c47780c 100644 --- a/apps/api/internal/app/maildir_write.go +++ b/apps/api/internal/app/maildir_write.go @@ -120,6 +120,7 @@ func (a *App) writeRawMessageToMaildirFolder(ctx context.Context, messageID, fol filename := maildirFilename(messageID, state.MessageID) tmpPath := filepath.Join(folderBase, "tmp", filename) finalPath := filepath.Join(folderBase, subdir, filename) + finalPath = maildirPathWithFlags(finalPath, state.IsRead, state.IsStarred) if err := os.WriteFile(tmpPath, raw, 0o600); err != nil { return err } @@ -490,15 +491,17 @@ func maildirPathWithFlags(path string, read, starred bool) string { } base := name sep := maildirFlagSeparator() + existingFlags := "" if idx := strings.LastIndex(base, sep); idx >= 0 { + existingFlags = base[idx+len(sep):] base = base[:idx] } - flags := "" + flags := preserveMaildirFlags(existingFlags, "SF") if read { - flags += "S" + flags = appendMaildirFlag(flags, 'S') } if starred { - flags += "F" + flags = appendMaildirFlag(flags, 'F') } if flags != "" { base += sep + flags @@ -506,6 +509,24 @@ func maildirPathWithFlags(path string, read, starred bool) string { return filepath.Join(dir, base) } +func preserveMaildirFlags(flags, managed string) string { + var b strings.Builder + for _, flag := range flags { + if strings.ContainsRune(managed, flag) || strings.ContainsRune(b.String(), flag) { + continue + } + b.WriteRune(flag) + } + return b.String() +} + +func appendMaildirFlag(flags string, flag rune) string { + if strings.ContainsRune(flags, flag) { + return flags + } + return flags + string(flag) +} + func maildirFlagSeparator() string { if runtime.GOOS == "windows" { return "!2,"