Files
mailgo/internal/db/db.go
kevin 07ba01a6f6 fix(imap): 修复 Thunderbird 未读状态丢失(SQLite 并发写 + 客户端 seq 错位)
A. SQLite 并发写失败被吞(高概率根因):
- DSN 追加 _busy_timeout=5000&_journal_mode=WAL&_synchronous=NORMAL,
  WAL 下读不阻塞写,消除瞬时 SQLITE_BUSY
- UpdateMessagesFlags 不再吞 MarkReadState/MarkFlagged 错误:
  出错记日志并返回 error(客户端收到 NO 会重试)
- Web MarkRead/Delete 同款静默丢错补日志

B. 客户端 seq 视图错位(集成测试复现后修复):
- 真实服务器+脚本客户端测试复现:客户端按日期倒序自编号发
  seq 式 STORE 时,旧排序(id ASC)把最旧邮件标为已读
- 规范排序改为 date DESC, id DESC(最新在前),与主流客户端
  默认视图一致;buildNewMessageUpdate 改用 seqOf 取真实序号
- 新增 UID STORE / 服务器下发 seq STORE / 自编号 seq STORE 集成测试

注:go-imap v1.2.1 存在库内 *conn.silent() 数据竞争(启用推送后
必然触发),集成测试以 //go:build !race 排除,-race 下由单元测试
覆盖推送逻辑
2026-08-19 20:30:06 +08:00

65 lines
1.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package db
import (
"fmt"
"os"
"path/filepath"
"strings"
"mail_go/config"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// InitDB initializes the database connection and performs auto-migration.
// It selects the appropriate driver based on cfg.Driver and resolves
// the DSN path for SQLite relative to the storage base directory.
func InitDB(cfg config.DatabaseConfig, storageCfg config.StorageConfig) (*gorm.DB, error) {
var dialector gorm.Dialector
switch cfg.Driver {
case "sqlite":
dsn := cfg.DSN
// If the DSN is the default relative path, prepend the storage base directory
if dsn == config.DefaultDSNWin || dsn == config.DefaultDSNLinux {
dsn = filepath.Join(storageCfg.BaseDir, "mail.db")
}
// Ensure the parent directory exists for SQLite
dir := filepath.Dir(dsn)
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("创建数据库目录失败 %s: %w", dir, err)
}
// 多连接并发(SMTP/IMAP/POP3/Web/外发 worker/推送)下:
// - WAL 模式:读不阻塞写,消除瞬时 SQLITE_BUSY 导致写失败被吞
// - busy_timeout=5000ms:写竞争时等待而非立刻失败
// - synchronous=NORMALWAL 下安全且写入更快
sep := "?"
if strings.Contains(dsn, "?") {
sep = "&"
}
dsn = dsn + sep + "_busy_timeout=5000&_journal_mode=WAL&_synchronous=NORMAL"
dialector = sqlite.Open(dsn)
case "mysql":
dialector = mysql.Open(cfg.DSN)
default:
return nil, fmt.Errorf("不支持的数据库驱动: %s", cfg.Driver)
}
db, err := gorm.Open(dialector, &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
})
if err != nil {
return nil, fmt.Errorf("连接数据库失败: %w", err)
}
// Auto-migrate all models
if err := db.AutoMigrate(&User{}, &Domain{}, &Message{}, &Attachment{}, &BanEntry{}, &OutboundMessage{}, &ProtocolLog{}); err != nil {
return nil, fmt.Errorf("数据库迁移失败: %w", err)
}
return db, nil
}