package coding import ( "fmt" "io" "mime" "strings" ) // NewExtMimeDecoder creates new MIME word decoder which allows decoding of additional charsets. func NewExtMimeDecoder() *mime.WordDecoder { return &mime.WordDecoder{ CharsetReader: NewCharsetReader, } } // DecodeExtHeader decodes a single line (per RFC 2047, aka Message Header Extensions) using Golang's // mime.WordDecoder. func DecodeExtHeader(input string) string { if !strings.Contains(input, "=?") { // Don't scan if there is nothing to do here return input } header, err := NewExtMimeDecoder().DecodeHeader(input) if err != nil { return input } return header } // RFC2047Decode returns a decoded string if the input uses RFC2047 encoding, otherwise it will // return the input. // // RFC2047 Example: `=?UTF-8?B?bmFtZT0iw7DCn8KUwoo=?=` func RFC2047Decode(s string) string { // Convert CR/LF to spaces. s = strings.Map(func(r rune) rune { if r == '\n' || r == '\r' { return ' ' } return r }, s) var err error decoded := false for { s, err = rfc2047Recurse(s) switch err { case nil: decoded = true continue default: if decoded { keyValuePair := strings.SplitAfter(s, "=") if len(keyValuePair) < 2 { return s } // Add quotes as needed. if !strings.HasPrefix(keyValuePair[1], "\"") { keyValuePair[1] = fmt.Sprintf("\"%s", keyValuePair[1]) } if !strings.HasSuffix(keyValuePair[1], "\"") { keyValuePair[1] = fmt.Sprintf("%s\"", keyValuePair[1]) } return strings.Join(keyValuePair, "") } return s } } } // rfc2047Recurse is called for if the value contains content encoded in RFC2047 format and decodes // it. func rfc2047Recurse(s string) (string, error) { us := strings.ToUpper(s) if !strings.Contains(us, "?Q?") && !strings.Contains(us, "?B?") { return s, io.EOF } var val string if val = DecodeExtHeader(s); val == s { if val = DecodeExtHeader(fixRFC2047String(val)); val == s { return val, io.EOF } } return val, nil } // fixRFC2047String removes the following characters from charset and encoding segments of an // RFC2047 string: '\n', '\r' and ' ' func fixRFC2047String(s string) string { inString := false isWithinTerminatingEqualSigns := false questionMarkCount := 0 sb := &strings.Builder{} for _, v := range s { switch v { case '=': if questionMarkCount == 3 { inString = false } else { isWithinTerminatingEqualSigns = true } sb.WriteRune(v) case '?': if isWithinTerminatingEqualSigns { inString = true } else { questionMarkCount++ } isWithinTerminatingEqualSigns = false sb.WriteRune(v) case '\n', '\r', ' ': if !inString { sb.WriteRune(v) } isWithinTerminatingEqualSigns = false default: isWithinTerminatingEqualSigns = false sb.WriteRune(v) } } return sb.String() }