diff --git a/message/part.go b/message/part.go index 21b3f47..4d158d7 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 9237624..74d609f 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 9a6598c..46276c6 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