- 新增 internal/outbound 模块:MX 查询、SMTP 出站客户端(EHLO/STARTTLS/ MAIL/RCPT/DATA/QUIT)、4xx 临时失败与 5xx 永久失败分类、8BITMIME 支持 - 新增 outbound_messages 队列表与 OutboundStore,后台 worker 指数退避重试 - 永久失败/超限退信到发件人收件箱,包含原因与目标收件人 - 外发邮件使用域名 DKIM 私钥签名(go-msgauth) - SMTP 提交集成:认证用户可发外部收件人,MAIL FROM 必须等于登录用户邮箱, 未认证外部投递明确拒绝(防开放中继) - Web 发信集成:外部收件人自动入队,附件以 multipart/mixed + base64 编码 加入邮件正文 - 每用户每分钟/每日发送限速(max_per_day=0 可禁用外部投递) - 管理后台新增外发队列页面:状态统计、失败原因、手动重试/取消 - 新增 [outbound] 配置段并更新 README / todo.md
138 lines
5.7 KiB
Go
138 lines
5.7 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"`
|
||
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:text" json:"text_body"`
|
||
HtmlBody string `gorm:"type:text" 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"`
|
||
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 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"`
|
||
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" }
|
||
|
||
// Attachment represents a file attached to an email message.
|
||
type Attachment struct {
|
||
ID uint `gorm:"primaryKey" json:"id"`
|
||
MessageID uint `gorm:"index;not null" json:"message_id"`
|
||
Message Message `gorm:"foreignKey:MessageID" json:"message"`
|
||
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"
|
||
}
|