package store import ( "context" "os" "regexp" "strings" "testing" "time" "github.com/mjl-/bstore" "github.com/mjl-/sconf" "github.com/mjl-/mox/config" "github.com/mjl-/mox/message" "github.com/mjl-/mox/mlog" "github.com/mjl-/mox/mox-" ) var ctxbg = context.Background() func tcheck(t *testing.T, err error, msg string) { t.Helper() if err != nil { t.Fatalf("%s: %s", msg, err) } } func TestMailbox(t *testing.T) { os.RemoveAll("../testdata/store/data") mox.ConfigStaticPath = "../testdata/store/mox.conf" mox.MustLoadConfig(false) acc, err := OpenAccount("mjl") tcheck(t, err, "open account") defer acc.Close() switchDone := Switchboard() defer close(switchDone) log := mlog.New("store") msgFile, err := CreateMessageTemp("account-test") if err != nil { t.Fatalf("creating temp msg file: %s", err) } defer msgFile.Close() msgWriter := &message.Writer{Writer: msgFile} if _, err := msgWriter.Write([]byte(" message")); err != nil { t.Fatalf("writing to temp message: %s", err) } msgPrefix := []byte("From: \r\nCc: Subject: test\r\nMessage-Id: \r\n\r\n") msgPrefixCatchall := []byte("Subject: catchall\r\n\r\n") m := Message{ Received: time.Now(), Size: int64(len(msgPrefix)) + msgWriter.Size, MsgPrefix: msgPrefix, } msent := m var mbsent Mailbox mbrejects := Mailbox{Name: "Rejects", UIDValidity: 1, UIDNext: 1} mreject := m mconsumed := Message{ Received: m.Received, Size: int64(len(msgPrefixCatchall)) + msgWriter.Size, MsgPrefix: msgPrefixCatchall, } acc.WithWLock(func() { conf, _ := acc.Conf() err := acc.Deliver(xlog, conf.Destinations["mjl"], &m, msgFile, false) tcheck(t, err, "deliver without consume") err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error { var err error mbsent, err = bstore.QueryTx[Mailbox](tx).FilterNonzero(Mailbox{Name: "Sent"}).Get() tcheck(t, err, "sent mailbox") msent.MailboxID = mbsent.ID msent.MailboxOrigID = mbsent.ID err = acc.DeliverMessage(xlog, tx, &msent, msgFile, false, true, true, false) tcheck(t, err, "deliver message") err = tx.Insert(&mbrejects) tcheck(t, err, "insert rejects mailbox") mreject.MailboxID = mbrejects.ID mreject.MailboxOrigID = mbrejects.ID err = acc.DeliverMessage(xlog, tx, &mreject, msgFile, false, false, true, false) tcheck(t, err, "deliver message") return nil }) tcheck(t, err, "deliver as sent and rejects") err = acc.Deliver(xlog, conf.Destinations["mjl"], &mconsumed, msgFile, true) tcheck(t, err, "deliver with consume") err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error { m.Junk = true l := []Message{m} err = acc.RetrainMessages(ctxbg, log, tx, l, false) tcheck(t, err, "train as junk") m = l[0] return nil }) tcheck(t, err, "train messages") }) m.Junk = false m.Notjunk = true jf, _, err := acc.OpenJunkFilter(ctxbg, log) tcheck(t, err, "open junk filter") err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error { return acc.RetrainMessage(ctxbg, log, tx, jf, &m, false) }) tcheck(t, err, "retraining as non-junk") err = jf.Close() tcheck(t, err, "close junk filter") m.Notjunk = false err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error { return acc.RetrainMessages(ctxbg, log, tx, []Message{m}, false) }) tcheck(t, err, "untraining non-junk") err = acc.SetPassword("testtest") tcheck(t, err, "set password") key0, err := acc.Subjectpass("test@localhost") tcheck(t, err, "subjectpass") key1, err := acc.Subjectpass("test@localhost") tcheck(t, err, "subjectpass") if key0 != key1 { t.Fatalf("different keys for same address") } key2, err := acc.Subjectpass("test2@localhost") tcheck(t, err, "subjectpass") if key2 == key0 { t.Fatalf("same key for different address") } acc.WithWLock(func() { err := acc.DB.Write(ctxbg, func(tx *bstore.Tx) error { _, _, err := acc.MailboxEnsure(tx, "Testbox", true) return err }) tcheck(t, err, "ensure mailbox exists") err = acc.DB.Read(ctxbg, func(tx *bstore.Tx) error { _, _, err := acc.MailboxEnsure(tx, "Testbox", true) return err }) tcheck(t, err, "ensure mailbox exists") err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error { _, _, err := acc.MailboxEnsure(tx, "Testbox2", false) tcheck(t, err, "create mailbox") exists, err := acc.MailboxExists(tx, "Testbox2") tcheck(t, err, "checking that mailbox exists") if !exists { t.Fatalf("mailbox does not exist") } exists, err = acc.MailboxExists(tx, "Testbox3") tcheck(t, err, "checking that mailbox does not exist") if exists { t.Fatalf("mailbox does exist") } xmb, err := acc.MailboxFind(tx, "Testbox3") tcheck(t, err, "finding non-existing mailbox") if xmb != nil { t.Fatalf("did find Testbox3: %v", xmb) } xmb, err = acc.MailboxFind(tx, "Testbox2") tcheck(t, err, "finding existing mailbox") if xmb == nil { t.Fatalf("did not find Testbox2") } changes, err := acc.SubscriptionEnsure(tx, "Testbox2") tcheck(t, err, "ensuring new subscription") if len(changes) == 0 { t.Fatalf("new subscription did not result in changes") } changes, err = acc.SubscriptionEnsure(tx, "Testbox2") tcheck(t, err, "ensuring already present subscription") if len(changes) != 0 { t.Fatalf("already present subscription resulted in changes") } return nil }) tcheck(t, err, "write tx") // todo: check that messages are removed and changes sent. hasSpace, err := acc.TidyRejectsMailbox(log, "Rejects") tcheck(t, err, "tidy rejects mailbox") if !hasSpace { t.Fatalf("no space for more rejects") } acc.RejectsRemove(log, "Rejects", "m01@mox.example") }) // Run the auth tests twice for possible cache effects. for i := 0; i < 2; i++ { _, err := OpenEmailAuth("mjl@mox.example", "bogus") if err != ErrUnknownCredentials { t.Fatalf("got %v, expected ErrUnknownCredentials", err) } } for i := 0; i < 2; i++ { acc2, err := OpenEmailAuth("mjl@mox.example", "testtest") tcheck(t, err, "open for email with auth") err = acc2.Close() tcheck(t, err, "close account") } acc2, err := OpenEmailAuth("other@mox.example", "testtest") tcheck(t, err, "open for email with auth") err = acc2.Close() tcheck(t, err, "close account") _, err = OpenEmailAuth("bogus@mox.example", "testtest") if err != ErrUnknownCredentials { t.Fatalf("got %v, expected ErrUnknownCredentials", err) } _, err = OpenEmailAuth("mjl@test.example", "testtest") if err != ErrUnknownCredentials { t.Fatalf("got %v, expected ErrUnknownCredentials", err) } } func TestWriteFile(t *testing.T) { name := "../testdata/account.test" os.Remove(name) defer os.Remove(name) err := writeFile(name, strings.NewReader("test")) if err != nil { t.Fatalf("writeFile, unexpected error %v", err) } buf, err := os.ReadFile(name) if err != nil || string(buf) != "test" { t.Fatalf("writeFile, read file, got err %v, data %q", err, buf) } } func TestMessageRuleset(t *testing.T) { f, err := os.Open("/dev/null") tcheck(t, err, "open") defer f.Close() msgBuf := []byte(strings.ReplaceAll(`List-ID: test `, "\n", "\r\n")) const destConf = ` Rulesets: - HeadersRegexp: list-id: Mailbox: test ` var dest config.Destination err = sconf.Parse(strings.NewReader(destConf), &dest) tcheck(t, err, "parse config") // todo: should use regular config initialization functions for this. var hdrs [][2]*regexp.Regexp for k, v := range dest.Rulesets[0].HeadersRegexp { rk, err := regexp.Compile(k) tcheck(t, err, "compile key") rv, err := regexp.Compile(v) tcheck(t, err, "compile value") hdrs = append(hdrs, [...]*regexp.Regexp{rk, rv}) } dest.Rulesets[0].HeadersRegexpCompiled = hdrs c := MessageRuleset(xlog, dest, &Message{}, msgBuf, f) if c == nil { t.Fatalf("expected ruleset match") } msg2Buf := []byte(strings.ReplaceAll(`From: test `, "\n", "\r\n")) c = MessageRuleset(xlog, dest, &Message{}, msg2Buf, f) if c != nil { t.Fatalf("expected no ruleset match") } // todo: test the SMTPMailFrom and VerifiedDomains rule. }