package checker import ( "bytes" "io" "net" "strings" "testing" "time" ) // fakeConn turns a pair of in-memory buffers into a net.Conn stub. It is // just enough surface for smtpConn to read responses and record what it // wrote: no deadlines, no addressing. type fakeConn struct { reader io.Reader writer bytes.Buffer } func (f *fakeConn) Read(b []byte) (int, error) { return f.reader.Read(b) } func (f *fakeConn) Write(b []byte) (int, error) { return f.writer.Write(b) } func (f *fakeConn) Close() error { return nil } func (f *fakeConn) LocalAddr() net.Addr { return nil } func (f *fakeConn) RemoteAddr() net.Addr { return nil } func (f *fakeConn) SetDeadline(t time.Time) error { return nil } func (f *fakeConn) SetReadDeadline(t time.Time) error { return nil } func (f *fakeConn) SetWriteDeadline(t time.Time) error { return nil } func newFakeConn(script string) *fakeConn { return &fakeConn{reader: strings.NewReader(script)} } func TestReadResponse_Multiline(t *testing.T) { script := "250-mx.example.com Hello\r\n" + "250-PIPELINING\r\n" + "250-SIZE 52428800\r\n" + "250-STARTTLS\r\n" + "250-AUTH PLAIN LOGIN\r\n" + "250 8BITMIME\r\n" fc := newFakeConn(script) sc := newSMTPConn(fc, 0) code, _, lines, err := sc.readResponse() if err != nil { t.Fatalf("unexpected error: %v", err) } if code != 250 { t.Errorf("code: want 250, got %d", code) } if len(lines) != 6 { t.Fatalf("want 6 lines, got %d", len(lines)) } greeting, exts := parseEHLO(lines) if greeting != "mx.example.com Hello" { t.Errorf("greeting: got %q", greeting) } want := []string{"PIPELINING", "SIZE 52428800", "STARTTLS", "AUTH PLAIN LOGIN", "8BITMIME"} if len(exts) != len(want) { t.Fatalf("want %d extensions, got %d: %v", len(want), len(exts), exts) } for i, w := range want { if exts[i] != w { t.Errorf("ext[%d]: want %q, got %q", i, w, exts[i]) } } idx := buildExtensions(exts) if !idx.has("STARTTLS") { t.Error("want STARTTLS") } if !idx.has("PIPELINING") { t.Error("want PIPELINING") } if got := idx.parseSize(); got != 52428800 { t.Errorf("SIZE: want 52428800, got %d", got) } auth := idx.parseAuth() if len(auth) != 2 || auth[0] != "PLAIN" || auth[1] != "LOGIN" { t.Errorf("AUTH: want [PLAIN LOGIN], got %v", auth) } } func TestParseBanner(t *testing.T) { cases := []struct { in, want string }{ {"mail.example.org ESMTP Postfix", "mail.example.org"}, {"mx1.gmail.com ESMTP", "mx1.gmail.com"}, {"server ready", ""}, // no dot, not a FQDN {"220 mailbox", ""}, // no dot either {"smtp-in.googlemail.com ESMTP qrs123 - gsmtp", "smtp-in.googlemail.com"}, } for _, c := range cases { if got := parseBanner(c.in); got != c.want { t.Errorf("parseBanner(%q): want %q, got %q", c.in, c.want, got) } } } func TestSplitMail(t *testing.T) { d, l, ok := splitMail("a@b.com") if !ok || d != "b.com" || l != "a" { t.Errorf("got (%q,%q,%v)", d, l, ok) } if _, _, ok := splitMail("nouser"); ok { t.Error("missing @ should fail") } if _, _, ok := splitMail("@trailing"); ok { t.Error("empty local should fail") } if _, _, ok := splitMail("trailing@"); ok { t.Error("empty domain should fail") } } func TestDeriveIssues_NullMX(t *testing.T) { d := &SMTPData{ Domain: "example.com", MX: MXLookup{NullMX: true}, } issues := deriveIssues(d) if len(issues) != 1 || issues[0].Code != CodeNullMX { t.Fatalf("want single null-mx issue, got %+v", issues) } if issues[0].Severity != SeverityInfo { t.Errorf("null-MX severity: want info, got %q", issues[0].Severity) } } func TestDeriveIssues_OpenRelay(t *testing.T) { tr := true d := &SMTPData{ Domain: "example.com", MX: MXLookup{Records: []MXRecord{ {Preference: 10, Target: "mx.example.com", IPv4: []string{"198.51.100.1"}}, }}, Endpoints: []EndpointProbe{ { Target: "mx.example.com", IP: "198.51.100.1", Address: "198.51.100.1:25", TCPConnected: true, BannerReceived: true, BannerCode: 220, EHLOReceived: true, STARTTLSOffered: true, STARTTLSUpgraded: true, OpenRelay: &tr, OpenRelayResponse: "250 OK", OpenRelayRecipient: "postmaster@example.org", FCrDNSPass: true, PTR: "mx.example.com", HasPipelining: true, Has8BITMIME: true, }, }, } issues := deriveIssues(d) var sawOpen bool for _, is := range issues { if is.Code == CodeOpenRelay { sawOpen = true if is.Severity != SeverityCrit { t.Errorf("open-relay severity: want crit, got %q", is.Severity) } } } if !sawOpen { t.Errorf("expected open-relay issue, got %+v", issues) } } func TestDeriveIssues_MXCNAME(t *testing.T) { d := &SMTPData{ Domain: "example.com", MX: MXLookup{Records: []MXRecord{ {Preference: 10, Target: "mail.example.com", IsCNAME: true, CNAMEChain: []string{"mail.example.com", "real.example.net"}, IPv4: []string{"198.51.100.1"}}, }}, } issues := deriveIssues(d) var sawCNAME bool for _, is := range issues { if is.Code == CodeMXCNAME { sawCNAME = true } } if !sawCNAME { t.Errorf("expected CNAME issue, got %+v", issues) } }