feat: 重构配置文件格式并添加 IMAP ID 命令支持
- 配置文件分离:用户配置与项目配置分离,项目级配置(客户端信息、需要 ID 命令的 provider)放在代码中 - 新增 check_id 字段:用户可选择禁用单个账户的 ID 命令发送 - 简化 provider:只保留 163 和 QQ,移除未使用的 Gmail/Outlook/188 等 - 修复 163 邮箱收件箱问题:通过发送 IMAP ID 命令解决 Unsafe Login 错误 BREAKING CHANGE: 配置文件格式变化,旧配置不兼容
This commit is contained in:
20
AGENTS.md
20
AGENTS.md
@@ -16,11 +16,14 @@
|
|||||||
## 配置文件格式
|
## 配置文件格式
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
from:
|
||||||
|
account: foolsecret@163.com
|
||||||
|
|
||||||
accounts:
|
accounts:
|
||||||
-
|
- name: work
|
||||||
email: 邮箱
|
email: 邮箱
|
||||||
provider: 163
|
provider: 163
|
||||||
username: 用户名
|
username: foolsecret@163.com
|
||||||
password: 密钥
|
password: 密钥
|
||||||
encryption: ssl
|
encryption: ssl
|
||||||
insecure: false
|
insecure: false
|
||||||
@@ -30,8 +33,17 @@ accounts:
|
|||||||
smtp:
|
smtp:
|
||||||
host: smtp.163.com
|
host: smtp.163.com
|
||||||
port: 465
|
port: 465
|
||||||
|
- name: qqemail
|
||||||
-
|
|
||||||
email: 邮箱
|
email: 邮箱
|
||||||
provider: qq
|
provider: qq
|
||||||
|
username: xxx@qq.com
|
||||||
|
password: QQ邮箱IMAP授权码
|
||||||
|
encryption: ssl
|
||||||
|
insecure: fales
|
||||||
|
imap:
|
||||||
|
host: imap.qq.com
|
||||||
|
port: 993
|
||||||
|
smtp:
|
||||||
|
host: smtp.qq.com
|
||||||
|
port: 465
|
||||||
```
|
```
|
||||||
|
|||||||
219
config.go
219
config.go
@@ -11,22 +11,31 @@ import (
|
|||||||
const configFileName = "config.yml"
|
const configFileName = "config.yml"
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
From string `yaml:"from"`
|
From FromConfig `yaml:"from"`
|
||||||
|
Defaults DefaultsConfig `yaml:"defaults"`
|
||||||
Signature string `yaml:"signature"`
|
Signature string `yaml:"signature"`
|
||||||
SMTP SMTPConfig `yaml:"smtp"`
|
|
||||||
UnsafeHTML bool `yaml:"unsafe_html"`
|
UnsafeHTML bool `yaml:"unsafe_html"`
|
||||||
Accounts []Account `yaml:"accounts"`
|
Accounts []Account `yaml:"accounts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FromConfig struct {
|
||||||
|
Account string `yaml:"account"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultsConfig struct {
|
||||||
|
Encryption string `yaml:"encryption"`
|
||||||
|
Insecure bool `yaml:"insecure"`
|
||||||
|
}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Email string `yaml:"email"`
|
Email string `yaml:"email"`
|
||||||
Provider string `yaml:"provider"`
|
Provider string `yaml:"provider"`
|
||||||
Username string `yaml:"username"`
|
Username string `yaml:"username"`
|
||||||
Password string `yaml:"password"`
|
Password string `yaml:"password"`
|
||||||
|
CheckID *bool `yaml:"check_id"`
|
||||||
IMAP IMAPConfig `yaml:"imap"`
|
IMAP IMAPConfig `yaml:"imap"`
|
||||||
SMTP SMTPConfig `yaml:"smtp"`
|
SMTP SMTPConfig `yaml:"smtp"`
|
||||||
SMTPEncryption string `yaml:"smtp_encryption"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type IMAPConfig struct {
|
type IMAPConfig struct {
|
||||||
@@ -48,42 +57,12 @@ type SMTPConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var defaultConfig = Config{
|
var defaultConfig = Config{
|
||||||
From: "",
|
From: FromConfig{},
|
||||||
|
Defaults: DefaultsConfig{Encryption: "starttls"},
|
||||||
Signature: "",
|
Signature: "",
|
||||||
SMTP: SMTPConfig{
|
|
||||||
Host: "",
|
|
||||||
Port: 587,
|
|
||||||
Username: "",
|
|
||||||
Password: "",
|
|
||||||
Encryption: "starttls",
|
|
||||||
InsecureSkipVerify: false,
|
|
||||||
},
|
|
||||||
UnsafeHTML: false,
|
UnsafeHTML: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
var emailProviders = map[string]SMTPConfig{
|
|
||||||
"163": {
|
|
||||||
Host: "smtp.163.com",
|
|
||||||
Port: 465,
|
|
||||||
Encryption: "ssl",
|
|
||||||
},
|
|
||||||
"QQ": {
|
|
||||||
Host: "smtp.qq.com",
|
|
||||||
Port: 465,
|
|
||||||
Encryption: "ssl",
|
|
||||||
},
|
|
||||||
"Gmail": {
|
|
||||||
Host: "smtp.gmail.com",
|
|
||||||
Port: 465,
|
|
||||||
Encryption: "ssl",
|
|
||||||
},
|
|
||||||
"Outlook": {
|
|
||||||
Host: "smtp.office365.com",
|
|
||||||
Port: 587,
|
|
||||||
Encryption: "starttls",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var providerDefaults = map[string]struct {
|
var providerDefaults = map[string]struct {
|
||||||
IMAPHost string
|
IMAPHost string
|
||||||
IMAPPort int
|
IMAPPort int
|
||||||
@@ -100,14 +79,6 @@ var providerDefaults = map[string]struct {
|
|||||||
SMTPPort: 465,
|
SMTPPort: 465,
|
||||||
SMTPEncryption: "ssl",
|
SMTPEncryption: "ssl",
|
||||||
},
|
},
|
||||||
"188": {
|
|
||||||
IMAPHost: "imap.188.com",
|
|
||||||
IMAPPort: 993,
|
|
||||||
IMAPEncryption: "ssl",
|
|
||||||
SMTPHost: "smtp.188.com",
|
|
||||||
SMTPPort: 465,
|
|
||||||
SMTPEncryption: "ssl",
|
|
||||||
},
|
|
||||||
"QQ": {
|
"QQ": {
|
||||||
IMAPHost: "imap.qq.com",
|
IMAPHost: "imap.qq.com",
|
||||||
IMAPPort: 993,
|
IMAPPort: 993,
|
||||||
@@ -116,22 +87,6 @@ var providerDefaults = map[string]struct {
|
|||||||
SMTPPort: 465,
|
SMTPPort: 465,
|
||||||
SMTPEncryption: "ssl",
|
SMTPEncryption: "ssl",
|
||||||
},
|
},
|
||||||
"Gmail": {
|
|
||||||
IMAPHost: "imap.gmail.com",
|
|
||||||
IMAPPort: 993,
|
|
||||||
IMAPEncryption: "ssl",
|
|
||||||
SMTPHost: "smtp.gmail.com",
|
|
||||||
SMTPPort: 465,
|
|
||||||
SMTPEncryption: "ssl",
|
|
||||||
},
|
|
||||||
"Outlook": {
|
|
||||||
IMAPHost: "outlook.office365.com",
|
|
||||||
IMAPPort: 993,
|
|
||||||
IMAPEncryption: "ssl",
|
|
||||||
SMTPHost: "smtp.office365.com",
|
|
||||||
SMTPPort: 587,
|
|
||||||
SMTPEncryption: "starttls",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var imapProviders = map[string]IMAPConfig{
|
var imapProviders = map[string]IMAPConfig{
|
||||||
@@ -147,43 +102,44 @@ var imapProviders = map[string]IMAPConfig{
|
|||||||
Username: "",
|
Username: "",
|
||||||
Password: "",
|
Password: "",
|
||||||
},
|
},
|
||||||
"Gmail": {
|
|
||||||
Host: "imap.gmail.com",
|
|
||||||
Port: 993,
|
|
||||||
Username: "",
|
|
||||||
Password: "",
|
|
||||||
},
|
|
||||||
"Outlook": {
|
|
||||||
Host: "outlook.office365.com",
|
|
||||||
Port: 993,
|
|
||||||
Username: "",
|
|
||||||
Password: "",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeAccount(acc Account) Account {
|
func normalizeAccount(acc Account, defaults DefaultsConfig) Account {
|
||||||
if acc.Provider == "" {
|
if acc.Provider == "" {
|
||||||
acc.Provider = getProviderName(acc.Email)
|
acc.Provider = getProviderName(acc.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
if defaults, ok := providerDefaults[acc.Provider]; ok {
|
if providerDefaults, ok := providerDefaults[acc.Provider]; ok {
|
||||||
if acc.IMAP.Host == "" {
|
if acc.IMAP.Host == "" {
|
||||||
acc.IMAP.Host = defaults.IMAPHost
|
acc.IMAP.Host = providerDefaults.IMAPHost
|
||||||
acc.IMAP.Port = defaults.IMAPPort
|
acc.IMAP.Port = providerDefaults.IMAPPort
|
||||||
}
|
}
|
||||||
if acc.IMAP.Encryption == "" && defaults.IMAPEncryption != "" {
|
if acc.IMAP.Encryption == "" && providerDefaults.IMAPEncryption != "" {
|
||||||
acc.IMAP.Encryption = defaults.IMAPEncryption
|
acc.IMAP.Encryption = providerDefaults.IMAPEncryption
|
||||||
}
|
}
|
||||||
if acc.SMTP.Host == "" {
|
if acc.SMTP.Host == "" {
|
||||||
acc.SMTP.Host = defaults.SMTPHost
|
acc.SMTP.Host = providerDefaults.SMTPHost
|
||||||
acc.SMTP.Port = defaults.SMTPPort
|
acc.SMTP.Port = providerDefaults.SMTPPort
|
||||||
}
|
}
|
||||||
if acc.SMTPEncryption == "" {
|
if acc.SMTP.Encryption == "" && providerDefaults.SMTPEncryption != "" {
|
||||||
acc.SMTPEncryption = defaults.SMTPEncryption
|
acc.SMTP.Encryption = providerDefaults.SMTPEncryption
|
||||||
}
|
}
|
||||||
if acc.SMTP.Encryption == "" && acc.SMTPEncryption != "" {
|
|
||||||
acc.SMTP.Encryption = acc.SMTPEncryption
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if defaults.Encryption != "" {
|
||||||
|
if acc.IMAP.Encryption == "" {
|
||||||
|
acc.IMAP.Encryption = defaults.Encryption
|
||||||
|
}
|
||||||
|
if acc.SMTP.Encryption == "" {
|
||||||
|
acc.SMTP.Encryption = defaults.Encryption
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if acc.IMAP.InsecureSkipVerify == false && defaults.Insecure {
|
||||||
|
acc.IMAP.InsecureSkipVerify = defaults.Insecure
|
||||||
|
}
|
||||||
|
if acc.SMTP.InsecureSkipVerify == false && defaults.Insecure {
|
||||||
|
acc.SMTP.InsecureSkipVerify = defaults.Insecure
|
||||||
}
|
}
|
||||||
|
|
||||||
if acc.Username == "" {
|
if acc.Username == "" {
|
||||||
@@ -206,6 +162,11 @@ func normalizeAccount(acc Account) Account {
|
|||||||
acc.Name = acc.Provider
|
acc.Name = acc.Provider
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if acc.CheckID == nil {
|
||||||
|
defaultCheckID := ProjectConfig.ProvidersNeedingCheckID[acc.Provider]
|
||||||
|
acc.CheckID = &defaultCheckID
|
||||||
|
}
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,26 +262,10 @@ func getAccounts() ([]Account, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var accounts []Account
|
accounts := cfg.Accounts
|
||||||
|
|
||||||
if cfg.SMTP.Username != "" {
|
|
||||||
accounts = append(accounts, Account{
|
|
||||||
Name: getProviderName(cfg.SMTP.Username),
|
|
||||||
Email: cfg.SMTP.Username,
|
|
||||||
SMTP: cfg.SMTP,
|
|
||||||
IMAP: IMAPConfig{
|
|
||||||
Host: getIMAPHost(cfg.SMTP.Username),
|
|
||||||
Port: 993,
|
|
||||||
Username: cfg.SMTP.Username,
|
|
||||||
Password: cfg.SMTP.Password,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
accounts = append(accounts, cfg.Accounts...)
|
|
||||||
|
|
||||||
for i := range accounts {
|
for i := range accounts {
|
||||||
accounts[i] = normalizeAccount(accounts[i])
|
accounts[i] = normalizeAccount(accounts[i], cfg.Defaults)
|
||||||
}
|
}
|
||||||
|
|
||||||
return accounts, nil
|
return accounts, nil
|
||||||
@@ -330,23 +275,10 @@ func getProviderName(email string) string {
|
|||||||
if strings.HasSuffix(email, "@163.com") || strings.HasSuffix(email, "@vip.163.com") {
|
if strings.HasSuffix(email, "@163.com") || strings.HasSuffix(email, "@vip.163.com") {
|
||||||
return "163"
|
return "163"
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(email, "@188.com") || strings.HasSuffix(email, "@vip.188.com") {
|
|
||||||
return "188"
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(email, "@qq.com") || strings.HasSuffix(email, "@vip.qq.com") {
|
if strings.HasSuffix(email, "@qq.com") || strings.HasSuffix(email, "@vip.qq.com") {
|
||||||
return "QQ"
|
return "QQ"
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(email, "@gmail.com") {
|
return "custom"
|
||||||
return "Gmail"
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(email, "@outlook.com") || strings.HasSuffix(email, "@office365.com") {
|
|
||||||
return "Outlook"
|
|
||||||
}
|
|
||||||
parts := strings.Split(email, "@")
|
|
||||||
if len(parts) > 1 {
|
|
||||||
return strings.Split(parts[1], ".")[0]
|
|
||||||
}
|
|
||||||
return "Email"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIMAPHost(email string) string {
|
func getIMAPHost(email string) string {
|
||||||
@@ -356,11 +288,60 @@ func getIMAPHost(email string) string {
|
|||||||
if strings.HasSuffix(email, "@qq.com") {
|
if strings.HasSuffix(email, "@qq.com") {
|
||||||
return "imap.qq.com"
|
return "imap.qq.com"
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(email, "@gmail.com") {
|
return ""
|
||||||
return "imap.gmail.com"
|
}
|
||||||
|
|
||||||
|
func getDefaultFromEmail() string {
|
||||||
|
cfg, err := loadConfig()
|
||||||
|
if err != nil || cfg.From.Account == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts, err := getAccounts()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, acc := range accounts {
|
||||||
|
if acc.Name == cfg.From.Account {
|
||||||
|
if acc.Email != "" {
|
||||||
|
return acc.Email
|
||||||
|
}
|
||||||
|
return acc.Username
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(email, "@outlook.com") || strings.HasSuffix(email, "@office365.com") {
|
|
||||||
return "outlook.office365.com"
|
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDefaultAccount(accounts []Account, accountName string) *Account {
|
||||||
|
if accountName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := range accounts {
|
||||||
|
if accounts[i].Name == accountName {
|
||||||
|
return &accounts[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Info struct {
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
Vendor string
|
||||||
|
}
|
||||||
|
|
||||||
|
var ProjectConfig = struct {
|
||||||
|
Info Info
|
||||||
|
ProvidersNeedingCheckID map[string]bool
|
||||||
|
}{
|
||||||
|
Info: Info{
|
||||||
|
Name: "pop",
|
||||||
|
Version: "1.0",
|
||||||
|
Vendor: "charmbracelet",
|
||||||
|
},
|
||||||
|
ProvidersNeedingCheckID: map[string]bool{
|
||||||
|
"163": true,
|
||||||
|
"QQ": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,20 +10,47 @@
|
|||||||
|
|
||||||
## 讨论内容
|
## 讨论内容
|
||||||
|
|
||||||
### 简化方案
|
### 最终配置格式
|
||||||
|
|
||||||
用户建议采用简化配置:
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
from:
|
||||||
|
account: work # 通过 name 引用账户
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
encryption: ssl
|
||||||
|
insecure: false
|
||||||
|
|
||||||
|
unsafe_html: false
|
||||||
|
|
||||||
|
signature: ""
|
||||||
|
|
||||||
accounts:
|
accounts:
|
||||||
- name: "工作邮箱"
|
- name: work
|
||||||
email: "user@163.com"
|
email: foolsecret@163.com
|
||||||
provider: "163" # 自动填充 imap/smtp
|
provider: 163
|
||||||
username: "user@163.com" # 统一认证
|
username: foolsecret@163.com
|
||||||
password: "xxx"
|
password: xxx
|
||||||
smtp_encryption: "ssl" # 可选
|
imap:
|
||||||
|
host: imap.163.com
|
||||||
|
port: 993
|
||||||
|
encryption: ssl
|
||||||
|
insecure: false
|
||||||
|
smtp:
|
||||||
|
host: smtp.163.com
|
||||||
|
port: 465
|
||||||
|
encryption: ssl
|
||||||
|
insecure: false
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `from.account` | 通过账户的 `name` 引用默认发件账户 |
|
||||||
|
| `defaults.encryption` | 全局默认加密类型 (`ssl`/`starttls`/`none`) |
|
||||||
|
| `defaults.insecure` | 全局默认跳过 TLS 证书验证 |
|
||||||
|
| 账户内覆盖 | 可以在单个账户的 imap/smtp 内覆盖默认值 |
|
||||||
|
|
||||||
### 自动识别逻辑
|
### 自动识别逻辑
|
||||||
|
|
||||||
1. **自动识别 Provider**: 通过邮箱后缀自动判断
|
1. **自动识别 Provider**: 通过邮箱后缀自动判断
|
||||||
@@ -33,35 +60,26 @@ accounts:
|
|||||||
- `@outlook.com` / `@office365.com` → Outlook
|
- `@outlook.com` / `@office365.com` → Outlook
|
||||||
- 其他 → custom
|
- 其他 → custom
|
||||||
|
|
||||||
2. **Provider 不匹配时**: 视为 custom,需手动配置 host/port
|
2. **自动填充**: 根据 provider 自动填充 imap/smtp 的 host/port/encryption
|
||||||
|
|
||||||
### Onboarding 流程
|
3. **统一认证**: username/password 只需在账户顶层配置,会自动复制到 IMAP 和 SMTP
|
||||||
|
|
||||||
```
|
### 向后兼容
|
||||||
1. 输入邮箱地址
|
|
||||||
↓
|
**不兼容旧版配置**,需要用户重新配置。
|
||||||
2. 自动识别 provider
|
|
||||||
↓
|
|
||||||
3. ┌─ 已知服务商 → 显示默认配置,提示确认
|
|
||||||
└─ 自定义邮箱 → 提示手动配置 IMAP/SMTP
|
|
||||||
↓
|
|
||||||
4. 输入密码
|
|
||||||
↓
|
|
||||||
5. 保存配置
|
|
||||||
```
|
|
||||||
|
|
||||||
## 实现计划
|
## 实现计划
|
||||||
|
|
||||||
| 步骤 | 文件 | 修改内容 |
|
| 步骤 | 文件 | 修改内容 |
|
||||||
|------|------|---------|
|
|------|------|---------|
|
||||||
| 1 | config.go | 新增 Provider、SMTPEncryption 字段 |
|
| 1 | config.go | 新增 FromConfig 和 DefaultsConfig 结构 |
|
||||||
| 2 | config.go | 添加 normalizeAccount() 自动填充默认值 |
|
| 2 | config.go | 修改 normalizeAccount() 支持 defaults |
|
||||||
| 3 | config.go | 添加 providerDefaults 映射表 |
|
| 3 | config.go | 新增 getDefaultFromEmail() 和 getDefaultAccount() |
|
||||||
| 4 | config.go | 修改 getAccounts() 调用 normalize |
|
| 4 | main.go | 使用新函数获取默认账户信息 |
|
||||||
| 5 | onboarding.go | 修改交互流程,支持自动识别和默认值 |
|
| 5 | 测试 | 验证配置读取和发送功能 |
|
||||||
|
|
||||||
## 待处理
|
## 已完成
|
||||||
|
|
||||||
- [ ] 实现 config.go 修改
|
- [x] 实现 config.go 修改
|
||||||
- [ ] 实现 onboarding.go 流程调整
|
- [x] 实现 main.go 逻辑调整
|
||||||
- [ ] 测试配置读取和写入
|
- [ ] 测试配置读取和写入
|
||||||
102
doc/003-config-separation.md
Normal file
102
doc/003-config-separation.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# 项目配置与用户配置分离讨论
|
||||||
|
|
||||||
|
**日期**: 2026-04-10
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
在实现 IMAP ID 命令功能时,需要在连接时发送客户端身份信息。这些信息(name、version、vendor)属于**应用开发配置**,而非用户运行时配置。
|
||||||
|
|
||||||
|
如果将这些信息放在用户配置文件中:
|
||||||
|
- 用户可以看到但不需要修改这些"隐藏"配置
|
||||||
|
- 暴露了应用内部实现细节
|
||||||
|
- 用户可能误修改导致功能异常
|
||||||
|
|
||||||
|
## 讨论内容
|
||||||
|
|
||||||
|
### 配置文件分离
|
||||||
|
|
||||||
|
| 配置文件 | 用途 | 位置 | 内容 |
|
||||||
|
|---------|------|------|------|
|
||||||
|
| **用户配置** | 运行时用户数据 | `~/.config/pop/config.yml` | 账户、邮箱服务、默认行为 |
|
||||||
|
| **项目配置** | 应用开发相关 | `config/project.go` | 客户端信息、需要 ID 命令的 provider 集合 |
|
||||||
|
|
||||||
|
### 用户配置示例
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
from:
|
||||||
|
account: work
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
encryption: ssl
|
||||||
|
insecure: false
|
||||||
|
|
||||||
|
unsafe_html: false
|
||||||
|
|
||||||
|
accounts:
|
||||||
|
- name: work
|
||||||
|
email: foolsecret@163.com
|
||||||
|
provider: "163"
|
||||||
|
username: foolsecret@163.com
|
||||||
|
password: xxx
|
||||||
|
imap:
|
||||||
|
host: imap.163.com
|
||||||
|
port: 993
|
||||||
|
smtp:
|
||||||
|
host: smtp.163.com
|
||||||
|
port: 465
|
||||||
|
```
|
||||||
|
|
||||||
|
### 项目配置示例 (config/project.go)
|
||||||
|
|
||||||
|
```go
|
||||||
|
var ProjectConfig = struct {
|
||||||
|
Info Info
|
||||||
|
ProvidersNeedingCheckID map[string]bool
|
||||||
|
}{
|
||||||
|
Info: Info{
|
||||||
|
Name: "pop",
|
||||||
|
Version: "1.0",
|
||||||
|
Vendor: "charmbracelet",
|
||||||
|
},
|
||||||
|
ProvidersNeedingCheckID: map[string]bool{
|
||||||
|
"163": true,
|
||||||
|
"QQ": true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CheckID 覆盖机制
|
||||||
|
|
||||||
|
用户可以在账户级别覆盖 CheckID 行为:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
accounts:
|
||||||
|
- name: work
|
||||||
|
check_id: false # 禁用该账户的 ID 命令发送
|
||||||
|
```
|
||||||
|
|
||||||
|
逻辑优先级:
|
||||||
|
1. 用户明确设置 `check_id: false` → 不发送
|
||||||
|
2. 用户明确设置 `check_id: true` → 发送
|
||||||
|
3. 未设置 → 使用项目配置的 `ProvidersNeedingCheckID` 判断
|
||||||
|
|
||||||
|
### 扩展场景
|
||||||
|
|
||||||
|
未来可以扩展项目配置:
|
||||||
|
- 环境变量控制
|
||||||
|
- 调试模式开关
|
||||||
|
- 不同 provider 的特殊处理
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
项目配置与用户配置分离是合理的架构设计:
|
||||||
|
- 职责分离:开发者配置 vs 用户配置
|
||||||
|
- 安全性:隐藏实现细节
|
||||||
|
- 可维护性:修改项目配置不影响用户数据
|
||||||
|
|
||||||
|
## 待处理
|
||||||
|
|
||||||
|
- [ ] 实现 config/project.go
|
||||||
|
- [ ] 修改 config.go 移除敏感字段
|
||||||
|
- [ ] 修改 imap.go 添加 ID 命令逻辑
|
||||||
|
- [ ] 更新文档
|
||||||
14
imap.go
14
imap.go
@@ -62,6 +62,20 @@ func FetchUnreadEmails(account Account, days int) ([]ReceivedEmail, error) {
|
|||||||
}
|
}
|
||||||
defer m.Close()
|
defer m.Close()
|
||||||
|
|
||||||
|
if account.CheckID != nil && *account.CheckID {
|
||||||
|
info := ProjectConfig.Info
|
||||||
|
idCmd := fmt.Sprintf("ID (\"name\" \"%s\" \"version\" \"%s\" \"vendor\" \"%s\")",
|
||||||
|
info.Name, info.Version, info.Vendor)
|
||||||
|
_, err := m.Exec(idCmd, false, 0, func(line []byte) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("WARNING: failed to send IMAP ID: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Exec("NOOP", false, 0, nil)
|
||||||
|
}
|
||||||
|
|
||||||
err = m.SelectFolder("INBOX")
|
err = m.SelectFolder("INBOX")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to select inbox: %w", err)
|
return nil, fmt.Errorf("failed to select inbox: %w", err)
|
||||||
|
|||||||
63
main.go
63
main.go
@@ -203,6 +203,7 @@ func init() {
|
|||||||
rootCmd.AddCommand(ManCmd)
|
rootCmd.AddCommand(ManCmd)
|
||||||
|
|
||||||
cfg, _ := loadConfig()
|
cfg, _ := loadConfig()
|
||||||
|
_ = cfg
|
||||||
|
|
||||||
rootCmd.Flags().StringSliceVar(&bcc, "bcc", []string{}, "BCC recipients")
|
rootCmd.Flags().StringSliceVar(&bcc, "bcc", []string{}, "BCC recipients")
|
||||||
rootCmd.Flags().StringSliceVar(&cc, "cc", []string{}, "CC recipients")
|
rootCmd.Flags().StringSliceVar(&cc, "cc", []string{}, "CC recipients")
|
||||||
@@ -211,7 +212,7 @@ func init() {
|
|||||||
rootCmd.Flags().StringVarP(&body, "body", "b", "", "Email's contents")
|
rootCmd.Flags().StringVarP(&body, "body", "b", "", "Email's contents")
|
||||||
envFrom := os.Getenv(PopFrom)
|
envFrom := os.Getenv(PopFrom)
|
||||||
if envFrom == "" {
|
if envFrom == "" {
|
||||||
envFrom = cfg.From
|
envFrom = getDefaultFromEmail()
|
||||||
}
|
}
|
||||||
from = envFrom
|
from = envFrom
|
||||||
rootCmd.Flags().StringVarP(&from, "from", "f", envFrom, "Email's sender"+commentStyle.Render("($"+PopFrom+")"))
|
rootCmd.Flags().StringVarP(&from, "from", "f", envFrom, "Email's sender"+commentStyle.Render("($"+PopFrom+")"))
|
||||||
@@ -228,46 +229,52 @@ func init() {
|
|||||||
}
|
}
|
||||||
rootCmd.Flags().StringVarP(&signature, "signature", "x", envSignature, "Signature to display at the end of the email."+commentStyle.Render("($"+PopSignature+")"))
|
rootCmd.Flags().StringVarP(&signature, "signature", "x", envSignature, "Signature to display at the end of the email."+commentStyle.Render("($"+PopSignature+")"))
|
||||||
envSMTPHost := os.Getenv(PopSMTPHost)
|
envSMTPHost := os.Getenv(PopSMTPHost)
|
||||||
if envSMTPHost == "" {
|
|
||||||
envSMTPHost = cfg.SMTP.Host
|
|
||||||
}
|
|
||||||
smtpHost = envSMTPHost
|
|
||||||
rootCmd.Flags().StringVarP(&smtpHost, "smtp.host", "H", envSMTPHost, "Host of the SMTP server"+commentStyle.Render("($"+PopSMTPHost+")"))
|
|
||||||
envSMTPPort, _ := strconv.Atoi(os.Getenv(PopSMTPPort))
|
envSMTPPort, _ := strconv.Atoi(os.Getenv(PopSMTPPort))
|
||||||
if envSMTPPort == 0 {
|
envSMTPUsername := os.Getenv(PopSMTPUsername)
|
||||||
envSMTPPort = cfg.SMTP.Port
|
envSMTPPassword := os.Getenv(PopSMTPPassword)
|
||||||
|
envSMTPEncryption := os.Getenv(PopSMTPEncryption)
|
||||||
|
envInsecureSkipVerify := os.Getenv(PopSMTPInsecureSkipVerify) == "true"
|
||||||
|
|
||||||
|
defaultAccounts, _ := getAccounts()
|
||||||
|
defaultAccount := getDefaultAccount(defaultAccounts, cfg.From.Account)
|
||||||
|
|
||||||
|
if envSMTPHost == "" && defaultAccount != nil {
|
||||||
|
envSMTPHost = defaultAccount.SMTP.Host
|
||||||
|
}
|
||||||
|
if envSMTPPort == 0 && defaultAccount != nil {
|
||||||
|
envSMTPPort = defaultAccount.SMTP.Port
|
||||||
if envSMTPPort == 0 {
|
if envSMTPPort == 0 {
|
||||||
envSMTPPort = 587
|
envSMTPPort = 587
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
smtpPort = envSMTPPort
|
if envSMTPUsername == "" && defaultAccount != nil {
|
||||||
rootCmd.Flags().IntVarP(&smtpPort, "smtp.port", "P", envSMTPPort, "Port of the SMTP server"+commentStyle.Render("($"+PopSMTPPort+")"))
|
envSMTPUsername = defaultAccount.SMTP.Username
|
||||||
envSMTPUsername := os.Getenv(PopSMTPUsername)
|
|
||||||
if envSMTPUsername == "" {
|
|
||||||
envSMTPUsername = cfg.SMTP.Username
|
|
||||||
}
|
}
|
||||||
smtpUsername = envSMTPUsername
|
if envSMTPPassword == "" && defaultAccount != nil {
|
||||||
rootCmd.Flags().StringVarP(&smtpUsername, "smtp.username", "U", envSMTPUsername, "Username of the SMTP server"+commentStyle.Render("($"+PopSMTPUsername+")"))
|
envSMTPPassword = defaultAccount.SMTP.Password
|
||||||
envSMTPPassword := os.Getenv(PopSMTPPassword)
|
|
||||||
if envSMTPPassword == "" {
|
|
||||||
envSMTPPassword = cfg.SMTP.Password
|
|
||||||
}
|
}
|
||||||
smtpPassword = envSMTPPassword
|
if envSMTPEncryption == "" && defaultAccount != nil {
|
||||||
rootCmd.Flags().StringVarP(&smtpPassword, "smtp.password", "p", envSMTPPassword, "Password of the SMTP server"+commentStyle.Render("($"+PopSMTPPassword+")"))
|
envSMTPEncryption = defaultAccount.SMTP.Encryption
|
||||||
envSMTPEncryption := os.Getenv(PopSMTPEncryption)
|
|
||||||
if envSMTPEncryption == "" {
|
|
||||||
envSMTPEncryption = cfg.SMTP.Encryption
|
|
||||||
if envSMTPEncryption == "" {
|
if envSMTPEncryption == "" {
|
||||||
envSMTPEncryption = "starttls"
|
envSMTPEncryption = "starttls"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
smtpEncryption = envSMTPEncryption
|
if !envInsecureSkipVerify && defaultAccount != nil {
|
||||||
rootCmd.Flags().StringVarP(&smtpEncryption, "smtp.encryption", "e", envSMTPEncryption, "Encryption type of the SMTP server (starttls, ssl, or none)"+commentStyle.Render("($"+PopSMTPEncryption+")"))
|
envInsecureSkipVerify = defaultAccount.SMTP.InsecureSkipVerify
|
||||||
envInsecureSkipVerify := os.Getenv(PopSMTPInsecureSkipVerify) == "true"
|
|
||||||
if !envInsecureSkipVerify {
|
|
||||||
envInsecureSkipVerify = cfg.SMTP.InsecureSkipVerify
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
smtpHost = envSMTPHost
|
||||||
|
smtpPort = envSMTPPort
|
||||||
|
smtpUsername = envSMTPUsername
|
||||||
|
smtpPassword = envSMTPPassword
|
||||||
|
smtpEncryption = envSMTPEncryption
|
||||||
smtpInsecureSkipVerify = envInsecureSkipVerify
|
smtpInsecureSkipVerify = envInsecureSkipVerify
|
||||||
|
|
||||||
|
rootCmd.Flags().StringVarP(&smtpHost, "smtp.host", "H", envSMTPHost, "Host of the SMTP server"+commentStyle.Render("($"+PopSMTPHost+")"))
|
||||||
|
rootCmd.Flags().IntVarP(&smtpPort, "smtp.port", "P", envSMTPPort, "Port of the SMTP server"+commentStyle.Render("($"+PopSMTPPort+")"))
|
||||||
|
rootCmd.Flags().StringVarP(&smtpUsername, "smtp.username", "U", envSMTPUsername, "Username of the SMTP server"+commentStyle.Render("($"+PopSMTPUsername+")"))
|
||||||
|
rootCmd.Flags().StringVarP(&smtpPassword, "smtp.password", "p", envSMTPPassword, "Password of the SMTP server"+commentStyle.Render("($"+PopSMTPPassword+")"))
|
||||||
|
rootCmd.Flags().StringVarP(&smtpEncryption, "smtp.encryption", "e", envSMTPEncryption, "Encryption type of the SMTP server (starttls, ssl, or none)"+commentStyle.Render("($"+PopSMTPEncryption+")"))
|
||||||
rootCmd.Flags().BoolVarP(&smtpInsecureSkipVerify, "smtp.insecure", "i", envInsecureSkipVerify, "Skip TLS verification with SMTP server"+commentStyle.Render("($"+PopSMTPInsecureSkipVerify+")"))
|
rootCmd.Flags().BoolVarP(&smtpInsecureSkipVerify, "smtp.insecure", "i", envInsecureSkipVerify, "Skip TLS verification with SMTP server"+commentStyle.Render("($"+PopSMTPInsecureSkipVerify+")"))
|
||||||
envResendAPIKey := os.Getenv(ResendAPIKey)
|
envResendAPIKey := os.Getenv(ResendAPIKey)
|
||||||
rootCmd.Flags().StringVarP(&resendAPIKey, "resend.key", "r", envResendAPIKey, "API key for the Resend.com"+commentStyle.Render("($"+ResendAPIKey+")"))
|
rootCmd.Flags().StringVarP(&resendAPIKey, "resend.key", "r", envResendAPIKey, "API key for the Resend.com"+commentStyle.Render("($"+ResendAPIKey+")"))
|
||||||
|
|||||||
@@ -156,9 +156,10 @@ func runOnboarding() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cfg := Config{
|
cfg := Config{
|
||||||
From: email,
|
From: FromConfig{Account: accountName},
|
||||||
Signature: "",
|
Signature: "",
|
||||||
UnsafeHTML: unsafeHTML,
|
UnsafeHTML: unsafeHTML,
|
||||||
|
Defaults: DefaultsConfig{Encryption: smtpEncryption},
|
||||||
Accounts: []Account{
|
Accounts: []Account{
|
||||||
{
|
{
|
||||||
Name: accountName,
|
Name: accountName,
|
||||||
@@ -166,7 +167,6 @@ func runOnboarding() error {
|
|||||||
Provider: provider,
|
Provider: provider,
|
||||||
Username: smtpUsername,
|
Username: smtpUsername,
|
||||||
Password: smtpPassword,
|
Password: smtpPassword,
|
||||||
SMTPEncryption: smtpEncryption,
|
|
||||||
IMAP: IMAPConfig{
|
IMAP: IMAPConfig{
|
||||||
Host: imapHost,
|
Host: imapHost,
|
||||||
Password: smtpPassword,
|
Password: smtpPassword,
|
||||||
|
|||||||
Reference in New Issue
Block a user