From fb65ec0676ab382bcbc42e0b34a2473c228bed60 Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Sat, 5 Oct 2024 09:39:46 +0200 Subject: [PATCH] webmail: fix loading a "view" (messages in a mailbox) when the "initial" message cannot be parsed when we send a list of messages from the mox backend to the js frontend, we include a parsed form of the "initial" message: the one we immediately show, typically the top-most (unread) message. however, if that message could not be parsed (due to invalid header syntax), we would fail the entire operation of loading the view. with this change, we simply don't return a parsed form of an initial message if we cannot parse it. that will cause the webmail frontend to not select & display a message immediately. if you then try to open the message, you'll still get an error message as before. but at least the view has been loaded, and you can open the raw message to inspect the contents. for issue #219 by wneessen --- message/part.go | 9 +++++++++ webmail/message.go | 2 +- webmail/view.go | 23 ++++++++++++++++------- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/message/part.go b/message/part.go index 21b3f474f0..4d158d737a 100644 --- a/message/part.go +++ b/message/part.go @@ -33,6 +33,7 @@ var Pedantic bool var ( ErrBadContentType = errors.New("bad content-type") + ErrHeader = errors.New("bad message header") ) var ( @@ -394,6 +395,8 @@ func newPart(log mlog.Log, strict bool, r io.ReaderAt, offset int64, parent *Par } // Header returns the parsed header of this part. +// +// Returns a ErrHeader for messages with invalid header syntax. func (p *Part) Header() (textproto.MIMEHeader, error) { if p.header != nil { return p.header, nil @@ -431,6 +434,12 @@ func parseHeader(r io.Reader) (textproto.MIMEHeader, error) { } msg, err := mail.ReadMessage(bytes.NewReader(buf)) if err != nil { + // Recognize parsing errors from net/mail.ReadMessage. + // todo: replace with own message parsing code that returns proper error types. + errstr := err.Error() + if strings.HasPrefix(errstr, "malformed initial line:") || strings.HasPrefix(errstr, "malformed header line:") { + err = fmt.Errorf("%w: %v", ErrHeader, err) + } return zero, err } return textproto.MIMEHeader(msg.Header), nil diff --git a/webmail/message.go b/webmail/message.go index 9237624c77..74d609f114 100644 --- a/webmail/message.go +++ b/webmail/message.go @@ -232,7 +232,7 @@ func parsedMessage(log mlog.Log, m store.Message, state *msgState, full, msgitem if full && state.part.BodyOffset > 0 { hdrs, err := state.part.Header() if err != nil { - return ParsedMessage{}, fmt.Errorf("parsing headers: %v", err) + return ParsedMessage{}, fmt.Errorf("parsing headers: %w", err) } pm.Headers = hdrs diff --git a/webmail/view.go b/webmail/view.go index 9a6598c228..46276c644f 100644 --- a/webmail/view.go +++ b/webmail/view.go @@ -1486,16 +1486,19 @@ func queryMessages(ctx context.Context, log mlog.Log, acc *store.Account, tx *bs var pm *ParsedMessage if m.ID == page.DestMessageID || page.DestMessageID == 0 && have == 0 && page.AnchorMessageID == 0 { - // For threads, if there was not DestMessageID, we may be getting the newest + // For threads, if there was no DestMessageID, we may be getting the newest // message. For an initial view, this isn't necessarily the first the user is // expected to read first, that would be the first unread, which we'll get below // when gathering the thread. found = true xpm, err := parsedMessage(log, m, &state, true, false) - if err != nil { + if err != nil && errors.Is(err, message.ErrHeader) { + log.Debug("not returning parsed message due to invalid headers", slog.Int64("msgid", m.ID), slog.Any("err", err)) + } else if err != nil { return fmt.Errorf("parsing message %d: %v", m.ID, err) + } else { + pm = &xpm } - pm = &xpm } mi, err := messageItem(log, m, &state) @@ -1613,10 +1616,13 @@ func gatherThread(log mlog.Log, tx *bstore.Tx, acc *store.Account, v view, m sto if tm.ID == destMessageID || destMessageID == 0 && first && (pm == nil || !firstUnread && !tm.Seen) { firstUnread = !tm.Seen xpm, err := parsedMessage(log, tm, &xstate, true, false) - if err != nil { + if err != nil && errors.Is(err, message.ErrHeader) { + log.Debug("not returning parsed message due to invalid headers", slog.Int64("msgid", m.ID), slog.Any("err", err)) + } else if err != nil { return fmt.Errorf("parsing thread message %d: %v", tm.ID, err) + } else { + pm = &xpm } - pm = &xpm } return nil }() @@ -1631,10 +1637,13 @@ func gatherThread(log mlog.Log, tx *bstore.Tx, acc *store.Account, v view, m sto xstate := msgState{acc: acc} defer xstate.clear() xpm, err := parsedMessage(log, m, &xstate, true, false) - if err != nil { + if err != nil && errors.Is(err, message.ErrHeader) { + log.Debug("not returning parsed message due to invalid headers", slog.Int64("msgid", m.ID), slog.Any("err", err)) + } else if err != nil { return nil, nil, fmt.Errorf("parsing thread message %d: %v", m.ID, err) + } else { + pm = &xpm } - pm = &xpm } return mil, pm, nil