Files
mail/imap.go
titor 52c5eb5ae8 feat: 重构配置文件格式并添加 IMAP ID 命令支持
- 配置文件分离:用户配置与项目配置分离,项目级配置(客户端信息、需要 ID 命令的 provider)放在代码中
- 新增 check_id 字段:用户可选择禁用单个账户的 ID 命令发送
- 简化 provider:只保留 163 和 QQ,移除未使用的 Gmail/Outlook/188 等
- 修复 163 邮箱收件箱问题:通过发送 IMAP ID 命令解决 Unsafe Login 错误

BREAKING CHANGE: 配置文件格式变化,旧配置不兼容
2026-04-10 00:39:06 +08:00

196 lines
3.9 KiB
Go

package main
import (
"fmt"
"time"
imap "github.com/BrianLeishman/go-imap"
)
type ReceivedEmail struct {
UID uint32
From string
FromName string
Subject string
Date time.Time
Preview string
Account string
AccountID string
}
func getEmailProviderName(email string) string {
if len(email) == 0 {
return "Email"
}
atIdx := -1
for i := len(email) - 1; i >= 0; i-- {
if email[i] == '@' {
atIdx = i
break
}
}
if atIdx == -1 {
return "Email"
}
domain := email[atIdx+1:]
for i := len(domain) - 1; i >= 0; i-- {
if domain[i] == '.' {
return domain[:i]
}
}
return domain
}
func FetchUnreadEmails(account Account, days int) ([]ReceivedEmail, error) {
if account.IMAP.Host == "" {
return nil, fmt.Errorf("IMAP host not configured for account: %s", account.Email)
}
port := account.IMAP.Port
if port == 0 {
port = 993
}
username := account.IMAP.Username
if username == "" {
username = account.Email
}
m, err := imap.New(username, account.IMAP.Password, account.IMAP.Host, port)
if err != nil {
return nil, fmt.Errorf("failed to connect to IMAP server: %w", err)
}
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")
if err != nil {
return nil, fmt.Errorf("failed to select inbox: %w", err)
}
sinceDate := time.Now().AddDate(0, 0, -days)
uids, err := m.GetUIDs(imap.Search().Since(sinceDate).Unseen().Build())
if err != nil {
return nil, fmt.Errorf("failed to search emails: %w", err)
}
if len(uids) == 0 {
return nil, nil
}
overviews, err := m.GetOverviews(uids...)
if err != nil {
return nil, fmt.Errorf("failed to fetch emails: %w", err)
}
var emails []ReceivedEmail
accountName := getEmailProviderName(account.Email)
accountID := account.Email
if account.Name != "" {
accountName = account.Name
accountID = account.Name
}
for uid, email := range overviews {
from := ""
if len(email.From) > 0 {
for _, addr := range email.From {
from = addr
break
}
}
emails = append(emails, ReceivedEmail{
UID: uint32(uid),
From: from,
FromName: "",
Subject: email.Subject,
Date: email.Sent,
Preview: fmt.Sprintf("(%.1f KB)", float64(email.Size)/1024),
Account: accountName,
AccountID: accountID,
})
}
return emails, nil
}
func GetAccountByEmail(email string) (Account, error) {
accounts, err := getAccounts()
if err != nil {
return Account{}, err
}
for _, acc := range accounts {
if acc.Email == email || acc.Name == email {
return acc, nil
}
}
return Account{}, fmt.Errorf("account not found: %s", email)
}
func GetAccountByID(id string) (Account, error) {
accounts, err := getAccounts()
if err != nil {
return Account{}, err
}
for _, acc := range accounts {
if acc.Email == id || acc.Name == id {
return acc, nil
}
}
return Account{}, fmt.Errorf("account not found: %s", id)
}
func FetchAllUnreadEmails(days int) ([]ReceivedEmail, error) {
accounts, err := getAccounts()
if err != nil {
return nil, err
}
if len(accounts) == 0 {
return nil, fmt.Errorf("no accounts configured")
}
var allEmails []ReceivedEmail
for _, account := range accounts {
emails, err := FetchUnreadEmails(account, days)
if err != nil {
continue
}
allEmails = append(allEmails, emails...)
}
sortEmailsByDate(allEmails)
return allEmails, nil
}
func sortEmailsByDate(emails []ReceivedEmail) {
for i := 0; i < len(emails)-1; i++ {
for j := i + 1; j < len(emails); j++ {
if emails[j].Date.After(emails[i].Date) {
emails[i], emails[j] = emails[j], emails[i]
}
}
}
}