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")
22func (a *Account) HasJunkFilter() bool {
24 return conf.JunkFilter != nil
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) {
34 return nil, nil, ErrAccountUnknown
38 return nil, jf, ErrNoJunkFilter
41 basePath := mox.DataDirPath("accounts")
42 dbPath := filepath.Join(basePath, a.Name, "junkfilter.db")
43 bloomPath := filepath.Join(basePath, a.Name, "junkfilter.bloom")
45 if _, xerr := os.Stat(dbPath); xerr != nil && os.IsNotExist(xerr) {
46 f, err := junk.NewFilter(ctx, log, jf.Params, dbPath, bloomPath)
49 f, err := junk.OpenFilter(ctx, log, jf.Params, dbPath, bloomPath, false)
53func (a *Account) ensureJunkFilter(ctx context.Context, log mlog.Log, jfOpt *junk.Filter) (jf *junk.Filter, opened bool, err error) {
55 return jfOpt, false, nil
58 jf, _, err = a.OpenJunkFilter(ctx, log)
60 return nil, false, fmt.Errorf("open junk filter: %v", err)
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) {
75 if !msgs[i].NeedsTraining() {
79 // Lazy open the junk filter.
82 jf, _, err = a.OpenJunkFilter(ctx, log)
83 if err != nil && errors.Is(err, ErrNoJunkFilter) {
84 // No junk filter configured. Nothing more to do.
86 } else if err != nil {
87 return fmt.Errorf("open junk filter: %v", err)
91 err := jf.CloseDiscard()
92 log.Check(err, "close junk filter without saving")
98 if err := a.RetrainMessage(ctx, log, tx, jf, &msgs[i]); err != nil {
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()
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))
118 mr := a.MessageReader(*m)
121 log.Check(err, "closing message reader after retraining")
124 p, err := m.LoadPart(mr)
126 log.Errorx("loading part for message", err)
130 words, err := jf.ParseMessage(p)
132 log.Infox("parsing message for updating junk filter", err, slog.Any("parse", ""))
137 err := jf.Untrain(ctx, !untrainJunk, words)
144 err := jf.Train(ctx, !trainJunk, words)
148 m.TrainedJunk = &trainJunk
150 if err := tx.Update(m); err != nil {
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)
162 log.Check(err, "closing message after training")
165 p, err := m.LoadPart(mr)
167 log.Errorx("loading part for message", err)
171 words, err := jf.ParseMessage(p)
173 log.Infox("parsing message for updating junk filter", err, slog.Any("parse", ""))
177 return true, jf.Train(ctx, ham, words)