Files
mailgo/internal/db/models.go
T
kevin dc164fdf66 feat(mailbox): 文件夹数据驱动,Web 通过 IMAP 共享服务层操作邮箱
- 新增 mailboxes 表与 MailboxStore(系统文件夹幂等创建,自定义文件夹 CRUD)
- 提取 MailboxService:IMAP 会话与 Web handler 共用,LIST 返回什么 Web 就显示什么
- IMAP 支持 CREATE/DELETE/RENAME/SUBSCRIBE(系统文件夹禁删改、非空禁删)
- Web 删除改为 IMAP 语义:移入 Trash,新增恢复/彻底删除/清空
- 新增通用 /folder/:name 页面与动态侧边栏,/inbox /sent /drafts 保留兼容重定向
2026-08-20 01:25:40 +08:00

230 lines
10 KiB
Go
Raw 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 (
"time"
)
// User represents a mail user in the system.
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:64;not null" json:"username"`
PasswordHash string `gorm:"size:255;not null" json:"-"`
DomainID uint `gorm:"index" json:"domain_id"`
Domain Domain `gorm:"foreignKey:DomainID" json:"domain"`
QuotaBytes int64 `gorm:"default:5368709120" json:"quota_bytes"`
UsedBytes int64 `gorm:"default:0" json:"used_bytes"`
IsActive bool `gorm:"default:true" json:"is_active"`
IsAdmin bool `gorm:"default:false" json:"is_admin"`
// MustChangePassword 为 true 时该用户(通常是初始管理员或被重置密码的
// 用户)在首次登录后必须修改密码。
MustChangePassword bool `gorm:"default:false" json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName specifies the table name for User.
func (User) TableName() string {
return "users"
}
// Domain represents a mail domain in the system.
type Domain struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:255;uniqueIndex;not null" json:"name"`
SmtpPort int `gorm:"default:25" json:"smtp_port"`
ImapPort int `gorm:"default:143" json:"imap_port"`
Pop3Port int `gorm:"default:110" json:"pop3_port"`
TlsCertPath string `gorm:"size:512" json:"tls_cert_path"`
TlsKeyPath string `gorm:"size:512" json:"tls_key_path"`
TlsEnabled bool `gorm:"default:false" json:"tls_enabled"`
DkimSelector string `gorm:"size:64;default:default" json:"dkim_selector"`
DkimPrivateKey string `gorm:"size:4096" json:"-"`
DkimPublicKey string `gorm:"size:1024" json:"-"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName specifies the table name for Domain.
func (Domain) TableName() string {
return "domains"
}
// Message represents an email message in the system.
type Message struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"index;not null" json:"user_id"`
User User `gorm:"foreignKey:UserID" json:"user"`
MessageID string `gorm:"size:255;index" json:"message_id"`
Folder string `gorm:"size:64;default:INBOX;index" json:"folder"`
FromAddr string `gorm:"size:512;not null" json:"from_addr"`
ToAddr string `gorm:"size:2048;not null" json:"to_addr"`
CcAddr string `gorm:"size:2048" json:"cc_addr"`
Subject string `gorm:"size:1024" json:"subject"`
TextBody string `gorm:"type:mediumtext" json:"text_body"`
HtmlBody string `gorm:"type:mediumtext" json:"html_body"`
RawData string `gorm:"type:mediumtext" json:"raw_data"`
IsRead bool `gorm:"default:false" json:"is_read"`
IsFlagged bool `gorm:"default:false" json:"is_flagged"`
// IsDeleted 持久化 IMAP \Deleted 标记(STORE +FLAGS \Deleted 写入,
// EXPUNGE/UID EXPUNGE 时按此删除)。此前该标记只存于内存会话
// imapMailbox.deleted map),重选文件夹/重连即丢失,导致客户端
// COPY→Trash 后 EXPUNGE 删不掉原邮件(垃圾桶只有副本)。
IsDeleted bool `gorm:"default:false;index" json:"is_deleted"`
Date time.Time `json:"date"`
CreatedAt time.Time `json:"created_at"`
}
// TableName specifies the table name for Message.
func (Message) TableName() string {
return "messages"
}
// Outbound message delivery statuses.
const (
OutboundStatusPending = "pending" // 等待发送
OutboundStatusSending = "sending" // 发送中
OutboundStatusSent = "sent" // 已送达
OutboundStatusDeferred = "deferred" // 临时失败,等待重试
OutboundStatusFailed = "failed" // 永久失败/超过重试上限
OutboundStatusCanceled = "canceled" // 管理员取消
)
// OutboundMessage represents a message queued for delivery to an external domain.
type OutboundMessage struct {
ID uint `gorm:"primaryKey" json:"id"`
MessageID string `gorm:"size:255;index" json:"message_id"`
UserID uint `gorm:"index" json:"user_id"` // 发件用户 IDWeb/SMTP 提交用户)
FromAddr string `gorm:"size:512;not null" json:"from_addr"`
ToAddr string `gorm:"size:512;not null" json:"to_addr"`
RecipientDom string `gorm:"size:255;index" json:"recipient_dom"`
RawData string `gorm:"type:mediumtext" json:"-"` // DKIM 签名后的完整邮件
Status string `gorm:"size:32;default:pending;index" json:"status"`
Attempts int `gorm:"default:0" json:"attempts"`
NextAttemptAt time.Time `gorm:"index" json:"next_attempt_at"`
LastResponse string `gorm:"size:1024" json:"last_response"`
LastError string `gorm:"size:1024" json:"last_error"`
CompletedAt *time.Time `json:"completed_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName specifies the table name for OutboundMessage.
func (OutboundMessage) TableName() string {
return "outbound_messages"
}
// BanEntry represents an IP address that has been banned due to excessive login failures.
type BanEntry struct {
ID uint `gorm:"primaryKey" json:"id"`
IPAddress string `gorm:"size:45;index;not null" json:"ip_address"`
Reason string `gorm:"size:255" json:"reason"`
FailCount int `gorm:"default:0" json:"fail_count"`
// BanCount 是该 IP 累计达到失败阈值的次数(含未封禁的前几次)。
// 阶段封禁依据:前 3 次只计数不封禁,第 4 次起按档位递增时长。
// 成功登录或管理员解封会删除记录,次数随之清零。
BanCount int `gorm:"default:0" json:"ban_count"`
ExpiresAt time.Time `gorm:"index" json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName specifies the table name for BanEntry.
func (BanEntry) TableName() string { return "ban_entries" }
// Protocol log statuses.
const (
ProtocolSMTP = "smtp"
ProtocolIMAP = "imap"
ProtocolPOP3 = "pop3"
)
// ProtocolLog records one SMTP/IMAP/POP3 connection session: auth result,
// failure reason and source IP, for admin analysis of attacks/abuse.
type ProtocolLog struct {
ID uint `gorm:"primaryKey" json:"id"`
Protocol string `gorm:"size:16;index;not null" json:"protocol"` // smtp | imap | pop3
Port int `json:"port"` // 25/465/587/143/993/110/995
ClientIP string `gorm:"size:64;index;not null" json:"client_ip"`
Username string `gorm:"size:255;index" json:"username"`
Success bool `gorm:"index" json:"success"`
FailReason string `gorm:"size:512" json:"fail_reason"`
Detail string `gorm:"size:2048" json:"detail"`
MsgCount int `json:"msg_count"`
DurationMs int64 `json:"duration_ms"`
CreatedAt time.Time `gorm:"index" json:"created_at"`
}
// TableName specifies the table name for ProtocolLog.
func (ProtocolLog) TableName() string {
return "protocol_logs"
}
// Attachment represents a file attached to an email message.
// 注意:不声明 Message 关联(避免 GORM 外键名 MessageID 与
// Message.MessageID 字符串字段冲突,导致 AutoMigrate 生成错误外键)。
type Attachment struct {
ID uint `gorm:"primaryKey" json:"id"`
MessageID uint `gorm:"index;not null" json:"message_id"`
FileName string `gorm:"size:255;not null" json:"file_name"`
FilePath string `gorm:"size:512;not null" json:"file_path"`
ContentType string `gorm:"size:128" json:"content_type"`
FileSize int64 `json:"file_size"`
CreatedAt time.Time `json:"created_at"`
}
// TableName specifies the table name for Attachment.
func (Attachment) TableName() string {
return "attachments"
}
// SystemMailbox 定义一个系统预置文件夹(RFC 6154 SPECIAL-USE 角色)。
type SystemMailbox struct {
Name string // 规范名(INBOX 大小写不敏感)
SpecialUse string // Sent / Drafts / TrashINBOX 为 ""
}
// SystemMailboxes 是系统预置文件夹清单:IMAP LIST 与 Web 侧边栏
// 共用此定义,保证「IMAP 返回什么,Web 就显示什么」。
var SystemMailboxes = []SystemMailbox{
{Name: "INBOX", SpecialUse: ""},
{Name: "Sent", SpecialUse: "Sent"},
{Name: "Drafts", SpecialUse: "Drafts"},
{Name: "Trash", SpecialUse: "Trash"},
}
// Mailbox 表示一个用户文件夹(IMAP mailbox / Web 侧边栏条目)。
// 系统文件夹在首次访问时由 MailboxStore.EnsureSystem 幂等创建;
// 自定义文件夹经 IMAP CREATE 创建。
type Mailbox struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"uniqueIndex:idx_mailbox_user_name;not null" json:"user_id"`
Name string `gorm:"size:64;uniqueIndex:idx_mailbox_user_name;not null" json:"name"`
SpecialUse string `gorm:"size:16" json:"special_use"` // 自定义文件夹为空
IsSubscribed bool `gorm:"default:true" json:"is_subscribed"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName specifies the table name for Mailbox.
func (Mailbox) TableName() string {
return "mailboxes"
}
// 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"
}