1package store
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "log/slog"
8 "os"
9 "path/filepath"
10
11 "github.com/mjl-/bstore"
12
13 "github.com/mjl-/mox/config"
14 "github.com/mjl-/mox/junk"
15 "github.com/mjl-/mox/mlog"
16 "github.com/mjl-/mox/mox-"
17)
18
19// ErrNoJunkFilter indicates user did not configure/enable a junk filter.
20var ErrNoJunkFilter = errors.New("junkfilter: not configured")
21
22// OpenJunkFilter returns an opened junk filter for the account.
23// If the account does not have a junk filter enabled, ErrNotConfigured is returned.
24// Do not forget to save the filter after modifying, and to always close the filter when done.
25// An empty filter is initialized on first access of the filter.
26func (a *Account) OpenJunkFilter(ctx context.Context, log mlog.Log) (*junk.Filter, *config.JunkFilter, error) {
27 conf, ok := mox.Conf.Account(a.Name)
28 if !ok {
29 return nil, nil, ErrAccountUnknown
30 }
31 jf := conf.JunkFilter
32 if jf == nil {
33 return nil, jf, ErrNoJunkFilter
34 }
35
36 basePath := mox.DataDirPath("accounts")
37 dbPath := filepath.Join(basePath, a.Name, "junkfilter.db")
38 bloomPath := filepath.Join(basePath, a.Name, "junkfilter.bloom")
39
40 if _, xerr := os.Stat(dbPath); xerr != nil && os.IsNotExist(xerr) {
41 f, err := junk.NewFilter(ctx, log, jf.Params, dbPath, bloomPath)
42 return f, jf, err
43 }
44 f, err := junk.OpenFilter(ctx, log, jf.Params, dbPath, bloomPath, false)
45 return f, jf, err
46}
47
48// RetrainMessages (un)trains messages, if relevant given their flags. Updates
49// m.TrainedJunk after retraining.
50func (a *Account) RetrainMessages(ctx context.Context, log mlog.Log, tx *bstore.Tx, msgs []Message, absentOK bool) (rerr error) {
51 if len(msgs) == 0 {
52 return nil
53 }
54
55 var jf *junk.Filter
56
57 for i := range msgs {
58 if !msgs[i].NeedsTraining() {
59 continue
60 }
61
62 // Lazy open the junk filter.
63 if jf == nil {
64 var err error
65 jf, _, err = a.OpenJunkFilter(ctx, log)
66 if err != nil && errors.Is(err, ErrNoJunkFilter) {
67 // No junk filter configured. Nothing more to do.
68 return nil
69 } else if err != nil {
70 return fmt.Errorf("open junk filter: %v", err)
71 }
72 defer func() {
73 if jf != nil {
74 err := jf.Close()
75 if rerr == nil {
76 rerr = err
77 }
78 }
79 }()
80 }
81 if err := a.RetrainMessage(ctx, log, tx, jf, &msgs[i], absentOK); err != nil {
82 return err
83 }
84 }
85 return nil
86}
87
88// RetrainMessage untrains and/or trains a message, if relevant given m.TrainedJunk
89// and m.Junk/m.Notjunk. Updates m.TrainedJunk after retraining.
90func (a *Account) RetrainMessage(ctx context.Context, log mlog.Log, tx *bstore.Tx, jf *junk.Filter, m *Message, absentOK bool) error {
91 untrain := m.TrainedJunk != nil
92 untrainJunk := untrain && *m.TrainedJunk
93 train := m.Junk || m.Notjunk && !(m.Junk && m.Notjunk)
94 trainJunk := m.Junk
95
96 if !untrain && !train || (untrain && train && untrainJunk == trainJunk) {
97 return nil
98 }
99
100 log.Debug("updating junk filter",
101 slog.Bool("untrain", untrain),
102 slog.Bool("untrainjunk", untrainJunk),
103 slog.Bool("train", train),
104 slog.Bool("trainjunk", trainJunk))
105
106 mr := a.MessageReader(*m)
107 defer func() {
108 err := mr.Close()
109 log.Check(err, "closing message reader after retraining")
110 }()
111
112 p, err := m.LoadPart(mr)
113 if err != nil {
114 log.Errorx("loading part for message", err)
115 return nil
116 }
117
118 words, err := jf.ParseMessage(p)
119 if err != nil {
120 log.Errorx("parsing message for updating junk filter", err, slog.Any("parse", ""))
121 return nil
122 }
123
124 if untrain {
125 err := jf.Untrain(ctx, !untrainJunk, words)
126 if err != nil {
127 return err
128 }
129 m.TrainedJunk = nil
130 }
131 if train {
132 err := jf.Train(ctx, !trainJunk, words)
133 if err != nil {
134 return err
135 }
136 m.TrainedJunk = &trainJunk
137 }
138 if err := tx.Update(m); err != nil && (!absentOK || err != bstore.ErrAbsent) {
139 return err
140 }
141 return nil
142}
143
144// TrainMessage trains the junk filter based on the current m.Junk/m.Notjunk flags,
145// disregarding m.TrainedJunk and not updating that field.
146func (a *Account) TrainMessage(ctx context.Context, log mlog.Log, jf *junk.Filter, m Message) (bool, error) {
147 if !m.Junk && !m.Notjunk || (m.Junk && m.Notjunk) {
148 return false, nil
149 }
150
151 mr := a.MessageReader(m)
152 defer func() {
153 err := mr.Close()
154 log.Check(err, "closing message after training")
155 }()
156
157 p, err := m.LoadPart(mr)
158 if err != nil {
159 log.Errorx("loading part for message", err)
160 return false, nil
161 }
162
163 words, err := jf.ParseMessage(p)
164 if err != nil {
165 log.Errorx("parsing message for updating junk filter", err, slog.Any("parse", ""))
166 return false, nil
167 }
168
169 return true, jf.Train(ctx, m.Notjunk, words)
170}
171