26 KiB
26 KiB
MailGo 系统设计文档
项目:mail_go —— 自托管 Go 邮件系统
架构师:高见远
版本:v1.0
日期:2025-07
1. 实现方案分析
1.1 核心技术挑战
| 挑战 | 说明 | 应对策略 |
|---|---|---|
| SMTP/IMAP/POP3 三协议并行 | 三个邮件协议服务需同时运行、共享同一套用户与邮件数据 | 统一 Backend 接口层,三个协议服务均委托给同一 MailStore + UserStore |
| 邮件解析与持久化 | go-message 解析 MIME 后需拆分为元数据 + 正文 + 附件 | 统一 MessageParser 服务,解析后原子写入 DB + 文件系统 |
| IMAP 文件夹/标记/搜索 | go-imap/v2 需实现完整的 Backend / User / Mailbox / Message 接口 | 采用"数据库为单一真相源"架构,IMAP 操作全部映射到 GORM 查询 |
| 跨平台路径 | Linux/Windows 配置与数据路径不同 | config 层根据 runtime.GOOS 自动映射路径前缀 |
| 附件存储一致性 | 附件写入文件系统后需与 DB 记录对齐 | 事务内写 DB + 事后写文件,失败时异步清理孤儿文件 |
| Web 认证与权限 | 普通用户 vs 管理员,Session 管理 | Gin 中间件:AuthMiddleware(登录检查)+ AdminMiddleware(管理员检查) |
1.2 框架与库选型
| 组件 | 选型 | 理由 |
|---|---|---|
| SMTP 服务端 | github.com/emersion/go-smtp |
PRD 指定,成熟稳定 |
| IMAP 服务端 | github.com/emersion/go-imap/v2 |
PRD 指定,v2 API 更现代 |
| POP3 服务端 | 手工实现(TCP 监听 + 文本协议) | emersion 系列无官方 go-pop3,POP3 协议简单,自实现可控 |
| 邮件解析 | github.com/emersion/go-message |
与 go-smtp/go-imap 生态一致 |
| Web 框架 | gin-gonic/gin |
PRD 指定,高性能 |
| 模板引擎 | html/template |
Go 标准库,自动转义防 XSS |
| ORM | gorm.io/gorm + gorm.io/driver/sqlite + gorm.io/driver/mysql |
PRD 指定,双数据库支持 |
| 配置解析 | github.com/BurntSushi/toml |
TOML 格式标准库 |
| 密码哈希 | golang.org/x/crypto/bcrypt |
PRD 指定 |
| UUID | github.com/google/uuid |
PRD 指定,用于附件文件名等 |
| Session | github.com/gin-contrib/sessions + cookie store |
轻量,无需 Redis |
1.3 架构模式
采用 分层架构 + 共享 Store 层:
┌──────────────────────────────────────────────┐
│ main.go (启动入口) │
├──────────┬──────────┬──────────┬───────────────┤
│ SMTP │ IMAP │ POP3 │ Web (Gin) │
│ Server │ Server │ Server │ Router │
├──────────┴──────────┴──────────┴───────────────┤
│ Store Layer (共享) │
│ UserStore │ MailStore │ DomainStore │ AttachmentStore │
├────────────────────────────────────────────────┤
│ GORM (SQLite / MySQL) │
└────────────────────────────────────────────────┘
- 协议层(SMTP/IMAP/POP3)仅负责协议握手,业务逻辑委托 Store 层
- Web 层(Gin + html/template)负责 UI 渲染和 HTTP API
- Store 层封装所有数据库操作,是唯一的数据访问入口
- Config 在启动时加载为全局单例,通过依赖注入传递给各 Server
2. 项目文件列表
mail_go/
├── main.go # 程序入口:配置加载 → DB 初始化 → 各服务启动
├── go.mod # Go 模块定义
├── go.sum # 依赖校验
├── config/
│ ├── config.go # 配置结构体 + 加载 + 自动补全逻辑
│ └── defaults.go # 默认值常量(端口、路径等)
├── internal/
│ ├── db/
│ │ ├── db.go # GORM 初始化 + AutoMigrate
│ │ └── models.go # User / Domain / Message / Attachment 模型
│ ├── store/
│ │ ├── user_store.go # 用户 CRUD + 认证
│ │ ├── mail_store.go # 邮件 CRUD + 文件夹查询
│ │ ├── domain_store.go # 域名 CRUD
│ │ └── attachment_store.go # 附件元数据 CRUD
│ ├── smtp_server/
│ │ └── server.go # SMTP 服务端(go-smtp Backend 实现)
│ ├── imap_server/
│ │ ├── server.go # IMAP 服务端启动
│ │ └── backend.go # go-imap Backend/User/Mailbox/Message 实现
│ ├── pop3_server/
│ │ └── server.go # POP3 服务端(TCP 监听 + 文本协议)
│ ├── web/
│ │ ├── server.go # Gin 引擎初始化 + 路由注册
│ │ ├── middleware/
│ │ │ ├── auth.go # 登录认证中间件
│ │ │ └── admin.go # 管理员鉴权中间件
│ │ ├── handlers/
│ │ │ ├── auth.go # 登录/登出处理
│ │ │ ├── mail.go # 收件箱/发件箱/撰写/查看
│ │ │ └── admin.go # 管理后台处理
│ │ └── templates/
│ │ ├── base.html # 基础布局模板
│ │ ├── login.html # 登录页
│ │ ├── inbox.html # 收件箱
│ │ ├── compose.html # 撰写邮件
│ │ ├── sent.html # 发件箱
│ │ ├── view.html # 邮件阅读
│ │ └── admin/
│ │ ├── dashboard.html # 管理首页
│ │ ├── domains.html # 域名管理
│ │ └── users.html # 用户管理
│ └── storage/
│ └── attachment.go # 附件文件读写(磁盘操作)
├── scripts/
│ └── install.sh # Linux 安装脚本(创建目录、设置权限)
└── docs/
├── prd.md
└── architecture.md # 本文档
3. 数据结构与接口
3.1 数据库模型(GORM)
classDiagram
class User {
+uint ID
+string Username
+string PasswordHash
+uint DomainID
+Domain Domain
+int64 QuotaBytes
+int64 UsedBytes
+bool IsActive
+bool IsAdmin
+time CreatedAt
+time UpdatedAt
}
class Domain {
+uint ID
+string Name
+int SmtpPort
+int ImapPort
+int Pop3Port
+string TlsCertPath
+string TlsKeyPath
+bool TlsEnabled
+time CreatedAt
+time UpdatedAt
}
class Message {
+uint ID
+uint UserID
+User User
+string MessageID
+string Folder
+string FromAddr
+string ToAddr
+string CcAddr
+string Subject
+string TextBody
+string HtmlBody
+bool IsRead
+bool IsFlagged
+time Date
+time CreatedAt
}
class Attachment {
+uint ID
+uint MessageID
+Message Message
+string FileName
+string FilePath
+string ContentType
+int64 FileSize
+time CreatedAt
}
Domain "1" --> "*" User : has
User "1" --> "*" Message : owns
Message "1" --> "*" Attachment : has
详细字段说明
User
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键 |
| Username | string | 用户名(@ 前部分) |
| PasswordHash | string | bcrypt 哈希 |
| DomainID | uint | 所属域名外键 |
| Domain | Domain | 所属域名 |
| QuotaBytes | int64 | 附件配额(字节),默认 5GB |
| UsedBytes | int64 | 已用空间(字节) |
| IsActive | bool | 是否启用 |
| IsAdmin | bool | 是否管理员 |
| CreatedAt | time.Time | 创建时间 |
| UpdatedAt | time.Time | 更新时间 |
Domain
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键 |
| Name | string | 域名(如 example.com) |
| SmtpPort | int | SMTP 端口 |
| ImapPort | int | IMAP 端口 |
| Pop3Port | int | POP3 端口 |
| TlsCertPath | string | TLS 证书路径 |
| TlsKeyPath | string | TLS 私钥路径 |
| TlsEnabled | bool | 是否启用 TLS |
| CreatedAt | time.Time | 创建时间 |
| UpdatedAt | time.Time | 更新时间 |
Message
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键 |
| UserID | uint | 所属用户外键 |
| User | User | 所属用户 |
| MessageID | string | RFC 2822 Message-ID |
| Folder | string | 文件夹:INBOX / Sent / Drafts / Trash |
| FromAddr | string | 发件人地址 |
| ToAddr | string | 收件人地址(逗号分隔) |
| CcAddr | string | 抄送地址(逗号分隔) |
| Subject | string | 主题 |
| TextBody | string | 纯文本正文 |
| HtmlBody | string | HTML 正文 |
| IsRead | bool | 是否已读 |
| IsFlagged | bool | 是否标记 |
| Date | time.Time | 邮件日期 |
| CreatedAt | time.Time | 入库时间 |
Attachment
| 字段 | 类型 | 说明 |
|---|---|---|
| ID | uint | 主键 |
| MessageID | uint | 所属邮件外键 |
| Message | Message | 所属邮件 |
| FileName | string | 原始文件名 |
| FilePath | string | 磁盘相对路径 |
| ContentType | string | MIME 类型 |
| FileSize | int64 | 文件大小(字节) |
| CreatedAt | time.Time | 创建时间 |
3.2 核心接口设计
Config 结构体
// config/config.go
type Config struct {
Database DatabaseConfig
Storage StorageConfig
Web WebConfig
SMTP SMTPConfig
IMAP IMAPConfig
POP3 POP3Config
}
type DatabaseConfig struct {
Driver string // "sqlite" | "mysql"
DSN string
}
type StorageConfig struct {
BaseDir string
AttachDir string
}
type WebConfig struct {
Listen string // ":8080" 或 Unix socket 路径
}
type SMTPConfig struct {
Port int
TLSPort int
Cert string
Key string
}
type IMAPConfig struct {
Port int
TLSPort int
Cert string
Key string
}
type POP3Config struct {
Port int
TLSPort int
Cert string
Key string
}
Store 接口
// internal/store/
type UserStore interface {
Create(user *models.User) error
GetByID(id uint) (*models.User, error)
GetByUsername(username string, domainID uint) (*models.User, error)
GetByEmail(email string) (*models.User, error)
Authenticate(email, password string) (*models.User, error)
Update(user *models.User) error
Delete(id uint) error
List(domainID uint, page, size int) ([]models.User, int64, error)
UpdateUsedBytes(id uint, delta int64) error
}
type MailStore interface {
Create(msg *models.Message) error
GetByID(id uint) (*models.Message, error)
ListByUserAndFolder(userID uint, folder string, page, size int) ([]models.Message, int64, error)
MarkRead(id uint) error
MarkFlagged(id uint, flagged bool) error
MoveToFolder(id uint, folder string) error
Delete(id uint) error
CountUnread(userID uint, folder string) (int64, error)
}
type DomainStore interface {
Create(domain *models.Domain) error
GetByID(id uint) (*models.Domain, error)
GetByName(name string) (*models.Domain, error)
Update(domain *models.Domain) error
Delete(id uint) error
List(page, size int) ([]models.Domain, int64, error)
}
type AttachmentStore interface {
Create(att *models.Attachment) error
ListByMessage(messageID uint) ([]models.Attachment, error)
Delete(id uint) error
DeleteByMessage(messageID uint) error
}
SMTP Backend 接口(go-smtp 要求)
// internal/smtp_server/server.go
// 实现 go-smtp 的 Backend 接口
type smtpBackend struct {
userStore store.UserStore
mailStore store.MailStore
domainStore store.DomainStore
attStore store.AttachmentStore
storage *storage.AttachmentStorage
}
func (b *smtpBackend) NewSession(c *smtp.Conn) (smtp.Session, error)
func (s *smtpSession) AuthPlain(username, password string) error
func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error
func (s *smtpSession) Rcpt(to string, opts *smtp.RcptOptions) error
func (s *smtpSession) Data(r io.Reader) error
func (s *smtpSession) Logout() error
IMAP Backend 接口(go-imap/v2 要求)
// internal/imap_server/backend.go
// 实现 go-imap/v2 的 backend.Backend 接口
type imapBackend struct {
userStore store.UserStore
mailStore store.MailStore
domainStore store.DomainStore
attStore store.AttachmentStore
}
func (b *imapBackend) Login(username, password string) (backend.User, error)
// imapUser 实现 backend.User
type imapUser struct {
user *models.User
store *mailStoreGorm
}
func (u *imapUser) ListMailboxes(subscribed bool) ([]imap.Mailbox, error)
func (u *imapUser) GetMailbox(name string) (backend.Mailbox, error)
// imapMailbox 实现 backend.Mailbox
type imapMailbox struct {
user *models.User
folder string
store *mailStoreGorm
}
// imapMessage 实现 backend.Message
4. Web 路由表
| Method | Path | Handler | 中间件 | 说明 |
|---|---|---|---|---|
| GET | /login | auth.ShowLogin | — | 登录页 |
| POST | /login | auth.DoLogin | — | 登录提交 |
| POST | /logout | auth.DoLogout | Auth | 登出 |
| GET | / | mail.Inbox | Auth | 收件箱(重定向到 /inbox) |
| GET | /inbox | mail.Inbox | Auth | 收件箱列表 |
| GET | /inbox/:id | mail.View | Auth | 查看邮件 |
| GET | /compose | mail.Compose | Auth | 撰写页面 |
| POST | /compose | mail.DoSend | Auth | 发送邮件 |
| GET | /sent | mail.Sent | Auth | 发件箱 |
| GET | /sent/:id | mail.View | Auth | 查看已发送邮件 |
| POST | /mail/delete/:id | mail.Delete | Auth | 删除邮件 |
| POST | /mail/read/:id | mail.MarkRead | Auth | 标记已读 |
| GET | /attachment/:id | mail.DownloadAttachment | Auth | 下载附件 |
| GET | /admin | admin.Dashboard | Auth + Admin | 管理后台首页 |
| GET | /admin/domains | admin.ListDomains | Auth + Admin | 域名列表 |
| GET | /admin/domains/new | admin.NewDomain | Auth + Admin | 新增域名页 |
| POST | /admin/domains | admin.CreateDomain | Auth + Admin | 创建域名 |
| POST | /admin/domains/:id/delete | admin.DeleteDomain | Auth + Admin | 删除域名 |
| GET | /admin/domains/:id/dns | admin.DNSHint | Auth + Admin | DNS 配置提示 |
| GET | /admin/users | admin.ListUsers | Auth + Admin | 用户列表 |
| GET | /admin/users/new | admin.NewUser | Auth + Admin | 新增用户页 |
| POST | /admin/users | admin.CreateUser | Auth + Admin | 创建用户 |
| POST | /admin/users/:id/delete | admin.DeleteUser | Auth + Admin | 删除用户 |
| GET | /admin/users/:id/edit | admin.EditUser | Auth + Admin | 编辑用户页 |
| POST | /admin/users/:id | admin.UpdateUser | Auth + Admin | 更新用户 |
5. 程序调用流程
5.1 启动流程
sequenceDiagram
participant Main as main.go
participant Cfg as config
participant DB as db
participant Store as Store Layer
participant SMTP as SMTP Server
participant IMAP as IMAP Server
participant POP3 as POP3 Server
participant Web as Web Server (Gin)
Main->>Cfg: LoadConfig("config.toml")
Cfg->>Cfg: 检测文件是否存在
alt 配置文件不存在
Cfg->>Cfg: 生成默认配置并写入文件
else 配置文件存在但字段缺失
Cfg->>Cfg: 补全缺失字段为默认值
end
Cfg-->>Main: 返回 *Config
Main->>DB: InitDB(cfg.Database)
DB->>DB: 根据 Driver 选择 SQLite/MySQL
DB->>DB: gorm.Open(...)
DB->>DB: db.AutoMigrate(User, Domain, Message, Attachment)
DB-->>Main: 返回 *gorm.DB
Main->>Store: NewStoreLayer(db, cfg.Storage)
Store-->>Main: 返回 Store 实例集合
Main->>Store: 检查是否存在 admin 用户
alt 无 admin 用户
Main->>Store: 创建默认 admin 账户
end
par 并行启动服务
Main->>SMTP: NewSMTPServer(cfg.SMTP, stores)
SMTP->>SMTP: smtp.NewServer(backend)
SMTP->>SMTP: 监听 Port + TLSPort
and
Main->>IMAP: NewIMAPServer(cfg.IMAP, stores)
IMAP->>IMAP: imapserver.New(backend)
IMAP->>IMAP: 监听 Port + TLSPort
and
Main->>POP3: NewPOP3Server(cfg.POP3, stores)
POP3->>POP3: net.Listen + 协议循环
POP3->>POP3: 监听 Port + TLSPort
and
Main->>Web: NewWebServer(cfg.Web, stores)
Web->>Web: 注册路由 + 中间件
Web->>Web: gin.Run(cfg.Web.Listen)
end
5.2 用户收信流程(SMTP 入站 → Web 展示)
sequenceDiagram
participant Remote as 远程 MTA
participant SMTP as SMTP Server
participant Parser as MessageParser
participant MS as MailStore
participant AS as AttachmentStore
participant FS as FileSystem
participant User as 浏览器用户
participant Web as Web Server (Gin)
Remote->>SMTP: TCP 连接 + EHLO/MAIL FROM/RCPT TO
SMTP->>SMTP: 验证收件人域名是否在本系统
SMTP->>SMTP: 查找对应用户
Remote->>SMTP: DATA (邮件内容)
SMTP->>Parser: ParseMIME(r io.Reader)
Parser->>Parser: go-message 解析 MIME
Parser-->>SMTP: 返回 ParsedMessage{From, To, Subject, TextBody, HtmlBody, Attachments}
SMTP->>MS: Create(Message{Folder:"INBOX", ...})
MS->>MS: GORM INSERT messages
loop 每个附件
SMTP->>AS: Create(Attachment{FileName, ContentType, Size})
AS->>AS: GORM INSERT attachments
SMTP->>FS: WriteFile(attachmentsDir/uuid.ext, data)
end
SMTP-->>Remote: 250 OK
Note over User,Web: 用户稍后登录 Web 查看邮件
User->>Web: GET /inbox
Web->>Web: AuthMiddleware 检查 Session
Web->>MS: ListByUserAndFolder(userID, "INBOX", page, size)
MS-->>Web: []Message + total
Web->>Web: 渲染 inbox.html
Web-->>User: 收件箱页面
User->>Web: GET /inbox/42
Web->>MS: GetByID(42)
MS-->>Web: Message 详情
Web->>AS: ListByMessage(42)
AS-->>Web: []Attachment
Web->>Web: 渲染 view.html
Web-->>User: 邮件阅读页
User->>Web: GET /attachment/7
Web->>AS: GetByID(7) → FilePath
Web->>FS: OpenFile(attachmentsDir/filepath)
Web-->>User: 文件下载流
6. 有序任务列表
| Task ID | 任务名称 | 依赖 | 涉及文件 | 优先级 |
|---|---|---|---|---|
| T01 | 项目基础设施:go.mod + 入口 + 配置系统 + 数据库层 | — | go.mod, main.go, config/config.go, config/defaults.go, internal/db/db.go, internal/db/models.go, internal/store/*.go | P0 |
| T02 | 邮件协议服务端(SMTP + IMAP + POP3) | T01 | internal/smtp_server/server.go, internal/imap_server/server.go, internal/imap_server/backend.go, internal/pop3_server/server.go | P0 |
| T03 | Web 服务核心:路由 + 中间件 + 认证 + 邮件页面 | T01 | internal/web/server.go, internal/web/middleware/auth.go, internal/web/middleware/admin.go, internal/web/handlers/auth.go, internal/web/handlers/mail.go | P0 |
| T04 | 管理后台 + 附件存储 + 模板 | T01, T03 | internal/web/handlers/admin.go, internal/storage/attachment.go, internal/web/templates/*.html | P0 |
| T05 | 集成调试 + 安装脚本 | T02, T03, T04 | main.go(更新), scripts/install.sh | P1 |
任务详细说明
T01: 项目基础设施
- 初始化 go.mod,声明所有依赖
- 实现
config.LoadConfig():读取 TOML → 缺失字段补全默认值 → 写回文件 - 实现
config中的路径映射:Linux 用/etc/mail_go/+/srv/mail_go/,Windows 用./win/etc/mail_go/+./win/srv/mail_go/ - 实现
db.InitDB():根据 driver 选择 SQLite/MySQL,执行 AutoMigrate - 定义所有 GORM 模型:User, Domain, Message, Attachment
- 实现 Store 层所有接口的 GORM 实现
- main.go 骨架:加载配置 → 初始化 DB → 创建 Store → 首次启动生成 admin
T02: 邮件协议服务端
- SMTP:实现 go-smtp 的 Backend/Session 接口,收信后调用 MailStore 写入
- IMAP:实现 go-imap/v2 的 Backend/User/Mailbox 接口,查询 MailStore
- POP3:TCP 监听 + 文本协议实现(USER/PASS/STAT/LIST/RETR/DELE/QUIT),查询 MailStore
- 三个服务均接受 Store 实例作为构造参数
T03: Web 服务核心
- Gin 引擎初始化 + Session 中间件配置
- AuthMiddleware:检查 Session 中的 userID,未登录重定向 /login
- AdminMiddleware:检查用户 IsAdmin 字段
- 登录/登出 handler:bcrypt 校验 + Session 写入
- 收件箱/发件箱/撰写 handler:渲染模板 + Store 调用
- 发送邮件 handler:构造 MIME 消息,通过本地 SMTP 发送
T04: 管理后台 + 附件存储 + 模板
- 管理后台 handler:域名 CRUD、用户 CRUD、DNS 提示
- AttachmentStorage:附件文件写入/读取/删除磁盘操作
- 所有 HTML 模板:base 布局 + login/inbox/compose/sent/view/admin 系列页面
- 附件上传(compose 页面多文件上传)+ 下载 handler
T05: 集成调试 + 安装脚本
- main.go 更新:并行启动 SMTP/IMAP/POP3/Web 四个服务,graceful shutdown
- 端汇总测试:SMTP → Store → Web 展示 全链路
- install.sh:Linux 下创建 /etc/mail_go/ /srv/mail_go/ 目录、设置权限
7. 依赖包列表(go.mod 草稿)
module mail_go
go 1.22
require (
github.com/emersion/go-smtp v0.21.0
github.com/emersion/go-imap/v2 v2.0.0-beta.5
github.com/emersion/go-message v0.18.0
github.com/gin-gonic/gin v1.10.0
github.com/gin-contrib/sessions v0.0.5
github.com/BurntSushi/toml v1.3.2
gorm.io/gorm v1.25.12
gorm.io/driver/sqlite v1.5.6
gorm.io/driver/mysql v1.5.7
github.com/google/uuid v1.6.0
golang.org/x/crypto v0.25.0
)
版本号为撰写时最新稳定版参考,实际以
go get拉取为准。
8. 跨文件共享约定(Shared Knowledge)
8.1 配置传递
main.go加载Config单例,通过构造函数注入到各 Server- 所有 Server 签名统一为
NewXxxServer(cfg XxxConfig, stores *Stores, ...) *XxxServer - 禁止在 Store/Handler 层直接读取配置文件,一律通过参数传递
8.2 Session 约定
- Session 名称:
mail_go_session - 登录后写入 Session 的 key:
userID→ uintuserEmail→ string(完整邮箱地址,如 admin@example.com)isAdmin→ bool
- Session 使用 Cookie 存储,
MaxAge = 86400(24 小时) - Cookie
HttpOnly = true,SameSite = Lax
8.3 错误处理
- Store 层错误统一返回 Go
error,不做日志输出 - Web Handler 层捕获 Store 错误后:
- 5xx → 渲染
base.html中的错误提示区 - 4xx → 重定向或渲染对应页面并显示错误消息
- 5xx → 渲染
- SMTP/IMAP/POP3 协议层错误直接返回协议规定的错误码
8.4 邮件地址规范
- 系统内部统一使用完整邮箱地址(user@domain.com)标识用户
User.Username仅存储 @ 前部分- 查找用户时通过
User.Username + Domain.Name组合查询 Message.FromAddr/ToAddr/CcAddr均为完整邮箱地址
8.5 文件夹命名
| 内部名称 | IMAP 映射 | 说明 |
|---|---|---|
| INBOX | INBOX | 收件箱 |
| Sent | Sent Messages | 发件箱 |
| Drafts | Drafts | 草稿箱 |
| Trash | Trash | 废纸篓 |
8.6 附件存储路径
- 附件存储根目录:
cfg.Storage.AttachDir - 单个附件路径:
{AttachDir}/{uuid}{ext},其中 uuid 由google/uuid生成,ext 保留原始扩展名 Attachment.FilePath存储相对路径(不含 AttachDir 前缀)
8.7 数据库约定
- 所有表名使用蛇形复数:
users,domains,messages,attachments - 时间字段统一使用
time.Time(UTC),GORM 自动管理created_at/updated_at - 软删除暂不实现,删除操作为硬删除
- 分页统一参数:
page(从 1 开始)、size(默认 20)
9. 任务依赖图
graph LR
T01[T01: 项目基础设施] --> T02[T02: 邮件协议服务端]
T01 --> T03[T03: Web 服务核心]
T01 --> T04[T04: 管理后台+附件+模板]
T03 --> T04
T02 --> T05[T05: 集成调试+安装脚本]
T03 --> T05
T04 --> T05
T02 与 T03 可并行开发(仅依赖 T01),T04 依赖 T03(管理后台 handler 需要 Web 框架),T05 为最终集成。
10. 待明确事项
| # | 问题 | 假设 | 影响范围 |
|---|---|---|---|
| 1 | DKIM 私钥由系统自动生成还是管理员手动导入?(PRD OQ-1) | 第一期仅管理员手动导入,/admin 域名配置页提供私钥文件路径输入框 | admin handler, Domain 模型 |
| 2 | 是否支持邮件转发规则?(PRD OQ-2) | 第一期不支持,SMTP 收信仅投递到本地用户 | smtp_server |
| 3 | 附件大小上限由全局配置还是按用户配额?(PRD OQ-3) | 第一期采用全局配置 + 用户配额双重限制:单文件上限 25MB(硬编码),总空间不超过用户 QuotaBytes | compose handler, AttachmentStorage |
| 4 | Web 撰写是否支持富文本?(PRD OQ-4) | 第一期仅支持纯文本(textarea),后续可引入 Markdown 或 TinyMCE | compose.html, mail handler |
| 5 | 多域名是否纳入第一期?(PRD OQ-5) | 第一期支持多域名(Domain 表设计已包含),但 DNS 提示仅展示通用模板 | admin handler, DNS 提示 |
| 6 | 是否支持 OAuth2/LDAP 认证?(PRD OQ-6) | 第一期仅本地账户认证,bcrypt 密码校验 | auth handler |
| 7 | go-imap/v2 仍为 beta 版本,API 可能变更 | 锁定特定 commit hash,在 go.mod 中 replace 指向稳定版本 | go.mod, imap_server |
| 8 | POP3 协议实现范围 | 实现 USER/PASS/STAT/LIST/RETR/DELE/NOOP/RSET/QUIT 基本命令,不支持 APOP/UIDL | pop3_server |
| 9 | 首次启动 admin 账户的默认密码 | admin(首次登录后强制修改,或 /admin 页面提示修改) |
main.go |
| 10 | Windows 上是否支持 Unix Socket | 不支持,Windows 下 web.listen 仅支持 TCP 端口格式 :8080 |
config, web server |