Compare commits
2
Commits
3a4636c2c5
...
6d5a7a8ed4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d5a7a8ed4 | ||
|
|
7ede29a18a |
+1
-1
@@ -56,7 +56,7 @@ func InitDB(cfg config.DatabaseConfig, storageCfg config.StorageConfig) (*gorm.D
|
||||
}
|
||||
|
||||
// Auto-migrate all models
|
||||
if err := db.AutoMigrate(&User{}, &Domain{}, &Message{}, &Attachment{}, &BanEntry{}, &OutboundMessage{}, &ProtocolLog{}); err != nil {
|
||||
if err := db.AutoMigrate(&User{}, &Domain{}, &Message{}, &Attachment{}, &BanEntry{}, &OutboundMessage{}, &ProtocolLog{}, &MailboxState{}); err != nil {
|
||||
return nil, fmt.Errorf("数据库迁移失败: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -170,3 +170,21 @@ type Attachment struct {
|
||||
func (Attachment) TableName() string {
|
||||
return "attachments"
|
||||
}
|
||||
|
||||
// MailboxState 记录每个邮箱(用户+文件夹)的持久化 IMAP 状态。
|
||||
// UidValidity 在首次访问时随机生成并持久化:数据库重建(消息 ID 空间
|
||||
// 变化)后该值随之改变,客户端(Thunderbird 等)会据此丢弃本地缓存
|
||||
// 并全量重新同步。此前硬编码为 1,数据库重建后客户端缓存永不失效,
|
||||
// 导致只显示/下载少量"缺失"邮件。
|
||||
type MailboxState struct {
|
||||
UserID uint `gorm:"primaryKey" json:"user_id"`
|
||||
Folder string `gorm:"primaryKey;size:64" json:"folder"`
|
||||
UidValidity uint32 `gorm:"not null" json:"uid_validity"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// TableName specifies the table name for MailboxState.
|
||||
func (MailboxState) TableName() string {
|
||||
return "mailbox_states"
|
||||
}
|
||||
@@ -354,7 +354,6 @@ func (m *imapMailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
status.Messages = uint32(len(messages))
|
||||
|
||||
var unseenCount uint32
|
||||
@@ -370,7 +369,16 @@ func (m *imapMailbox) Status(items []imap.StatusItem) (*imap.MailboxStatus, erro
|
||||
return nil, err
|
||||
}
|
||||
status.UidNext = uint32(maxID + 1)
|
||||
status.UidValidity = 1
|
||||
// UIDVALIDITY 持久化随机值(RFC 3501):数据库重建导致消息 ID 空间
|
||||
// 变化时该值随之改变,客户端才会丢弃旧缓存全量重同步。此前硬编码 1,
|
||||
// 数据库重建后 Thunderbird 等客户端缓存永不失效(只下载"新增"的
|
||||
// UID),表现为列表只剩少量邮件。
|
||||
uidValidity, err := m.stores.MailboxState.UidValidity(m.user.id, m.name)
|
||||
if err != nil {
|
||||
log.Printf("IMAP: 获取 UIDVALIDITY 失败 folder=%s: %v", m.name, err)
|
||||
uidValidity = 1
|
||||
}
|
||||
status.UidValidity = uidValidity
|
||||
|
||||
return status, nil
|
||||
}
|
||||
@@ -394,7 +402,6 @@ func (m *imapMailbox) ListMessages(uid bool, seqset *imap.SeqSet, items []imap.F
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(dbMessages) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -567,7 +574,6 @@ func (m *imapMailbox) SearchMessages(uid bool, criteria *imap.SearchCriteria) ([
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
|
||||
"mail_go/internal/db"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MailboxStateStore 提供 IMAP 邮箱持久化状态(UIDVALIDITY)的存取。
|
||||
type MailboxStateStore interface {
|
||||
// UidValidity 返回邮箱的持久化 UIDVALIDITY;首次访问时随机生成并落库。
|
||||
UidValidity(userID uint, folder string) (uint32, error)
|
||||
}
|
||||
|
||||
type mailboxStateStoreGorm struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func newMailboxStateStore(database *gorm.DB) *mailboxStateStoreGorm {
|
||||
return &mailboxStateStoreGorm{db: database}
|
||||
}
|
||||
|
||||
// UidValidity 返回邮箱的持久化 UIDVALIDITY;首次访问时随机生成并落库。
|
||||
// 随机值保证:邮箱内容身份变化(如数据库重建导致消息 ID 空间变化)时,
|
||||
// 新库生成的新值会让客户端丢弃旧缓存全量重同步(RFC 3501 UIDVALIDITY
|
||||
// 语义)。绝不返回 0(0 不是合法 UIDVALIDITY)。
|
||||
func (s *mailboxStateStoreGorm) UidValidity(userID uint, folder string) (uint32, error) {
|
||||
var st db.MailboxState
|
||||
err := s.db.Where("user_id = ? AND folder = ?", userID, folder).First(&st).Error
|
||||
if err == nil {
|
||||
if st.UidValidity != 0 {
|
||||
return st.UidValidity, nil
|
||||
}
|
||||
} else if err != gorm.ErrRecordNotFound {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 首次访问(或旧数据为 0):随机生成并持久化
|
||||
var buf [4]byte
|
||||
if _, err := rand.Read(buf[:]); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
v := binary.BigEndian.Uint32(buf[:])
|
||||
if v == 0 {
|
||||
v = 1
|
||||
}
|
||||
|
||||
st = db.MailboxState{UserID: userID, Folder: folder, UidValidity: v}
|
||||
if err := s.db.Save(&st).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"mail_go/internal/db"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TestMailboxStateUidValidity 验证 UIDVALIDITY:首次访问随机生成、重复访问
|
||||
// 稳定返回、0 值被修正、不同邮箱互不影响。
|
||||
func TestMailboxStateUidValidity(t *testing.T) {
|
||||
gdb, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gdb.AutoMigrate(&db.MailboxState{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := newMailboxStateStore(gdb)
|
||||
|
||||
// 首次访问:随机非 0
|
||||
v1, err := s.UidValidity(1, "INBOX")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v1 == 0 {
|
||||
t.Fatal("UIDVALIDITY 不应为 0")
|
||||
}
|
||||
// 重复访问:稳定
|
||||
v2, err := s.UidValidity(1, "INBOX")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v1 != v2 {
|
||||
t.Fatalf("UIDVALIDITY 不稳定: %d != %d", v1, v2)
|
||||
}
|
||||
// 不同邮箱:独立
|
||||
v3, err := s.UidValidity(1, "Sent")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v3 == v1 {
|
||||
t.Fatal("不同邮箱的 UIDVALIDITY 不应相同")
|
||||
}
|
||||
// 不同用户:独立
|
||||
v4, err := s.UidValidity(2, "INBOX")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v4 == v1 {
|
||||
t.Fatal("不同用户的 UIDVALIDITY 不应相同")
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ type Stores struct {
|
||||
Bans BanStore
|
||||
Outbound OutboundStore
|
||||
ProtocolLogs ProtocolLogStore
|
||||
MailboxState MailboxStateStore
|
||||
}
|
||||
|
||||
// NewStores creates a new Stores instance with all GORM-backed implementations.
|
||||
@@ -27,6 +28,7 @@ func NewStores(database *gorm.DB) *Stores {
|
||||
Bans: newBanStore(database),
|
||||
Outbound: newOutboundStore(database),
|
||||
ProtocolLogs: newProtocolLogStore(database),
|
||||
MailboxState: newMailboxStateStore(database),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,3 +39,4 @@ var _ = db.Message{}
|
||||
var _ = db.Attachment{}
|
||||
var _ = db.BanEntry{}
|
||||
var _ = db.ProtocolLog{}
|
||||
var _ = db.MailboxState{}
|
||||
Reference in New Issue
Block a user