1//go:build !integration
2
3package main
4
5import (
6 "context"
7 "crypto/ed25519"
8 cryptorand "crypto/rand"
9 "crypto/x509"
10 "flag"
11 "fmt"
12 "math/big"
13 "net"
14 "os"
15 "path/filepath"
16 "testing"
17 "time"
18
19 "github.com/mjl-/mox/config"
20 "github.com/mjl-/mox/dmarcdb"
21 "github.com/mjl-/mox/dns"
22 "github.com/mjl-/mox/imapclient"
23 "github.com/mjl-/mox/mlog"
24 "github.com/mjl-/mox/mox-"
25 "github.com/mjl-/mox/mtastsdb"
26 "github.com/mjl-/mox/queue"
27 "github.com/mjl-/mox/smtp"
28 "github.com/mjl-/mox/store"
29 "github.com/mjl-/mox/tlsrptdb"
30)
31
32var ctxbg = context.Background()
33var pkglog = mlog.New("ctl", nil)
34
35func tcheck(t *testing.T, err error, errmsg string) {
36 if err != nil {
37 t.Helper()
38 t.Fatalf("%s: %v", errmsg, err)
39 }
40}
41
42// TestCtl executes commands through ctl. This tests at least the protocols (who
43// sends when/what) is tested. We often don't check the actual results, but
44// unhandled errors would cause a panic.
45func TestCtl(t *testing.T) {
46 os.RemoveAll("testdata/ctl/data")
47 mox.ConfigStaticPath = filepath.FromSlash("testdata/ctl/config/mox.conf")
48 mox.ConfigDynamicPath = filepath.FromSlash("testdata/ctl/config/domains.conf")
49 if errs := mox.LoadConfig(ctxbg, pkglog, true, false); len(errs) > 0 {
50 t.Fatalf("loading mox config: %v", errs)
51 }
52 err := store.Init(ctxbg)
53 tcheck(t, err, "store init")
54 defer store.Close()
55 defer store.Switchboard()()
56
57 err = queue.Init()
58 tcheck(t, err, "queue init")
59 defer queue.Shutdown()
60
61 var cid int64
62
63 testctl := func(fn func(clientxctl *ctl)) {
64 t.Helper()
65
66 cconn, sconn := net.Pipe()
67 clientxctl := ctl{conn: cconn, log: pkglog}
68 serverxctl := ctl{conn: sconn, log: pkglog}
69 done := make(chan struct{})
70 go func() {
71 cid++
72 servectlcmd(ctxbg, &serverxctl, cid, func() {})
73 close(done)
74 }()
75 fn(&clientxctl)
76 cconn.Close()
77 <-done
78 sconn.Close()
79 }
80
81 // "deliver"
82 testctl(func(xctl *ctl) {
83 ctlcmdDeliver(xctl, "mjl@mox.example")
84 })
85
86 // "setaccountpassword"
87 testctl(func(xctl *ctl) {
88 ctlcmdSetaccountpassword(xctl, "mjl", "test4321")
89 })
90
91 testctl(func(xctl *ctl) {
92 ctlcmdQueueHoldrulesList(xctl)
93 })
94
95 // All messages.
96 testctl(func(xctl *ctl) {
97 ctlcmdQueueHoldrulesAdd(xctl, "", "", "")
98 })
99 testctl(func(xctl *ctl) {
100 ctlcmdQueueHoldrulesAdd(xctl, "mjl", "", "")
101 })
102 testctl(func(xctl *ctl) {
103 ctlcmdQueueHoldrulesAdd(xctl, "", "☺.mox.example", "")
104 })
105 testctl(func(xctl *ctl) {
106 ctlcmdQueueHoldrulesAdd(xctl, "mox", "☺.mox.example", "example.com")
107 })
108
109 testctl(func(xctl *ctl) {
110 ctlcmdQueueHoldrulesRemove(xctl, 1)
111 })
112
113 // Queue a message to list/change/dump.
114 msg := "Subject: subject\r\n\r\nbody\r\n"
115 msgFile, err := store.CreateMessageTemp(pkglog, "queuedump-test")
116 tcheck(t, err, "temp file")
117 _, err = msgFile.Write([]byte(msg))
118 tcheck(t, err, "write message")
119 _, err = msgFile.Seek(0, 0)
120 tcheck(t, err, "rewind message")
121 defer os.Remove(msgFile.Name())
122 defer msgFile.Close()
123 addr, err := smtp.ParseAddress("mjl@mox.example")
124 tcheck(t, err, "parse address")
125 qml := []queue.Msg{queue.MakeMsg(addr.Path(), addr.Path(), false, false, int64(len(msg)), "<random@localhost>", nil, nil, time.Now(), "subject")}
126 queue.Add(ctxbg, pkglog, "mjl", msgFile, qml...)
127 qmid := qml[0].ID
128
129 // Has entries now.
130 testctl(func(xctl *ctl) {
131 ctlcmdQueueHoldrulesList(xctl)
132 })
133
134 // "queuelist"
135 testctl(func(xctl *ctl) {
136 ctlcmdQueueList(xctl, queue.Filter{}, queue.Sort{})
137 })
138
139 // "queueholdset"
140 testctl(func(xctl *ctl) {
141 ctlcmdQueueHoldSet(xctl, queue.Filter{}, true)
142 })
143 testctl(func(xctl *ctl) {
144 ctlcmdQueueHoldSet(xctl, queue.Filter{}, false)
145 })
146
147 // "queueschedule"
148 testctl(func(xctl *ctl) {
149 ctlcmdQueueSchedule(xctl, queue.Filter{}, true, time.Minute)
150 })
151
152 // "queuetransport"
153 testctl(func(xctl *ctl) {
154 ctlcmdQueueTransport(xctl, queue.Filter{}, "socks")
155 })
156
157 // "queuerequiretls"
158 testctl(func(xctl *ctl) {
159 ctlcmdQueueRequireTLS(xctl, queue.Filter{}, nil)
160 })
161
162 // "queuedump"
163 testctl(func(xctl *ctl) {
164 ctlcmdQueueDump(xctl, fmt.Sprintf("%d", qmid))
165 })
166
167 // "queuefail"
168 testctl(func(xctl *ctl) {
169 ctlcmdQueueFail(xctl, queue.Filter{})
170 })
171
172 // "queuedrop"
173 testctl(func(xctl *ctl) {
174 ctlcmdQueueDrop(xctl, queue.Filter{})
175 })
176
177 // "queueholdruleslist"
178 testctl(func(xctl *ctl) {
179 ctlcmdQueueHoldrulesList(xctl)
180 })
181
182 // "queueholdrulesadd"
183 testctl(func(xctl *ctl) {
184 ctlcmdQueueHoldrulesAdd(xctl, "mjl", "", "")
185 })
186 testctl(func(xctl *ctl) {
187 ctlcmdQueueHoldrulesAdd(xctl, "mjl", "localhost", "")
188 })
189
190 // "queueholdrulesremove"
191 testctl(func(xctl *ctl) {
192 ctlcmdQueueHoldrulesRemove(xctl, 2)
193 })
194 testctl(func(xctl *ctl) {
195 ctlcmdQueueHoldrulesList(xctl)
196 })
197
198 // "queuesuppresslist"
199 testctl(func(xctl *ctl) {
200 ctlcmdQueueSuppressList(xctl, "mjl")
201 })
202
203 // "queuesuppressadd"
204 testctl(func(xctl *ctl) {
205 ctlcmdQueueSuppressAdd(xctl, "mjl", "base@localhost")
206 })
207 testctl(func(xctl *ctl) {
208 ctlcmdQueueSuppressAdd(xctl, "mjl", "other@localhost")
209 })
210
211 // "queuesuppresslookup"
212 testctl(func(xctl *ctl) {
213 ctlcmdQueueSuppressLookup(xctl, "mjl", "base@localhost")
214 })
215
216 // "queuesuppressremove"
217 testctl(func(xctl *ctl) {
218 ctlcmdQueueSuppressRemove(xctl, "mjl", "base@localhost")
219 })
220 testctl(func(xctl *ctl) {
221 ctlcmdQueueSuppressList(xctl, "mjl")
222 })
223
224 // "queueretiredlist"
225 testctl(func(xctl *ctl) {
226 ctlcmdQueueRetiredList(xctl, queue.RetiredFilter{}, queue.RetiredSort{})
227 })
228
229 // "queueretiredprint"
230 testctl(func(xctl *ctl) {
231 ctlcmdQueueRetiredPrint(xctl, "1")
232 })
233
234 // "queuehooklist"
235 testctl(func(xctl *ctl) {
236 ctlcmdQueueHookList(xctl, queue.HookFilter{}, queue.HookSort{})
237 })
238
239 // "queuehookschedule"
240 testctl(func(xctl *ctl) {
241 ctlcmdQueueHookSchedule(xctl, queue.HookFilter{}, true, time.Minute)
242 })
243
244 // "queuehookprint"
245 testctl(func(xctl *ctl) {
246 ctlcmdQueueHookPrint(xctl, "1")
247 })
248
249 // "queuehookcancel"
250 testctl(func(xctl *ctl) {
251 ctlcmdQueueHookCancel(xctl, queue.HookFilter{})
252 })
253
254 // "queuehookretiredlist"
255 testctl(func(xctl *ctl) {
256 ctlcmdQueueHookRetiredList(xctl, queue.HookRetiredFilter{}, queue.HookRetiredSort{})
257 })
258
259 // "queuehookretiredprint"
260 testctl(func(xctl *ctl) {
261 ctlcmdQueueHookRetiredPrint(xctl, "1")
262 })
263
264 // "importmbox"
265 testctl(func(xctl *ctl) {
266 ctlcmdImport(xctl, true, "mjl", "inbox", "testdata/importtest.mbox")
267 })
268
269 // "importmaildir"
270 testctl(func(xctl *ctl) {
271 ctlcmdImport(xctl, false, "mjl", "inbox", "testdata/importtest.maildir")
272 })
273
274 // "domainadd"
275 testctl(func(xctl *ctl) {
276 ctlcmdConfigDomainAdd(xctl, false, dns.Domain{ASCII: "mox2.example"}, "mjl", "")
277 })
278
279 // "accountadd"
280 testctl(func(xctl *ctl) {
281 ctlcmdConfigAccountAdd(xctl, "mjl2", "mjl2@mox2.example")
282 })
283
284 // "addressadd"
285 testctl(func(xctl *ctl) {
286 ctlcmdConfigAddressAdd(xctl, "mjl3@mox2.example", "mjl2")
287 })
288
289 // Add a message.
290 testctl(func(xctl *ctl) {
291 ctlcmdDeliver(xctl, "mjl3@mox2.example")
292 })
293 // "retrain", retrain junk filter.
294 testctl(func(xctl *ctl) {
295 ctlcmdRetrain(xctl, "mjl2")
296 })
297
298 // "addressrm"
299 testctl(func(xctl *ctl) {
300 ctlcmdConfigAddressRemove(xctl, "mjl3@mox2.example")
301 })
302
303 // "accountdisabled"
304 testctl(func(xctl *ctl) {
305 ctlcmdConfigAccountDisabled(xctl, "mjl2", "testing")
306 })
307 testctl(func(xctl *ctl) {
308 ctlcmdConfigAccountDisabled(xctl, "mjl2", "")
309 })
310
311 // "accountrm"
312 testctl(func(xctl *ctl) {
313 ctlcmdConfigAccountRemove(xctl, "mjl2")
314 })
315
316 // "domaindisabled"
317 testctl(func(xctl *ctl) {
318 ctlcmdConfigDomainDisabled(xctl, dns.Domain{ASCII: "mox2.example"}, true)
319 })
320 testctl(func(xctl *ctl) {
321 ctlcmdConfigDomainDisabled(xctl, dns.Domain{ASCII: "mox2.example"}, false)
322 })
323
324 // "domainrm"
325 testctl(func(xctl *ctl) {
326 ctlcmdConfigDomainRemove(xctl, dns.Domain{ASCII: "mox2.example"})
327 })
328
329 // "aliasadd"
330 testctl(func(xctl *ctl) {
331 ctlcmdConfigAliasAdd(xctl, "support@mox.example", config.Alias{Addresses: []string{"mjl@mox.example"}})
332 })
333
334 // "aliaslist"
335 testctl(func(xctl *ctl) {
336 ctlcmdConfigAliasList(xctl, "mox.example")
337 })
338
339 // "aliasprint"
340 testctl(func(xctl *ctl) {
341 ctlcmdConfigAliasPrint(xctl, "support@mox.example")
342 })
343
344 // "aliasupdate"
345 testctl(func(xctl *ctl) {
346 ctlcmdConfigAliasUpdate(xctl, "support@mox.example", "true", "true", "true")
347 })
348
349 // "aliasaddaddr"
350 testctl(func(xctl *ctl) {
351 ctlcmdConfigAliasAddaddr(xctl, "support@mox.example", []string{"mjl2@mox.example"})
352 })
353
354 // "aliasrmaddr"
355 testctl(func(xctl *ctl) {
356 ctlcmdConfigAliasRmaddr(xctl, "support@mox.example", []string{"mjl2@mox.example"})
357 })
358
359 // "aliasrm"
360 testctl(func(xctl *ctl) {
361 ctlcmdConfigAliasRemove(xctl, "support@mox.example")
362 })
363
364 // accounttlspubkeyadd
365 certDER := fakeCert(t)
366 testctl(func(xctl *ctl) {
367 ctlcmdConfigTlspubkeyAdd(xctl, "mjl@mox.example", "testkey", false, certDER)
368 })
369
370 // "accounttlspubkeylist"
371 testctl(func(xctl *ctl) {
372 ctlcmdConfigTlspubkeyList(xctl, "")
373 })
374 testctl(func(xctl *ctl) {
375 ctlcmdConfigTlspubkeyList(xctl, "mjl")
376 })
377
378 tpkl, err := store.TLSPublicKeyList(ctxbg, "")
379 tcheck(t, err, "list tls public keys")
380 if len(tpkl) != 1 {
381 t.Fatalf("got %d tls public keys, expected 1", len(tpkl))
382 }
383 fingerprint := tpkl[0].Fingerprint
384
385 // "accounttlspubkeyget"
386 testctl(func(xctl *ctl) {
387 ctlcmdConfigTlspubkeyGet(xctl, fingerprint)
388 })
389
390 // "accounttlspubkeyrm"
391 testctl(func(xctl *ctl) {
392 ctlcmdConfigTlspubkeyRemove(xctl, fingerprint)
393 })
394
395 tpkl, err = store.TLSPublicKeyList(ctxbg, "")
396 tcheck(t, err, "list tls public keys")
397 if len(tpkl) != 0 {
398 t.Fatalf("got %d tls public keys, expected 0", len(tpkl))
399 }
400
401 // "loglevels"
402 testctl(func(xctl *ctl) {
403 ctlcmdLoglevels(xctl)
404 })
405
406 // "setloglevels"
407 testctl(func(xctl *ctl) {
408 ctlcmdSetLoglevels(xctl, "", "debug")
409 })
410 testctl(func(xctl *ctl) {
411 ctlcmdSetLoglevels(xctl, "smtpserver", "debug")
412 })
413
414 // Export data, import it again
415 xcmdExport(true, false, []string{filepath.FromSlash("testdata/ctl/data/tmp/export/mbox/"), filepath.FromSlash("testdata/ctl/data/accounts/mjl")}, &cmd{log: pkglog})
416 xcmdExport(false, false, []string{filepath.FromSlash("testdata/ctl/data/tmp/export/maildir/"), filepath.FromSlash("testdata/ctl/data/accounts/mjl")}, &cmd{log: pkglog})
417 testctl(func(xctl *ctl) {
418 ctlcmdImport(xctl, true, "mjl", "inbox", filepath.FromSlash("testdata/ctl/data/tmp/export/mbox/Inbox.mbox"))
419 })
420 testctl(func(xctl *ctl) {
421 ctlcmdImport(xctl, false, "mjl", "inbox", filepath.FromSlash("testdata/ctl/data/tmp/export/maildir/Inbox"))
422 })
423
424 // "recalculatemailboxcounts"
425 testctl(func(xctl *ctl) {
426 ctlcmdRecalculateMailboxCounts(xctl, "mjl")
427 })
428
429 // "fixmsgsize"
430 testctl(func(xctl *ctl) {
431 ctlcmdFixmsgsize(xctl, "mjl")
432 })
433 testctl(func(xctl *ctl) {
434 acc, err := store.OpenAccount(xctl.log, "mjl", false)
435 tcheck(t, err, "open account")
436 defer func() {
437 acc.Close()
438 acc.WaitClosed()
439 }()
440
441 content := []byte("Subject: hi\r\n\r\nbody\r\n")
442
443 deliver := func(m *store.Message) {
444 t.Helper()
445 m.Size = int64(len(content))
446 msgf, err := store.CreateMessageTemp(xctl.log, "ctltest")
447 tcheck(t, err, "create temp file")
448 defer os.Remove(msgf.Name())
449 defer msgf.Close()
450 _, err = msgf.Write(content)
451 tcheck(t, err, "write message file")
452
453 acc.WithWLock(func() {
454 err = acc.DeliverMailbox(xctl.log, "Inbox", m, msgf)
455 tcheck(t, err, "deliver message")
456 })
457 }
458
459 var msgBadSize store.Message
460 deliver(&msgBadSize)
461
462 msgBadSize.Size = 1
463 err = acc.DB.Update(ctxbg, &msgBadSize)
464 tcheck(t, err, "update message to bad size")
465 mb := store.Mailbox{ID: msgBadSize.MailboxID}
466 err = acc.DB.Get(ctxbg, &mb)
467 tcheck(t, err, "get db")
468 mb.Size -= int64(len(content))
469 mb.Size += 1
470 err = acc.DB.Update(ctxbg, &mb)
471 tcheck(t, err, "update mailbox size")
472
473 // Fix up the size.
474 ctlcmdFixmsgsize(xctl, "")
475
476 err = acc.DB.Get(ctxbg, &msgBadSize)
477 tcheck(t, err, "get message")
478 if msgBadSize.Size != int64(len(content)) {
479 t.Fatalf("after fixing, message size is %d, should be %d", msgBadSize.Size, len(content))
480 }
481 })
482
483 // "reparse"
484 testctl(func(xctl *ctl) {
485 ctlcmdReparse(xctl, "mjl")
486 })
487 testctl(func(xctl *ctl) {
488 ctlcmdReparse(xctl, "")
489 })
490
491 // "reassignthreads"
492 testctl(func(xctl *ctl) {
493 ctlcmdReassignthreads(xctl, "mjl")
494 })
495 testctl(func(xctl *ctl) {
496 ctlcmdReassignthreads(xctl, "")
497 })
498
499 // "backup", backup account.
500 err = dmarcdb.Init()
501 tcheck(t, err, "dmarcdb init")
502 defer dmarcdb.Close()
503 err = mtastsdb.Init(false)
504 tcheck(t, err, "mtastsdb init")
505 defer mtastsdb.Close()
506 err = tlsrptdb.Init()
507 tcheck(t, err, "tlsrptdb init")
508 defer tlsrptdb.Close()
509 testctl(func(xctl *ctl) {
510 os.RemoveAll("testdata/ctl/data/tmp/backup")
511 err := os.WriteFile("testdata/ctl/data/receivedid.key", make([]byte, 16), 0600)
512 tcheck(t, err, "writing receivedid.key")
513 ctlcmdBackup(xctl, filepath.FromSlash("testdata/ctl/data/tmp/backup"), false)
514 })
515
516 // Verify the backup.
517 xcmd := cmd{
518 flag: flag.NewFlagSet("", flag.ExitOnError),
519 flagArgs: []string{filepath.FromSlash("testdata/ctl/data/tmp/backup/data")},
520 }
521 cmdVerifydata(&xcmd)
522
523 // IMAP connection.
524 testctl(func(xctl *ctl) {
525 a, b := net.Pipe()
526 go func() {
527 client, err := imapclient.New(mox.Cid(), a, true)
528 tcheck(t, err, "new imapclient")
529 client.Select("inbox")
530 client.Logout()
531 defer a.Close()
532 }()
533 ctlcmdIMAPServe(xctl, "mjl@mox.example", b, b)
534 })
535}
536
537func fakeCert(t *testing.T) []byte {
538 t.Helper()
539 seed := make([]byte, ed25519.SeedSize)
540 privKey := ed25519.NewKeyFromSeed(seed) // Fake key, don't use this for real!
541 template := &x509.Certificate{
542 SerialNumber: big.NewInt(1), // Required field...
543 }
544 localCertBuf, err := x509.CreateCertificate(cryptorand.Reader, template, template, privKey.Public(), privKey)
545 tcheck(t, err, "making certificate")
546 return localCertBuf
547}
548