177 lines
5 KiB
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)
|
|
}
|
|
}
|