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