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