15 "github.com/mjl-/bstore"
16 "github.com/mjl-/sconf"
18 "github.com/mjl-/mox/config"
19 "github.com/mjl-/mox/message"
20 "github.com/mjl-/mox/mlog"
21 "github.com/mjl-/mox/mox-"
24var ctxbg = context.Background()
25var pkglog = mlog.New("store", nil)
27func tcheck(t *testing.T, err error, msg string) {
30 t.Fatalf("%s: %s", msg, err)
34func tcompare(t *testing.T, got, expect any) {
36 if !reflect.DeepEqual(got, expect) {
37 t.Fatalf("got:\n%#v\nexpected:\n%#v", got, expect)
41func TestMailbox(t *testing.T) {
42 log := mlog.New("store", nil)
43 os.RemoveAll("../testdata/store/data")
44 mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
45 mox.MustLoadConfig(true, false)
47 tcheck(t, err, "init")
50 tcheck(t, err, "close")
53 acc, err := OpenAccount(log, "mjl", false)
54 tcheck(t, err, "open account")
57 tcheck(t, err, "closing account")
61 msgFile, err := CreateMessageTemp(log, "account-test")
62 tcheck(t, err, "create temp message file")
63 defer CloseRemoveTempFile(log, msgFile, "temp message file")
64 msgWriter := message.NewWriter(msgFile)
65 _, err = msgWriter.Write([]byte(" message"))
66 tcheck(t, err, "write message")
68 msgPrefix := []byte("From: <mjl@mox.example\r\nTo: <mjl@mox.example>\r\nCc: <mjl@mox.example>Subject: test\r\nMessage-Id: <m01@mox.example>\r\n\r\n")
69 msgPrefixCatchall := []byte("Subject: catchall\r\n\r\n")
72 Size: int64(len(msgPrefix)) + msgWriter.Size,
77 m.ThreadCollapsed = true
82 Size: int64(len(msgPrefixCatchall)) + msgWriter.Size,
83 MsgPrefix: msgPrefixCatchall,
85 acc.WithWLock(func() {
87 err := acc.DeliverDestination(log, conf.Destinations["mjl"], &m, msgFile)
88 tcheck(t, err, "deliver without consume")
90 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
92 mbsent, err = bstore.QueryTx[Mailbox](tx).FilterNonzero(Mailbox{Name: "Sent"}).Get()
93 tcheck(t, err, "sent mailbox")
94 msent.MailboxID = mbsent.ID
95 msent.MailboxOrigID = mbsent.ID
96 err = acc.MessageAdd(log, tx, &mbsent, &msent, msgFile, AddOpts{SkipSourceFileSync: true, SkipDirSync: true})
97 tcheck(t, err, "deliver message")
98 if !msent.ThreadMuted || !msent.ThreadCollapsed {
99 t.Fatalf("thread muted & collapsed should have been copied from parent (duplicate message-id) m")
101 err = tx.Update(&mbsent)
102 tcheck(t, err, "update mbsent")
104 modseq, err := acc.NextModSeq(tx)
105 tcheck(t, err, "get next modseq")
106 mbrejects := Mailbox{Name: "Rejects", UIDValidity: 1, UIDNext: 1, ModSeq: modseq, CreateSeq: modseq, HaveCounts: true}
107 err = tx.Insert(&mbrejects)
108 tcheck(t, err, "insert rejects mailbox")
109 mreject.MailboxID = mbrejects.ID
110 mreject.MailboxOrigID = mbrejects.ID
111 err = acc.MessageAdd(log, tx, &mbrejects, &mreject, msgFile, AddOpts{SkipSourceFileSync: true, SkipDirSync: true})
112 tcheck(t, err, "deliver message")
113 err = tx.Update(&mbrejects)
114 tcheck(t, err, "update mbrejects")
118 tcheck(t, err, "deliver as sent and rejects")
120 err = acc.DeliverDestination(log, conf.Destinations["mjl"], &mconsumed, msgFile)
121 tcheck(t, err, "deliver with consume")
123 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
126 err = acc.RetrainMessages(ctxbg, log, tx, l)
127 tcheck(t, err, "train as junk")
131 tcheck(t, err, "train messages")
136 jf, _, err := acc.OpenJunkFilter(ctxbg, log)
137 tcheck(t, err, "open junk filter")
138 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
139 return acc.RetrainMessage(ctxbg, log, tx, jf, &m)
141 tcheck(t, err, "retraining as non-junk")
143 tcheck(t, err, "close junk filter")
146 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
147 return acc.RetrainMessages(ctxbg, log, tx, []Message{m})
149 tcheck(t, err, "untraining non-junk")
151 err = acc.SetPassword(log, "testtest")
152 tcheck(t, err, "set password")
154 key0, err := acc.Subjectpass("test@localhost")
155 tcheck(t, err, "subjectpass")
156 key1, err := acc.Subjectpass("test@localhost")
157 tcheck(t, err, "subjectpass")
159 t.Fatalf("different keys for same address")
161 key2, err := acc.Subjectpass("test2@localhost")
162 tcheck(t, err, "subjectpass")
164 t.Fatalf("same key for different address")
168 acc.WithWLock(func() {
171 err := acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
172 _, _, err := acc.MailboxEnsure(tx, "Testbox", true, SpecialUse{}, &modseq)
175 tcheck(t, err, "ensure mailbox exists")
176 err = acc.DB.Read(ctxbg, func(tx *bstore.Tx) error {
177 _, _, err := acc.MailboxEnsure(tx, "Testbox", true, SpecialUse{}, &modseq)
180 tcheck(t, err, "ensure mailbox exists")
182 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
183 _, _, err := acc.MailboxEnsure(tx, "Testbox2", false, SpecialUse{}, &modseq)
184 tcheck(t, err, "create mailbox")
186 exists, err := acc.MailboxExists(tx, "Testbox2")
187 tcheck(t, err, "checking that mailbox exists")
189 t.Fatalf("mailbox does not exist")
192 exists, err = acc.MailboxExists(tx, "Testbox3")
193 tcheck(t, err, "checking that mailbox does not exist")
195 t.Fatalf("mailbox does exist")
198 xmb, err := acc.MailboxFind(tx, "Testbox3")
199 tcheck(t, err, "finding non-existing mailbox")
201 t.Fatalf("did find Testbox3: %v", xmb)
203 xmb, err = acc.MailboxFind(tx, "Testbox2")
204 tcheck(t, err, "finding existing mailbox")
206 t.Fatalf("did not find Testbox2")
209 nchanges, err := acc.SubscriptionEnsure(tx, "Testbox2")
210 tcheck(t, err, "ensuring new subscription")
211 if len(nchanges) == 0 {
212 t.Fatalf("new subscription did not result in changes")
214 changes = append(changes, nchanges...)
215 nchanges, err = acc.SubscriptionEnsure(tx, "Testbox2")
216 tcheck(t, err, "ensuring already present subscription")
217 if len(nchanges) != 0 {
218 t.Fatalf("already present subscription resulted in changes")
221 // todo: check that messages are removed.
222 mbRej, err := bstore.QueryTx[Mailbox](tx).FilterNonzero(Mailbox{Name: "Rejects"}).Get()
223 tcheck(t, err, "get rejects mailbox")
224 nchanges, hasSpace, err := acc.TidyRejectsMailbox(log, tx, &mbRej)
225 tcheck(t, err, "tidy rejects mailbox")
226 changes = append(changes, nchanges...)
228 t.Fatalf("no space for more rejects")
233 tcheck(t, err, "write tx")
235 BroadcastChanges(acc, changes)
237 acc.RejectsRemove(log, "Rejects", "m01@mox.example")
240 // Run the auth tests twice for possible cache effects.
242 _, _, err := OpenEmailAuth(log, "mjl@mox.example", "bogus", false)
243 if err != ErrUnknownCredentials {
244 t.Fatalf("got %v, expected ErrUnknownCredentials", err)
249 acc2, _, err := OpenEmailAuth(log, "mjl@mox.example", "testtest", false)
250 tcheck(t, err, "open for email with auth")
252 tcheck(t, err, "close account")
255 acc2, _, err := OpenEmailAuth(log, "other@mox.example", "testtest", false)
256 tcheck(t, err, "open for email with auth")
258 tcheck(t, err, "close account")
260 _, _, err = OpenEmailAuth(log, "bogus@mox.example", "testtest", false)
261 if err != ErrUnknownCredentials {
262 t.Fatalf("got %v, expected ErrUnknownCredentials", err)
265 _, _, err = OpenEmailAuth(log, "mjl@test.example", "testtest", false)
266 if err != ErrUnknownCredentials {
267 t.Fatalf("got %v, expected ErrUnknownCredentials", err)
271func TestMessageRuleset(t *testing.T) {
272 log := mlog.New("store", nil)
273 f, err := CreateMessageTemp(log, "msgruleset")
274 tcheck(t, err, "creating temp msg file")
275 defer CloseRemoveTempFile(log, f, "temp message file")
277 msgBuf := []byte(strings.ReplaceAll(`List-ID: <test.mox.example>
286 list-id: <test\.mox\.example>
289 var dest config.Destination
290 err = sconf.Parse(strings.NewReader(destConf), &dest)
291 tcheck(t, err, "parse config")
292 // todo: should use regular config initialization functions for this.
293 var hdrs [][2]*regexp.Regexp
294 for k, v := range dest.Rulesets[0].HeadersRegexp {
295 rk, err := regexp.Compile(k)
296 tcheck(t, err, "compile key")
297 rv, err := regexp.Compile(v)
298 tcheck(t, err, "compile value")
299 hdrs = append(hdrs, [...]*regexp.Regexp{rk, rv})
301 dest.Rulesets[0].HeadersRegexpCompiled = hdrs
303 c := MessageRuleset(log, dest, &Message{}, msgBuf, f)
305 t.Fatalf("expected ruleset match")
308 msg2Buf := []byte(strings.ReplaceAll(`From: <mjl@mox.example>
312 c = MessageRuleset(log, dest, &Message{}, msg2Buf, f)
314 t.Fatalf("expected no ruleset match")
317 // todo: test the SMTPMailFrom and VerifiedDomains rule.
320// Check that opening an account forwards the Message.ID used for new additions if
321// message files already exist in the file system.
322func TestNextMessageID(t *testing.T) {
323 log := mlog.New("store", nil)
324 os.RemoveAll("../testdata/store/data")
325 mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
326 mox.MustLoadConfig(true, false)
328 tcheck(t, err, "init")
331 tcheck(t, err, "close")
333 defer Switchboard()()
335 // Ensure account exists.
336 acc, err := OpenAccount(log, "mjl", false)
337 tcheck(t, err, "open account")
339 tcheck(t, err, "closing account")
343 // Create file on disk to occupy the first Message.ID that would otherwise be used for deliveries..
344 msgData := []byte("a: b\r\n\r\ntest\r\n")
345 msgDir := filepath.FromSlash("../testdata/store/data/accounts/mjl/msg")
346 os.MkdirAll(filepath.Join(msgDir, "a"), 0700)
347 msgPath := filepath.Join(msgDir, "a", "1")
348 err = os.WriteFile(msgPath, msgData, 0700)
349 tcheck(t, err, "write message file")
351 msgPathBogus := filepath.Join(msgDir, "a", "bogus")
352 err = os.WriteFile(msgPathBogus, []byte("test"), 0700)
353 tcheck(t, err, "create message file")
354 msgPathBadID := filepath.Join(msgDir, "a", "10000") // Out of range.
355 err = os.WriteFile(msgPathBadID, []byte("test"), 0700)
356 tcheck(t, err, "create message file")
358 // Open account. This should increase the next message ID.
359 acc, err = OpenAccount(log, "mjl", false)
360 tcheck(t, err, "open account")
362 // Deliver a message. It should get ID 2.
363 mf, err := CreateMessageTemp(log, "account-test")
364 tcheck(t, err, "creating temp message file")
365 _, err = mf.Write(msgData)
366 tcheck(t, err, "write file")
367 defer CloseRemoveTempFile(log, mf, "temp message file")
369 Size: int64(len(msgData)),
371 err = acc.DeliverMailbox(log, "Inbox", &m, mf)
372 tcheck(t, err, "deliver mailbox")
374 t.Fatalf("got message id %d, expected 2", m.ID)
377 // Ensure account consistency check won't complain.
378 err = os.Remove(msgPath)
379 tcheck(t, err, "removing message path")
380 err = os.Remove(msgPathBogus)
381 tcheck(t, err, "removing message path")
382 err = os.Remove(msgPathBadID)
383 tcheck(t, err, "removing message path")
386 tcheck(t, err, "closing account")
389 // Try again, but also create next message directory, but no file.
390 os.MkdirAll(filepath.Join(msgDir, "b"), 0700)
391 os.MkdirAll(filepath.Join(msgDir, "d"), 0700) // Not used.
393 // Open account again, increasing next message ID.
394 acc, err = OpenAccount(log, "mjl", false)
395 tcheck(t, err, "open account")
397 // Deliver a message. It should get ID $msgFilesPerDir+1.
398 mf, err = CreateMessageTemp(log, "account-test")
399 tcheck(t, err, "creating temp message file")
400 _, err = mf.Write(msgData)
401 tcheck(t, err, "write file")
402 defer CloseRemoveTempFile(log, mf, "temp message file")
404 Size: int64(len(msgData)),
406 err = acc.DeliverMailbox(log, "Inbox", &m, mf)
407 tcheck(t, err, "deliver mailbox")
408 if m.ID != msgFilesPerDir+1 {
409 t.Fatalf("got message id %d, expected $msgFilesPerDir+1", m.ID)
413 tcheck(t, err, "closing account")
417func TestRemove(t *testing.T) {
418 log := mlog.New("store", nil)
419 os.RemoveAll("../testdata/store/data")
420 mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
421 mox.MustLoadConfig(true, false)
423 tcheck(t, err, "init")
426 tcheck(t, err, "close")
428 defer Switchboard()()
430 // Note: we are not removing the account from the config file. Nothing currently
431 // has a problem with that.
433 // Ensure account exists.
434 acc, err := OpenAccount(log, "mjl", false)
435 tcheck(t, err, "open account")
437 // Mark account removed. It will only be removed when we close the account.
438 err = acc.Remove(context.Background())
439 tcheck(t, err, "remove account")
441 p := filepath.Join(mox.DataDirPath("accounts"), "mjl")
443 tcheck(t, err, "stat account dir")
446 tcheck(t, err, "closing account")
450 if _, err := os.Stat(p); err == nil || !errors.Is(err, fs.ErrNotExist) {
451 t.Fatalf(`got stat err %v for account directory, expected "does not exist"`, err)
454 // Recreate files and directories. We will reinitialize store/ without closing our
455 // account reference. This will apply the account removal. We only drop our (now
456 // broken) account reference when done.
457 acc, err = OpenAccount(log, "mjl", false)
458 tcheck(t, err, "open account")
460 acc.Close() // Ignore errors.
462 CheckConsistencyOnClose = true
465 // Init below will remove the directory, we are no longer consistent.
466 CheckConsistencyOnClose = false
468 err = acc.Remove(context.Background())
469 tcheck(t, err, "remove account")
472 tcheck(t, err, "stat account dir")
475 tcheck(t, err, "close store")
477 tcheck(t, err, "init store")
478 if _, err := os.Stat(p); err == nil || !errors.Is(err, fs.ErrNotExist) {
479 t.Fatalf(`got stat err %v for account directory, expected "does not exist"`, err)
481 exists, err := bstore.QueryDB[AccountRemove](ctxbg, AuthDB).Exists()
482 tcheck(t, err, "checking for account removals")
483 tcompare(t, exists, false)