#18: implement SMTP Submission (587/465), SASL PLAIN/LOGIN auth and preflight diagnostics
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user