internal/mail/mail_test.go
165 lines · 4046 bytes
9 symbols in this file
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}
143
144func TestReplyToHeader(t *testing.T) {
145 relay := startRelay(t, nil)
146 cfg := mailCfg(relay.addr)
147 if err := SendReplyTo(cfg, "a@example.test", "reply+tok@example.test", "subject", "body"); err != nil {
148 t.Fatal(err)
149 }
150 if err := Send(cfg, "a@example.test", "subject", "body"); err != nil {
151 t.Fatal(err)
152 }
153 relay.mu.Lock()
154 with, without := relay.data[0], relay.data[1]
155 relay.mu.Unlock()
156 if !strings.Contains(with, "\nReply-To: reply+tok@example.test\n") {
157 t.Fatalf("no Reply-To:\n%s", with)
158 }
159 if strings.Contains(without, "Reply-To:") {
160 t.Fatalf("Send added a Reply-To:\n%s", without)
161 }
162 if err := SendReplyTo(cfg, "a@example.test", "x@example.test\r\nBcc: b@example.test", "s", "b"); err == nil {
163 t.Fatal("a line break in the reply address was sent")
164 }
165}