package mailx import ( "bufio" "bytes" "errors" "fmt" "io" "net/textproto" "strings" "pkg.jfrech.com/brief/text" ) // Split is a convenience wrapper around NewSplitter. // r must read a syntactically valid RFC 822 message (NOTE: some field body // encodings may not be supported). // Split MUST see a CRLF line after the header. // No byte is lost except the CRLF separating header from body. // // BUG(jfrech): 2024-05-20: Emit a better error than io.ErrUnexpectedEOF when, for example, the empty byte slice is split. func Split(r io.Reader) ([]Field, io.Reader, error) { splitter := NewSplitter(r) var fields []Field for { field, err := splitter.NextField() if errors.Is(err, EOH) { break } else if err != nil { return nil, nil, err } fields = append(fields, field) } return fields, splitter, nil } // SplitHeaderPartial interprets r as an RFC 822 message, reads the entire // header and returns a textproto.MIMEHeader containing the fields whose name // matches one of fieldNames. Only these fields are decoded. func SplitHeaderPartial(r io.Reader, fieldNames ...string) (textproto.MIMEHeader, error) { emit := make(map[string]bool) for _, fn := range fieldNames { emit[textproto.CanonicalMIMEHeaderKey(fn)] = true } h := make(textproto.MIMEHeader) splitter := NewSplitter(r) for { rawfield, err := splitter.NextRawField() if errors.Is(err, EOH) { break } else if err != nil { return nil, err } if !emit[rawfield.CanonicalName()] { continue } field, err := rawfield.Decode() if err != nil { return nil, err } h.Add(field.Name, field.Body) } return h, nil } func JoinFields(fn string, fields []Field) string { b := new(strings.Builder) for _, field := range fields { if !field.Is(fn) { continue } if b.Len() > 0 { b.WriteRune(' ') } b.WriteString(field.Body) } return b.String() } func NewSplitter(r io.Reader) *Splitter { return &Splitter{ br: bufio.NewReader(r), foldbuffer: new(bytes.Buffer), } } var EOH = fmt.Errorf("%w: end of header", io.EOF) type Splitter struct { br *bufio.Reader rawfields []RawField foldbuffer *bytes.Buffer nomorerawfields bool err error } func notEOF(err error) error { if errors.Is(err, io.EOF) { return fmt.Errorf("%w: %v", io.ErrUnexpectedEOF, err) } return err } func (s *Splitter) NextField() (Field, error) { rawfield, err := s.NextRawField() if err != nil { return Field{}, err } return rawfield.Decode() } func (s *Splitter) Read(p []byte) (int, error) { for s.err == nil && !s.nomorerawfields { s.readRawField() } if s.err != nil { return 0, s.err } n, err := s.br.Read(p) s.e(err) return n, err } func (s *Splitter) e(err error) bool { if s.err == nil { s.err = notEOF(err) } return s.err == nil } func (s *Splitter) NextRawField() (RawField, error) { if len(s.rawfields) > 0 { rawfield := s.rawfields[0] s.rawfields = s.rawfields[1:] return rawfield, nil } else if s.nomorerawfields { return nil, EOH } s.readRawField() if s.err != nil { return nil, s.err } return s.NextRawField() } // readField will either add one field to s.fields or do nothing (EOH) or set s.err. func (s *Splitter) readRawField() { if s.err != nil || s.nomorerawfields { return } flush := func() { defer s.foldbuffer.Reset() if s.foldbuffer.Len() == 0 { return } s.rawfields = append(s.rawfields, RawField(bytes.Clone(s.foldbuffer.Bytes()))) } for { line, err := s.br.ReadBytes('\n') if !s.e(err) { return } if len(line) <= 2 { if bytes.Equal(line, []byte(text.CRLF)) { flush() s.nomorerawfields = true return } s.e(fmt.Errorf("%w: %q", ErrNotCRLFTerminated, string(line))) return } if !bytes.HasSuffix(line, []byte(text.CRLF)) { s.e(fmt.Errorf("%w: %q", ErrNotCRLFTerminated, string(line))) return } // the line is not a folded part of a bigger logical line, so flush // the old logical line and start a new one if line[0] != '\t' && line[0] != ' ' { flush() s.foldbuffer.Write(line) return } s.foldbuffer.Write(line) } }