1package fetcher
2
3import (
4 "bufio"
5 "crypto/rand"
6 "crypto/rsa"
7 "crypto/tls"
8 "crypto/x509"
9 "crypto/x509/pkix"
10 "encoding/pem"
11 "fmt"
12 "math/big"
13 "net"
14 "strconv"
15 "strings"
16 "sync"
17 "testing"
18 "time"
19
20 "github.com/floatpane/matcha/config"
21)
22
23func TestFetchMailboxEmailsUsesRequestedLimitForSmallFetchChunks(t *testing.T) {
24 fetchCommands := make(chan string, 1)
25 addr, closeServer := startFetchRecorderIMAPServer(t, 100, fetchCommands)
26 defer closeServer()
27
28 host, portText, err := net.SplitHostPort(addr)
29 if err != nil {
30 t.Fatalf("SplitHostPort(%q): %v", addr, err)
31 }
32 port, err := strconv.Atoi(portText)
33 if err != nil {
34 t.Fatalf("Atoi(%q): %v", portText, err)
35 }
36
37 account := &config.Account{
38 ID: "test-account",
39 Email: "user@example.com",
40 Password: "password",
41 ServiceProvider: "custom",
42 IMAPServer: host,
43 IMAPPort: port,
44 Insecure: true,
45 CatchAll: true,
46 SC: &config.SessionCache{},
47 }
48 done := make(chan error, 1)
49 go func() {
50 _, err := FetchMailboxEmails(account, "INBOX", 5, 0)
51 done <- err
52 }()
53
54 select {
55 case command := <-fetchCommands:
56 if !strings.Contains(command, "96:100") {
57 t.Fatalf("first FETCH command = %q, want range 96:100", command)
58 }
59 case <-time.After(2 * time.Second):
60 t.Fatal("timed out waiting for FETCH command")
61 }
62
63 closeServer()
64 select {
65 case <-done:
66 case <-time.After(2 * time.Second):
67 t.Fatal("FetchMailboxEmails did not return after server closed")
68 }
69}
70
71func startFetchRecorderIMAPServer(t *testing.T, messages uint32, fetchCommands chan<- string) (string, func()) {
72 t.Helper()
73
74 listener, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{
75 Certificates: []tls.Certificate{newTestTLSCertificate(t)},
76 })
77 if err != nil {
78 t.Fatalf("starting test IMAP server: %v", err)
79 }
80
81 var closeOnce sync.Once
82 var connMu sync.Mutex
83 var conn net.Conn
84 closeServer := func() {
85 closeOnce.Do(func() {
86 connMu.Lock()
87 if conn != nil {
88 _ = conn.Close()
89 }
90 connMu.Unlock()
91 _ = listener.Close()
92 })
93 }
94
95 go func() {
96 accepted, err := listener.Accept()
97 if err != nil {
98 return
99 }
100 connMu.Lock()
101 conn = accepted
102 connMu.Unlock()
103 serveFetchRecorderIMAPConn(accepted, messages, fetchCommands)
104 }()
105
106 return listener.Addr().String(), closeServer
107}
108
109func serveFetchRecorderIMAPConn(conn net.Conn, messages uint32, fetchCommands chan<- string) {
110 defer conn.Close()
111
112 reader := bufio.NewReader(conn)
113 writer := bufio.NewWriter(conn)
114 writeIMAPLine := func(format string, args ...any) bool {
115 if _, err := fmt.Fprintf(writer, format+"\r\n", args...); err != nil {
116 return false
117 }
118 return writer.Flush() == nil
119 }
120
121 if !writeIMAPLine("* OK matcha test server") {
122 return
123 }
124
125 for {
126 line, err := reader.ReadString('\n')
127 if err != nil {
128 return
129 }
130 line = strings.TrimRight(line, "\r\n")
131 fields := strings.Fields(line)
132 if len(fields) < 2 {
133 return
134 }
135
136 tag := fields[0]
137 switch strings.ToUpper(fields[1]) {
138 case "CAPABILITY":
139 if !writeIMAPLine("* CAPABILITY IMAP4rev1 AUTH=PLAIN") {
140 return
141 }
142 if !writeIMAPLine("%s OK CAPABILITY completed", tag) {
143 return
144 }
145 case "LOGIN":
146 if !writeIMAPLine("%s OK LOGIN completed", tag) {
147 return
148 }
149 case "SELECT":
150 if !writeIMAPLine("* %d EXISTS", messages) {
151 return
152 }
153 if !writeIMAPLine("* FLAGS (\\Seen)") {
154 return
155 }
156 if !writeIMAPLine("%s OK [READ-WRITE] SELECT completed", tag) {
157 return
158 }
159 case "FETCH":
160 fetchCommands <- line
161 _ = writeIMAPLine("%s NO recorded FETCH command", tag)
162 return
163 case "LOGOUT":
164 if !writeIMAPLine("* BYE logging out") {
165 return
166 }
167 _ = writeIMAPLine("%s OK LOGOUT completed", tag)
168 return
169 default:
170 if !writeIMAPLine("%s OK completed", tag) {
171 return
172 }
173 }
174 }
175}
176
177func newTestTLSCertificate(t *testing.T) tls.Certificate {
178 t.Helper()
179
180 key, err := rsa.GenerateKey(rand.Reader, 2048)
181 if err != nil {
182 t.Fatalf("generating private key: %v", err)
183 }
184 template := &x509.Certificate{
185 SerialNumber: big.NewInt(1),
186 Subject: pkix.Name{CommonName: "127.0.0.1"},
187 NotBefore: time.Now().Add(-time.Hour),
188 NotAfter: time.Now().Add(time.Hour),
189 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
190 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
191 IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
192 }
193 certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
194 if err != nil {
195 t.Fatalf("creating certificate: %v", err)
196 }
197
198 certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
199 keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
200 cert, err := tls.X509KeyPair(certPEM, keyPEM)
201 if err != nil {
202 t.Fatalf("parsing certificate: %v", err)
203 }
204 return cert
205}