feat(maildir): 支持邮件写入与同步联动

- 为已发送邮件、草稿、收件与规则移动增加 maildir 文件写入、重写和删除流程。
- 补充 SQLite 仅有消息的回填逻辑,并在同步时补齐现有 raw_path。
- 调整 Dovecot 共享邮箱行为为订阅方式,避免重复创建文件夹。
- 新增覆盖发送、草稿、移动、删除和回填场景的测试。
This commit is contained in:
LanQin_
2026-06-24 15:18:06 +08:00
parent adcc822c9a
commit 1788d49a59
9 changed files with 732 additions and 28 deletions
+29 -2
View File
@@ -53,7 +53,7 @@ func (a *App) maildirWorker(ctx context.Context) {
if n, err := a.syncMaildirOnce(ctx); err != nil {
a.log.Warn("initial maildir sync failed", "error", err)
} else if n > 0 {
a.log.Info("initial maildir sync imported messages", "count", n)
a.log.Info("initial maildir sync processed messages", "count", n)
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
@@ -69,7 +69,7 @@ func (a *App) maildirWorker(ctx context.Context) {
continue
}
if n > 0 {
a.log.Info("maildir sync imported messages", "count", n)
a.log.Info("maildir sync processed messages", "count", n)
}
}
}
@@ -132,6 +132,11 @@ func (a *App) syncMaildirOnce(ctx context.Context) (int, error) {
}
}
}
backfilled, err := a.backfillSQLiteMessagesToMaildir(ctx)
if err != nil {
return imported, err
}
imported += backfilled
return imported, nil
}
@@ -248,6 +253,7 @@ func (a *App) syncUnregisteredMaildirFile(ctx context.Context, mb maildirMailbox
if exists, err := a.unregisteredMaildirMessageExists(ctx, path, msg.MessageID, msg.RecipientAddr); err != nil {
return false, err
} else if exists {
a.attachUnregisteredMaildirRawPathToExisting(ctx, path, msg.MessageID, msg.RecipientAddr)
return false, nil
}
_, err = a.insertMessage(ctx, msg, attachments)
@@ -311,6 +317,7 @@ func (a *App) syncMaildirFile(ctx context.Context, mb maildirMailbox, folder mai
if exists, err := a.maildirMessageExists(ctx, mb.ID, folder.ID, path, msg.MessageID); err != nil {
return false, err
} else if exists {
a.attachMaildirRawPathToExisting(ctx, mb.ID, folder.ID, path, msg.MessageID)
return false, nil
}
id, err := a.insertMessage(ctx, msg, attachments)
@@ -338,6 +345,26 @@ func (a *App) unregisteredMaildirMessageExists(ctx context.Context, rawPath, mes
return count > 0, nil
}
func (a *App) attachMaildirRawPathToExisting(ctx context.Context, mailboxID, folderID, rawPath, messageID string) {
if strings.TrimSpace(messageID) == "" || strings.TrimSpace(rawPath) == "" {
return
}
if _, err := a.db.ExecContext(ctx, `UPDATE messages SET raw_path=?,updated_at=? WHERE mailbox_id=? AND folder_id=? AND message_id=? AND message_id <> '' AND raw_path=''`,
rawPath, a.now().UTC().Format(time.RFC3339Nano), mailboxID, folderID, messageID); err != nil {
a.log.Warn("failed to attach maildir raw path to existing message", "path", rawPath, "error", err)
}
}
func (a *App) attachUnregisteredMaildirRawPathToExisting(ctx context.Context, rawPath, messageID, recipient string) {
if strings.TrimSpace(messageID) == "" || strings.TrimSpace(rawPath) == "" {
return
}
if _, err := a.db.ExecContext(ctx, `UPDATE messages SET raw_path=?,updated_at=? WHERE mailbox_id IS NULL AND recipient_addr=? AND message_id=? AND message_id <> '' AND raw_path=''`,
rawPath, a.now().UTC().Format(time.RFC3339Nano), recipient, messageID); err != nil {
a.log.Warn("failed to attach unregistered maildir raw path to existing message", "path", rawPath, "error", err)
}
}
func unregisteredRecipientFromMessage(msg storedMessage, domain string) string {
domain = normalizeDomain(domain)
for _, address := range append(append([]string{}, msg.To...), msg.CC...) {