package smtpsrv import ( "net" "strings" "testing" "blitiri.com.ar/go/chasquid/internal/domaininfo" "blitiri.com.ar/go/chasquid/internal/trace" "blitiri.com.ar/go/spf" ) func TestSecLevel(t *testing.T) { // We can't simulate this externally because of the SPF record // requirement, so do a narrow test on Conn.secLevelCheck. dinfo, err := domaininfo.New(t.ArtifactDir()) if err != nil { t.Fatalf("Failed to create domain info: %v", err) } c := &Conn{ tr: trace.New("testconn", "testconn"), dinfo: dinfo, } // No SPF, skip security checks. c.spfResult = spf.None c.onTLS = true if !c.secLevelCheck("from@slc") { t.Fatalf("TLS seclevel failed") } c.onTLS = false if !c.secLevelCheck("from@slc") { t.Fatalf("plain seclevel failed, even though SPF does not exist") } // Now the real checks, once SPF passes. c.spfResult = spf.Pass if !c.secLevelCheck("from@slc") { t.Fatalf("plain seclevel failed") } c.onTLS = true if !c.secLevelCheck("from@slc") { t.Fatalf("TLS seclevel failed") } c.onTLS = false if c.secLevelCheck("from@slc") { t.Fatalf("plain seclevel worked, downgrade was allowed") } } func TestIsHeader(t *testing.T) { no := []string{ "a", "\n", "\n\n", " \n", " ", "a:b", "a: b\nx: y", "\na:b\n", " a\nb:c\n", } for _, s := range no { if isHeader([]byte(s)) { t.Errorf("%q accepted as header, should be rejected", s) } } yes := []string{ "", "a:b\n", "X-Post-Data: success\n", } for _, s := range yes { if !isHeader([]byte(s)) { t.Errorf("%q rejected as header, should be accepted", s) } } } func TestAddrLiteral(t *testing.T) { // TCP addresses. casesTCP := []struct { addr net.IP expected string }{ {net.IPv4(1, 2, 3, 4), "1.2.3.4"}, {net.IPv4(0, 0, 0, 0), "0.0.0.0"}, {net.ParseIP("1.2.3.4"), "1.2.3.4"}, {net.ParseIP("2001:db8::68"), "IPv6:2001:db8::68"}, {net.ParseIP("::1"), "IPv6:::1"}, } for _, c := range casesTCP { tcp := &net.TCPAddr{ IP: c.addr, Port: 12345, } s := addrLiteral(tcp) if s != c.expected { t.Errorf("%v: expected %q, got %q", tcp, c.expected, s) } } // Non-TCP addresses. We expect these to match addr.String(). casesOther := []net.Addr{ &net.UDPAddr{ IP: net.ParseIP("1.2.3.4"), Port: 12345, }, } for _, addr := range casesOther { s := addrLiteral(addr) if s != addr.String() { t.Errorf("%v: expected %q, got %q", addr, addr.String(), s) } } } func TestReceivedHeaderEnvelopeFrom(t *testing.T) { c := &Conn{ hostname: "mx.example.com", ehloDomain: "client.example.com", remoteAddr: &net.TCPAddr{IP: net.ParseIP("192.0.2.1"), Port: 1234}, tr: trace.New("test", "TestReceivedHeaderEnvelopeFrom"), // The local part could have been quoted, see isSafeAddress. mailFrom: `x)(y; z\@example.org`, data: []byte("Subject: test\n\nbody\n"), } defer c.tr.Finish() c.addReceivedHeader() hdr, _, _ := strings.Cut(string(c.data), "Subject:") want := `envelope from "x\)\(y; z\\@example.org")` if !strings.Contains(hdr, want) { t.Errorf("Received header does not contain %q:\n%s", want, hdr) } // Parentheses must be balanced, considering the escaped ones. depth := 0 for i := 0; i < len(hdr); i++ { switch hdr[i] { case '\\': i++ // Skip the escaped character. case '(': depth++ case ')': depth-- if depth < 0 { t.Fatalf("unbalanced ')' at %d:\n%s", i, hdr) } } } if depth != 0 { t.Errorf("unbalanced comments (depth %d):\n%s", depth, hdr) } } func TestSanitizeEHLODomain(t *testing.T) { equal := []string{ "domain", "do.main", "do-main", "1.2.3.4", "a:b:c", "[a:b:c]", "abz", "AbZ", } for _, str := range equal { if got := sanitizeEHLODomain(str); got != str { t.Errorf("sanitizeEHLODomain(%q) returned %q, expected %q", str, got, str) } } invalid := []struct { str string expected string }{ {"ñaca", "aca"}, {"a\nb", "ab"}, {"a\x00b", "ab"}, {"a\x7fb", "ab"}, {"a/z", "az"}, {"a;b", "ab"}, {"a$b", "ab"}, {"a^b", "ab"}, {"a b", "ab"}, {"a+b", "ab"}, {"a@b", "ab"}, {`a"b`, "ab"}, {`a\b`, "ab"}, } for _, c := range invalid { if got := sanitizeEHLODomain(c.str); got != c.expected { t.Errorf("sanitizeEHLODomain(%q) returned %q, expected %q", c.str, got, c.expected) } } } func TestEHLODomainIsSanitized(t *testing.T) { cases := []struct { params string expected string }{ {"mx.example.com", "mx.example.com"}, {"[1.2.3.4] extra params", "[1.2.3.4]"}, {"evil;(x", "evilx"}, {"!!!", invalidEHLODomain}, } for _, c := range cases { for _, cmd := range []string{"HELO", "EHLO"} { conn := &Conn{} if cmd == "HELO" { conn.HELO(c.params) } else { conn.EHLO(c.params) } if conn.ehloDomain != c.expected { t.Errorf("%s %q: got EHLO domain %q, expected %q", cmd, c.params, conn.ehloDomain, c.expected) } } } } func TestIsSafeAddress(t *testing.T) { safe := []string{ "john@example.com", "ñandú@example.com", "用户@例子.广告", // Characters that need quoting are fine, we're lenient with them. "a;b@example.com", "a(b)@example.com", `a"b@example.com`, } for _, addr := range safe { if !isSafeAddress(addr) { t.Errorf("%q: expected safe, got unsafe", addr) } } unsafe := []string{ // More than one "@" (or none). "a@b@example.com", "@a@example.com", "example.com", // Control characters (C1, U+009B is CSI). "a\u0081b@example.com", "a\u009b31mb@example.com", "a@exam\u0085ple.com", } for _, addr := range unsafe { if isSafeAddress(addr) { t.Errorf("%q: expected unsafe, got safe", addr) } } }