- 添加邮件详情面板显示(主题、发件人、收件人、抄送、账户、时间、正文) - 优化邮件列表卡片样式,增加选中高亮效果 - 窗口宽度 >= 80 时启用双面板布局,左侧列表右侧详情 - 简化依赖包,从 charm.land 使用统一导入路径 - 删除未使用的 golangci/goreleaser 配置文件
338 lines
6.8 KiB
Go
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
|
|
}
|