mail_test.go (4770 bytes)
1 package main 2 3 import ( 4 "crypto/tls" 5 "crypto/x509" 6 "encoding/base64" 7 "net" 8 "net/http" 9 "net/http/httptest" 10 "net/mail" 11 "net/textproto" 12 "strings" 13 "sync" 14 "testing" 15 ) 16 17 // fakeSMTP plays an SMTP server and records what the client sends. With 18 // tls set, it offers STARTTLS, or it uses TLS from the start when implicit 19 // is set. 20 type fakeSMTP struct { 21 ln net.Listener 22 tls *tls.Config 23 implicit bool 24 25 mu sync.Mutex 26 auth string // "user password" of AUTH PLAIN 27 authTLS bool // the connection had TLS at AUTH 28 from, to string // arguments of MAIL and RCPT 29 data string 30 } 31 32 func newFakeSMTP(t *testing.T, tc *tls.Config, implicit bool) *fakeSMTP { 33 ln, err := net.Listen("tcp", "127.0.0.1:0") 34 if err != nil { 35 t.Fatal(err) 36 } 37 t.Cleanup(func() { ln.Close() }) 38 f := &fakeSMTP{ln: ln, tls: tc, implicit: implicit} 39 go func() { 40 for { 41 conn, err := ln.Accept() 42 if err != nil { 43 return 44 } 45 go f.session(conn) 46 } 47 }() 48 return f 49 } 50 51 func (f *fakeSMTP) session(conn net.Conn) { 52 defer func() { conn.Close() }() 53 secure := f.implicit 54 if secure { 55 conn = tls.Server(conn, f.tls) 56 } 57 tp := textproto.NewConn(conn) 58 tp.PrintfLine("220 fake ESMTP") 59 for { 60 line, err := tp.ReadLine() 61 if err != nil { 62 return 63 } 64 verb, arg, _ := strings.Cut(line, " ") 65 f.mu.Lock() 66 switch strings.ToUpper(verb) { 67 case "EHLO": 68 tp.PrintfLine("250-fake") 69 if f.tls != nil && !secure { 70 tp.PrintfLine("250-STARTTLS") 71 } 72 tp.PrintfLine("250 AUTH PLAIN") 73 case "STARTTLS": 74 tp.PrintfLine("220 go ahead") 75 conn = tls.Server(conn, f.tls) 76 tp = textproto.NewConn(conn) 77 secure = true 78 case "AUTH": 79 _, resp, _ := strings.Cut(arg, " ") 80 b, _ := base64.StdEncoding.DecodeString(resp) 81 f.auth = strings.TrimPrefix(strings.ReplaceAll(string(b), "\x00", " "), " ") 82 f.authTLS = secure 83 tp.PrintfLine("235 ok") 84 case "MAIL": 85 f.from = arg 86 tp.PrintfLine("250 ok") 87 case "RCPT": 88 f.to = arg 89 tp.PrintfLine("250 ok") 90 case "DATA": 91 tp.PrintfLine("354 go on") 92 b, _ := tp.ReadDotBytes() 93 f.data = string(b) 94 tp.PrintfLine("250 ok") 95 case "QUIT": 96 tp.PrintfLine("221 bye") 97 f.mu.Unlock() 98 return 99 default: 100 tp.PrintfLine("502 no such command") 101 } 102 f.mu.Unlock() 103 } 104 } 105 106 // testTLS gives a certificate for 127.0.0.1 and a pool that trusts it. 107 func testTLS(t *testing.T) (*tls.Config, *x509.CertPool) { 108 ts := httptest.NewTLSServer(http.NotFoundHandler()) 109 t.Cleanup(ts.Close) 110 pool := x509.NewCertPool() 111 pool.AddCert(ts.Certificate()) 112 return &tls.Config{Certificates: ts.TLS.Certificates}, pool 113 } 114 115 func testMailer(f *fakeSMTP) *Mailer { 116 return &Mailer{Addr: f.ln.Addr().String(), Hello: "localhost", 117 From: &mail.Address{Name: "sbm Sync", Address: "sync@example.org"}} 118 } 119 120 func TestMailerSends(t *testing.T) { 121 f := newFakeSMTP(t, nil, false) 122 if err := testMailer(f).send("you@example.org", "Hello", "line 1\n.line 2\n"); err != nil { 123 t.Fatal(err) 124 } 125 f.mu.Lock() 126 defer f.mu.Unlock() 127 if f.from != "FROM:<sync@example.org>" || f.to != "TO:<you@example.org>" { 128 t.Errorf("envelope: %q %q", f.from, f.to) 129 } 130 for _, want := range []string{ 131 "From: \"sbm Sync\" <sync@example.org>\n", 132 "To: you@example.org\n", 133 "Subject: Hello\n", 134 "Message-ID: <", 135 "@example.org>\n", 136 "Content-Type: text/plain; charset=utf-8\n", 137 "\n\nline 1\n.line 2\n", 138 } { 139 if !strings.Contains(f.data, want) { 140 t.Errorf("the email lacks %q:\n%s", want, f.data) 141 } 142 } 143 } 144 145 func TestMailerStartTLS(t *testing.T) { 146 tc, pool := testTLS(t) 147 f := newFakeSMTP(t, tc, false) 148 m := testMailer(f) 149 m.User, m.Password, m.Roots = "me", "secret", pool 150 if err := m.send("you@example.org", "Hello", "hi\n"); err != nil { 151 t.Fatal(err) 152 } 153 f.mu.Lock() 154 defer f.mu.Unlock() 155 if f.auth != "me secret" || !f.authTLS { 156 t.Errorf("auth %q, with TLS %v", f.auth, f.authTLS) 157 } 158 } 159 160 func TestMailerTLSFromTheStart(t *testing.T) { 161 tc, pool := testTLS(t) 162 f := newFakeSMTP(t, tc, true) 163 m := testMailer(f) 164 m.TLS, m.User, m.Password, m.Roots = true, "me", "secret", pool 165 if err := m.send("you@example.org", "Hello", "hi\n"); err != nil { 166 t.Fatal(err) 167 } 168 f.mu.Lock() 169 defer f.mu.Unlock() 170 if f.auth != "me secret" || !f.authTLS || !strings.Contains(f.data, "hi\n") { 171 t.Errorf("auth %q, with TLS %v, data %q", f.auth, f.authTLS, f.data) 172 } 173 } 174 175 // A certificate that the system does not trust stops the email before the 176 // password goes out. 177 func TestMailerChecksTheCertificate(t *testing.T) { 178 tc, _ := testTLS(t) 179 f := newFakeSMTP(t, tc, false) 180 m := testMailer(f) 181 m.User, m.Password = "me", "secret" 182 if err := m.send("you@example.org", "Hello", "hi\n"); err == nil { 183 t.Error("the mailer trusts any certificate") 184 } 185 f.mu.Lock() 186 defer f.mu.Unlock() 187 if f.auth != "" { 188 t.Errorf("the password went out: %q", f.auth) 189 } 190 }