一阶段ok
This commit is contained in:
@@ -0,0 +1,721 @@
|
||||
# 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 |
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
# PRD · MailGo 邮件系统
|
||||
|
||||
- **项目名称**:mail_go
|
||||
- **编程语言**:Go
|
||||
- **核心依赖**:go-smtp / go-imap / go-pop3(emersion 系列)+ Gin + html/template
|
||||
- **文档版本**:v0.1
|
||||
- **作者**:许清楚(PM)
|
||||
|
||||
---
|
||||
|
||||
## 一、原始需求
|
||||
|
||||
用 Go 语言开发一套完整的自托管邮件系统,包含 SMTP/IMAP/POP3 服务端、Web 管理后台(/admin)和用户邮箱界面(/),配置遵循 FHS 标准,支持 SQLite/MySQL 双数据库,支持 Linux 生产环境与 Windows 本地测试。
|
||||
|
||||
---
|
||||
|
||||
## 二、产品目标
|
||||
|
||||
1. **可自托管的完整邮件服务**:提供生产可用的 SMTP/IMAP/POP3 服务端,开箱即用,无需依赖第三方邮件服务商。
|
||||
2. **低门槛运维管理**:通过 Web 管理后台完成域名配置、用户管理、DNS 指引,运维人员无需手动编辑配置文件。
|
||||
3. **标准化配置与数据存储**:严格遵循 FHS 目录规范,配置自动补全,支持 Linux/Windows 双环境,降低部署与迁移成本。
|
||||
|
||||
---
|
||||
|
||||
## 三、用户故事
|
||||
|
||||
| # | 角色 | 需求 | 价值 |
|
||||
|---|------|------|------|
|
||||
| US-1 | 系统管理员 | 通过 /admin 配置域名、端口和 TLS 证书 | 无需 SSH 手动改配置即可上线邮件服务 |
|
||||
| US-2 | 系统管理员 | 创建/删除/修改域名下的邮件用户并设置附件配额 | 集中管理账户,控制存储资源占用 |
|
||||
| US-3 | 普通用户 | 登录后在浏览器中收发邮件并上传附件 | 随时随地通过 Web 界面使用邮箱 |
|
||||
| US-4 | 普通用户 | 用标准邮件客户端(IMAP/POP3/SMTP)连接账户 | 继续使用 Outlook、Thunderbird 等熟悉工具 |
|
||||
| US-5 | 系统管理员 | 启动时配置文件缺失项自动补全,并在 /admin 查看 DNS 配置提示 | 减少初始化错误,快速完成 DNS 配置 |
|
||||
|
||||
---
|
||||
|
||||
## 四、功能需求池
|
||||
|
||||
### P0 · 必须实现
|
||||
|
||||
| ID | 功能 | 说明 |
|
||||
|----|------|------|
|
||||
| P0-01 | SMTP 服务端 | 基于 go-smtp,支持收信/转发,TLS 可配置 |
|
||||
| P0-02 | IMAP 服务端 | 基于 go-imap,支持标准 IMAP4 操作(文件夹、标记、搜索) |
|
||||
| P0-03 | POP3 服务端 | 基于 go-pop3,支持标准 POP3 收信 |
|
||||
| P0-04 | 配置文件自动补全 | 启动时检查 `/etc/mail_go/`(Windows:`./win/etc/mail_go/`),缺失项写入默认值 |
|
||||
| P0-05 | 数据库支持 | 默认 SQLite,可切换 MySQL;数据目录 `/srv/mail_go`(Windows:`./win/srv/mail_go`) |
|
||||
| P0-06 | 邮件持久化 | 收/发邮件元数据及正文存入数据库 |
|
||||
| P0-07 | 附件存储 | 附件保存至 `/srv/mail_go/attachments`(Windows:`./win/srv/mail_go/attachments`) |
|
||||
| P0-08 | 默认 admin 账户 | 首次启动自动生成 admin 管理员账户 |
|
||||
| P0-09 | 用户登录认证 | Web 界面登录,Session 管理,未登录重定向 |
|
||||
| P0-10 | 收件箱页面 | 邮件列表 + 邮件阅读(HTML/纯文本) |
|
||||
| P0-11 | 撰写/发送邮件 | 支持附件上传,调用本地 SMTP 发送 |
|
||||
| P0-12 | Web 框架 | Gin + html/template,/admin 仅管理员可访问 |
|
||||
|
||||
### P1 · 应当实现
|
||||
|
||||
| ID | 功能 | 说明 |
|
||||
|----|------|------|
|
||||
| P1-01 | 发件箱页面 | 展示已发送邮件列表 |
|
||||
| P1-02 | 域名管理(/admin) | 配置域名、SMTP/IMAP 端口、TLS 证书路径 |
|
||||
| P1-03 | 用户管理(/admin) | 创建/删除/修改用户,设置附件空间配额(默认 5 GB) |
|
||||
| P1-04 | DNS 配置提示(/admin) | 展示 MX、SPF、DKIM、DMARC 记录的填写示例 |
|
||||
| P1-05 | Web 监听配置 | 支持 TCP 端口和 Unix Socket(Windows 仅 TCP) |
|
||||
| P1-06 | DKIM 签名 | 发信时自动附加 DKIM 签名 |
|
||||
|
||||
### P2 · 有则更好
|
||||
|
||||
| ID | 功能 | 说明 |
|
||||
|----|------|------|
|
||||
| P2-01 | 邮件搜索 | Web 界面关键词搜索收件箱 |
|
||||
| P2-02 | 垃圾邮件过滤 | 基础规则过滤或集成第三方库 |
|
||||
| P2-03 | 多域名支持 | 单实例托管多个邮件域名 |
|
||||
| P2-04 | 邮件分组/标签 | 用户自定义文件夹或标签 |
|
||||
| P2-05 | 操作审计日志 | /admin 记录管理操作日志 |
|
||||
|
||||
---
|
||||
|
||||
## 五、关键 UI 页面描述
|
||||
|
||||
### 5.1 登录页(`/login`)
|
||||
- 居中卡片:邮箱地址 + 密码输入框 + 登录按钮
|
||||
- 登录失败提示错误信息
|
||||
|
||||
### 5.2 收件箱(`/inbox`)
|
||||
- 左侧导航:收件箱 / 发件箱 / 撰写 / 退出
|
||||
- 右侧主区:邮件列表(发件人、主题、时间、未读标记)
|
||||
- 点击邮件展开正文,底部显示附件下载链接
|
||||
|
||||
### 5.3 撰写邮件(`/compose`)
|
||||
- 表单:收件人、抄送(可选)、主题、正文(textarea)
|
||||
- 附件上传按钮(支持多文件)
|
||||
- 发送 / 存草稿 按钮
|
||||
|
||||
### 5.4 发件箱(`/sent`)
|
||||
- 与收件箱布局一致,展示已发送邮件列表
|
||||
|
||||
### 5.5 管理后台 - 域名配置(`/admin/domains`)
|
||||
- 表格列:域名、SMTP 端口、IMAP 端口、TLS 状态、操作
|
||||
- 新增/编辑弹层:域名、端口、证书路径输入框
|
||||
- DNS 提示区:自动生成 MX / SPF / DKIM / DMARC 记录文本,可一键复制
|
||||
|
||||
### 5.6 管理后台 - 用户管理(`/admin/users`)
|
||||
- 表格列:用户名、域名、配额、已用空间、状态、操作
|
||||
- 新增/编辑弹层:用户名、密码、所属域名、附件配额(GB)
|
||||
|
||||
---
|
||||
|
||||
## 六、配置文件结构(参考)
|
||||
|
||||
```toml
|
||||
# /etc/mail_go/config.toml
|
||||
|
||||
[database]
|
||||
driver = "sqlite" # sqlite | mysql
|
||||
dsn = "/srv/mail_go/mail.db"
|
||||
|
||||
[storage]
|
||||
base_dir = "/srv/mail_go"
|
||||
attachments = "/srv/mail_go/attachments"
|
||||
|
||||
[web]
|
||||
listen = ":8080" # TCP 端口或 Unix socket 路径(仅 Linux)
|
||||
|
||||
[smtp]
|
||||
port = 25
|
||||
tls_port = 465
|
||||
cert = ""
|
||||
key = ""
|
||||
|
||||
[imap]
|
||||
port = 143
|
||||
tls_port = 993
|
||||
cert = ""
|
||||
key = ""
|
||||
|
||||
[pop3]
|
||||
port = 110
|
||||
tls_port = 995
|
||||
cert = ""
|
||||
key = ""
|
||||
```
|
||||
|
||||
Windows 测试时路径前缀替换为 `./win`。
|
||||
|
||||
---
|
||||
|
||||
## 七、待确认问题(Open Questions)
|
||||
|
||||
| # | 问题 | 影响范围 |
|
||||
|---|------|----------|
|
||||
| OQ-1 | DKIM 私钥由系统自动生成还是管理员手动导入?影响 /admin 域名配置页交互设计 | P1-02、P1-06 |
|
||||
| OQ-2 | 是否需要支持邮件转发规则(Forward Rule)?影响 SMTP 服务端逻辑复杂度 | P0-01 |
|
||||
| OQ-3 | 附件大小上限由全局配置还是按用户配额单独限制? | P0-07、P1-03 |
|
||||
| OQ-4 | Web 界面是否需要支持富文本(HTML)邮件撰写,还是纯文本即可? | P0-11 |
|
||||
| OQ-5 | 多域名(P2-03)是否纳入第一期范围?影响数据库 Schema 设计 | P2-03 |
|
||||
| OQ-6 | 是否需要 OAuth2 / 外部 LDAP 认证,还是仅本地账户? | P0-09 |
|
||||
Reference in New Issue
Block a user