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