package mailx import ( "fmt" "net/textproto" "bytes" "pkg.jfrech.com/brief/decode" "pkg.jfrech.com/brief/text" ) type RawField []byte func (rawfield RawField) Is(fh string) bool { return textproto.CanonicalMIMEHeaderKey(fix(fh)) == rawfield.CanonicalName() } // XXX this performs no checking if the rawfield is syntactically valid // // As [Field.CanonicalName] does, the field name is also fixed. func (rawfield RawField) CanonicalName() string { k := bytes.IndexByte(rawfield, ':') if k == -1 { return fix("") } return textproto.CanonicalMIMEHeaderKey(fix(string(rawfield[:k]))) } // Field.Name == textproto.CanonicalMIMEHeaderKey(Field.Name) // BUG(jfrech): 2024-06-02: Keep the pristine bytes intact! func (rawfield RawField) Decode() (Field, error) { e := func(format string, a ...any) (Field, error) { return Field{}, fmt.Errorf("%w: " + format, append([]any{ErrMalformedField,}, a...)...) } // RFC 822, 3.2 // field = field-name ":" [ field-body ] CRLF if len(rawfield) <= 0 { return e("no data") } k := bytes.IndexByte([]byte(rawfield), ':') if k == -1 { return e("no colon in sight") } name := []byte(rawfield)[:k] foldedbody := []byte(rawfield)[k+1:] // RFC 822, 3.2 // field-name = 1* // RFC 822, 3.3 // CHAR = ; ( 0-177, 0.-127.) // CTL = ; ( 40, 32.) if len(name) <= 0 { return e("empty field name") } for _, c := range name { if c < ' ' || c == ' ' || c == ':' || c > '~' { return e("field name contains invalid byte 0x%02X", int(c)) } } // [2023-10-23, jfrech] NOTE: The exact case of the field is kept intact. decodedname := string(name) body := new(bytes.Buffer) // RFC 822, 3.2 // field-body = field-body-contents // [CRLF LWSP-char field-body] // field-body-contents = // // RFC 822, 3.2 // CR = ; ( 15, 13.) // LF = ; ( 12, 10.) // SPACE = ; ( 40, 32.) // HTAB = ; ( 11, 9.) // CRLF = CR LF // LWSP-char = SPACE / HTAB ; semantics = SPACE foldedbody = bytes.TrimLeft(foldedbody, "\t ") k = bytes.Index(foldedbody, []byte(text.CRLF)) if k == -1 { return e("no CRLF in sight") } body.Write(foldedbody[:k]) foldedbody = foldedbody[k+len([]byte(text.CRLF)):] for len(foldedbody) > 0 { if foldedbody[0] != '\t' && foldedbody[0] != ' ' { return e("more than one field (fold does not start with HT or SP)") } k := bytes.Index(foldedbody, []byte(text.CRLF)) if k == -1 { return e("no CRLF in sight (folded)") } body.Write(foldedbody[:k]) foldedbody = foldedbody[k+len([]byte(text.CRLF)):] } // TODO self-implement // TODO think about the space trimming decodedbody, err := decode.MailHeader(string(body.Bytes())) if err != nil { return e("field body undecodable: %v", err) } return Field{ Name: decodedname, Body: decodedbody, }, nil }