#6: implement IMAP IDLE (RFC 2177) and CONDSTORE (RFC 7162)
This commit is contained in:
1 parent
0ec0698079
commit
6b1e4dc00a
20 files changed
+539
-95
No files matched your search
@@ -23,17 +23,37 @@ var ErrNoSuchFolder = errors.New("imap session: no such folder")
|
||||
// HandleImapCommandUseCase (this sub-project's scope: LOGIN, LIST, SELECT,
|
||||
// FETCH, STORE; SEARCH/EXPUNGE/IDLE/MOVE land in later sub-projects).
|
||||
type ImapSessionUseCase struct {
|
||||
auth port.AuthRepository
|
||||
folders port.ImapFolderRepository
|
||||
messages port.MessageQueryRepository
|
||||
flags port.FlagRepository
|
||||
blobs port.BlobReader
|
||||
expunger port.ExpungeRepository
|
||||
copier port.CopyRepository
|
||||
auth port.AuthRepository
|
||||
folders port.ImapFolderRepository
|
||||
messages port.MessageQueryRepository
|
||||
flags port.FlagRepository
|
||||
blobs port.BlobReader
|
||||
expunger port.ExpungeRepository
|
||||
copier port.CopyRepository
|
||||
trackerManager *MailboxTrackerManager
|
||||
}
|
||||
|
||||
func NewImapSessionUseCase(auth port.AuthRepository, folders port.ImapFolderRepository, messages port.MessageQueryRepository, flags port.FlagRepository, blobs port.BlobReader, expunger port.ExpungeRepository, copier port.CopyRepository) *ImapSessionUseCase {
|
||||
return &ImapSessionUseCase{auth: auth, folders: folders, messages: messages, flags: flags, blobs: blobs, expunger: expunger, copier: copier}
|
||||
return &ImapSessionUseCase{
|
||||
auth: auth,
|
||||
folders: folders,
|
||||
messages: messages,
|
||||
flags: flags,
|
||||
blobs: blobs,
|
||||
expunger: expunger,
|
||||
copier: copier,
|
||||
trackerManager: NewMailboxTrackerManager(),
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ImapSessionUseCase) SetTrackerManager(tm *MailboxTrackerManager) {
|
||||
if tm != nil {
|
||||
uc.trackerManager = tm
|
||||
}
|
||||
}
|
||||
|
||||
func (uc *ImapSessionUseCase) GetTrackerManager() *MailboxTrackerManager {
|
||||
return uc.trackerManager
|
||||
}
|
||||
|
||||
func (uc *ImapSessionUseCase) Login(ctx context.Context, address, password string) (string, error) {
|
||||
@@ -77,8 +97,8 @@ func (uc *ImapSessionUseCase) ReadBlob(ctx context.Context, blobID uuid.UUID) ([
|
||||
return uc.blobs.ReadByID(ctx, blobID)
|
||||
}
|
||||
|
||||
func (uc *ImapSessionUseCase) SetFlags(ctx context.Context, folderID string, uid uint32, op port.FlagOp, flags []string) error {
|
||||
return uc.flags.SetFlags(ctx, folderID, uid, op, flags)
|
||||
func (uc *ImapSessionUseCase) SetFlags(ctx context.Context, folderID string, uid uint32, op port.FlagOp, flags []string, unchangedSince uint64) (bool, error) {
|
||||
return uc.flags.SetFlags(ctx, folderID, uid, op, flags, unchangedSince)
|
||||
}
|
||||
|
||||
func (uc *ImapSessionUseCase) Expunge(ctx context.Context, folderID string, uids []uint32) error {
|
||||
|
||||
@@ -149,6 +149,32 @@ func TestImapSessionUseCase_Expunge_DelegatesToRepository(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type fakeFlagRepository struct {
|
||||
lastFolderID string
|
||||
lastUID uint32
|
||||
lastOp port.FlagOp
|
||||
lastFlags []string
|
||||
lastUnchangedSince uint64
|
||||
}
|
||||
|
||||
func (f *fakeFlagRepository) SetFlags(_ context.Context, folderID string, uid uint32, op port.FlagOp, flags []string, unchangedSince uint64) (bool, error) {
|
||||
f.lastFolderID, f.lastUID, f.lastOp, f.lastFlags, f.lastUnchangedSince = folderID, uid, op, flags, unchangedSince
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func TestImapSessionUseCase_SetFlags_DelegatesToRepository(t *testing.T) {
|
||||
flagRepo := &fakeFlagRepository{}
|
||||
uc := NewImapSessionUseCase(nil, nil, nil, flagRepo, nil, nil, nil)
|
||||
|
||||
updated, err := uc.SetFlags(context.Background(), "folder-1", 7, port.FlagOpAdd, []string{"\\Seen"}, 10)
|
||||
if err != nil || !updated {
|
||||
t.Fatalf("SetFlags error: %v, updated=%v", err, updated)
|
||||
}
|
||||
if flagRepo.lastFolderID != "folder-1" || flagRepo.lastUID != 7 || flagRepo.lastUnchangedSince != 10 {
|
||||
t.Errorf("flagRepo state = %+v, want folder-1 uid 7 unchangedSince 10", flagRepo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImapSessionUseCase_CopyMessages_ReturnsRepositoryResult(t *testing.T) {
|
||||
copier := &fakeCopyRepository{result: []port.CopiedMessage{{SourceUID: 3, DestUID: 1}, {SourceUID: 7, DestUID: 2}}}
|
||||
uc := NewImapSessionUseCase(nil, nil, nil, nil, nil, nil, copier)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/emersion/go-imap/v2"
|
||||
"github.com/emersion/go-imap/v2/imapserver"
|
||||
)
|
||||
|
||||
// MailboxTrackerManager maintains imapserver.MailboxTracker instances per folder ID.
|
||||
// This allows active IMAP sessions to receive real-time notifications when emails arrive,
|
||||
// flags change, or messages are expunged (IDLE / RFC 2177).
|
||||
type MailboxTrackerManager struct {
|
||||
mu sync.RWMutex
|
||||
trackers map[string]*imapserver.MailboxTracker
|
||||
}
|
||||
|
||||
func NewMailboxTrackerManager() *MailboxTrackerManager {
|
||||
return &MailboxTrackerManager{
|
||||
trackers: make(map[string]*imapserver.MailboxTracker),
|
||||
}
|
||||
}
|
||||
|
||||
// GetTracker retrieves or initializes the MailboxTracker for folderID.
|
||||
func (m *MailboxTrackerManager) GetTracker(folderID string, initialNumMessages uint32) *imapserver.MailboxTracker {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
tracker, ok := m.trackers[folderID]
|
||||
if !ok {
|
||||
tracker = imapserver.NewMailboxTracker(initialNumMessages)
|
||||
m.trackers[folderID] = tracker
|
||||
}
|
||||
return tracker
|
||||
}
|
||||
|
||||
// NotifyNumMessages informs all sessions watching folderID of a new EXISTS count.
|
||||
func (m *MailboxTrackerManager) NotifyNumMessages(folderID string, n uint32) {
|
||||
m.mu.RLock()
|
||||
tracker, ok := m.trackers[folderID]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if ok {
|
||||
tracker.QueueNumMessages(n)
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyMessageFlags informs sessions watching folderID of updated flags on a message.
|
||||
func (m *MailboxTrackerManager) NotifyMessageFlags(folderID string, seqNum uint32, uid uint32, flags []string, source *imapserver.SessionTracker) {
|
||||
m.mu.RLock()
|
||||
tracker, ok := m.trackers[folderID]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if ok {
|
||||
imapFlags := make([]imap.Flag, len(flags))
|
||||
for i, f := range flags {
|
||||
imapFlags[i] = imap.Flag(f)
|
||||
}
|
||||
tracker.QueueMessageFlags(seqNum, imap.UID(uid), imapFlags, source)
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyExpunge informs sessions watching folderID that seqNum was expunged.
|
||||
func (m *MailboxTrackerManager) NotifyExpunge(folderID string, seqNum uint32) {
|
||||
m.mu.RLock()
|
||||
tracker, ok := m.trackers[folderID]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if ok {
|
||||
tracker.QueueExpunge(seqNum)
|
||||
}
|
||||
}
|
||||
@@ -36,15 +36,22 @@ type ProcessInboundEmailInput struct {
|
||||
// resolution + durable persistence; SPF/DKIM/DMARC/spam/virus checks land in
|
||||
// later sub-projects).
|
||||
type ProcessInboundEmailUseCase struct {
|
||||
mailboxes port.MailboxRepository
|
||||
blobs port.BlobStorage
|
||||
messages port.InboundMessageRepository
|
||||
mailboxes port.MailboxRepository
|
||||
blobs port.BlobStorage
|
||||
messages port.InboundMessageRepository
|
||||
folders port.ImapFolderRepository
|
||||
trackerManager *MailboxTrackerManager
|
||||
}
|
||||
|
||||
func NewProcessInboundEmailUseCase(mailboxes port.MailboxRepository, blobs port.BlobStorage, messages port.InboundMessageRepository) *ProcessInboundEmailUseCase {
|
||||
return &ProcessInboundEmailUseCase{mailboxes: mailboxes, blobs: blobs, messages: messages}
|
||||
}
|
||||
|
||||
func (uc *ProcessInboundEmailUseCase) SetTrackerManager(tm *MailboxTrackerManager, folders port.ImapFolderRepository) {
|
||||
uc.trackerManager = tm
|
||||
uc.folders = folders
|
||||
}
|
||||
|
||||
// ResolveRecipient is called from the SMTP session's RCPT TO handler. It
|
||||
// returns every mailbox address should be delivered to (more than one for
|
||||
// an alias that fans out), or ErrRecipientNotFound / ErrMailboxQuotaExceeded.
|
||||
@@ -88,6 +95,13 @@ func (uc *ProcessInboundEmailUseCase) Handle(ctx context.Context, input ProcessI
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if uc.trackerManager != nil && uc.folders != nil {
|
||||
folderRec, err := uc.folders.FindByName(ctx, recipient.ID.String(), "INBOX")
|
||||
if err == nil && folderRec != nil {
|
||||
uc.trackerManager.NotifyNumMessages(folderRec.ID, folderRec.NumMessages)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user