| // Copyright 2015 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package mime |
| |
| import ( |
| "bytes" |
| "encoding/base64" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "strconv" |
| "strings" |
| "unicode" |
| ) |
| |
| // EncodeWord encodes a string into an RFC 2047 encoded-word. |
| func EncodeWord(s string) string { |
| // UTF-8 "Q" encoding |
| b := bytes.NewBufferString("=?utf-8?q?") |
| for i := 0; i < len(s); i++ { |
| switch c := s[i]; { |
| case c == ' ': |
| b.WriteByte('_') |
| case isVchar(c) && c != '=' && c != '?' && c != '_': |
| b.WriteByte(c) |
| default: |
| fmt.Fprintf(b, "=%02X", c) |
| } |
| } |
| b.WriteString("?=") |
| return b.String() |
| } |
| |
| // DecodeWord decodes an RFC 2047 encoded-word. |
| func DecodeWord(s string) (string, error) { |
| fields := strings.Split(s, "?") |
| if len(fields) != 5 || fields[0] != "=" || fields[4] != "=" { |
| return "", errors.New("address not RFC 2047 encoded") |
| } |
| charset, enc := strings.ToLower(fields[1]), strings.ToLower(fields[2]) |
| if charset != "us-ascii" && charset != "iso-8859-1" && charset != "utf-8" { |
| return "", fmt.Errorf("charset not supported: %q", charset) |
| } |
| |
| in := bytes.NewBufferString(fields[3]) |
| var r io.Reader |
| switch enc { |
| case "b": |
| r = base64.NewDecoder(base64.StdEncoding, in) |
| case "q": |
| r = qDecoder{r: in} |
| default: |
| return "", fmt.Errorf("RFC 2047 encoding not supported: %q", enc) |
| } |
| |
| dec, err := ioutil.ReadAll(r) |
| if err != nil { |
| return "", err |
| } |
| |
| switch charset { |
| case "us-ascii": |
| b := new(bytes.Buffer) |
| for _, c := range dec { |
| if c >= 0x80 { |
| b.WriteRune(unicode.ReplacementChar) |
| } else { |
| b.WriteRune(rune(c)) |
| } |
| } |
| return b.String(), nil |
| case "iso-8859-1": |
| b := new(bytes.Buffer) |
| for _, c := range dec { |
| b.WriteRune(rune(c)) |
| } |
| return b.String(), nil |
| case "utf-8": |
| return string(dec), nil |
| } |
| panic("unreachable") |
| } |
| |
| type qDecoder struct { |
| r io.Reader |
| scratch [2]byte |
| } |
| |
| func (qd qDecoder) Read(p []byte) (n int, err error) { |
| // This method writes at most one byte into p. |
| if len(p) == 0 { |
| return 0, nil |
| } |
| if _, err := qd.r.Read(qd.scratch[:1]); err != nil { |
| return 0, err |
| } |
| switch c := qd.scratch[0]; { |
| case c == '=': |
| if _, err := io.ReadFull(qd.r, qd.scratch[:2]); err != nil { |
| return 0, err |
| } |
| x, err := strconv.ParseInt(string(qd.scratch[:2]), 16, 64) |
| if err != nil { |
| return 0, fmt.Errorf("mime: invalid RFC 2047 encoding: %q", qd.scratch[:2]) |
| } |
| p[0] = byte(x) |
| case c == '_': |
| p[0] = ' ' |
| default: |
| p[0] = c |
| } |
| return 1, nil |
| } |
| |
| // isVchar returns true if c is an RFC 5322 VCHAR character. |
| func isVchar(c byte) bool { |
| // Visible (printing) characters. |
| return '!' <= c && c <= '~' |
| } |