mirror of
https://github.com/mjl-/mox.git
synced 2025-06-28 02:28:15 +03:00

Keeping the message files around, and the message details in the database, is useful for IMAP sessions that haven't seen/processed the removal of a message yet and try to fetch it. Before, we would return errors. Similarly, a session that has a mailbox selected that is removed can (at least in theory) still read messages. The mechanics to do this need keeping removed mailboxes around too. JMAP needs that anyway, so we now keep modseq/createseq/expunged history for mailboxes too. And while we're at it, for annotations as well. For future JMAP support, we now also keep the mailbox parent id around for a mailbox, with an upgrade step to set the field for existing mailboxes and fixing up potential missing parents (which could possibly have happened in an obscure corner case that I doubt anyone ran into).
315 lines
9.0 KiB
Go
315 lines
9.0 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.WaitClosed()
|
|
}()
|
|
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() {
|
|
var changes []Change
|
|
|
|
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")
|
|
}
|
|
|
|
nchanges, err := acc.SubscriptionEnsure(tx, "Testbox2")
|
|
tcheck(t, err, "ensuring new subscription")
|
|
if len(nchanges) == 0 {
|
|
t.Fatalf("new subscription did not result in changes")
|
|
}
|
|
changes = append(changes, nchanges...)
|
|
nchanges, err = acc.SubscriptionEnsure(tx, "Testbox2")
|
|
tcheck(t, err, "ensuring already present subscription")
|
|
if len(nchanges) != 0 {
|
|
t.Fatalf("already present subscription resulted in changes")
|
|
}
|
|
|
|
// todo: check that messages are removed.
|
|
mbRej, err := bstore.QueryTx[Mailbox](tx).FilterNonzero(Mailbox{Name: "Rejects"}).Get()
|
|
tcheck(t, err, "get rejects mailbox")
|
|
nchanges, hasSpace, err := acc.TidyRejectsMailbox(log, tx, &mbRej)
|
|
tcheck(t, err, "tidy rejects mailbox")
|
|
changes = append(changes, nchanges...)
|
|
if !hasSpace {
|
|
t.Fatalf("no space for more rejects")
|
|
}
|
|
|
|
return nil
|
|
})
|
|
tcheck(t, err, "write tx")
|
|
|
|
BroadcastChanges(acc, changes)
|
|
|
|
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.
|
|
}
|