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:
2026-04-10 00:39:06 +08:00
parent d54fd01001
commit 52c5eb5ae8
7 changed files with 334 additions and 200 deletions

241
config.go
View File

@@ -11,22 +11,31 @@ import (
const configFileName = "config.yml"
type Config struct {
From string `yaml:"from"`
Signature string `yaml:"signature"`
SMTP SMTPConfig `yaml:"smtp"`
UnsafeHTML bool `yaml:"unsafe_html"`
Accounts []Account `yaml:"accounts"`
From FromConfig `yaml:"from"`
Defaults DefaultsConfig `yaml:"defaults"`
Signature string `yaml:"signature"`
UnsafeHTML bool `yaml:"unsafe_html"`
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 {
Name string `yaml:"name"`
Email string `yaml:"email"`
Provider string `yaml:"provider"`
Username string `yaml:"username"`
Password string `yaml:"password"`
IMAP IMAPConfig `yaml:"imap"`
SMTP SMTPConfig `yaml:"smtp"`
SMTPEncryption string `yaml:"smtp_encryption"`
Name string `yaml:"name"`
Email string `yaml:"email"`
Provider string `yaml:"provider"`
Username string `yaml:"username"`
Password string `yaml:"password"`
CheckID *bool `yaml:"check_id"`
IMAP IMAPConfig `yaml:"imap"`
SMTP SMTPConfig `yaml:"smtp"`
}
type IMAPConfig struct {
@@ -48,42 +57,12 @@ type SMTPConfig struct {
}
var defaultConfig = Config{
From: "",
Signature: "",
SMTP: SMTPConfig{
Host: "",
Port: 587,
Username: "",
Password: "",
Encryption: "starttls",
InsecureSkipVerify: false,
},
From: FromConfig{},
Defaults: DefaultsConfig{Encryption: "starttls"},
Signature: "",
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 {
IMAPHost string
IMAPPort int
@@ -100,14 +79,6 @@ var providerDefaults = map[string]struct {
SMTPPort: 465,
SMTPEncryption: "ssl",
},
"188": {
IMAPHost: "imap.188.com",
IMAPPort: 993,
IMAPEncryption: "ssl",
SMTPHost: "smtp.188.com",
SMTPPort: 465,
SMTPEncryption: "ssl",
},
"QQ": {
IMAPHost: "imap.qq.com",
IMAPPort: 993,
@@ -116,22 +87,6 @@ var providerDefaults = map[string]struct {
SMTPPort: 465,
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{
@@ -147,43 +102,44 @@ var imapProviders = map[string]IMAPConfig{
Username: "",
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 == "" {
acc.Provider = getProviderName(acc.Email)
}
if defaults, ok := providerDefaults[acc.Provider]; ok {
if providerDefaults, ok := providerDefaults[acc.Provider]; ok {
if acc.IMAP.Host == "" {
acc.IMAP.Host = defaults.IMAPHost
acc.IMAP.Port = defaults.IMAPPort
acc.IMAP.Host = providerDefaults.IMAPHost
acc.IMAP.Port = providerDefaults.IMAPPort
}
if acc.IMAP.Encryption == "" && defaults.IMAPEncryption != "" {
acc.IMAP.Encryption = defaults.IMAPEncryption
if acc.IMAP.Encryption == "" && providerDefaults.IMAPEncryption != "" {
acc.IMAP.Encryption = providerDefaults.IMAPEncryption
}
if acc.SMTP.Host == "" {
acc.SMTP.Host = defaults.SMTPHost
acc.SMTP.Port = defaults.SMTPPort
acc.SMTP.Host = providerDefaults.SMTPHost
acc.SMTP.Port = providerDefaults.SMTPPort
}
if acc.SMTPEncryption == "" {
acc.SMTPEncryption = defaults.SMTPEncryption
if acc.SMTP.Encryption == "" && providerDefaults.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 == "" {
@@ -206,6 +162,11 @@ func normalizeAccount(acc Account) Account {
acc.Name = acc.Provider
}
if acc.CheckID == nil {
defaultCheckID := ProjectConfig.ProvidersNeedingCheckID[acc.Provider]
acc.CheckID = &defaultCheckID
}
return acc
}
@@ -301,26 +262,10 @@ func getAccounts() ([]Account, error) {
return nil, err
}
var accounts []Account
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...)
accounts := cfg.Accounts
for i := range accounts {
accounts[i] = normalizeAccount(accounts[i])
accounts[i] = normalizeAccount(accounts[i], cfg.Defaults)
}
return accounts, nil
@@ -330,23 +275,10 @@ func getProviderName(email string) string {
if strings.HasSuffix(email, "@163.com") || strings.HasSuffix(email, "@vip.163.com") {
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") {
return "QQ"
}
if strings.HasSuffix(email, "@gmail.com") {
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"
return "custom"
}
func getIMAPHost(email string) string {
@@ -356,11 +288,60 @@ func getIMAPHost(email string) string {
if strings.HasSuffix(email, "@qq.com") {
return "imap.qq.com"
}
if strings.HasSuffix(email, "@gmail.com") {
return "imap.gmail.com"
return ""
}
func getDefaultFromEmail() string {
cfg, err := loadConfig()
if err != nil || cfg.From.Account == "" {
return ""
}
if strings.HasSuffix(email, "@outlook.com") || strings.HasSuffix(email, "@office365.com") {
return "outlook.office365.com"
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
}
}
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,
},
}