switch to slog.Logger for logging, for easier reuse of packages by external software

we don't want external software to include internal details like mlog.
slog.Logger is/will be the standard.

we still have mlog for its helper functions, and its handler that logs in
concise logfmt used by mox.

packages that are not meant for reuse still pass around mlog.Log for
convenience.

we use golang.org/x/exp/slog because we also support the previous Go toolchain
version. with the next Go release, we'll switch to the builtin slog.
This commit is contained in:
Mechiel Lukkien
2023-12-05 13:35:58 +01:00
parent 56b2a9d980
commit 5b20cba50a
150 changed files with 5176 additions and 1898 deletions

View File

@ -44,6 +44,7 @@ import (
"golang.org/x/crypto/bcrypt"
"golang.org/x/exp/slices"
"golang.org/x/exp/slog"
"golang.org/x/text/unicode/norm"
"github.com/mjl-/bstore"
@ -67,8 +68,6 @@ import (
// false again.
var CheckConsistencyOnClose = true
var xlog = mlog.New("store")
var (
ErrUnknownMailbox = errors.New("no such mailbox")
ErrUnknownCredentials = errors.New("credentials not found")
@ -577,15 +576,15 @@ func (m *Message) PrepareExpunge() {
// PrepareThreading sets MessageID and SubjectBase (used in threading) based on the
// envelope in part.
func (m *Message) PrepareThreading(log *mlog.Log, part *message.Part) {
func (m *Message) PrepareThreading(log mlog.Log, part *message.Part) {
if part.Envelope == nil {
return
}
messageID, raw, err := message.MessageIDCanonical(part.Envelope.MessageID)
if err != nil {
log.Debugx("parsing message-id, ignoring", err, mlog.Field("messageid", part.Envelope.MessageID))
log.Debugx("parsing message-id, ignoring", err, slog.String("messageid", part.Envelope.MessageID))
} else if raw {
log.Debug("could not parse message-id as address, continuing with raw value", mlog.Field("messageid", part.Envelope.MessageID))
log.Debug("could not parse message-id as address, continuing with raw value", slog.String("messageid", part.Envelope.MessageID))
}
m.MessageID = messageID
m.SubjectBase, _ = message.ThreadSubject(part.Envelope.Subject, false)
@ -747,7 +746,7 @@ func closeAccount(acc *Account) (rerr error) {
//
// No additional data path prefix or ".db" suffix should be added to the name.
// A single shared account exists per name.
func OpenAccount(name string) (*Account, error) {
func OpenAccount(log mlog.Log, name string) (*Account, error) {
openAccounts.Lock()
defer openAccounts.Unlock()
if acc, ok := openAccounts.names[name]; ok {
@ -759,7 +758,7 @@ func OpenAccount(name string) (*Account, error) {
return nil, ErrAccountUnknown
}
acc, err := openAccount(name)
acc, err := openAccount(log, name)
if err != nil {
return nil, err
}
@ -768,15 +767,15 @@ func OpenAccount(name string) (*Account, error) {
}
// openAccount opens an existing account, or creates it if it is missing.
func openAccount(name string) (a *Account, rerr error) {
func openAccount(log mlog.Log, name string) (a *Account, rerr error) {
dir := filepath.Join(mox.DataDirPath("accounts"), name)
return OpenAccountDB(dir, name)
return OpenAccountDB(log, dir, name)
}
// OpenAccountDB opens an account database file and returns an initialized account
// or error. Only exported for use by subcommands that verify the database file.
// Almost all account opens must go through OpenAccount/OpenEmail/OpenEmailAuth.
func OpenAccountDB(accountDir, accountName string) (a *Account, rerr error) {
func OpenAccountDB(log mlog.Log, accountDir, accountName string) (a *Account, rerr error) {
dbpath := filepath.Join(accountDir, "index.db")
// Create account if it doesn't exist yet.
@ -823,7 +822,7 @@ func OpenAccountDB(accountDir, accountName string) (a *Account, rerr error) {
return bstore.QueryTx[Mailbox](tx).FilterEqual("HaveCounts", false).ForEach(func(mb Mailbox) error {
if !mentioned {
mentioned = true
xlog.Info("first calculation of mailbox counts for account", mlog.Field("account", accountName))
log.Info("first calculation of mailbox counts for account", slog.String("account", accountName))
}
mc, err := mb.CalculateCounts(tx)
if err != nil {
@ -866,17 +865,17 @@ func OpenAccountDB(accountDir, accountName string) (a *Account, rerr error) {
// Ensure all messages have a MessageID and SubjectBase, which are needed when
// matching threads.
// Then assign messages to threads, in the same way we do during imports.
xlog.Info("upgrading account for threading, in background", mlog.Field("account", acc.Name))
log.Info("upgrading account for threading, in background", slog.String("account", acc.Name))
go func() {
defer func() {
err := closeAccount(acc)
xlog.Check(err, "closing use of account after upgrading account storage for threads", mlog.Field("account", a.Name))
log.Check(err, "closing use of account after upgrading account storage for threads", slog.String("account", a.Name))
}()
defer func() {
x := recover() // Should not happen, but don't take program down if it does.
if x != nil {
xlog.Error("upgradeThreads panic", mlog.Field("err", x))
log.Error("upgradeThreads panic", slog.Any("err", x))
debug.PrintStack()
metrics.PanicInc(metrics.Upgradethreads)
acc.threadsErr = fmt.Errorf("panic during upgradeThreads: %v", x)
@ -886,12 +885,12 @@ func OpenAccountDB(accountDir, accountName string) (a *Account, rerr error) {
close(acc.threadsCompleted)
}()
err := upgradeThreads(mox.Shutdown, acc, &up)
err := upgradeThreads(mox.Shutdown, log, acc, &up)
if err != nil {
a.threadsErr = err
xlog.Errorx("upgrading account for threading, aborted", err, mlog.Field("account", a.Name))
log.Errorx("upgrading account for threading, aborted", err, slog.String("account", a.Name))
} else {
xlog.Info("upgrading account for threading, completed", mlog.Field("account", a.Name))
log.Info("upgrading account for threading, completed", slog.String("account", a.Name))
}
}()
return acc, nil
@ -901,7 +900,7 @@ func OpenAccountDB(accountDir, accountName string) (a *Account, rerr error) {
// account has completed, and returns an error if not successful.
//
// To be used before starting an import of messages.
func (a *Account) ThreadingWait(log *mlog.Log) error {
func (a *Account) ThreadingWait(log mlog.Log) error {
select {
case <-a.threadsCompleted:
return a.threadsErr
@ -1224,7 +1223,7 @@ func (a *Account) WithRLock(fn func()) {
// Caller must broadcast new message.
//
// Caller must update mailbox counts.
func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFile *os.File, sync, notrain, nothreads bool) error {
func (a *Account) DeliverMessage(log mlog.Log, tx *bstore.Tx, m *Message, msgFile *os.File, sync, notrain, nothreads bool) error {
if m.Expunged {
return fmt.Errorf("cannot deliver expunged message")
}
@ -1245,9 +1244,9 @@ func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFi
mr := FileMsgReader(m.MsgPrefix, msgFile) // We don't close, it would close the msgFile.
var part *message.Part
if m.ParsedBuf == nil {
p, err := message.EnsurePart(log, false, mr, m.Size)
p, err := message.EnsurePart(log.Logger, false, mr, m.Size)
if err != nil {
log.Infox("parsing delivered message", err, mlog.Field("parse", ""), mlog.Field("message", m.ID))
log.Infox("parsing delivered message", err, slog.String("parse", ""), slog.Int64("message", m.ID))
// We continue, p is still valid.
}
part = &p
@ -1259,7 +1258,7 @@ func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFi
} else {
var p message.Part
if err := json.Unmarshal(m.ParsedBuf, &p); err != nil {
log.Errorx("unmarshal parsed message, continuing", err, mlog.Field("parse", ""))
log.Errorx("unmarshal parsed message, continuing", err, slog.String("parse", ""))
} else {
part = &p
}
@ -1328,19 +1327,19 @@ func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFi
for _, addr := range addrs {
if addr.User == "" {
// Would trigger error because Recipient.Localpart must be nonzero. todo: we could allow empty localpart in db, and filter by not using FilterNonzero.
log.Info("to/cc/bcc address with empty localpart, not inserting as recipient", mlog.Field("address", addr))
log.Info("to/cc/bcc address with empty localpart, not inserting as recipient", slog.Any("address", addr))
continue
}
d, err := dns.ParseDomain(addr.Host)
if err != nil {
log.Debugx("parsing domain in to/cc/bcc address", err, mlog.Field("address", addr))
log.Debugx("parsing domain in to/cc/bcc address", err, slog.Any("address", addr))
continue
}
mr := Recipient{
MessageID: m.ID,
Localpart: smtp.Localpart(addr.User),
Domain: d.Name(),
OrgDomain: publicsuffix.Lookup(context.TODO(), d).Name(),
OrgDomain: publicsuffix.Lookup(context.TODO(), log.Logger, d).Name(),
Sent: sent,
}
if err := tx.Insert(&mr); err != nil {
@ -1365,9 +1364,9 @@ func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFi
}
if sync {
if err := moxio.SyncDir(msgDir); err != nil {
if err := moxio.SyncDir(log, msgDir); err != nil {
xerr := os.Remove(msgPath)
log.Check(xerr, "removing message after syncdir error", mlog.Field("path", msgPath))
log.Check(xerr, "removing message after syncdir error", slog.String("path", msgPath))
return fmt.Errorf("sync directory: %w", err)
}
}
@ -1376,7 +1375,7 @@ func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFi
l := []Message{*m}
if err := a.RetrainMessages(context.TODO(), log, tx, l, false); err != nil {
xerr := os.Remove(msgPath)
log.Check(xerr, "removing message after syncdir error", mlog.Field("path", msgPath))
log.Check(xerr, "removing message after syncdir error", slog.String("path", msgPath))
return fmt.Errorf("training junkfilter: %w", err)
}
*m = l[0]
@ -1387,7 +1386,7 @@ func (a *Account) DeliverMessage(log *mlog.Log, tx *bstore.Tx, m *Message, msgFi
// SetPassword saves a new password for this account. This password is used for
// IMAP, SMTP (submission) sessions and the HTTP account web page.
func (a *Account) SetPassword(password string) error {
func (a *Account) SetPassword(log mlog.Log, password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("generating password hash: %w", err)
@ -1439,7 +1438,7 @@ func (a *Account) SetPassword(password string) error {
return nil
})
if err == nil {
xlog.Info("new password set for account", mlog.Field("account", a.Name))
log.Info("new password set for account", slog.String("account", a.Name))
}
return err
}
@ -1590,21 +1589,21 @@ func (a *Account) SubscriptionEnsure(tx *bstore.Tx, name string) ([]Change, erro
// MessageRuleset returns the first ruleset (if any) that message the message
// represented by msgPrefix and msgFile, with smtp and validation fields from m.
func MessageRuleset(log *mlog.Log, dest config.Destination, m *Message, msgPrefix []byte, msgFile *os.File) *config.Ruleset {
func MessageRuleset(log mlog.Log, dest config.Destination, m *Message, msgPrefix []byte, msgFile *os.File) *config.Ruleset {
if len(dest.Rulesets) == 0 {
return nil
}
mr := FileMsgReader(msgPrefix, msgFile) // We don't close, it would close the msgFile.
p, err := message.Parse(log, false, mr)
p, err := message.Parse(log.Logger, false, mr)
if err != nil {
log.Errorx("parsing message for evaluating rulesets, continuing with headers", err, mlog.Field("parse", ""))
log.Errorx("parsing message for evaluating rulesets, continuing with headers", err, slog.String("parse", ""))
// note: part is still set.
}
// todo optimize: only parse header if needed for rulesets. and probably reuse an earlier parsing.
header, err := p.Header()
if err != nil {
log.Errorx("parsing message headers for evaluating rulesets, delivering to default mailbox", err, mlog.Field("parse", ""))
log.Errorx("parsing message headers for evaluating rulesets, delivering to default mailbox", err, slog.String("parse", ""))
// todo: reject message?
return nil
}
@ -1678,7 +1677,7 @@ func (a *Account) MessageReader(m Message) *MsgReader {
// Caller must hold account wlock (mailbox may be created).
// Message delivery, possible mailbox creation, and updated mailbox counts are
// broadcasted.
func (a *Account) DeliverDestination(log *mlog.Log, dest config.Destination, m *Message, msgFile *os.File) error {
func (a *Account) DeliverDestination(log mlog.Log, dest config.Destination, m *Message, msgFile *os.File) error {
var mailbox string
rs := MessageRuleset(log, dest, m, m.MsgPrefix, msgFile)
if rs != nil {
@ -1696,7 +1695,7 @@ func (a *Account) DeliverDestination(log *mlog.Log, dest config.Destination, m *
// Caller must hold account wlock (mailbox may be created).
// Message delivery, possible mailbox creation, and updated mailbox counts are
// broadcasted.
func (a *Account) DeliverMailbox(log *mlog.Log, mailbox string, m *Message, msgFile *os.File) error {
func (a *Account) DeliverMailbox(log mlog.Log, mailbox string, m *Message, msgFile *os.File) error {
var changes []Change
err := a.DB.Write(context.TODO(), func(tx *bstore.Tx) error {
mb, chl, err := a.MailboxEnsure(tx, mailbox, true)
@ -1734,7 +1733,7 @@ func (a *Account) DeliverMailbox(log *mlog.Log, mailbox string, m *Message, msgF
//
// Caller most hold account wlock.
// Changes are broadcasted.
func (a *Account) TidyRejectsMailbox(log *mlog.Log, rejectsMailbox string) (hasSpace bool, rerr error) {
func (a *Account) TidyRejectsMailbox(log mlog.Log, rejectsMailbox string) (hasSpace bool, rerr error) {
var changes []Change
var remove []Message
@ -1742,7 +1741,7 @@ func (a *Account) TidyRejectsMailbox(log *mlog.Log, rejectsMailbox string) (hasS
for _, m := range remove {
p := a.MessagePath(m.ID)
err := os.Remove(p)
log.Check(err, "removing rejects message file", mlog.Field("path", p))
log.Check(err, "removing rejects message file", slog.String("path", p))
}
}()
@ -1796,7 +1795,7 @@ func (a *Account) TidyRejectsMailbox(log *mlog.Log, rejectsMailbox string) (hasS
return hasSpace, nil
}
func (a *Account) rejectsRemoveMessages(ctx context.Context, log *mlog.Log, tx *bstore.Tx, mb *Mailbox, l []Message) ([]Change, error) {
func (a *Account) rejectsRemoveMessages(ctx context.Context, log mlog.Log, tx *bstore.Tx, mb *Mailbox, l []Message) ([]Change, error) {
if len(l) == 0 {
return nil, nil
}
@ -1858,7 +1857,7 @@ func (a *Account) rejectsRemoveMessages(ctx context.Context, log *mlog.Log, tx *
// RejectsRemove removes a message from the rejects mailbox if present.
// Caller most hold account wlock.
// Changes are broadcasted.
func (a *Account) RejectsRemove(log *mlog.Log, rejectsMailbox, messageID string) error {
func (a *Account) RejectsRemove(log mlog.Log, rejectsMailbox, messageID string) error {
var changes []Change
var remove []Message
@ -1866,7 +1865,7 @@ func (a *Account) RejectsRemove(log *mlog.Log, rejectsMailbox, messageID string)
for _, m := range remove {
p := a.MessagePath(m.ID)
err := os.Remove(p)
log.Check(err, "removing rejects message file", mlog.Field("path", p))
log.Check(err, "removing rejects message file", slog.String("path", p))
}
}()
@ -1933,8 +1932,8 @@ func manageAuthCache() {
// OpenEmailAuth opens an account given an email address and password.
//
// The email address may contain a catchall separator.
func OpenEmailAuth(email string, password string) (acc *Account, rerr error) {
acc, _, rerr = OpenEmail(email)
func OpenEmailAuth(log mlog.Log, email string, password string) (acc *Account, rerr error) {
acc, _, rerr = OpenEmail(log, email)
if rerr != nil {
return
}
@ -1942,7 +1941,7 @@ func OpenEmailAuth(email string, password string) (acc *Account, rerr error) {
defer func() {
if rerr != nil && acc != nil {
err := acc.Close()
xlog.Check(err, "closing account after open auth failure")
log.Check(err, "closing account after open auth failure")
acc = nil
}
}()
@ -1973,7 +1972,7 @@ func OpenEmailAuth(email string, password string) (acc *Account, rerr error) {
// OpenEmail opens an account given an email address.
//
// The email address may contain a catchall separator.
func OpenEmail(email string) (*Account, config.Destination, error) {
func OpenEmail(log mlog.Log, email string) (*Account, config.Destination, error) {
addr, err := smtp.ParseAddress(email)
if err != nil {
return nil, config.Destination{}, fmt.Errorf("%w: %v", ErrUnknownCredentials, err)
@ -1984,7 +1983,7 @@ func OpenEmail(email string) (*Account, config.Destination, error) {
} else if err != nil {
return nil, config.Destination{}, fmt.Errorf("looking up address: %v", err)
}
acc, err := OpenAccount(accountName)
acc, err := OpenAccount(log, accountName)
if err != nil {
return nil, config.Destination{}, err
}
@ -2383,7 +2382,7 @@ func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc Mailbox, dst string) (chang
// indicates that and an error is returned.
//
// Caller should broadcast the changes and remove files for the removed message IDs.
func (a *Account) MailboxDelete(ctx context.Context, log *mlog.Log, tx *bstore.Tx, mailbox Mailbox) (changes []Change, removeMessageIDs []int64, hasChildren bool, rerr error) {
func (a *Account) MailboxDelete(ctx context.Context, log mlog.Log, tx *bstore.Tx, mailbox Mailbox) (changes []Change, removeMessageIDs []int64, hasChildren bool, rerr error) {
// Look for existence of child mailboxes. There is a lot of text in the IMAP RFCs about
// NoInferior and NoSelect. We just require only leaf mailboxes are deleted.
qmb := bstore.QueryTx[Mailbox](tx)

View File

@ -19,6 +19,7 @@ import (
)
var ctxbg = context.Background()
var pkglog = mlog.New("store", nil)
func tcheck(t *testing.T, err error, msg string) {
t.Helper()
@ -28,10 +29,11 @@ func tcheck(t *testing.T, err error, msg string) {
}
func TestMailbox(t *testing.T) {
log := mlog.New("store", nil)
os.RemoveAll("../testdata/store/data")
mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
mox.MustLoadConfig(true, false)
acc, err := OpenAccount("mjl")
acc, err := OpenAccount(log, "mjl")
tcheck(t, err, "open account")
defer func() {
err = acc.Close()
@ -39,9 +41,7 @@ func TestMailbox(t *testing.T) {
}()
defer Switchboard()()
log := mlog.New("store")
msgFile, err := CreateMessageTemp("account-test")
msgFile, err := CreateMessageTemp(log, "account-test")
if err != nil {
t.Fatalf("creating temp msg file: %s", err)
}
@ -72,7 +72,7 @@ func TestMailbox(t *testing.T) {
}
acc.WithWLock(func() {
conf, _ := acc.Conf()
err := acc.DeliverDestination(xlog, conf.Destinations["mjl"], &m, msgFile)
err := acc.DeliverDestination(log, conf.Destinations["mjl"], &m, msgFile)
tcheck(t, err, "deliver without consume")
err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
@ -81,7 +81,7 @@ func TestMailbox(t *testing.T) {
tcheck(t, err, "sent mailbox")
msent.MailboxID = mbsent.ID
msent.MailboxOrigID = mbsent.ID
err = acc.DeliverMessage(xlog, tx, &msent, msgFile, true, false, false)
err = acc.DeliverMessage(pkglog, tx, &msent, msgFile, true, false, false)
tcheck(t, err, "deliver message")
if !msent.ThreadMuted || !msent.ThreadCollapsed {
t.Fatalf("thread muted & collapsed should have been copied from parent (duplicate message-id) m")
@ -97,7 +97,7 @@ func TestMailbox(t *testing.T) {
tcheck(t, err, "insert rejects mailbox")
mreject.MailboxID = mbrejects.ID
mreject.MailboxOrigID = mbrejects.ID
err = acc.DeliverMessage(xlog, tx, &mreject, msgFile, true, false, false)
err = acc.DeliverMessage(pkglog, tx, &mreject, msgFile, true, false, false)
tcheck(t, err, "deliver message")
err = tx.Get(&mbrejects)
@ -110,7 +110,7 @@ func TestMailbox(t *testing.T) {
})
tcheck(t, err, "deliver as sent and rejects")
err = acc.DeliverDestination(xlog, conf.Destinations["mjl"], &mconsumed, msgFile)
err = acc.DeliverDestination(pkglog, conf.Destinations["mjl"], &mconsumed, msgFile)
tcheck(t, err, "deliver with consume")
err = acc.DB.Write(ctxbg, func(tx *bstore.Tx) error {
@ -141,7 +141,7 @@ func TestMailbox(t *testing.T) {
})
tcheck(t, err, "untraining non-junk")
err = acc.SetPassword("testtest")
err = acc.SetPassword(log, "testtest")
tcheck(t, err, "set password")
key0, err := acc.Subjectpass("test@localhost")
@ -223,37 +223,37 @@ func TestMailbox(t *testing.T) {
// Run the auth tests twice for possible cache effects.
for i := 0; i < 2; i++ {
_, err := OpenEmailAuth("mjl@mox.example", "bogus")
_, err := OpenEmailAuth(log, "mjl@mox.example", "bogus")
if err != ErrUnknownCredentials {
t.Fatalf("got %v, expected ErrUnknownCredentials", err)
}
}
for i := 0; i < 2; i++ {
acc2, err := OpenEmailAuth("mjl@mox.example", "testtest")
acc2, err := OpenEmailAuth(log, "mjl@mox.example", "testtest")
tcheck(t, err, "open for email with auth")
err = acc2.Close()
tcheck(t, err, "close account")
}
acc2, err := OpenEmailAuth("other@mox.example", "testtest")
acc2, err := OpenEmailAuth(log, "other@mox.example", "testtest")
tcheck(t, err, "open for email with auth")
err = acc2.Close()
tcheck(t, err, "close account")
_, err = OpenEmailAuth("bogus@mox.example", "testtest")
_, err = OpenEmailAuth(log, "bogus@mox.example", "testtest")
if err != ErrUnknownCredentials {
t.Fatalf("got %v, expected ErrUnknownCredentials", err)
}
_, err = OpenEmailAuth("mjl@test.example", "testtest")
_, err = OpenEmailAuth(log, "mjl@test.example", "testtest")
if err != ErrUnknownCredentials {
t.Fatalf("got %v, expected ErrUnknownCredentials", err)
}
}
func TestMessageRuleset(t *testing.T) {
f, err := CreateMessageTemp("msgruleset")
f, err := CreateMessageTemp(pkglog, "msgruleset")
tcheck(t, err, "creating temp msg file")
defer os.Remove(f.Name())
defer f.Close()
@ -284,7 +284,7 @@ Rulesets:
}
dest.Rulesets[0].HeadersRegexpCompiled = hdrs
c := MessageRuleset(xlog, dest, &Message{}, msgBuf, f)
c := MessageRuleset(pkglog, dest, &Message{}, msgBuf, f)
if c == nil {
t.Fatalf("expected ruleset match")
}
@ -293,7 +293,7 @@ Rulesets:
test
`, "\n", "\r\n"))
c = MessageRuleset(xlog, dest, &Message{}, msg2Buf, f)
c = MessageRuleset(pkglog, dest, &Message{}, msg2Buf, f)
if c != nil {
t.Fatalf("expected no ruleset match")
}

View File

@ -3,15 +3,17 @@ package store
import (
"os"
"golang.org/x/exp/slog"
"github.com/mjl-/mox/mlog"
)
// CloseRemoveTempFile closes and removes f, a file described by descr. Often
// used in a defer after creating a temporary file.
func CloseRemoveTempFile(log *mlog.Log, f *os.File, descr string) {
func CloseRemoveTempFile(log mlog.Log, f *os.File, descr string) {
name := f.Name()
err := f.Close()
log.Check(err, "closing temporary file", mlog.Field("kind", descr))
log.Check(err, "closing temporary file", slog.String("kind", descr))
err = os.Remove(name)
log.Check(err, "removing temporary file", mlog.Field("kind", descr))
log.Check(err, "removing temporary file", slog.String("kind", descr))
}

View File

@ -14,6 +14,8 @@ import (
"strings"
"time"
"golang.org/x/exp/slog"
"github.com/mjl-/bstore"
"github.com/mjl-/mox/mlog"
@ -106,7 +108,7 @@ func (a DirArchiver) Close() error {
// Some errors are not fatal and result in skipped messages. In that happens, a
// file "errors.txt" is added to the archive describing the errors. The goal is to
// let users export (hopefully) most messages even in the face of errors.
func ExportMessages(ctx context.Context, log *mlog.Log, db *bstore.DB, accountDir string, archiver Archiver, maildir bool, mailboxOpt string) error {
func ExportMessages(ctx context.Context, log mlog.Log, db *bstore.DB, accountDir string, archiver Archiver, maildir bool, mailboxOpt string) error {
// todo optimize: should prepare next file to add to archive (can be an mbox with many messages) while writing a file to the archive (which typically compresses, which takes time).
// Start transaction without closure, we are going to close it early, but don't
@ -291,7 +293,7 @@ func ExportMessages(ctx context.Context, log *mlog.Log, db *bstore.DB, accountDi
err = mboxtmp.Close()
log.Check(err, "closing temporary mbox file")
err = os.Remove(name)
log.Check(err, "removing temporary mbox file", mlog.Field("path", name))
log.Check(err, "removing temporary mbox file", slog.String("path", name))
mboxwriter = nil
mboxtmp = nil
return nil

View File

@ -22,14 +22,14 @@ func TestExport(t *testing.T) {
os.RemoveAll("../testdata/store/data")
mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
mox.MustLoadConfig(true, false)
acc, err := OpenAccount("mjl")
acc, err := OpenAccount(pkglog, "mjl")
tcheck(t, err, "open account")
defer acc.Close()
defer Switchboard()()
log := mlog.New("export")
log := mlog.New("export", nil)
msgFile, err := CreateMessageTemp("mox-test-export")
msgFile, err := CreateMessageTemp(pkglog, "mox-test-export")
tcheck(t, err, "create temp")
defer os.Remove(msgFile.Name()) // To be sure.
defer msgFile.Close()
@ -38,11 +38,11 @@ func TestExport(t *testing.T) {
tcheck(t, err, "write message")
m := Message{Received: time.Now(), Size: int64(len(msg))}
err = acc.DeliverMailbox(xlog, "Inbox", &m, msgFile)
err = acc.DeliverMailbox(pkglog, "Inbox", &m, msgFile)
tcheck(t, err, "deliver")
m = Message{Received: time.Now(), Size: int64(len(msg))}
err = acc.DeliverMailbox(xlog, "Trash", &m, msgFile)
err = acc.DeliverMailbox(pkglog, "Trash", &m, msgFile)
tcheck(t, err, "deliver")
var maildirZip, maildirTar, mboxZip, mboxTar bytes.Buffer

View File

@ -13,6 +13,7 @@ import (
"time"
"golang.org/x/exp/maps"
"golang.org/x/exp/slog"
"github.com/mjl-/mox/mlog"
)
@ -25,25 +26,25 @@ type MsgSource interface {
// MboxReader reads messages from an mbox file, implementing MsgSource.
type MboxReader struct {
createTemp func(pattern string) (*os.File, error)
log mlog.Log
createTemp func(log mlog.Log, pattern string) (*os.File, error)
path string
line int
r *bufio.Reader
prevempty bool
nonfirst bool
log *mlog.Log
eof bool
fromLine string // "From "-line for this message.
header bool // Now in header section.
}
func NewMboxReader(createTemp func(pattern string) (*os.File, error), filename string, r io.Reader, log *mlog.Log) *MboxReader {
func NewMboxReader(log mlog.Log, createTemp func(log mlog.Log, pattern string) (*os.File, error), filename string, r io.Reader) *MboxReader {
return &MboxReader{
log: log,
createTemp: createTemp,
path: filename,
line: 1,
r: bufio.NewReader(r),
log: log,
}
}
@ -78,7 +79,7 @@ func (mr *MboxReader) Next() (*Message, *os.File, string, error) {
mr.fromLine = strings.TrimSpace(string(line))
}
f, err := mr.createTemp("mboxreader")
f, err := mr.createTemp(mr.log, "mboxreader")
if err != nil {
return nil, nil, mr.Position(), err
}
@ -202,22 +203,22 @@ func (mr *MboxReader) Next() (*Message, *os.File, string, error) {
}
type MaildirReader struct {
createTemp func(pattern string) (*os.File, error)
log mlog.Log
createTemp func(log mlog.Log, pattern string) (*os.File, error)
newf, curf *os.File
f *os.File // File we are currently reading from. We first read newf, then curf.
dir string // Name of directory for f. Can be empty on first call.
entries []os.DirEntry
dovecotFlags []string // Lower-case flags/keywords.
log *mlog.Log
}
func NewMaildirReader(createTemp func(pattern string) (*os.File, error), newf, curf *os.File, log *mlog.Log) *MaildirReader {
func NewMaildirReader(log mlog.Log, createTemp func(log mlog.Log, pattern string) (*os.File, error), newf, curf *os.File) *MaildirReader {
mr := &MaildirReader{
log: log,
createTemp: createTemp,
newf: newf,
curf: curf,
f: newf,
log: log,
}
// Best-effort parsing of dovecot keywords.
@ -263,7 +264,7 @@ func (mr *MaildirReader) Next() (*Message, *os.File, string, error) {
err := sf.Close()
mr.log.Check(err, "closing message file after error")
}()
f, err := mr.createTemp("maildirreader")
f, err := mr.createTemp(mr.log, "maildirreader")
if err != nil {
return nil, nil, p, err
}
@ -273,7 +274,7 @@ func (mr *MaildirReader) Next() (*Message, *os.File, string, error) {
err := f.Close()
mr.log.Check(err, "closing temporary message file after maildir read error")
err = os.Remove(name)
mr.log.Check(err, "removing temporary message file after maildir read error", mlog.Field("path", name))
mr.log.Check(err, "removing temporary message file after maildir read error", slog.String("path", name))
}
}()
@ -370,7 +371,7 @@ func (mr *MaildirReader) Next() (*Message, *os.File, string, error) {
// returns valid flags/keywords, as lower-case. If an error is encountered and
// returned, any keywords that were found are still returned. The returned list has
// both system/well-known flags and custom keywords.
func ParseDovecotKeywordsFlags(r io.Reader, log *mlog.Log) ([]string, error) {
func ParseDovecotKeywordsFlags(r io.Reader, log mlog.Log) ([]string, error) {
/*
If the dovecot-keywords file is present, we parse its additional flags, see
https://doc.dovecot.org/admin_manual/mailbox_formats/maildir/

View File

@ -10,7 +10,7 @@ import (
)
func TestMboxReader(t *testing.T) {
createTemp := func(pattern string) (*os.File, error) {
createTemp := func(log mlog.Log, pattern string) (*os.File, error) {
return os.CreateTemp("", pattern)
}
mboxf, err := os.Open("../testdata/importtest.mbox")
@ -19,7 +19,8 @@ func TestMboxReader(t *testing.T) {
}
defer mboxf.Close()
mr := NewMboxReader(createTemp, mboxf.Name(), mboxf, mlog.New("mboxreader"))
log := mlog.New("mboxreader", nil)
mr := NewMboxReader(log, createTemp, mboxf.Name(), mboxf)
_, mf0, _, err := mr.Next()
if err != nil {
t.Fatalf("next mbox message: %v", err)
@ -41,7 +42,7 @@ func TestMboxReader(t *testing.T) {
}
func TestMaildirReader(t *testing.T) {
createTemp := func(pattern string) (*os.File, error) {
createTemp := func(log mlog.Log, pattern string) (*os.File, error) {
return os.CreateTemp("", pattern)
}
// todo: rename 1642966915.1.mox to "1642966915.1.mox:2,"? cannot have that name in the git repo because go module (or the proxy) doesn't like it. could also add some flags and test they survive the import.
@ -57,7 +58,8 @@ func TestMaildirReader(t *testing.T) {
}
defer curf.Close()
mr := NewMaildirReader(createTemp, newf, curf, mlog.New("maildirreader"))
log := mlog.New("maildirreader", nil)
mr := NewMaildirReader(log, createTemp, newf, curf)
_, mf0, _, err := mr.Next()
if err != nil {
t.Fatalf("next maildir message: %v", err)
@ -85,7 +87,7 @@ func TestParseDovecotKeywords(t *testing.T) {
3 $Forwarded
4 $Junk
`
flags, err := ParseDovecotKeywordsFlags(strings.NewReader(data), mlog.New("dovecotkeywords"))
flags, err := ParseDovecotKeywordsFlags(strings.NewReader(data), mlog.New("dovecotkeywords", nil))
if err != nil {
t.Fatalf("parsing dovecot-keywords: %v", err)
}

View File

@ -57,7 +57,7 @@ func PrepareWordSearch(words, notWords []string) WordSearch {
// The search terms are matched against content-transfer-decoded and
// charset-decoded bodies and optionally headers.
// HTML parts are currently treated as regular text, without parsing HTML.
func (ws WordSearch) MatchPart(log *mlog.Log, p *message.Part, headerToo bool) (bool, error) {
func (ws WordSearch) MatchPart(log mlog.Log, p *message.Part, headerToo bool) (bool, error) {
seen := map[int]bool{}
miss, err := ws.matchPart(log, p, headerToo, seen)
match := err == nil && !miss && len(seen) == len(ws.words)
@ -73,7 +73,7 @@ func (ws WordSearch) isQuickHit(seen map[int]bool) bool {
// search a part as text and/or its subparts, recursively. Once we know we have
// a miss, we stop (either due to not-word match or error). In case of
// non-miss, the caller checks if there was a hit.
func (ws WordSearch) matchPart(log *mlog.Log, p *message.Part, headerToo bool, seen map[int]bool) (miss bool, rerr error) {
func (ws WordSearch) matchPart(log mlog.Log, p *message.Part, headerToo bool, seen map[int]bool) (miss bool, rerr error) {
if headerToo {
miss, err := ws.searchReader(log, p.HeaderReader(), seen)
if miss || err != nil || ws.isQuickHit(seen) {
@ -108,7 +108,7 @@ func (ws WordSearch) matchPart(log *mlog.Log, p *message.Part, headerToo bool, s
return false, nil
}
func (ws WordSearch) searchReader(log *mlog.Log, r io.Reader, seen map[int]bool) (miss bool, rerr error) {
func (ws WordSearch) searchReader(log mlog.Log, r io.Reader, seen map[int]bool) (miss bool, rerr error) {
// We will be reading through the content, stopping as soon as we known an answer:
// when all words have been seen and there are no "not words" (true), or one "not
// word" has been seen (false). We use bytes.Contains to look for the words. We

View File

@ -11,6 +11,7 @@ import (
"time"
"golang.org/x/exp/slices"
"golang.org/x/exp/slog"
"github.com/mjl-/bstore"
@ -26,7 +27,7 @@ import (
// may have a threadid 0. That results in this message getting threadid 0, which
// will handled by the background upgrade process assigning a threadid when it gets
// to this message.
func assignThread(log *mlog.Log, tx *bstore.Tx, m *Message, part *message.Part) error {
func assignThread(log mlog.Log, tx *bstore.Tx, m *Message, part *message.Part) error {
if m.MessageID != "" {
// Match against existing different message with same Message-ID.
q := bstore.QueryTx[Message](tx)
@ -47,11 +48,11 @@ func assignThread(log *mlog.Log, tx *bstore.Tx, m *Message, part *message.Part)
h, err := part.Header()
if err != nil {
log.Errorx("assigning threads: parsing references/in-reply-to headers, not matching by message-id", err, mlog.Field("msgid", m.ID))
log.Errorx("assigning threads: parsing references/in-reply-to headers, not matching by message-id", err, slog.Int64("msgid", m.ID))
}
messageIDs, err := message.ReferencedIDs(h.Values("References"), h.Values("In-Reply-To"))
if err != nil {
log.Errorx("assigning threads: parsing references/in-reply-to headers, not matching by message-id", err, mlog.Field("msgid", m.ID))
log.Errorx("assigning threads: parsing references/in-reply-to headers, not matching by message-id", err, slog.Int64("msgid", m.ID))
}
for i := len(messageIDs) - 1; i >= 0; i-- {
messageID := messageIDs[i]
@ -123,7 +124,7 @@ func assignParent(m *Message, pm Message, updateSeen bool) {
//
// ModSeq is not changed. Calles should bump the uid validity of the mailboxes
// to propagate the changes to IMAP clients.
func (a *Account) ResetThreading(ctx context.Context, log *mlog.Log, batchSize int, clearIDs bool) (int, error) {
func (a *Account) ResetThreading(ctx context.Context, log mlog.Log, batchSize int, clearIDs bool) (int, error) {
// todo: should this send Change events for ThreadMuted and ThreadCollapsed? worth it?
var lastID int64
@ -147,13 +148,13 @@ func (a *Account) ResetThreading(ctx context.Context, log *mlog.Log, batchSize i
Envelope *message.Envelope
}
if err := json.Unmarshal(m.ParsedBuf, &part); err != nil {
log.Errorx("unmarshal json parsedbuf for setting message-id, skipping", err, mlog.Field("msgid", m.ID))
log.Errorx("unmarshal json parsedbuf for setting message-id, skipping", err, slog.Int64("msgid", m.ID))
} else {
m.MessageID = ""
if part.Envelope != nil && part.Envelope.MessageID != "" {
s, _, err := message.MessageIDCanonical(part.Envelope.MessageID)
if err != nil {
log.Debugx("parsing message-id, skipping", err, mlog.Field("msgid", m.ID), mlog.Field("messageid", part.Envelope.MessageID))
log.Debugx("parsing message-id, skipping", err, slog.Int64("msgid", m.ID), slog.String("messageid", part.Envelope.MessageID))
}
m.MessageID = s
}
@ -231,7 +232,7 @@ func (a *Account) ResetThreading(ctx context.Context, log *mlog.Log, batchSize i
// Does not set Seen flag for muted threads.
//
// Progress is written to progressWriter, every 100k messages.
func (a *Account) AssignThreads(ctx context.Context, log *mlog.Log, txOpt *bstore.Tx, startMessageID int64, batchSize int, progressWriter io.Writer) error {
func (a *Account) AssignThreads(ctx context.Context, log mlog.Log, txOpt *bstore.Tx, startMessageID int64, batchSize int, progressWriter io.Writer) error {
// We use a more basic version of the thread-matching algorithm describe in:
// ../rfc/5256:443
// The algorithm assumes you'll select messages, then group into threads. We normally do
@ -294,7 +295,7 @@ func (a *Account) AssignThreads(ctx context.Context, log *mlog.Log, txOpt *bstor
refids, err := message.ReferencedIDs(references, inReplyTo)
if err != nil {
log.Errorx("assigning threads: parsing references/in-reply-to headers, not matching by message-id", err, mlog.Field("msgid", m.ID))
log.Errorx("assigning threads: parsing references/in-reply-to headers, not matching by message-id", err, slog.Int64("msgid", m.ID))
}
for i := len(refids) - 1; i >= 0; i-- {
@ -527,7 +528,7 @@ func (a *Account) AssignThreads(ctx context.Context, log *mlog.Log, txOpt *bstor
}
nassigned += n
if nassigned%100000 == 0 {
log.Debug("assigning threads, progress", mlog.Field("count", nassigned), mlog.Field("unresolved", len(pending)))
log.Debug("assigning threads, progress", slog.Int("count", nassigned), slog.Int("unresolved", len(pending)))
if _, err := fmt.Fprintf(progressWriter, "assigning threads, progress: %d messages\n", nassigned); err != nil {
return fmt.Errorf("writing progress: %v", err)
}
@ -537,7 +538,7 @@ func (a *Account) AssignThreads(ctx context.Context, log *mlog.Log, txOpt *bstor
return fmt.Errorf("writing progress: %v", err)
}
log.Debug("assigning threads, mostly done, finishing with resolving of cyclic messages", mlog.Field("count", nassigned), mlog.Field("unresolved", len(pending)))
log.Debug("assigning threads, mostly done, finishing with resolving of cyclic messages", slog.Int("count", nassigned), slog.Int("unresolved", len(pending)))
if _, err := fmt.Fprintf(progressWriter, "assigning threads, resolving %d cyclic pending message-ids\n", len(pending)); err != nil {
return fmt.Errorf("writing progress: %v", err)
@ -723,8 +724,8 @@ func lookupThreadMessageSubject(tx *bstore.Tx, m Message, subjectBase string) (*
return &tm, nil
}
func upgradeThreads(ctx context.Context, acc *Account, up *Upgrade) error {
log := xlog.Fields(mlog.Field("account", acc.Name))
func upgradeThreads(ctx context.Context, log mlog.Log, acc *Account, up *Upgrade) error {
log = log.With(slog.String("account", acc.Name))
if up.Threads == 0 {
// Step 1 in the threads upgrade is storing the canonicalized Message-ID for each
@ -745,7 +746,7 @@ func upgradeThreads(ctx context.Context, acc *Account, up *Upgrade) error {
up.Threads = 0
return fmt.Errorf("saving upgrade process while upgrading account to threads storage, step 1/2: %w", err)
}
log.Info("upgrading account for threading, step 1/2: completed", mlog.Field("duration", time.Since(t0)), mlog.Field("messages", total))
log.Info("upgrading account for threading, step 1/2: completed", slog.Duration("duration", time.Since(t0)), slog.Int("messages", total))
}
if up.Threads == 1 {
@ -765,7 +766,7 @@ func upgradeThreads(ctx context.Context, acc *Account, up *Upgrade) error {
up.Threads = 1
return fmt.Errorf("saving upgrade process for thread storage, step 2/2: %w", err)
}
log.Info("upgrading account for threading, step 2/2: completed", mlog.Field("duration", time.Since(t0)))
log.Info("upgrading account for threading, step 2/2: completed", slog.Duration("duration", time.Since(t0)))
}
// Note: Not bumping uidvalidity or setting modseq. Clients haven't been able to

View File

@ -15,10 +15,11 @@ import (
)
func TestThreadingUpgrade(t *testing.T) {
log := mlog.New("store", nil)
os.RemoveAll("../testdata/store/data")
mox.ConfigStaticPath = filepath.FromSlash("../testdata/store/mox.conf")
mox.MustLoadConfig(true, false)
acc, err := OpenAccount("mjl")
acc, err := OpenAccount(log, "mjl")
tcheck(t, err, "open account")
defer func() {
err = acc.Close()
@ -26,12 +27,10 @@ func TestThreadingUpgrade(t *testing.T) {
}()
defer Switchboard()()
log := mlog.New("store")
// New account already has threading. Add some messages, check the threading.
deliver := func(recv time.Time, s string, expThreadID int64) Message {
t.Helper()
f, err := CreateMessageTemp("account-test")
f, err := CreateMessageTemp(log, "account-test")
tcheck(t, err, "temp file")
defer os.Remove(f.Name())
defer f.Close()
@ -141,7 +140,7 @@ func TestThreadingUpgrade(t *testing.T) {
tcheck(t, err, "closing db")
// Open the account again, that should get the account upgraded. Wait for upgrade to finish.
acc, err = OpenAccount("mjl")
acc, err = OpenAccount(log, "mjl")
tcheck(t, err, "open account")
err = acc.ThreadingWait(log)
tcheck(t, err, "wait for threading")

View File

@ -3,6 +3,7 @@ package store
import (
"os"
"github.com/mjl-/mox/mlog"
"github.com/mjl-/mox/mox-"
)
@ -12,7 +13,7 @@ import (
// responsible for closing and possibly removing the file. The caller should ensure
// the contents of the file are synced to disk before attempting to deliver the
// message.
func CreateMessageTemp(pattern string) (*os.File, error) {
func CreateMessageTemp(log mlog.Log, pattern string) (*os.File, error) {
dir := mox.DataDirPath("tmp")
os.MkdirAll(dir, 0770)
f, err := os.CreateTemp(dir, pattern)
@ -22,7 +23,7 @@ func CreateMessageTemp(pattern string) (*os.File, error) {
err = f.Chmod(0660)
if err != nil {
xerr := f.Close()
xlog.Check(xerr, "closing temp message file after chmod error")
log.Check(xerr, "closing temp message file after chmod error")
return nil, err
}
return f, err

View File

@ -7,6 +7,8 @@ import (
"os"
"path/filepath"
"golang.org/x/exp/slog"
"github.com/mjl-/bstore"
"github.com/mjl-/mox/config"
@ -22,7 +24,7 @@ var ErrNoJunkFilter = errors.New("junkfilter: not configured")
// If the account does not have a junk filter enabled, ErrNotConfigured is returned.
// Do not forget to save the filter after modifying, and to always close the filter when done.
// An empty filter is initialized on first access of the filter.
func (a *Account) OpenJunkFilter(ctx context.Context, log *mlog.Log) (*junk.Filter, *config.JunkFilter, error) {
func (a *Account) OpenJunkFilter(ctx context.Context, log mlog.Log) (*junk.Filter, *config.JunkFilter, error) {
conf, ok := mox.Conf.Account(a.Name)
if !ok {
return nil, nil, ErrAccountUnknown
@ -46,7 +48,7 @@ func (a *Account) OpenJunkFilter(ctx context.Context, log *mlog.Log) (*junk.Filt
// RetrainMessages (un)trains messages, if relevant given their flags. Updates
// m.TrainedJunk after retraining.
func (a *Account) RetrainMessages(ctx context.Context, log *mlog.Log, tx *bstore.Tx, msgs []Message, absentOK bool) (rerr error) {
func (a *Account) RetrainMessages(ctx context.Context, log mlog.Log, tx *bstore.Tx, msgs []Message, absentOK bool) (rerr error) {
if len(msgs) == 0 {
return nil
}
@ -86,7 +88,7 @@ func (a *Account) RetrainMessages(ctx context.Context, log *mlog.Log, tx *bstore
// RetrainMessage untrains and/or trains a message, if relevant given m.TrainedJunk
// and m.Junk/m.Notjunk. Updates m.TrainedJunk after retraining.
func (a *Account) RetrainMessage(ctx context.Context, log *mlog.Log, tx *bstore.Tx, jf *junk.Filter, m *Message, absentOK bool) error {
func (a *Account) RetrainMessage(ctx context.Context, log mlog.Log, tx *bstore.Tx, jf *junk.Filter, m *Message, absentOK bool) error {
untrain := m.TrainedJunk != nil
untrainJunk := untrain && *m.TrainedJunk
train := m.Junk || m.Notjunk && !(m.Junk && m.Notjunk)
@ -96,7 +98,7 @@ func (a *Account) RetrainMessage(ctx context.Context, log *mlog.Log, tx *bstore.
return nil
}
log.Debug("updating junk filter", mlog.Field("untrain", untrain), mlog.Field("untrainjunk", untrainJunk), mlog.Field("train", train), mlog.Field("trainjunk", trainJunk))
log.Debug("updating junk filter", slog.Bool("untrain", untrain), slog.Bool("untrainjunk", untrainJunk), slog.Bool("train", train), slog.Bool("trainjunk", trainJunk))
mr := a.MessageReader(*m)
defer func() {
@ -112,7 +114,7 @@ func (a *Account) RetrainMessage(ctx context.Context, log *mlog.Log, tx *bstore.
words, err := jf.ParseMessage(p)
if err != nil {
log.Errorx("parsing message for updating junk filter", err, mlog.Field("parse", ""))
log.Errorx("parsing message for updating junk filter", err, slog.Any("parse", ""))
return nil
}
@ -138,7 +140,7 @@ func (a *Account) RetrainMessage(ctx context.Context, log *mlog.Log, tx *bstore.
// TrainMessage trains the junk filter based on the current m.Junk/m.Notjunk flags,
// disregarding m.TrainedJunk and not updating that field.
func (a *Account) TrainMessage(ctx context.Context, log *mlog.Log, jf *junk.Filter, m Message) (bool, error) {
func (a *Account) TrainMessage(ctx context.Context, log mlog.Log, jf *junk.Filter, m Message) (bool, error) {
if !m.Junk && !m.Notjunk || (m.Junk && m.Notjunk) {
return false, nil
}
@ -157,7 +159,7 @@ func (a *Account) TrainMessage(ctx context.Context, log *mlog.Log, jf *junk.Filt
words, err := jf.ParseMessage(p)
if err != nil {
log.Errorx("parsing message for updating junk filter", err, mlog.Field("parse", ""))
log.Errorx("parsing message for updating junk filter", err, slog.Any("parse", ""))
return false, nil
}