fix(maildir): 同步邮件已读状态到 maildir 标记
- 读取邮件时同步更新 maildir 的 `S` 标记,避免数据库已读状态与文件名不同步。 - 写入邮件到 `maildir` 时保留已有非托管标记,并确保已读/星标标记正确拼接。 - 补充测试,校验已发送邮件和草稿的原始路径包含已读标记。
This commit is contained in:
@@ -2418,6 +2418,9 @@ func TestWebmailSentWritesMaildirSent(t *testing.T) {
|
|||||||
if !strings.Contains(filepath.ToSlash(rawPath), "/.Sent/cur/") {
|
if !strings.Contains(filepath.ToSlash(rawPath), "/.Sent/cur/") {
|
||||||
t.Fatalf("raw_path=%q, want .Sent/cur", rawPath)
|
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)
|
raw, err := os.ReadFile(rawPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -2506,6 +2509,9 @@ func TestDraftWritesAndUpdatesMaildirDrafts(t *testing.T) {
|
|||||||
if !strings.Contains(filepath.ToSlash(rawPath), "/.Drafts/cur/") {
|
if !strings.Contains(filepath.ToSlash(rawPath), "/.Drafts/cur/") {
|
||||||
t.Fatalf("raw_path=%q, want .Drafts/cur", rawPath)
|
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
|
oldRawPath := rawPath
|
||||||
|
|
||||||
payload["subject"] = "draft two"
|
payload["subject"] = "draft two"
|
||||||
|
|||||||
@@ -328,6 +328,10 @@ func (a *App) handleMailMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.URL.Query().Get("markRead") != "0" && !msg.IsRead && userHasPermission(currentUser(r), PermissionMailOrganize) {
|
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)
|
_, _ = 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
|
msg.IsRead = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ func (a *App) writeRawMessageToMaildirFolder(ctx context.Context, messageID, fol
|
|||||||
filename := maildirFilename(messageID, state.MessageID)
|
filename := maildirFilename(messageID, state.MessageID)
|
||||||
tmpPath := filepath.Join(folderBase, "tmp", filename)
|
tmpPath := filepath.Join(folderBase, "tmp", filename)
|
||||||
finalPath := filepath.Join(folderBase, subdir, filename)
|
finalPath := filepath.Join(folderBase, subdir, filename)
|
||||||
|
finalPath = maildirPathWithFlags(finalPath, state.IsRead, state.IsStarred)
|
||||||
if err := os.WriteFile(tmpPath, raw, 0o600); err != nil {
|
if err := os.WriteFile(tmpPath, raw, 0o600); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -490,15 +491,17 @@ func maildirPathWithFlags(path string, read, starred bool) string {
|
|||||||
}
|
}
|
||||||
base := name
|
base := name
|
||||||
sep := maildirFlagSeparator()
|
sep := maildirFlagSeparator()
|
||||||
|
existingFlags := ""
|
||||||
if idx := strings.LastIndex(base, sep); idx >= 0 {
|
if idx := strings.LastIndex(base, sep); idx >= 0 {
|
||||||
|
existingFlags = base[idx+len(sep):]
|
||||||
base = base[:idx]
|
base = base[:idx]
|
||||||
}
|
}
|
||||||
flags := ""
|
flags := preserveMaildirFlags(existingFlags, "SF")
|
||||||
if read {
|
if read {
|
||||||
flags += "S"
|
flags = appendMaildirFlag(flags, 'S')
|
||||||
}
|
}
|
||||||
if starred {
|
if starred {
|
||||||
flags += "F"
|
flags = appendMaildirFlag(flags, 'F')
|
||||||
}
|
}
|
||||||
if flags != "" {
|
if flags != "" {
|
||||||
base += sep + flags
|
base += sep + flags
|
||||||
@@ -506,6 +509,24 @@ func maildirPathWithFlags(path string, read, starred bool) string {
|
|||||||
return filepath.Join(dir, base)
|
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 {
|
func maildirFlagSeparator() string {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return "!2,"
|
return "!2,"
|
||||||
|
|||||||
Reference in New Issue
Block a user