Files
mailgo/internal/imap_server/service.go
T
kevin dc164fdf66 feat(mailbox): 文件夹数据驱动,Web 通过 IMAP 共享服务层操作邮箱
- 新增 mailboxes 表与 MailboxStore(系统文件夹幂等创建,自定义文件夹 CRUD)
- 提取 MailboxService:IMAP 会话与 Web handler 共用,LIST 返回什么 Web 就显示什么
- IMAP 支持 CREATE/DELETE/RENAME/SUBSCRIBE(系统文件夹禁删改、非空禁删)
- Web 删除改为 IMAP 语义:移入 Trash,新增恢复/彻底删除/清空
- 新增通用 /folder/:name 页面与动态侧边栏,/inbox /sent /drafts 保留兼容重定向
2026-08-20 01:25:40 +08:00

336 lines
9.2 KiB
Go

package imap_server
// MailboxService 是邮箱(文件夹)与消息操作的服务层:
// IMAP 会话与 Web handler 共用同一份实现,保证「IMAP LIST 返回什么,
// Web 就显示什么」,且移动/删除等操作走同一语义(如移入 Trash)。
import (
"log"
"strings"
"mail_go/internal/db"
"mail_go/internal/store"
"github.com/emersion/go-imap/v2"
)
// FolderInfo 是一个文件夹在列表页(IMAP LIST / Web 侧边栏)
// 展示所需的信息。
type FolderInfo struct {
Name string
SpecialUse string
Subscribed bool
Total int64
Unseen int64
}
// MailboxService 提供用户级邮箱操作。
type MailboxService struct {
stores *store.Stores
}
// NewMailboxService creates a MailboxService backed by stores.
func NewMailboxService(stores *store.Stores) *MailboxService {
return &MailboxService{stores: stores}
}
// ListAll 返回用户全部文件夹(先确保系统文件夹存在)。
func (s *MailboxService) ListAll(userID uint) ([]db.Mailbox, error) {
if err := s.stores.Mailboxes.EnsureSystem(userID); err != nil {
return nil, err
}
return s.stores.Mailboxes.List(userID)
}
// List 返回用户全部文件夹及统计信息(IMAP LIST / Web 侧边栏同源)。
func (s *MailboxService) List(userID uint) ([]FolderInfo, error) {
mbs, err := s.ListAll(userID)
if err != nil {
return nil, err
}
infos := make([]FolderInfo, 0, len(mbs))
for _, mb := range mbs {
total, err := s.stores.Mails.CountByUserAndFolder(userID, mb.Name)
if err != nil {
log.Printf("mailbox: 统计 %s 邮件数失败: %v", mb.Name, err)
}
unseen, err := s.stores.Mails.CountUnread(userID, mb.Name)
if err != nil {
log.Printf("mailbox: 统计 %s 未读数失败: %v", mb.Name, err)
}
infos = append(infos, FolderInfo{
Name: mb.Name,
SpecialUse: mb.SpecialUse,
Subscribed: mb.IsSubscribed,
Total: total,
Unseen: unseen,
})
}
return infos, nil
}
// Canonical 规范化邮箱名:INBOX 大小写不敏感,其余按 DB 实际名称
// 精确匹配。会先确保系统文件夹存在(客户端可能不 LIST 直接 SELECT)。
func (s *MailboxService) Canonical(userID uint, name string) (string, bool) {
name = strings.TrimSpace(name)
if name == "" {
return "", false
}
if err := s.stores.Mailboxes.EnsureSystem(userID); err != nil {
log.Printf("mailbox: EnsureSystem 失败 user=%d: %v", userID, err)
return "", false
}
if strings.EqualFold(name, "INBOX") {
return "INBOX", true
}
mb, err := s.stores.Mailboxes.GetByName(userID, name)
if err != nil {
return "", false
}
return mb.Name, true
}
// Messages 分页返回用户某文件夹的邮件。
func (s *MailboxService) Messages(userID uint, name string, page, size int) ([]db.Message, int64, error) {
return s.stores.Mails.ListByUserAndFolder(userID, name, page, size)
}
// Select 计算 SELECT 响应数据(会话负责 hub 绑定与选中状态)。
func (s *MailboxService) Select(userID uint, name string) (*imap.SelectData, error) {
msgs, err := s.stores.Mails.ListAllByUserAndFolder(userID, name)
if err != nil {
return nil, err
}
maxID, err := s.stores.Mails.MaxIDByUserAndFolder(userID, name)
if err != nil {
return nil, err
}
uidValidity, err := s.stores.MailboxState.UidValidity(userID, name)
if err != nil {
log.Printf("IMAP: 获取 UIDVALIDITY 失败 folder=%s: %v", name, err)
uidValidity = 1
}
flags := []imap.Flag{imap.FlagAnswered, imap.FlagFlagged, imap.FlagDeleted, imap.FlagSeen, imap.FlagDraft}
return &imap.SelectData{
Flags: flags,
PermanentFlags: append(flags, imap.FlagWildcard),
NumMessages: uint32(len(msgs)),
NumRecent: 0,
UIDNext: imap.UID(maxID + 1),
UIDValidity: uidValidity,
}, nil
}
// Status 计算 STATUS 响应数据。
func (s *MailboxService) Status(userID uint, name string, options *imap.StatusOptions) (*imap.StatusData, error) {
msgs, err := s.stores.Mails.ListAllByUserAndFolder(userID, name)
if err != nil {
return nil, err
}
data := &imap.StatusData{Mailbox: name}
if options.NumMessages || options.NumUnseen || options.NumDeleted || options.Size {
var unseen, deleted uint32
var size int64
for i := range msgs {
if !msgs[i].IsRead {
unseen++
}
if msgs[i].IsDeleted {
deleted++
}
size += int64(len(messageRawData(&msgs[i])))
}
if options.NumMessages {
n := uint32(len(msgs))
data.NumMessages = &n
}
if options.NumUnseen {
data.NumUnseen = &unseen
}
if options.NumDeleted {
data.NumDeleted = &deleted
}
if options.Size {
data.Size = &size
}
}
if options.NumRecent {
zero := uint32(0)
data.NumRecent = &zero
}
if options.UIDNext {
maxID, err := s.stores.Mails.MaxIDByUserAndFolder(userID, name)
if err != nil {
return nil, err
}
data.UIDNext = imap.UID(maxID + 1)
}
if options.UIDValidity {
uidValidity, err := s.stores.MailboxState.UidValidity(userID, name)
if err != nil {
return nil, err
}
data.UIDValidity = uidValidity
}
return data, nil
}
// validateMailboxName 校验自定义文件夹名(IMAP CREATE/RENAME 用)。
func validateMailboxName(name string) error {
if name == "" || len(name) > 64 {
return store.ErrMailboxInvalid
}
if strings.Contains(name, "/") || name == "." || name == ".." {
return store.ErrMailboxInvalid
}
for _, r := range name {
if r < 0x20 || r == 0x7f {
return store.ErrMailboxInvalid
}
}
return nil
}
// Create 创建自定义文件夹。
func (s *MailboxService) Create(userID uint, name string) error {
name = strings.TrimSpace(name)
if err := validateMailboxName(name); err != nil {
return err
}
if strings.EqualFold(name, "INBOX") {
return store.ErrMailboxExists
}
if err := s.stores.Mailboxes.EnsureSystem(userID); err != nil {
return err
}
if _, err := s.stores.Mailboxes.GetByName(userID, name); err == nil {
return store.ErrMailboxExists
}
return s.stores.Mailboxes.Create(&db.Mailbox{
UserID: userID,
Name: name,
IsSubscribed: true,
})
}
// Delete 删除空的自定义文件夹(系统文件夹拒绝)。
func (s *MailboxService) Delete(userID uint, name string) error {
name, ok := s.Canonical(userID, name)
if !ok {
return store.ErrMailboxNotFound
}
if isSystemMailboxName(name) {
return store.ErrMailboxSystem
}
return s.stores.Mailboxes.Delete(userID, name)
}
// Rename 重命名自定义文件夹(系统文件夹拒绝)。
func (s *MailboxService) Rename(userID uint, oldName, newName string) error {
oldName, ok := s.Canonical(userID, oldName)
if !ok {
return store.ErrMailboxNotFound
}
if isSystemMailboxName(oldName) {
return store.ErrMailboxSystem
}
newName = strings.TrimSpace(newName)
if err := validateMailboxName(newName); err != nil {
return err
}
if strings.EqualFold(newName, "INBOX") || isSystemMailboxName(newName) {
return store.ErrMailboxInvalid
}
if _, err := s.stores.Mailboxes.GetByName(userID, newName); err == nil {
return store.ErrMailboxExists
}
return s.stores.Mailboxes.Rename(userID, oldName, newName)
}
// SetSubscribed 更新文件夹订阅状态(LSUB 过滤用)。
func (s *MailboxService) SetSubscribed(userID uint, name string, subscribed bool) error {
name, ok := s.Canonical(userID, name)
if !ok {
return store.ErrMailboxNotFound
}
return s.stores.Mailboxes.SetSubscribed(userID, name, subscribed)
}
// Move 把多封邮件移动到目标文件夹(web 删除=移入 Trash、恢复等共用)。
// 不属于该用户的邮件被跳过。
func (s *MailboxService) Move(userID uint, msgIDs []uint, dest string) error {
if len(msgIDs) == 0 {
return nil
}
if _, ok := s.Canonical(userID, dest); !ok {
return store.ErrMailboxNotFound
}
for _, id := range msgIDs {
msg, err := s.stores.Mails.GetByID(id)
if err != nil || msg.UserID != userID {
continue
}
if err := s.stores.Mails.MoveToFolder(id, dest); err != nil {
return err
}
}
return nil
}
// Purge 永久删除文件夹中的邮件;msgIDs 为空时删除全部。
// 返回被删除前的邮件列表(调用方据此推送 EXPUNGE)。
func (s *MailboxService) Purge(userID uint, name string, msgIDs []uint) ([]db.Message, error) {
var msgs []db.Message
var err error
if len(msgIDs) == 0 {
msgs, err = s.stores.Mails.ListAllByUserAndFolder(userID, name)
if err != nil {
return nil, err
}
msgIDs = make([]uint, 0, len(msgs))
for i := range msgs {
msgIDs = append(msgIDs, msgs[i].ID)
}
} else {
for _, id := range msgIDs {
msg, err := s.stores.Mails.GetByID(id)
if err != nil || msg.UserID != userID || msg.Folder != name {
continue
}
msgs = append(msgs, *msg)
}
}
if len(msgIDs) == 0 {
return nil, nil
}
if err := s.stores.Mails.DeleteMany(msgIDs); err != nil {
return nil, err
}
return msgs, nil
}
// isSystemMailboxName reports whether name is one of the system mailboxes.
func isSystemMailboxName(name string) bool {
for _, def := range db.SystemMailboxes {
if def.Name == name {
return true
}
}
return false
}
// mailboxAttrs 把 SpecialUse 映射为 RFC 6154 的 IMAP 属性。
func mailboxAttrs(mb db.Mailbox) []imap.MailboxAttr {
switch mb.SpecialUse {
case "Sent":
return []imap.MailboxAttr{imap.MailboxAttrSent}
case "Drafts":
return []imap.MailboxAttr{imap.MailboxAttrDrafts}
case "Trash":
return []imap.MailboxAttr{imap.MailboxAttrTrash}
default:
return nil
}
}