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