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