722 lines
26 KiB
Markdown
722 lines
26 KiB
Markdown
# 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)
|
||
|
||
```mermaid
|
||
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 结构体
|
||
|
||
```go
|
||
// 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 接口
|
||
|
||
```go
|
||
// 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 要求)
|
||
|
||
```go
|
||
// 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 要求)
|
||
|
||
```go
|
||
// 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 启动流程
|
||
|
||
```mermaid
|
||
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 展示)
|
||
|
||
```mermaid
|
||
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` → uint
|
||
- `userEmail` → 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 → 重定向或渲染对应页面并显示错误消息
|
||
- 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. 任务依赖图
|
||
|
||
```mermaid
|
||
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 |
|