package usecase import ( "bytes" "context" "strings" "testing" "github.com/google/uuid" "lambdamail/protocols/internal/application/port" "lambdamail/protocols/internal/domain/valueobject" ) // junkingScanner files everything as spam, standing in for the real scanner // scoring a bounce-shaped message with no SPF result. type junkingScanner struct{ called bool } func (s *junkingScanner) Scan(_ context.Context, _ port.ScanInput) (*valueobject.ScanResult, error) { s.called = true return &valueobject.ScanResult{ Verdict: valueobject.ScanVerdictSpamJunk, Score: 12.0, Symbols: map[string]float64{}, HeadersToAdd: map[string]string{}, }, nil } // A delivery status notification this server wrote itself must never be // spam-filtered. // // The report goes through the same inbound path as mail from the internet, but // it is injected with no client IP, so SPF can only be "none" and the content // looks exactly like a bounce. The result was that "your message to X was // delayed" arrived in the sender's own Junk folder - the single notification // they most need to see, filed where they will not look. func TestSystemGeneratedMessageIsNotSpamFiltered(t *testing.T) { blobs := &stubBlobStorage{ref: port.BlobRef{ID: uuid.New(), SHA256: "dsn", SizeBytes: 10}} messages := &recordingMessageRepository{} scanner := &junkingScanner{} uc := NewProcessInboundEmailUseCase(nil, blobs, messages) uc.SetScanner(scanner) err := uc.Handle(context.Background(), ProcessInboundEmailInput{ Sender: "postmaster@mail.example.test", Recipients: []port.MailboxRecord{{ID: uuid.New(), MaxMessageBytes: 100000}}, RecipientAddresses: []string{"user@example.test"}, Body: bytes.NewReader([]byte("Subject: Delivery Delay Notification\r\n\r\ndelayed\r\n")), SystemGenerated: true, }) if err != nil { t.Fatalf("Handle: %v", err) } if scanner.called { t.Error("the scanner ran on a message this server generated itself") } if len(messages.persisted) != 1 { t.Fatalf("want 1 persisted message, got %d", len(messages.persisted)) } if folder := messages.persisted[0].TargetFolderName; folder == "Junk" { t.Errorf("a postmaster notification was filed as Junk") } } // The exemption must be narrow: anything actually received from a peer is // still scanned, or marking a message "system generated" would be a way past // the spam filter. func TestOrdinaryMessageIsStillScanned(t *testing.T) { blobs := &stubBlobStorage{ref: port.BlobRef{ID: uuid.New(), SHA256: "x", SizeBytes: 10}} messages := &recordingMessageRepository{} scanner := &junkingScanner{} uc := NewProcessInboundEmailUseCase(nil, blobs, messages) uc.SetScanner(scanner) err := uc.Handle(context.Background(), ProcessInboundEmailInput{ Sender: "stranger@spam.test", Recipients: []port.MailboxRecord{{ID: uuid.New(), MaxMessageBytes: 100000}}, RecipientAddresses: []string{"user@example.test"}, Body: bytes.NewReader([]byte("Subject: buy pills\r\n\r\nspam\r\n")), }) if err != nil { t.Fatalf("Handle: %v", err) } if !scanner.called { t.Fatal("an ordinary inbound message skipped the scanner") } if messages.persisted[0].TargetFolderName != "Junk" { t.Errorf("a message the scanner called spam was not filed as Junk, got %q", messages.persisted[0].TargetFolderName) } } // A DSN announces itself as automatic so the far side's vacation responder // does not answer it. RFC 3834 section 5 distinguishes the two values, and // filters read the header. func TestDsnDeclaresItselfAutoGenerated(t *testing.T) { payload, loop := valueobject.BuildDsnReport( valueobject.DsnActionDelayed, "mail.example.test", "sender@example.test", "far@remote.test", "abc", "timeout", ) if loop { t.Fatal("unexpected loop prevention") } if !strings.Contains(string(payload), "Auto-Submitted: auto-generated") { t.Errorf("a delivery report is auto-generated, not auto-replied:\n%s", payload) } }