Files
mail/imap.go
titor c9b77feabe feat: 收件箱功能新增按回车查看详情面板
- 添加邮件详情面板显示(主题、发件人、收件人、抄送、账户、时间、正文)
- 优化邮件列表卡片样式,增加选中高亮效果
- 窗口宽度 >= 80 时启用双面板布局,左侧列表右侧详情
- 简化依赖包,从 charm.land 使用统一导入路径
- 删除未使用的 golangci/goreleaser 配置文件
2026-04-10 04:41:22 +08:00

338 lines
6.8 KiB
Go

package main
import (
"fmt"
"strings"
"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]
}
}
}
}
type EmailDetail struct {
UID uint32
From string
FromName string
To string
Cc string
Subject string
Date time.Time
TextBody string
HTMLBody string
Account string
AccountID string
}
func FetchEmailDetailByUID(accountID string, uid uint32) (*EmailDetail, error) {
account, err := GetAccountByID(accountID)
if err != nil {
return nil, err
}
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)
}
emails, err := m.GetEmails(int(uid))
if err != nil {
return nil, fmt.Errorf("failed to fetch email: %w", err)
}
if len(emails) == 0 {
return nil, fmt.Errorf("email not found")
}
var email imap.Email
found := false
for i := range emails {
if emails[i] != nil {
email = *emails[i]
found = true
break
}
}
if !found {
return nil, fmt.Errorf("email not found or has been removed")
}
accountName := getEmailProviderName(account.Email)
accountIDName := account.Email
if account.Name != "" {
accountName = account.Name
accountIDName = account.Name
}
var fromName string
var fromAddr string
if email.From != nil {
for name, addr := range email.From {
fromName = name
fromAddr = addr
break
}
}
var toList []string
if email.To != nil {
for name, addr := range email.To {
if name != "" {
toList = append(toList, fmt.Sprintf("%s <%s>", name, addr))
} else {
toList = append(toList, addr)
}
}
}
var ccList []string
if email.CC != nil {
for name, addr := range email.CC {
if name != "" {
ccList = append(ccList, fmt.Sprintf("%s <%s>", name, addr))
} else {
ccList = append(ccList, addr)
}
}
}
fromStr := fromAddr
if fromName != "" {
fromStr = fmt.Sprintf("%s <%s>", fromName, fromAddr)
}
return &EmailDetail{
UID: uint32(email.UID),
From: fromStr,
FromName: fromName,
To: strings.Join(toList, ", "),
Cc: strings.Join(ccList, ", "),
Subject: email.Subject,
Date: email.Sent,
TextBody: email.Text,
HTMLBody: email.HTML,
Account: accountName,
AccountID: accountIDName,
}, nil
}