internal/mail/mail_test.go

f8b976a97290a20d552056a999511f5d27d8e8ec
gitbay/internal/mail/mail_test.go history · blame · raw

142 lines · 3273 bytes

  1package mail
  2
  3import (
  4	"bufio"
  5	"crypto/tls"
  6	"crypto/x509"
  7	"fmt"
  8	"net"
  9	"net/http"
 10	"net/http/httptest"
 11	"strings"
 12	"sync"
 13	"testing"
 14
 15	"gitbay.org/gitbay/internal/config"
 16)
 17
 18// fakeRelay is an SMTP server that never offers STARTTLS. Given a TLS
 19// config it speaks TLS from the first byte, as a port-465 relay does.
 20type fakeRelay struct {
 21	addr string
 22	mu   sync.Mutex
 23	data []string
 24}
 25
 26func startRelay(t *testing.T, tlsCfg *tls.Config) *fakeRelay {
 27	t.Helper()
 28	ln, err := net.Listen("tcp", "127.0.0.1:0")
 29	if err != nil {
 30		t.Fatal(err)
 31	}
 32	if tlsCfg != nil {
 33		ln = tls.NewListener(ln, tlsCfg)
 34	}
 35	t.Cleanup(func() { ln.Close() })
 36	f := &fakeRelay{addr: ln.Addr().String()}
 37	go func() {
 38		for {
 39			conn, err := ln.Accept()
 40			if err != nil {
 41				return
 42			}
 43			go f.serve(conn)
 44		}
 45	}()
 46	return f
 47}
 48
 49func (f *fakeRelay) serve(conn net.Conn) {
 50	defer conn.Close()
 51	r := bufio.NewReader(conn)
 52	fmt.Fprint(conn, "220 fake\r\n")
 53	var body strings.Builder
 54	inData := false
 55	for {
 56		line, err := r.ReadString('\n')
 57		if err != nil {
 58			return
 59		}
 60		line = strings.TrimRight(line, "\r\n")
 61		switch {
 62		case inData && line == ".":
 63			f.mu.Lock()
 64			f.data = append(f.data, body.String())
 65			f.mu.Unlock()
 66			inData = false
 67			fmt.Fprint(conn, "250 ok\r\n")
 68		case inData:
 69			body.WriteString(line + "\n")
 70		case strings.HasPrefix(line, "EHLO"), strings.HasPrefix(line, "HELO"):
 71			fmt.Fprint(conn, "250-fake\r\n250 SIZE 1000000\r\n")
 72		case line == "DATA":
 73			inData = true
 74			fmt.Fprint(conn, "354 go\r\n")
 75		case line == "QUIT":
 76			fmt.Fprint(conn, "221 bye\r\n")
 77			return
 78		default:
 79			fmt.Fprint(conn, "250 ok\r\n")
 80		}
 81	}
 82}
 83
 84func (f *fakeRelay) delivered() int {
 85	f.mu.Lock()
 86	defer f.mu.Unlock()
 87	return len(f.data)
 88}
 89
 90func mailCfg(host string) config.Config {
 91	var cfg config.Config
 92	cfg.Mail.SMTPHost, cfg.Mail.From = host, "gitbay@example.test"
 93	return cfg
 94}
 95
 96func TestRequireTLSRefusesPlaintextRelay(t *testing.T) {
 97	relay := startRelay(t, nil)
 98	cfg := mailCfg(relay.addr)
 99	on := true
100	cfg.Mail.RequireTLS = &on
101	err := Send(cfg, "a@example.test", "subject", "body")
102	if err == nil || !strings.Contains(err.Error(), "STARTTLS") {
103		t.Fatalf("Send = %v, want a refusal naming STARTTLS", err)
104	}
105	if n := relay.delivered(); n != 0 {
106		t.Fatalf("%d message(s) sent in clear", n)
107	}
108}
109
110// A loopback relay has no network to cross; the default leaves it in
111// clear, which is what the e2e suite's fake relay relies on.
112func TestLoopbackRelayDefaultsToPlaintext(t *testing.T) {
113	relay := startRelay(t, nil)
114	if err := Send(mailCfg(relay.addr), "a@example.test", "subject", "body"); err != nil {
115		t.Fatal(err)
116	}
117	if n := relay.delivered(); n != 1 {
118		t.Fatalf("delivered %d, want 1", n)
119	}
120}
121
122func TestImplicitTLS(t *testing.T) {
123	ts := httptest.NewTLSServer(http.NotFoundHandler())
124	defer ts.Close()
125	pool := x509.NewCertPool()
126	pool.AddCert(ts.Certificate())
127	prev := rootCAs
128	rootCAs = pool
129	defer func() { rootCAs = prev }()
130
131	relay := startRelay(t, &tls.Config{Certificates: ts.TLS.Certificates})
132	cfg := mailCfg(relay.addr)
133	cfg.Mail.TLS = "implicit"
134	on := true
135	cfg.Mail.RequireTLS = &on
136	if err := Send(cfg, "a@example.test", "subject", "body"); err != nil {
137		t.Fatal(err)
138	}
139	if n := relay.delivered(); n != 1 {
140		t.Fatalf("delivered %d, want 1", n)
141	}
142}