#7: implement POP3 protocol server (RFC 1939, RFC 2449, STLS)

This commit is contained in:
Lucas Rayan committed 2026-07-24 11:51:27 -03:00
1 parent 883c78e48c
commit 9ca97298bd
7 files changed
+1238

No files matched your search

@@ -0,0 +1,86 @@
package usecase
import (
"context"
"fmt"
"github.com/alexedwards/argon2id"
"github.com/google/uuid"
"lambdamail/protocols/internal/application/port"
)
type Pop3SessionUseCase struct {
authRepo port.AuthRepository
folderRepo port.ImapFolderRepository
messageQuery port.MessageQueryRepository
blobReader port.BlobReader
expungeRepo port.ExpungeRepository
}
func NewPop3SessionUseCase(
authRepo port.AuthRepository,
folderRepo port.ImapFolderRepository,
messageQuery port.MessageQueryRepository,
blobReader port.BlobReader,
expungeRepo port.ExpungeRepository,
) *Pop3SessionUseCase {
return &Pop3SessionUseCase{
authRepo: authRepo,
folderRepo: folderRepo,
messageQuery: messageQuery,
blobReader: blobReader,
expungeRepo: expungeRepo,
}
}
func (uc *Pop3SessionUseCase) Login(ctx context.Context, username, password string) (string, error) {
rec, err := uc.authRepo.FindByAddress(ctx, username)
if err != nil {
return "", fmt.Errorf("lookup mailbox %q: %w", username, err)
}
if rec == nil {
return "", ErrAuthFailed
}
match, err := argon2id.ComparePasswordAndHash(password, rec.PasswordHash)
if err != nil {
return "", fmt.Errorf("verify password hash: %w", err)
}
if !match {
return "", ErrAuthFailed
}
return rec.ID.String(), nil
}
func (uc *Pop3SessionUseCase) GetInbox(ctx context.Context, mailboxID string) (*port.ImapFolderRecord, []port.MessageRecord, error) {
folder, err := uc.folderRepo.FindByName(ctx, mailboxID, "INBOX")
if err != nil {
return nil, nil, fmt.Errorf("find INBOX for mailbox %s: %w", mailboxID, err)
}
if folder == nil {
return nil, nil, ErrNoSuchFolder
}
messages, err := uc.messageQuery.ListMessages(ctx, folder.ID)
if err != nil {
return nil, nil, fmt.Errorf("list messages for INBOX folder %s: %w", folder.ID, err)
}
return folder, messages, nil
}
func (uc *Pop3SessionUseCase) ReadBlob(ctx context.Context, blobID uuid.UUID) ([]byte, error) {
content, err := uc.blobReader.ReadByID(ctx, blobID)
if err != nil {
return nil, fmt.Errorf("read blob %s: %w", blobID, err)
}
return content, nil
}
func (uc *Pop3SessionUseCase) ExpungeMessages(ctx context.Context, folderID string, uids []uint32) error {
if len(uids) == 0 {
return nil
}
if err := uc.expungeRepo.Expunge(ctx, folderID, uids); err != nil {
return fmt.Errorf("expunge messages in folder %s: %w", folderID, err)
}
return nil
}
@@ -0,0 +1,76 @@
package usecase
import (
"context"
"errors"
"testing"
"github.com/google/uuid"
"lambdamail/protocols/internal/application/port"
)
func TestPop3SessionUseCase_Login_SucceedsForCorrectPassword(t *testing.T) {
mailboxID := uuid.New()
auth := &fakeAuthRepository{byAddress: map[string]port.MailboxAuth{
"user@example.test": {ID: mailboxID, PasswordHash: testPasswordHash},
}}
uc := NewPop3SessionUseCase(auth, nil, nil, nil, nil)
gotID, err := uc.Login(context.Background(), "user@example.test", testPassword)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gotID != mailboxID.String() {
t.Errorf("mailboxID = %q, want %q", gotID, mailboxID.String())
}
}
func TestPop3SessionUseCase_Login_FailsForWrongPassword(t *testing.T) {
auth := &fakeAuthRepository{byAddress: map[string]port.MailboxAuth{
"user@example.test": {ID: uuid.New(), PasswordHash: testPasswordHash},
}}
uc := NewPop3SessionUseCase(auth, nil, nil, nil, nil)
_, err := uc.Login(context.Background(), "user@example.test", "wrong-password")
if !errors.Is(err, ErrAuthFailed) {
t.Fatalf("error = %v, want ErrAuthFailed", err)
}
}
type fakeMessageQueryRepository struct {
messages []port.MessageRecord
}
func (f *fakeMessageQueryRepository) ListMessages(_ context.Context, _ string) ([]port.MessageRecord, error) {
return f.messages, nil
}
func TestPop3SessionUseCase_GetInbox_ReturnsFolderAndMessages(t *testing.T) {
folders := &fakeImapFolderRepository{byName: map[string]port.ImapFolderRecord{
"INBOX": {ID: "folder-1", Name: "INBOX", NumMessages: 1},
}}
messages := &fakeMessageQueryRepository{messages: []port.MessageRecord{
{UID: 1, SizeBytes: 100},
}}
uc := NewPop3SessionUseCase(nil, folders, messages, nil, nil)
folder, recs, err := uc.GetInbox(context.Background(), "mailbox-1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if folder.ID != "folder-1" || len(recs) != 1 || recs[0].UID != 1 {
t.Errorf("unexpected inbox result: folder=%+v, recs=%+v", folder, recs)
}
}
func TestPop3SessionUseCase_ExpungeMessages_DelegatesToRepo(t *testing.T) {
expunger := &fakeExpungeRepository{}
uc := NewPop3SessionUseCase(nil, nil, nil, nil, expunger)
if err := uc.ExpungeMessages(context.Background(), "folder-1", []uint32{1, 2}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(expunger.calls) != 1 || expunger.calls[0].folderID != "folder-1" {
t.Fatalf("unexpected expunger calls: %+v", expunger.calls)
}
}