mox/store/account_test.go
Mechiel Lukkien 2beb30cc20
Refactor how messages are added to mailboxes
DeliverMessage() is now MessageAdd(), and it takes a Mailbox object that it
modifies but doesn't write to the database (the caller must do it, and plenty
of times can do it more efficiently by doing it once for multiple messages).
The new AddOpts let the caller influence how many checks and how much of the
work MessageAdd() does. The zero-value AddOpts enable all checks and all the
work, but callers can take responsibility of some of the checks/work if it can
do it more efficiently itself.

This simplifies the code in most places, and makes it more efficient. The
checks to update per-mailbox keywords is a bit simpler too now.

We are also more careful to close the junk filter without saving it in case of
errors.

Still part of more upcoming changes.
2025-03-06 11:35:43 +01:00

307 lines
8.7 KiB
Go

package store
import (
"context"
"os"
"path/filepath"
"reflect"
"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()
var pkglog = mlog.New("store", nil)
func tcheck(t *testing.T, err error, msg string) {
t.Helper()
if err != nil {
t.Fatalf("%s: %s", msg, err)
}
}
func tcompare(t *testing.T, got, expect any) {
t.Helper()
if !reflect.DeepEqual(got, expect) {
t.Fatalf("got:\n%#v\nexpected:\n%#v", got, expect)
}
}
func TestMailbox(t *testing.T) {
log := mlog.New("store", nil)
os.RemoveAll("../testdata/store/data")
mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
mox.MustLoadConfig(true, false)
acc, err := OpenAccount(log, "mjl", false)
tcheck(t, err, "open account")
defer func() {
err = acc.Close()
tcheck(t, err, "closing account")
acc.CheckClosed()
}()
defer Switchboard()()
msgFile, err := CreateMessageTemp(log, "account-test")
if err != nil {
t.Fatalf("creating temp msg file: %s", err)
}
defer os.Remove(msgFile.Name())
defer msgFile.Close()
msgWriter := message.NewWriter(msgFile)
if _, err := msgWriter.Write([]byte(" message")); err != nil {
t.Fatalf("writing to temp message: %s", err)
}
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")
msgPrefixCatchall := []byte("Subject: catchall\r\n\r\n")
m := Message{
Received: time.Now(),
Size: int64(len(msgPrefix)) + msgWriter.Size,
MsgPrefix: msgPrefix,
}
msent := m
m.ThreadMuted = true
m.ThreadCollapsed = true
var mbsent Mailbox
mreject := m
mconsumed := Message{
Received: m.Received,
Size: int64(len(msgPrefixCatchall)) + msgWriter.Size,
MsgPrefix: msgPrefixCatchall,
}
acc.WithWLock(func() {
conf, _ := acc.Conf()
err := acc.DeliverDestination(log, conf.Destinations["mjl"], &m, msgFile)
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.MessageAdd(pkglog, tx, &mbsent, &msent, msgFile, AddOpts{SkipSourceFileSync: true, SkipDirSync: true})
tcheck(t, err, "deliver message")
if !msent.ThreadMuted || !msent.ThreadCollapsed {
t.Fatalf("thread muted & collapsed should have been copied from parent (duplicate message-id) m")
}
err = tx.Update(&mbsent)
tcheck(t, err, "update mbsent")
modseq, err := acc.NextModSeq(tx)
tcheck(t, err, "get next modseq")
mbrejects := Mailbox{Name: "Rejects", UIDValidity: 1, UIDNext: 1, ModSeq: modseq, CreateSeq: modseq, HaveCounts: true}
err = tx.Insert(&mbrejects)
tcheck(t, err, "insert rejects mailbox")
mreject.MailboxID = mbrejects.ID
mreject.MailboxOrigID = mbrejects.ID
err = acc.MessageAdd(pkglog, tx, &mbrejects, &mreject, msgFile, AddOpts{SkipSourceFileSync: true, SkipDirSync: true})
tcheck(t, err, "deliver message")
err = tx.Update(&mbrejects)
tcheck(t, err, "update mbrejects")
return nil
})
tcheck(t, err, "deliver as sent and rejects")
err = acc.DeliverDestination(pkglog, conf.Destinations["mjl"], &mconsumed, msgFile)
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)
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)
})
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})
})
tcheck(t, err, "untraining non-junk")
err = acc.SetPassword(log, "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")
}
var modseq ModSeq
acc.WithWLock(func() {
err := acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
_, _, err := acc.MailboxEnsure(tx, "Testbox", true, SpecialUse{}, &modseq)
return err
})
tcheck(t, err, "ensure mailbox exists")
err = acc.DB.Read(ctxbg, func(tx *bstore.Tx) error {
_, _, err := acc.MailboxEnsure(tx, "Testbox", true, SpecialUse{}, &modseq)
return err
})
tcheck(t, err, "ensure mailbox exists")
err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
_, _, err := acc.MailboxEnsure(tx, "Testbox2", false, SpecialUse{}, &modseq)
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.
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(log, "mjl@mox.example", "bogus", false)
if err != ErrUnknownCredentials {
t.Fatalf("got %v, expected ErrUnknownCredentials", err)
}
}
for i := 0; i < 2; i++ {
acc2, _, err := OpenEmailAuth(log, "mjl@mox.example", "testtest", false)
tcheck(t, err, "open for email with auth")
err = acc2.Close()
tcheck(t, err, "close account")
}
acc2, _, err := OpenEmailAuth(log, "other@mox.example", "testtest", false)
tcheck(t, err, "open for email with auth")
err = acc2.Close()
tcheck(t, err, "close account")
_, _, err = OpenEmailAuth(log, "bogus@mox.example", "testtest", false)
if err != ErrUnknownCredentials {
t.Fatalf("got %v, expected ErrUnknownCredentials", err)
}
_, _, err = OpenEmailAuth(log, "mjl@test.example", "testtest", false)
if err != ErrUnknownCredentials {
t.Fatalf("got %v, expected ErrUnknownCredentials", err)
}
}
func TestMessageRuleset(t *testing.T) {
f, err := CreateMessageTemp(pkglog, "msgruleset")
tcheck(t, err, "creating temp msg file")
defer os.Remove(f.Name())
defer f.Close()
msgBuf := []byte(strings.ReplaceAll(`List-ID: <test.mox.example>
test
`, "\n", "\r\n"))
const destConf = `
Rulesets:
-
HeadersRegexp:
list-id: <test\.mox\.example>
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(pkglog, dest, &Message{}, msgBuf, f)
if c == nil {
t.Fatalf("expected ruleset match")
}
msg2Buf := []byte(strings.ReplaceAll(`From: <mjl@mox.example>
test
`, "\n", "\r\n"))
c = MessageRuleset(pkglog, dest, &Message{}, msg2Buf, f)
if c != nil {
t.Fatalf("expected no ruleset match")
}
// todo: test the SMTPMailFrom and VerifiedDomains rule.
}