Recently Written · git

sbm-sync

Sync server for sbm bookmark files: one small Go program, plain files, AGPL

git clone https://github.com/equwal/sbm-sync

Log | Files | Refs


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 }