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