1package store
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "reflect"
8 "regexp"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/mjl-/bstore"
14 "github.com/mjl-/sconf"
15
16 "github.com/mjl-/mox/config"
17 "github.com/mjl-/mox/message"
18 "github.com/mjl-/mox/mlog"
19 "github.com/mjl-/mox/mox-"
20)
21
22var ctxbg = context.Background()
23var pkglog = mlog.New("store", nil)
24
25func tcheck(t *testing.T, err error, msg string) {
26 t.Helper()
27 if err != nil {
28 t.Fatalf("%s: %s", msg, err)
29 }
30}
31
32func tcompare(t *testing.T, got, expect any) {
33 t.Helper()
34 if !reflect.DeepEqual(got, expect) {
35 t.Fatalf("got:\n%#v\nexpected:\n%#v", got, expect)
36 }
37}
38
39func TestMailbox(t *testing.T) {
40 log := mlog.New("store", nil)
41 os.RemoveAll("../testdata/store/data")
42 mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
43 mox.MustLoadConfig(true, false)
44 acc, err := OpenAccount(log, "mjl", false)
45 tcheck(t, err, "open account")
46 defer func() {
47 err = acc.Close()
48 tcheck(t, err, "closing account")
49 acc.CheckClosed()
50 }()
51 defer Switchboard()()
52
53 msgFile, err := CreateMessageTemp(log, "account-test")
54 if err != nil {
55 t.Fatalf("creating temp msg file: %s", err)
56 }
57 defer os.Remove(msgFile.Name())
58 defer msgFile.Close()
59 msgWriter := message.NewWriter(msgFile)
60 if _, err := msgWriter.Write([]byte(" message")); err != nil {
61 t.Fatalf("writing to temp message: %s", err)
62 }
63
64 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")
65 msgPrefixCatchall := []byte("Subject: catchall\r\n\r\n")
66 m := Message{
67 Received: time.Now(),
68 Size: int64(len(msgPrefix)) + msgWriter.Size,
69 MsgPrefix: msgPrefix,
70 }
71 msent := m
72 m.ThreadMuted = true
73 m.ThreadCollapsed = true
74 var mbsent Mailbox
75 mbrejects := Mailbox{Name: "Rejects", UIDValidity: 1, UIDNext: 1, HaveCounts: true}
76 mreject := m
77 mconsumed := Message{
78 Received: m.Received,
79 Size: int64(len(msgPrefixCatchall)) + msgWriter.Size,
80 MsgPrefix: msgPrefixCatchall,
81 }
82 acc.WithWLock(func() {
83 conf, _ := acc.Conf()
84 err := acc.DeliverDestination(log, conf.Destinations["mjl"], &m, msgFile)
85 tcheck(t, err, "deliver without consume")
86
87 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
88 var err error
89 mbsent, err = bstore.QueryTx[Mailbox](tx).FilterNonzero(Mailbox{Name: "Sent"}).Get()
90 tcheck(t, err, "sent mailbox")
91 msent.MailboxID = mbsent.ID
92 msent.MailboxOrigID = mbsent.ID
93 err = acc.DeliverMessage(pkglog, tx, &msent, msgFile, true, false, false, true)
94 tcheck(t, err, "deliver message")
95 if !msent.ThreadMuted || !msent.ThreadCollapsed {
96 t.Fatalf("thread muted & collapsed should have been copied from parent (duplicate message-id) m")
97 }
98
99 err = tx.Get(&mbsent)
100 tcheck(t, err, "get mbsent")
101 mbsent.Add(msent.MailboxCounts())
102 err = tx.Update(&mbsent)
103 tcheck(t, err, "update mbsent")
104
105 err = tx.Insert(&mbrejects)
106 tcheck(t, err, "insert rejects mailbox")
107 mreject.MailboxID = mbrejects.ID
108 mreject.MailboxOrigID = mbrejects.ID
109 err = acc.DeliverMessage(pkglog, tx, &mreject, msgFile, true, false, false, true)
110 tcheck(t, err, "deliver message")
111
112 err = tx.Get(&mbrejects)
113 tcheck(t, err, "get mbrejects")
114 mbrejects.Add(mreject.MailboxCounts())
115 err = tx.Update(&mbrejects)
116 tcheck(t, err, "update mbrejects")
117
118 return nil
119 })
120 tcheck(t, err, "deliver as sent and rejects")
121
122 err = acc.DeliverDestination(pkglog, conf.Destinations["mjl"], &mconsumed, msgFile)
123 tcheck(t, err, "deliver with consume")
124
125 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
126 m.Junk = true
127 l := []Message{m}
128 err = acc.RetrainMessages(ctxbg, log, tx, l, false)
129 tcheck(t, err, "train as junk")
130 m = l[0]
131 return nil
132 })
133 tcheck(t, err, "train messages")
134 })
135
136 m.Junk = false
137 m.Notjunk = true
138 jf, _, err := acc.OpenJunkFilter(ctxbg, log)
139 tcheck(t, err, "open junk filter")
140 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
141 return acc.RetrainMessage(ctxbg, log, tx, jf, &m, false)
142 })
143 tcheck(t, err, "retraining as non-junk")
144 err = jf.Close()
145 tcheck(t, err, "close junk filter")
146
147 m.Notjunk = false
148 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
149 return acc.RetrainMessages(ctxbg, log, tx, []Message{m}, false)
150 })
151 tcheck(t, err, "untraining non-junk")
152
153 err = acc.SetPassword(log, "testtest")
154 tcheck(t, err, "set password")
155
156 key0, err := acc.Subjectpass("test@localhost")
157 tcheck(t, err, "subjectpass")
158 key1, err := acc.Subjectpass("test@localhost")
159 tcheck(t, err, "subjectpass")
160 if key0 != key1 {
161 t.Fatalf("different keys for same address")
162 }
163 key2, err := acc.Subjectpass("test2@localhost")
164 tcheck(t, err, "subjectpass")
165 if key2 == key0 {
166 t.Fatalf("same key for different address")
167 }
168
169 acc.WithWLock(func() {
170 err := acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
171 _, _, err := acc.MailboxEnsure(tx, "Testbox", true, SpecialUse{})
172 return err
173 })
174 tcheck(t, err, "ensure mailbox exists")
175 err = acc.DB.Read(ctxbg, func(tx *bstore.Tx) error {
176 _, _, err := acc.MailboxEnsure(tx, "Testbox", true, SpecialUse{})
177 return err
178 })
179 tcheck(t, err, "ensure mailbox exists")
180
181 err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
182 _, _, err := acc.MailboxEnsure(tx, "Testbox2", false, SpecialUse{})
183 tcheck(t, err, "create mailbox")
184
185 exists, err := acc.MailboxExists(tx, "Testbox2")
186 tcheck(t, err, "checking that mailbox exists")
187 if !exists {
188 t.Fatalf("mailbox does not exist")
189 }
190
191 exists, err = acc.MailboxExists(tx, "Testbox3")
192 tcheck(t, err, "checking that mailbox does not exist")
193 if exists {
194 t.Fatalf("mailbox does exist")
195 }
196
197 xmb, err := acc.MailboxFind(tx, "Testbox3")
198 tcheck(t, err, "finding non-existing mailbox")
199 if xmb != nil {
200 t.Fatalf("did find Testbox3: %v", xmb)
201 }
202 xmb, err = acc.MailboxFind(tx, "Testbox2")
203 tcheck(t, err, "finding existing mailbox")
204 if xmb == nil {
205 t.Fatalf("did not find Testbox2")
206 }
207
208 changes, err := acc.SubscriptionEnsure(tx, "Testbox2")
209 tcheck(t, err, "ensuring new subscription")
210 if len(changes) == 0 {
211 t.Fatalf("new subscription did not result in changes")
212 }
213 changes, err = acc.SubscriptionEnsure(tx, "Testbox2")
214 tcheck(t, err, "ensuring already present subscription")
215 if len(changes) != 0 {
216 t.Fatalf("already present subscription resulted in changes")
217 }
218
219 return nil
220 })
221 tcheck(t, err, "write tx")
222
223 // todo: check that messages are removed and changes sent.
224 hasSpace, err := acc.TidyRejectsMailbox(log, "Rejects")
225 tcheck(t, err, "tidy rejects mailbox")
226 if !hasSpace {
227 t.Fatalf("no space for more rejects")
228 }
229
230 acc.RejectsRemove(log, "Rejects", "m01@mox.example")
231 })
232
233 // Run the auth tests twice for possible cache effects.
234 for i := 0; i < 2; i++ {
235 _, _, err := OpenEmailAuth(log, "mjl@mox.example", "bogus", false)
236 if err != ErrUnknownCredentials {
237 t.Fatalf("got %v, expected ErrUnknownCredentials", err)
238 }
239 }
240
241 for i := 0; i < 2; i++ {
242 acc2, _, err := OpenEmailAuth(log, "mjl@mox.example", "testtest", false)
243 tcheck(t, err, "open for email with auth")
244 err = acc2.Close()
245 tcheck(t, err, "close account")
246 }
247
248 acc2, _, err := OpenEmailAuth(log, "other@mox.example", "testtest", false)
249 tcheck(t, err, "open for email with auth")
250 err = acc2.Close()
251 tcheck(t, err, "close account")
252
253 _, _, err = OpenEmailAuth(log, "bogus@mox.example", "testtest", false)
254 if err != ErrUnknownCredentials {
255 t.Fatalf("got %v, expected ErrUnknownCredentials", err)
256 }
257
258 _, _, err = OpenEmailAuth(log, "mjl@test.example", "testtest", false)
259 if err != ErrUnknownCredentials {
260 t.Fatalf("got %v, expected ErrUnknownCredentials", err)
261 }
262}
263
264func TestMessageRuleset(t *testing.T) {
265 f, err := CreateMessageTemp(pkglog, "msgruleset")
266 tcheck(t, err, "creating temp msg file")
267 defer os.Remove(f.Name())
268 defer f.Close()
269
270 msgBuf := []byte(strings.ReplaceAll(`List-ID: <test.mox.example>
271
272test
273`, "\n", "\r\n"))
274
275 const destConf = `
276Rulesets:
277 -
278 HeadersRegexp:
279 list-id: <test\.mox\.example>
280 Mailbox: test
281`
282 var dest config.Destination
283 err = sconf.Parse(strings.NewReader(destConf), &dest)
284 tcheck(t, err, "parse config")
285 // todo: should use regular config initialization functions for this.
286 var hdrs [][2]*regexp.Regexp
287 for k, v := range dest.Rulesets[0].HeadersRegexp {
288 rk, err := regexp.Compile(k)
289 tcheck(t, err, "compile key")
290 rv, err := regexp.Compile(v)
291 tcheck(t, err, "compile value")
292 hdrs = append(hdrs, [...]*regexp.Regexp{rk, rv})
293 }
294 dest.Rulesets[0].HeadersRegexpCompiled = hdrs
295
296 c := MessageRuleset(pkglog, dest, &Message{}, msgBuf, f)
297 if c == nil {
298 t.Fatalf("expected ruleset match")
299 }
300
301 msg2Buf := []byte(strings.ReplaceAll(`From: <mjl@mox.example>
302
303test
304`, "\n", "\r\n"))
305 c = MessageRuleset(pkglog, dest, &Message{}, msg2Buf, f)
306 if c != nil {
307 t.Fatalf("expected no ruleset match")
308 }
309
310 // todo: test the SMTPMailFrom and VerifiedDomains rule.
311}
312