The webmail creates app passwords (shown once, stored hashed, with scopes) but the protocol servers only compared a login against mailboxes.password_hash, so an app password was always refused with 535 / authentication failed. They exist because mail clients cannot do a second factor. - port.MailboxAuth carries the mailbox's app passwords (newest 20) with their scopes; the Postgres repository loads them and stamps last_used_at on use - verifyCredential accepts the mailbox password or an app password that holds the scope of the protocol; SMTP, IMAP and POP3 use it. ManageSieve keeps the mailbox password only, as no default scope covers it - a malformed app password hash is skipped instead of failing the login; a malformed mailbox hash still surfaces as the server-side error it was Tests: unit cases for scopes and malformed hashes, the three entry points, repository tests against Postgres, and an end-to-end test on the real submission server that fails with 535 without the fix and passes with it. Full `go test ./...` with TEST_DATABASE_URL, go vet and lang-lint are clean.
86 lines
2.3 KiB
Go
86 lines
2.3 KiB
Go
package usecase
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"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 := verifyCredential(ctx, uc.authRepo, rec, password, ScopePOP3)
|
|
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
|
|
}
|