11 "github.com/mjl-/bstore"
13 "github.com/mjl-/mox/config"
14 "github.com/mjl-/mox/junk"
15 "github.com/mjl-/mox/mlog"
16 "github.com/mjl-/mox/mox-"
19// ErrNoJunkFilter indicates user did not configure/enable a junk filter.
20var ErrNoJunkFilter = errors.New("junkfilter: not configured")
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)
29 return nil, nil, ErrAccountUnknown
33 return nil, jf, ErrNoJunkFilter
36 basePath := mox.DataDirPath("accounts")
37 dbPath := filepath.Join(basePath, a.Name, "junkfilter.db")
38 bloomPath := filepath.Join(basePath, a.Name, "junkfilter.bloom")
40 if _, xerr := os.Stat(dbPath); xerr != nil && os.IsNotExist(xerr) {
41 f, err := junk.NewFilter(ctx, log, jf.Params, dbPath, bloomPath)
44 f, err := junk.OpenFilter(ctx, log, jf.Params, dbPath, bloomPath, false)
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) {
58 if !msgs[i].NeedsTraining() {
62 // Lazy open the junk filter.
65 jf, _, err = a.OpenJunkFilter(ctx, log)
66 if err != nil && errors.Is(err, ErrNoJunkFilter) {
67 // No junk filter configured. Nothing more to do.
69 } else if err != nil {
70 return fmt.Errorf("open junk filter: %v", err)
81 if err := a.RetrainMessage(ctx, log, tx, jf, &msgs[i], absentOK); err != nil {
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)
96 if !untrain && !train || (untrain && train && untrainJunk == trainJunk) {
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))
106 mr := a.MessageReader(*m)
109 log.Check(err, "closing message reader after retraining")
112 p, err := m.LoadPart(mr)
114 log.Errorx("loading part for message", err)
118 words, err := jf.ParseMessage(p)
120 log.Errorx("parsing message for updating junk filter", err, slog.Any("parse", ""))
125 err := jf.Untrain(ctx, !untrainJunk, words)
132 err := jf.Train(ctx, !trainJunk, words)
136 m.TrainedJunk = &trainJunk
138 if err := tx.Update(m); err != nil && (!absentOK || err != bstore.ErrAbsent) {
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) {
151 mr := a.MessageReader(m)
154 log.Check(err, "closing message after training")
157 p, err := m.LoadPart(mr)
159 log.Errorx("loading part for message", err)
163 words, err := jf.ParseMessage(p)
165 log.Errorx("parsing message for updating junk filter", err, slog.Any("parse", ""))
169 return true, jf.Train(ctx, m.Notjunk, words)