checker-smtp/checker/smtp_test.go

177 lines
5 KiB
Go

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)
}
}