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