- 新增 internal/i18n 包:以原始中文字符串为 key 的翻译目录(zh 恒等, en 兜底,ja 缺译回退 en),t/tf 模板函数与 Accept-Language 解析 (按 q 值排序,兜底英语) - User 模型新增 language 字段(auto|en|zh|ja,默认 auto), AutoMigrate 自动加列;UserStore 新增 UpdateLanguage - 语言解析:登录用户按偏好(auto 时取浏览器 Accept-Language), 登录/封禁页按浏览器语言;所有 c.HTML 渲染注入 Lang 模板数据 - 全部页面模板(用户页 + 管理后台 12 页)接入翻译:导航、文件夹、 邮件列表/阅读/写信、设置、登录、封禁页、表单、表格、JS 确认框等; html lang 属性随语言切换;日期函数(shortDate/time12/time12m) 与 folderLabel 改为语言感知(上午/下午 vs AM/PM vs 午前/午後) - 设置页新增「界面语言」选择(自动/English/中文/日本語), /settings 表单支持单独提交语言偏好;管理后台用户表单可预设语言 - 封禁原因等入库中文文案在展示时经 t 翻译(未知 key 原样回退) - 测试:i18n 单测(解析/回退/译文)、render 测试补 Lang 数据、 时间格式化测试适配新签名;全量 go test 通过
235 lines
10 KiB
Go
235 lines
10 KiB
Go
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:"-"`
|
||
// Language 界面语言偏好:auto(跟随浏览器 Accept-Language)| en | zh | ja。
|
||
// 默认 auto,浏览器语言无法识别时回退英语。
|
||
Language string `gorm:"size:8;default:auto" json:"language"`
|
||
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"` // 发件用户 ID(Web/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 唯一索引:每 IP 恰好一条记录(观察计数与封禁状态共用),
|
||
// 防止并发写入产生重复行导致计数与档位读写错位。
|
||
IPAddress string `gorm:"size:45;uniqueIndex;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 / Trash;INBOX 为 ""
|
||
}
|
||
|
||
// 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"
|
||
}
|