package calls import ( "bytes" "context" "fmt" "log/slog" "strings" "testing" "amadan.net/rastrillo/rastrillo/mail" ) type failingSender struct{ err error } func (f failingSender) Send(ctx context.Context, to, subject, body string) error { return f.err } // smtpish is a distinct type so the log line's "type" field is worth // something: it names what failed without naming who it failed for. type smtpish struct{ msg string } func (e smtpish) Error() string { return e.msg } func capture() (*slog.Logger, *bytes.Buffer) { var buf bytes.Buffer return slog.New(slog.NewTextHandler(&buf, nil)), &buf } func TestMailErrorsCarryNoAddress(t *testing.T) { const address = "Guest@Example.com" inner := failingSender{err: fmt.Errorf("rastrillo/mail: send to %s: %w", address, smtpish{msg: "550 no mailbox for guest@example.com"})} logger, buf := capture() err := RedactMailErrors(inner, logger).Send(context.Background(), address, "s", "b") if err == nil { t.Fatal("a failed send answered nil") } if err.Error() != "calls: mail delivery failed" { t.Fatalf("returned error is %q; it must be derived from nothing", err) } if strings.Contains(strings.ToLower(err.Error()), "guest") { t.Fatalf("the returned error carries the address: %q", err) } log := buf.String() if strings.Contains(strings.ToLower(log), "guest") { t.Fatalf("the log line carries the address:\n%s", log) } if !strings.Contains(log, "calls.smtpish") { t.Fatalf("the log line does not name the error's type:\n%s", log) } if !strings.Contains(log, "address_seen=true") { t.Fatalf("the log line does not say the address was in the message:\n%s", log) } } // The rune trap the first version of this file walked into (codex round // 2, C2-1): it replaced the address in the message with "[address]" by // finding an offset in a strings.ToLower copy and slicing the original // at that offset. strings.ToLower can change a string's byte length — // KELVIN SIGN (U+212A, three bytes) folds to one-byte "k"; capital // dotted I (U+0130, two bytes) folds to two-byte "i̇" — so an offset // found in the lowered copy does not address the same bytes in the // original, and applying it could leave a suffix of the address in the // "redacted" text or cut a rune in half. // // This version logs nothing derived from the message — only a bool, via // strings.Contains over two independently-lowered strings, no slicing — // so there is nothing left to desynchronise. This test is the guard // that keeps it that way: two addresses carrying both trap runes, inside // one error message that also contains a run of ordinary ASCII "KKK"s // (an SMTP server rejecting a batch tends to shout), asserting the log // holds no "@" and no fragment of either address. func TestNoFragmentOfAnAddressReachesTheLog(t *testing.T) { const ( kelvin = "Kelvin@example.com" // ordinary ASCII K — not the Kelvin sign ines = "\u0130nes@example.com" // LATIN CAPITAL LETTER I WITH DOT ABOVE ) inner := failingSender{err: fmt.Errorf("rastrillo/mail: send to %s: %w", kelvin, smtpish{msg: "550 KKKKKKKKK unknown recipients " + kelvin + ", " + ines})} logger, buf := capture() err := RedactMailErrors(inner, logger).Send(context.Background(), kelvin, "s", "b") if err == nil { t.Fatal("a failed send answered nil") } if err.Error() != "calls: mail delivery failed" { t.Fatalf("returned error is %q, want exactly the fixed sentence", err) } log := buf.String() if strings.Contains(log, "@") { t.Fatalf("the log contains an @:\n%s", log) } for _, fragment := range []string{ kelvin, ines, strings.ToLower(kelvin), strings.ToLower(ines), "elvin", "nes", // the local parts, minus the leading trap character "example.com", "\u0130", // the trap rune itself } { if strings.Contains(log, fragment) { t.Errorf("the log carries a fragment of an address, %q:\n%s", fragment, log) } } if !strings.Contains(log, "address_seen=true") { t.Errorf("the address was in the message and the log does not say so:\n%s", log) } if strings.ContainsRune(log, '\uFFFD') { t.Errorf("the log holds a replacement rune, so something was cut mid-rune:\n%s", log) } } func TestASuccessfulSendIsUntouched(t *testing.T) { logger, buf := capture() if err := RedactMailErrors(failingSender{}, logger).Send(context.Background(), "a@b.test", "s", "b"); err != nil { t.Fatal(err) } if buf.Len() != 0 { t.Fatalf("a successful send logged:\n%s", buf.String()) } if RedactMailErrors(nil, logger) != nil { t.Fatal("a nil mailer was wrapped into a non-nil one, which would defeat the production refusal") } } func TestAddressSeenIsCaseInsensitiveAndNeedsNoSlicing(t *testing.T) { for _, tc := range []struct { msg, address string want bool }{ {"send to a@b.test: refused", "A@B.test", true}, // the recipient, folded {"send to a@b.test: refused", "c@d.test", false}, {"dial tcp 10.0.0.1:25: connection refused", "a@b.test", false}, {"connection reset", "", false}, {"550 " + "\u212A" + "elvin@example.com not found", "\u212A" + "elvin@example.com", true}, } { if got := addressSeen(tc.msg, tc.address); got != tc.want { t.Errorf("addressSeen(%q, %q) = %v, want %v", tc.msg, tc.address, got, tc.want) } } } // mail.IsConfigured asks a Sender whether it really delivers. The // wrapper has to forward that, or wrapping a logging mailer would make // it look like a relay (mail/mail.go:59). func TestTheWrapperForwardsDeliversAndSendList(t *testing.T) { logger, buf := capture() logged := RedactMailErrors(mail.Logged(logger), logger) if mail.IsConfigured(logged) { t.Fatal("a wrapped logging mailer claims to deliver") } if _, ok := logged.(mail.ListSender); !ok { t.Fatal("a wrapped ListSender lost SendList") } if err := logged.(mail.ListSender).SendList(context.Background(), mail.ListMessage{To: "a@b.test", Subject: "s", Body: "b"}); err != nil { t.Fatal(err) } buf.Reset() plain := RedactMailErrors(failingSender{}, logger) if !mail.IsConfigured(plain) { t.Fatal("a wrapped plain Sender is assumed not to deliver") } if _, ok := plain.(mail.ListSender); ok { t.Fatal("a wrapped plain Sender gained a SendList it cannot honour") } // A failing SendList redacts exactly as Send does. failing := RedactMailErrors(failingListSender{err: fmt.Errorf("send to a@b.test: %w", smtpish{"550"})}, logger) err := failing.(mail.ListSender).SendList(context.Background(), mail.ListMessage{To: "a@b.test"}) if err == nil || err.Error() != "calls: mail delivery failed" { t.Fatalf("SendList returned %v", err) } if strings.Contains(buf.String(), "a@b.test") { t.Fatalf("SendList's log line carries the address:\n%s", buf.String()) } } type failingListSender struct{ err error } func (f failingListSender) Send(ctx context.Context, to, subject, body string) error { return f.err } func (f failingListSender) SendList(ctx context.Context, m mail.ListMessage) error { return f.err }