#18: implement SMTP Submission (587/465), SASL PLAIN/LOGIN auth and preflight diagnostics

This commit is contained in:
Lucas Rayan committed 2026-08-01 22:49:02 -03:00
1 parent 7249419efc
commit d56f29d084
70 files changed
+5163 -659

No files matched your search

@@ -0,0 +1,99 @@
package usecase
import (
"bytes"
"context"
"io"
"strings"
"testing"
"github.com/google/uuid"
"lambdamail/protocols/internal/application/port"
"lambdamail/protocols/internal/domain/valueobject"
)
type capturingBlobStorage struct {
stored []byte
}
func (c *capturingBlobStorage) Store(_ context.Context, r io.Reader) (port.BlobRef, error) {
b, err := io.ReadAll(r)
c.stored = b
return port.BlobRef{ID: uuid.New(), SizeBytes: int64(len(b))}, err
}
type stubScanner struct {
result *valueobject.ScanResult
}
func (s *stubScanner) Scan(_ context.Context, _ port.ScanInput) (*valueobject.ScanResult, error) {
return s.result, nil
}
// The scan verdict is only actionable by the user - and by their Sieve rules -
// if it reaches the stored message. PLAN.md section 6.1 persists the EML
// together with the verdicts, so the spam headers the scanner produced must be
// present in the delivered message.
func TestUseCase_Handle_WritesScanHeadersIntoStoredMessage(t *testing.T) {
blobs := &capturingBlobStorage{}
scanner := &stubScanner{result: &valueobject.ScanResult{
Verdict: valueobject.ScanVerdictSpamJunk,
Score: 9.5,
HeadersToAdd: map[string]string{
"X-Spam-Flag": "YES",
"X-Spam-Score": "9.50",
},
}}
uc := NewProcessInboundEmailUseCase(nil, blobs, &recordingMessageRepository{})
uc.SetScanner(scanner)
err := uc.Handle(context.Background(), ProcessInboundEmailInput{
Sender: "spammer@example.test",
Recipients: []port.MailboxRecord{{ID: uuid.New()}},
RecipientAddresses: []string{"victim@example.test"},
Body: bytes.NewBufferString("Subject: buy now\r\n\r\nbody"),
})
if err != nil {
t.Fatalf("Handle: %v", err)
}
stored := string(blobs.stored)
for _, want := range []string{"X-Spam-Flag: YES", "X-Spam-Score: 9.50"} {
if !strings.Contains(stored, want) {
t.Errorf("stored message is missing %q:\n%s", want, stored)
}
}
if !strings.Contains(stored, "Subject: buy now") {
t.Errorf("original headers were lost:\n%s", stored)
}
// Added headers must precede the original ones and stay inside the header
// block, i.e. before the first empty line.
headerBlock, _, found := strings.Cut(stored, "\r\n\r\n")
if !found {
t.Fatalf("stored message has no header/body separator:\n%s", stored)
}
if !strings.Contains(headerBlock, "X-Spam-Flag: YES") {
t.Errorf("scan headers leaked into the body:\n%s", stored)
}
}
func TestUseCase_Handle_LeavesMessageUntouchedWithoutScanHeaders(t *testing.T) {
blobs := &capturingBlobStorage{}
uc := NewProcessInboundEmailUseCase(nil, blobs, &recordingMessageRepository{})
original := "Subject: hi\r\n\r\nbody"
err := uc.Handle(context.Background(), ProcessInboundEmailInput{
Recipients: []port.MailboxRecord{{ID: uuid.New()}},
RecipientAddresses: []string{"a@example.test"},
Body: bytes.NewBufferString(original),
})
if err != nil {
t.Fatalf("Handle: %v", err)
}
if string(blobs.stored) != original {
t.Errorf("message was rewritten without a scanner: got %q, want %q", blobs.stored, original)
}
}