├── pool_go12.go ├── pool.go ├── README.md ├── LICENSE ├── reader.go ├── writer.go ├── writer_test.go ├── reader_test.go ├── encodedword.go └── encodedword_test.go /pool_go12.go: -------------------------------------------------------------------------------- 1 | // +build !go1.3 2 | 3 | package quotedprintable 4 | 5 | import "bytes" 6 | 7 | var ch = make(chan *bytes.Buffer, 32) 8 | 9 | func getBuffer() *bytes.Buffer { 10 | select { 11 | case buf := <-ch: 12 | return buf 13 | default: 14 | } 15 | return new(bytes.Buffer) 16 | } 17 | 18 | func putBuffer(buf *bytes.Buffer) { 19 | buf.Reset() 20 | select { 21 | case ch <- buf: 22 | default: 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pool.go: -------------------------------------------------------------------------------- 1 | // +build go1.3 2 | 3 | package quotedprintable 4 | 5 | import ( 6 | "bytes" 7 | "sync" 8 | ) 9 | 10 | var bufPool = sync.Pool{ 11 | New: func() interface{} { 12 | return new(bytes.Buffer) 13 | }, 14 | } 15 | 16 | func getBuffer() *bytes.Buffer { 17 | return bufPool.Get().(*bytes.Buffer) 18 | } 19 | 20 | func putBuffer(buf *bytes.Buffer) { 21 | if buf.Len() > 1024 { 22 | return 23 | } 24 | buf.Reset() 25 | bufPool.Put(buf) 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quotedprintable 2 | 3 | ## Introduction 4 | 5 | Package quotedprintable implements quoted-printable and message header encoding 6 | as specified by RFC 2045 and RFC 2047. 7 | 8 | It is a copy of the Go 1.5 package `mime/quotedprintable`. It also includes 9 | the new functions of package `mime` concerning RFC 2047. 10 | 11 | This code has minor changes with the standard library code in order to work 12 | with Go 1.0 and newer. 13 | 14 | ## Documentation 15 | 16 | https://godoc.org/gopkg.in/alexcesaro/quotedprintable.v3 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alexandre Cesaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | // Package quotedprintable implements quoted-printable encoding as specified by 2 | // RFC 2045. 3 | package quotedprintable 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "fmt" 9 | "io" 10 | ) 11 | 12 | // Reader is a quoted-printable decoder. 13 | type Reader struct { 14 | br *bufio.Reader 15 | rerr error // last read error 16 | line []byte // to be consumed before more of br 17 | } 18 | 19 | // NewReader returns a quoted-printable reader, decoding from r. 20 | func NewReader(r io.Reader) *Reader { 21 | return &Reader{ 22 | br: bufio.NewReader(r), 23 | } 24 | } 25 | 26 | func fromHex(b byte) (byte, error) { 27 | switch { 28 | case b >= '0' && b <= '9': 29 | return b - '0', nil 30 | case b >= 'A' && b <= 'F': 31 | return b - 'A' + 10, nil 32 | // Accept badly encoded bytes. 33 | case b >= 'a' && b <= 'f': 34 | return b - 'a' + 10, nil 35 | } 36 | return 0, fmt.Errorf("quotedprintable: invalid hex byte 0x%02x", b) 37 | } 38 | 39 | func readHexByte(a, b byte) (byte, error) { 40 | var hb, lb byte 41 | var err error 42 | if hb, err = fromHex(a); err != nil { 43 | return 0, err 44 | } 45 | if lb, err = fromHex(b); err != nil { 46 | return 0, err 47 | } 48 | return hb<<4 | lb, nil 49 | } 50 | 51 | func isQPDiscardWhitespace(r rune) bool { 52 | switch r { 53 | case '\n', '\r', ' ', '\t': 54 | return true 55 | } 56 | return false 57 | } 58 | 59 | var ( 60 | crlf = []byte("\r\n") 61 | lf = []byte("\n") 62 | softSuffix = []byte("=") 63 | ) 64 | 65 | // Read reads and decodes quoted-printable data from the underlying reader. 66 | func (r *Reader) Read(p []byte) (n int, err error) { 67 | // Deviations from RFC 2045: 68 | // 1. in addition to "=\r\n", "=\n" is also treated as soft line break. 69 | // 2. it will pass through a '\r' or '\n' not preceded by '=', consistent 70 | // with other broken QP encoders & decoders. 71 | for len(p) > 0 { 72 | if len(r.line) == 0 { 73 | if r.rerr != nil { 74 | return n, r.rerr 75 | } 76 | r.line, r.rerr = r.br.ReadSlice('\n') 77 | 78 | // Does the line end in CRLF instead of just LF? 79 | hasLF := bytes.HasSuffix(r.line, lf) 80 | hasCR := bytes.HasSuffix(r.line, crlf) 81 | wholeLine := r.line 82 | r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace) 83 | if bytes.HasSuffix(r.line, softSuffix) { 84 | rightStripped := wholeLine[len(r.line):] 85 | r.line = r.line[:len(r.line)-1] 86 | if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) { 87 | r.rerr = fmt.Errorf("quotedprintable: invalid bytes after =: %q", rightStripped) 88 | } 89 | } else if hasLF { 90 | if hasCR { 91 | r.line = append(r.line, '\r', '\n') 92 | } else { 93 | r.line = append(r.line, '\n') 94 | } 95 | } 96 | continue 97 | } 98 | b := r.line[0] 99 | 100 | switch { 101 | case b == '=': 102 | if len(r.line[1:]) < 2 { 103 | return n, io.ErrUnexpectedEOF 104 | } 105 | b, err = readHexByte(r.line[1], r.line[2]) 106 | if err != nil { 107 | return n, err 108 | } 109 | r.line = r.line[2:] // 2 of the 3; other 1 is done below 110 | case b == '\t' || b == '\r' || b == '\n': 111 | break 112 | case b < ' ' || b > '~': 113 | return n, fmt.Errorf("quotedprintable: invalid unescaped byte 0x%02x in body", b) 114 | } 115 | p[0] = b 116 | p = p[1:] 117 | r.line = r.line[1:] 118 | n++ 119 | } 120 | return n, nil 121 | } 122 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package quotedprintable 2 | 3 | import "io" 4 | 5 | const lineMaxLen = 76 6 | 7 | // A Writer is a quoted-printable writer that implements io.WriteCloser. 8 | type Writer struct { 9 | // Binary mode treats the writer's input as pure binary and processes end of 10 | // line bytes as binary data. 11 | Binary bool 12 | 13 | w io.Writer 14 | i int 15 | line [78]byte 16 | cr bool 17 | } 18 | 19 | // NewWriter returns a new Writer that writes to w. 20 | func NewWriter(w io.Writer) *Writer { 21 | return &Writer{w: w} 22 | } 23 | 24 | // Write encodes p using quoted-printable encoding and writes it to the 25 | // underlying io.Writer. It limits line length to 76 characters. The encoded 26 | // bytes are not necessarily flushed until the Writer is closed. 27 | func (w *Writer) Write(p []byte) (n int, err error) { 28 | for i, b := range p { 29 | switch { 30 | // Simple writes are done in batch. 31 | case b >= '!' && b <= '~' && b != '=': 32 | continue 33 | case isWhitespace(b) || !w.Binary && (b == '\n' || b == '\r'): 34 | continue 35 | } 36 | 37 | if i > n { 38 | if err := w.write(p[n:i]); err != nil { 39 | return n, err 40 | } 41 | n = i 42 | } 43 | 44 | if err := w.encode(b); err != nil { 45 | return n, err 46 | } 47 | n++ 48 | } 49 | 50 | if n == len(p) { 51 | return n, nil 52 | } 53 | 54 | if err := w.write(p[n:]); err != nil { 55 | return n, err 56 | } 57 | 58 | return len(p), nil 59 | } 60 | 61 | // Close closes the Writer, flushing any unwritten data to the underlying 62 | // io.Writer, but does not close the underlying io.Writer. 63 | func (w *Writer) Close() error { 64 | if err := w.checkLastByte(); err != nil { 65 | return err 66 | } 67 | 68 | return w.flush() 69 | } 70 | 71 | // write limits text encoded in quoted-printable to 76 characters per line. 72 | func (w *Writer) write(p []byte) error { 73 | for _, b := range p { 74 | if b == '\n' || b == '\r' { 75 | // If the previous byte was \r, the CRLF has already been inserted. 76 | if w.cr && b == '\n' { 77 | w.cr = false 78 | continue 79 | } 80 | 81 | if b == '\r' { 82 | w.cr = true 83 | } 84 | 85 | if err := w.checkLastByte(); err != nil { 86 | return err 87 | } 88 | if err := w.insertCRLF(); err != nil { 89 | return err 90 | } 91 | continue 92 | } 93 | 94 | if w.i == lineMaxLen-1 { 95 | if err := w.insertSoftLineBreak(); err != nil { 96 | return err 97 | } 98 | } 99 | 100 | w.line[w.i] = b 101 | w.i++ 102 | w.cr = false 103 | } 104 | 105 | return nil 106 | } 107 | 108 | func (w *Writer) encode(b byte) error { 109 | if lineMaxLen-1-w.i < 3 { 110 | if err := w.insertSoftLineBreak(); err != nil { 111 | return err 112 | } 113 | } 114 | 115 | w.line[w.i] = '=' 116 | w.line[w.i+1] = upperhex[b>>4] 117 | w.line[w.i+2] = upperhex[b&0x0f] 118 | w.i += 3 119 | 120 | return nil 121 | } 122 | 123 | // checkLastByte encodes the last buffered byte if it is a space or a tab. 124 | func (w *Writer) checkLastByte() error { 125 | if w.i == 0 { 126 | return nil 127 | } 128 | 129 | b := w.line[w.i-1] 130 | if isWhitespace(b) { 131 | w.i-- 132 | if err := w.encode(b); err != nil { 133 | return err 134 | } 135 | } 136 | 137 | return nil 138 | } 139 | 140 | func (w *Writer) insertSoftLineBreak() error { 141 | w.line[w.i] = '=' 142 | w.i++ 143 | 144 | return w.insertCRLF() 145 | } 146 | 147 | func (w *Writer) insertCRLF() error { 148 | w.line[w.i] = '\r' 149 | w.line[w.i+1] = '\n' 150 | w.i += 2 151 | 152 | return w.flush() 153 | } 154 | 155 | func (w *Writer) flush() error { 156 | if _, err := w.w.Write(w.line[:w.i]); err != nil { 157 | return err 158 | } 159 | 160 | w.i = 0 161 | return nil 162 | } 163 | 164 | func isWhitespace(b byte) bool { 165 | return b == ' ' || b == '\t' 166 | } 167 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | package quotedprintable 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestWriter(t *testing.T) { 11 | testWriter(t, false) 12 | } 13 | 14 | func TestWriterBinary(t *testing.T) { 15 | testWriter(t, true) 16 | } 17 | 18 | func testWriter(t *testing.T, binary bool) { 19 | tests := []struct { 20 | in, want, wantB string 21 | }{ 22 | {in: "", want: ""}, 23 | {in: "foo bar", want: "foo bar"}, 24 | {in: "foo bar=", want: "foo bar=3D"}, 25 | {in: "foo bar\r", want: "foo bar\r\n", wantB: "foo bar=0D"}, 26 | {in: "foo bar\r\r", want: "foo bar\r\n\r\n", wantB: "foo bar=0D=0D"}, 27 | {in: "foo bar\n", want: "foo bar\r\n", wantB: "foo bar=0A"}, 28 | {in: "foo bar\r\n", want: "foo bar\r\n", wantB: "foo bar=0D=0A"}, 29 | {in: "foo bar\r\r\n", want: "foo bar\r\n\r\n", wantB: "foo bar=0D=0D=0A"}, 30 | {in: "foo bar ", want: "foo bar=20"}, 31 | {in: "foo bar\t", want: "foo bar=09"}, 32 | {in: "foo bar ", want: "foo bar =20"}, 33 | {in: "foo bar \n", want: "foo bar=20\r\n", wantB: "foo bar =0A"}, 34 | {in: "foo bar \r", want: "foo bar=20\r\n", wantB: "foo bar =0D"}, 35 | {in: "foo bar \r\n", want: "foo bar=20\r\n", wantB: "foo bar =0D=0A"}, 36 | {in: "foo bar \n", want: "foo bar =20\r\n", wantB: "foo bar =0A"}, 37 | {in: "foo bar \n ", want: "foo bar =20\r\n=20", wantB: "foo bar =0A=20"}, 38 | {in: "¡Hola Señor!", want: "=C2=A1Hola Se=C3=B1or!"}, 39 | { 40 | in: "\t !\"#$%&'()*+,-./ :;<>?@[\\]^_`{|}~", 41 | want: "\t !\"#$%&'()*+,-./ :;<>?@[\\]^_`{|}~", 42 | }, 43 | { 44 | in: strings.Repeat("a", 75), 45 | want: strings.Repeat("a", 75), 46 | }, 47 | { 48 | in: strings.Repeat("a", 76), 49 | want: strings.Repeat("a", 75) + "=\r\na", 50 | }, 51 | { 52 | in: strings.Repeat("a", 72) + "=", 53 | want: strings.Repeat("a", 72) + "=3D", 54 | }, 55 | { 56 | in: strings.Repeat("a", 73) + "=", 57 | want: strings.Repeat("a", 73) + "=\r\n=3D", 58 | }, 59 | { 60 | in: strings.Repeat("a", 74) + "=", 61 | want: strings.Repeat("a", 74) + "=\r\n=3D", 62 | }, 63 | { 64 | in: strings.Repeat("a", 75) + "=", 65 | want: strings.Repeat("a", 75) + "=\r\n=3D", 66 | }, 67 | { 68 | in: strings.Repeat(" ", 73), 69 | want: strings.Repeat(" ", 72) + "=20", 70 | }, 71 | { 72 | in: strings.Repeat(" ", 74), 73 | want: strings.Repeat(" ", 73) + "=\r\n=20", 74 | }, 75 | { 76 | in: strings.Repeat(" ", 75), 77 | want: strings.Repeat(" ", 74) + "=\r\n=20", 78 | }, 79 | { 80 | in: strings.Repeat(" ", 76), 81 | want: strings.Repeat(" ", 75) + "=\r\n=20", 82 | }, 83 | { 84 | in: strings.Repeat(" ", 77), 85 | want: strings.Repeat(" ", 75) + "=\r\n =20", 86 | }, 87 | } 88 | 89 | for _, tt := range tests { 90 | buf := new(bytes.Buffer) 91 | w := NewWriter(buf) 92 | 93 | want := tt.want 94 | if binary { 95 | w.Binary = true 96 | if tt.wantB != "" { 97 | want = tt.wantB 98 | } 99 | } 100 | 101 | if _, err := w.Write([]byte(tt.in)); err != nil { 102 | t.Errorf("Write(%q): %v", tt.in, err) 103 | continue 104 | } 105 | if err := w.Close(); err != nil { 106 | t.Errorf("Close(): %v", err) 107 | continue 108 | } 109 | got := buf.String() 110 | if got != want { 111 | t.Errorf("Write(%q), got:\n%q\nwant:\n%q", tt.in, got, want) 112 | } 113 | } 114 | } 115 | 116 | func TestRoundTrip(t *testing.T) { 117 | buf := new(bytes.Buffer) 118 | w := NewWriter(buf) 119 | if _, err := w.Write(testMsg); err != nil { 120 | t.Fatalf("Write: %v", err) 121 | } 122 | if err := w.Close(); err != nil { 123 | t.Fatalf("Close: %v", err) 124 | } 125 | 126 | r := NewReader(buf) 127 | gotBytes, err := ioutil.ReadAll(r) 128 | if err != nil { 129 | t.Fatalf("Error while reading from Reader: %v", err) 130 | } 131 | got := string(gotBytes) 132 | if got != string(testMsg) { 133 | t.Errorf("Encoding and decoding changed the message, got:\n%s", got) 134 | } 135 | } 136 | 137 | // From http://fr.wikipedia.org/wiki/Quoted-Printable 138 | var testMsg = []byte("Quoted-Printable (QP) est un format d'encodage de données codées sur 8 bits, qui utilise exclusivement les caractères alphanumériques imprimables du code ASCII (7 bits).\r\n" + 139 | "\r\n" + 140 | "En effet, les différents codages comprennent de nombreux caractères qui ne sont pas représentables en ASCII (par exemple les caractères accentués), ainsi que des caractères dits « non-imprimables ».\r\n" + 141 | "\r\n" + 142 | "L'encodage Quoted-Printable permet de remédier à ce problème, en procédant de la manière suivante :\r\n" + 143 | "\r\n" + 144 | "Un octet correspondant à un caractère imprimable de l'ASCII sauf le signe égal (donc un caractère de code ASCII entre 33 et 60 ou entre 62 et 126) ou aux caractères de saut de ligne (codes ASCII 13 et 10) ou une suite de tabulations et espaces non situées en fin de ligne (de codes ASCII respectifs 9 et 32) est représenté tel quel.\r\n" + 145 | "Un octet qui ne correspond pas à la définition ci-dessus (caractère non imprimable de l'ASCII, tabulation ou espaces non suivies d'un caractère imprimable avant la fin de la ligne ou signe égal) est représenté par un signe égal, suivi de son numéro, exprimé en hexadécimal.\r\n" + 146 | "Enfin, un signe égal suivi par un saut de ligne (donc la suite des trois caractères de codes ASCII 61, 13 et 10) peut être inséré n'importe où, afin de limiter la taille des lignes produites si nécessaire. Une limite de 76 caractères par ligne est généralement respectée.\r\n") 147 | 148 | func BenchmarkWriter(b *testing.B) { 149 | for i := 0; i < b.N; i++ { 150 | w := NewWriter(ioutil.Discard) 151 | w.Write(testMsg) 152 | w.Close() 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /reader_test.go: -------------------------------------------------------------------------------- 1 | package quotedprintable 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "io" 10 | "os/exec" 11 | "regexp" 12 | "sort" 13 | "strings" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | func TestReader(t *testing.T) { 19 | tests := []struct { 20 | in, want string 21 | err interface{} 22 | }{ 23 | {in: "", want: ""}, 24 | {in: "foo bar", want: "foo bar"}, 25 | {in: "foo bar=3D", want: "foo bar="}, 26 | {in: "foo bar=3d", want: "foo bar="}, // lax. 27 | {in: "foo bar=\n", want: "foo bar"}, 28 | {in: "foo bar\n", want: "foo bar\n"}, // somewhat lax. 29 | {in: "foo bar=0", want: "foo bar", err: io.ErrUnexpectedEOF}, 30 | {in: "foo bar=0D=0A", want: "foo bar\r\n"}, 31 | {in: " A B \r\n C ", want: " A B\r\n C"}, 32 | {in: " A B =\r\n C ", want: " A B C"}, 33 | {in: " A B =\n C ", want: " A B C"}, // lax. treating LF as CRLF 34 | {in: "foo=\nbar", want: "foobar"}, 35 | {in: "foo\x00bar", want: "foo", err: "quotedprintable: invalid unescaped byte 0x00 in body"}, 36 | {in: "foo bar\xff", want: "foo bar", err: "quotedprintable: invalid unescaped byte 0xff in body"}, 37 | 38 | // Equal sign. 39 | {in: "=3D30\n", want: "=30\n"}, 40 | {in: "=00=FF0=\n", want: "\x00\xff0"}, 41 | 42 | // Trailing whitespace 43 | {in: "foo \n", want: "foo\n"}, 44 | {in: "foo \n\nfoo =\n\nfoo=20\n\n", want: "foo\n\nfoo \nfoo \n\n"}, 45 | 46 | // Tests that we allow bare \n and \r through, despite it being strictly 47 | // not permitted per RFC 2045, Section 6.7 Page 22 bullet (4). 48 | {in: "foo\nbar", want: "foo\nbar"}, 49 | {in: "foo\rbar", want: "foo\rbar"}, 50 | {in: "foo\r\nbar", want: "foo\r\nbar"}, 51 | 52 | // Different types of soft line-breaks. 53 | {in: "foo=\r\nbar", want: "foobar"}, 54 | {in: "foo=\nbar", want: "foobar"}, 55 | {in: "foo=\rbar", want: "foo", err: "quotedprintable: invalid hex byte 0x0d"}, 56 | {in: "foo=\r\r\r \nbar", want: "foo", err: `quotedprintable: invalid bytes after =: "\r\r\r \n"`}, 57 | 58 | // Example from RFC 2045: 59 | {in: "Now's the time =\n" + "for all folk to come=\n" + " to the aid of their country.", 60 | want: "Now's the time for all folk to come to the aid of their country."}, 61 | } 62 | for _, tt := range tests { 63 | var buf bytes.Buffer 64 | _, err := io.Copy(&buf, NewReader(strings.NewReader(tt.in))) 65 | if got := buf.String(); got != tt.want { 66 | t.Errorf("for %q, got %q; want %q", tt.in, got, tt.want) 67 | } 68 | switch verr := tt.err.(type) { 69 | case nil: 70 | if err != nil { 71 | t.Errorf("for %q, got unexpected error: %v", tt.in, err) 72 | } 73 | case string: 74 | if got := fmt.Sprint(err); got != verr { 75 | t.Errorf("for %q, got error %q; want %q", tt.in, got, verr) 76 | } 77 | case error: 78 | if err != verr { 79 | t.Errorf("for %q, got error %q; want %q", tt.in, err, verr) 80 | } 81 | } 82 | } 83 | 84 | } 85 | 86 | func everySequence(base, alpha string, length int, fn func(string)) { 87 | if len(base) == length { 88 | fn(base) 89 | return 90 | } 91 | for i := 0; i < len(alpha); i++ { 92 | everySequence(base+alpha[i:i+1], alpha, length, fn) 93 | } 94 | } 95 | 96 | var useQprint = flag.Bool("qprint", false, "Compare against the 'qprint' program.") 97 | 98 | var badSoftRx = regexp.MustCompile(`=([^\r\n]+?\n)|([^\r\n]+$)|(\r$)|(\r[^\n]+\n)|( \r\n)`) 99 | 100 | func TestExhaustive(t *testing.T) { 101 | if *useQprint { 102 | _, err := exec.LookPath("qprint") 103 | if err != nil { 104 | t.Fatalf("Error looking for qprint: %v", err) 105 | } 106 | } 107 | 108 | var buf bytes.Buffer 109 | res := make(map[string]int) 110 | everySequence("", "0A \r\n=", 6, func(s string) { 111 | if strings.HasSuffix(s, "=") || strings.Contains(s, "==") { 112 | return 113 | } 114 | buf.Reset() 115 | _, err := io.Copy(&buf, NewReader(strings.NewReader(s))) 116 | if err != nil { 117 | errStr := err.Error() 118 | if strings.Contains(errStr, "invalid bytes after =:") { 119 | errStr = "invalid bytes after =" 120 | } 121 | res[errStr]++ 122 | if strings.Contains(errStr, "invalid hex byte ") { 123 | if strings.HasSuffix(errStr, "0x20") && (strings.Contains(s, "=0 ") || strings.Contains(s, "=A ") || strings.Contains(s, "= ")) { 124 | return 125 | } 126 | if strings.HasSuffix(errStr, "0x3d") && (strings.Contains(s, "=0=") || strings.Contains(s, "=A=")) { 127 | return 128 | } 129 | if strings.HasSuffix(errStr, "0x0a") || strings.HasSuffix(errStr, "0x0d") { 130 | // bunch of cases; since whitespace at the end of a line before \n is removed. 131 | return 132 | } 133 | } 134 | if strings.Contains(errStr, "unexpected EOF") { 135 | return 136 | } 137 | if errStr == "invalid bytes after =" && badSoftRx.MatchString(s) { 138 | return 139 | } 140 | t.Errorf("decode(%q) = %v", s, err) 141 | return 142 | } 143 | if *useQprint { 144 | cmd := exec.Command("qprint", "-d") 145 | cmd.Stdin = strings.NewReader(s) 146 | stderr, err := cmd.StderrPipe() 147 | if err != nil { 148 | panic(err) 149 | } 150 | qpres := make(chan interface{}, 2) 151 | go func() { 152 | br := bufio.NewReader(stderr) 153 | s, _ := br.ReadString('\n') 154 | if s != "" { 155 | qpres <- errors.New(s) 156 | if cmd.Process != nil { 157 | // It can get stuck on invalid input, like: 158 | // echo -n "0000= " | qprint -d 159 | cmd.Process.Kill() 160 | } 161 | } 162 | }() 163 | go func() { 164 | want, err := cmd.Output() 165 | if err == nil { 166 | qpres <- want 167 | } 168 | }() 169 | select { 170 | case got := <-qpres: 171 | if want, ok := got.([]byte); ok { 172 | if string(want) != buf.String() { 173 | t.Errorf("go decode(%q) = %q; qprint = %q", s, want, buf.String()) 174 | } 175 | } else { 176 | t.Logf("qprint -d(%q) = %v", s, got) 177 | } 178 | case <-time.After(5 * time.Second): 179 | t.Logf("qprint timeout on %q", s) 180 | } 181 | } 182 | res["OK"]++ 183 | }) 184 | var outcomes []string 185 | for k, v := range res { 186 | outcomes = append(outcomes, fmt.Sprintf("%v: %d", k, v)) 187 | } 188 | sort.Strings(outcomes) 189 | got := strings.Join(outcomes, "\n") 190 | want := `OK: 21576 191 | invalid bytes after =: 3397 192 | quotedprintable: invalid hex byte 0x0a: 1400 193 | quotedprintable: invalid hex byte 0x0d: 2700 194 | quotedprintable: invalid hex byte 0x20: 2490 195 | quotedprintable: invalid hex byte 0x3d: 440 196 | unexpected EOF: 3122` 197 | if got != want { 198 | t.Errorf("Got:\n%s\nWant:\n%s", got, want) 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /encodedword.go: -------------------------------------------------------------------------------- 1 | package quotedprintable 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "strings" 10 | "unicode" 11 | "unicode/utf8" 12 | ) 13 | 14 | // A WordEncoder is a RFC 2047 encoded-word encoder. 15 | type WordEncoder byte 16 | 17 | const ( 18 | // BEncoding represents Base64 encoding scheme as defined by RFC 2045. 19 | BEncoding = WordEncoder('b') 20 | // QEncoding represents the Q-encoding scheme as defined by RFC 2047. 21 | QEncoding = WordEncoder('q') 22 | ) 23 | 24 | var ( 25 | errInvalidWord = errors.New("mime: invalid RFC 2047 encoded-word") 26 | ) 27 | 28 | // Encode returns the encoded-word form of s. If s is ASCII without special 29 | // characters, it is returned unchanged. The provided charset is the IANA 30 | // charset name of s. It is case insensitive. 31 | func (e WordEncoder) Encode(charset, s string) string { 32 | if !needsEncoding(s) { 33 | return s 34 | } 35 | return e.encodeWord(charset, s) 36 | } 37 | 38 | func needsEncoding(s string) bool { 39 | for _, b := range s { 40 | if (b < ' ' || b > '~') && b != '\t' { 41 | return true 42 | } 43 | } 44 | return false 45 | } 46 | 47 | // encodeWord encodes a string into an encoded-word. 48 | func (e WordEncoder) encodeWord(charset, s string) string { 49 | buf := getBuffer() 50 | defer putBuffer(buf) 51 | 52 | buf.WriteString("=?") 53 | buf.WriteString(charset) 54 | buf.WriteByte('?') 55 | buf.WriteByte(byte(e)) 56 | buf.WriteByte('?') 57 | 58 | if e == BEncoding { 59 | w := base64.NewEncoder(base64.StdEncoding, buf) 60 | io.WriteString(w, s) 61 | w.Close() 62 | } else { 63 | enc := make([]byte, 3) 64 | for i := 0; i < len(s); i++ { 65 | b := s[i] 66 | switch { 67 | case b == ' ': 68 | buf.WriteByte('_') 69 | case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_': 70 | buf.WriteByte(b) 71 | default: 72 | enc[0] = '=' 73 | enc[1] = upperhex[b>>4] 74 | enc[2] = upperhex[b&0x0f] 75 | buf.Write(enc) 76 | } 77 | } 78 | } 79 | buf.WriteString("?=") 80 | return buf.String() 81 | } 82 | 83 | const upperhex = "0123456789ABCDEF" 84 | 85 | // A WordDecoder decodes MIME headers containing RFC 2047 encoded-words. 86 | type WordDecoder struct { 87 | // CharsetReader, if non-nil, defines a function to generate 88 | // charset-conversion readers, converting from the provided 89 | // charset into UTF-8. 90 | // Charsets are always lower-case. utf-8, iso-8859-1 and us-ascii charsets 91 | // are handled by default. 92 | // One of the the CharsetReader's result values must be non-nil. 93 | CharsetReader func(charset string, input io.Reader) (io.Reader, error) 94 | } 95 | 96 | // Decode decodes an encoded-word. If word is not a valid RFC 2047 encoded-word, 97 | // word is returned unchanged. 98 | func (d *WordDecoder) Decode(word string) (string, error) { 99 | fields := strings.Split(word, "?") // TODO: remove allocation? 100 | if len(fields) != 5 || fields[0] != "=" || fields[4] != "=" || len(fields[2]) != 1 { 101 | return "", errInvalidWord 102 | } 103 | 104 | content, err := decode(fields[2][0], fields[3]) 105 | if err != nil { 106 | return "", err 107 | } 108 | 109 | buf := getBuffer() 110 | defer putBuffer(buf) 111 | 112 | if err := d.convert(buf, fields[1], content); err != nil { 113 | return "", err 114 | } 115 | 116 | return buf.String(), nil 117 | } 118 | 119 | // DecodeHeader decodes all encoded-words of the given string. It returns an 120 | // error if and only if CharsetReader of d returns an error. 121 | func (d *WordDecoder) DecodeHeader(header string) (string, error) { 122 | // If there is no encoded-word, returns before creating a buffer. 123 | i := strings.Index(header, "=?") 124 | if i == -1 { 125 | return header, nil 126 | } 127 | 128 | buf := getBuffer() 129 | defer putBuffer(buf) 130 | 131 | buf.WriteString(header[:i]) 132 | header = header[i:] 133 | 134 | betweenWords := false 135 | for { 136 | start := strings.Index(header, "=?") 137 | if start == -1 { 138 | break 139 | } 140 | cur := start + len("=?") 141 | 142 | i := strings.Index(header[cur:], "?") 143 | if i == -1 { 144 | break 145 | } 146 | charset := header[cur : cur+i] 147 | cur += i + len("?") 148 | 149 | if len(header) < cur+len("Q??=") { 150 | break 151 | } 152 | encoding := header[cur] 153 | cur++ 154 | 155 | if header[cur] != '?' { 156 | break 157 | } 158 | cur++ 159 | 160 | j := strings.Index(header[cur:], "?=") 161 | if j == -1 { 162 | break 163 | } 164 | text := header[cur : cur+j] 165 | end := cur + j + len("?=") 166 | 167 | content, err := decode(encoding, text) 168 | if err != nil { 169 | betweenWords = false 170 | buf.WriteString(header[:start+2]) 171 | header = header[start+2:] 172 | continue 173 | } 174 | 175 | // Write characters before the encoded-word. White-space and newline 176 | // characters separating two encoded-words must be deleted. 177 | if start > 0 && (!betweenWords || hasNonWhitespace(header[:start])) { 178 | buf.WriteString(header[:start]) 179 | } 180 | 181 | if err := d.convert(buf, charset, content); err != nil { 182 | return "", err 183 | } 184 | 185 | header = header[end:] 186 | betweenWords = true 187 | } 188 | 189 | if len(header) > 0 { 190 | buf.WriteString(header) 191 | } 192 | 193 | return buf.String(), nil 194 | } 195 | 196 | func decode(encoding byte, text string) ([]byte, error) { 197 | switch encoding { 198 | case 'B', 'b': 199 | return base64.StdEncoding.DecodeString(text) 200 | case 'Q', 'q': 201 | return qDecode(text) 202 | } 203 | return nil, errInvalidWord 204 | } 205 | 206 | func (d *WordDecoder) convert(buf *bytes.Buffer, charset string, content []byte) error { 207 | switch { 208 | case strings.EqualFold("utf-8", charset): 209 | buf.Write(content) 210 | case strings.EqualFold("iso-8859-1", charset): 211 | for _, c := range content { 212 | buf.WriteRune(rune(c)) 213 | } 214 | case strings.EqualFold("us-ascii", charset): 215 | for _, c := range content { 216 | if c >= utf8.RuneSelf { 217 | buf.WriteRune(unicode.ReplacementChar) 218 | } else { 219 | buf.WriteByte(c) 220 | } 221 | } 222 | default: 223 | if d.CharsetReader == nil { 224 | return fmt.Errorf("mime: unhandled charset %q", charset) 225 | } 226 | r, err := d.CharsetReader(strings.ToLower(charset), bytes.NewReader(content)) 227 | if err != nil { 228 | return err 229 | } 230 | if _, err = buf.ReadFrom(r); err != nil { 231 | return err 232 | } 233 | } 234 | return nil 235 | } 236 | 237 | // hasNonWhitespace reports whether s (assumed to be ASCII) contains at least 238 | // one byte of non-whitespace. 239 | func hasNonWhitespace(s string) bool { 240 | for _, b := range s { 241 | switch b { 242 | // Encoded-words can only be separated by linear white spaces which does 243 | // not include vertical tabs (\v). 244 | case ' ', '\t', '\n', '\r': 245 | default: 246 | return true 247 | } 248 | } 249 | return false 250 | } 251 | 252 | // qDecode decodes a Q encoded string. 253 | func qDecode(s string) ([]byte, error) { 254 | dec := make([]byte, len(s)) 255 | n := 0 256 | for i := 0; i < len(s); i++ { 257 | switch c := s[i]; { 258 | case c == '_': 259 | dec[n] = ' ' 260 | case c == '=': 261 | if i+2 >= len(s) { 262 | return nil, errInvalidWord 263 | } 264 | b, err := readHexByte(s[i+1], s[i+2]) 265 | if err != nil { 266 | return nil, err 267 | } 268 | dec[n] = b 269 | i += 2 270 | case (c <= '~' && c >= ' ') || c == '\n' || c == '\r' || c == '\t': 271 | dec[n] = c 272 | default: 273 | return nil, errInvalidWord 274 | } 275 | n++ 276 | } 277 | 278 | return dec[:n], nil 279 | } 280 | -------------------------------------------------------------------------------- /encodedword_test.go: -------------------------------------------------------------------------------- 1 | package quotedprintable 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func ExampleWordEncoder_Encode() { 14 | fmt.Println(QEncoding.Encode("utf-8", "¡Hola, señor!")) 15 | fmt.Println(QEncoding.Encode("utf-8", "Hello!")) 16 | fmt.Println(BEncoding.Encode("UTF-8", "¡Hola, señor!")) 17 | fmt.Println(QEncoding.Encode("ISO-8859-1", "Caf\xE9")) 18 | // Output: 19 | // =?utf-8?q?=C2=A1Hola,_se=C3=B1or!?= 20 | // Hello! 21 | // =?UTF-8?b?wqFIb2xhLCBzZcOxb3Ih?= 22 | // =?ISO-8859-1?q?Caf=E9?= 23 | } 24 | 25 | func ExampleWordDecoder_Decode() { 26 | dec := new(WordDecoder) 27 | header, err := dec.Decode("=?utf-8?q?=C2=A1Hola,_se=C3=B1or!?=") 28 | if err != nil { 29 | panic(err) 30 | } 31 | fmt.Println(header) 32 | 33 | dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { 34 | switch charset { 35 | case "x-case": 36 | // Fake character set for example. 37 | // Real use would integrate with packages such 38 | // as code.google.com/p/go-charset 39 | content, err := ioutil.ReadAll(input) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return bytes.NewReader(bytes.ToUpper(content)), nil 44 | } 45 | return nil, fmt.Errorf("unhandled charset %q", charset) 46 | } 47 | header, err = dec.Decode("=?x-case?q?hello!?=") 48 | if err != nil { 49 | panic(err) 50 | } 51 | fmt.Println(header) 52 | // Output: 53 | // ¡Hola, señor! 54 | // HELLO! 55 | } 56 | 57 | func ExampleWordDecoder_DecodeHeader() { 58 | dec := new(WordDecoder) 59 | header, err := dec.DecodeHeader("=?utf-8?q?=C3=89ric?= , =?utf-8?q?Ana=C3=AFs?= ") 60 | if err != nil { 61 | panic(err) 62 | } 63 | fmt.Println(header) 64 | 65 | header, err = dec.DecodeHeader("=?utf-8?q?=C2=A1Hola,?= =?utf-8?q?_se=C3=B1or!?=") 66 | if err != nil { 67 | panic(err) 68 | } 69 | fmt.Println(header) 70 | 71 | dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { 72 | switch charset { 73 | case "x-case": 74 | // Fake character set for example. 75 | // Real use would integrate with packages such 76 | // as code.google.com/p/go-charset 77 | content, err := ioutil.ReadAll(input) 78 | if err != nil { 79 | return nil, err 80 | } 81 | return bytes.NewReader(bytes.ToUpper(content)), nil 82 | } 83 | return nil, fmt.Errorf("unhandled charset %q", charset) 84 | } 85 | header, err = dec.DecodeHeader("=?x-case?q?hello_?= =?x-case?q?world!?=") 86 | if err != nil { 87 | panic(err) 88 | } 89 | fmt.Println(header) 90 | // Output: 91 | // Éric , Anaïs 92 | // ¡Hola, señor! 93 | // HELLO WORLD! 94 | } 95 | 96 | func TestEncodeWord(t *testing.T) { 97 | utf8, iso88591 := "utf-8", "iso-8859-1" 98 | tests := []struct { 99 | enc WordEncoder 100 | charset string 101 | src, exp string 102 | }{ 103 | {QEncoding, utf8, "François-Jérôme", "=?utf-8?q?Fran=C3=A7ois-J=C3=A9r=C3=B4me?="}, 104 | {BEncoding, utf8, "Café", "=?utf-8?b?Q2Fmw6k=?="}, 105 | {QEncoding, iso88591, "La Seleção", "=?iso-8859-1?q?La_Sele=C3=A7=C3=A3o?="}, 106 | {QEncoding, utf8, "", ""}, 107 | {QEncoding, utf8, "A", "A"}, 108 | {QEncoding, iso88591, "a", "a"}, 109 | {QEncoding, utf8, "123 456", "123 456"}, 110 | {QEncoding, utf8, "\t !\"#$%&'()*+,-./ :;<>?@[\\]^_`{|}~", "\t !\"#$%&'()*+,-./ :;<>?@[\\]^_`{|}~"}, 111 | } 112 | 113 | for _, test := range tests { 114 | if s := test.enc.Encode(test.charset, test.src); s != test.exp { 115 | t.Errorf("Encode(%q) = %q, want %q", test.src, s, test.exp) 116 | } 117 | } 118 | } 119 | 120 | func TestDecodeWord(t *testing.T) { 121 | tests := []struct { 122 | src, exp string 123 | hasErr bool 124 | }{ 125 | {"=?UTF-8?Q?=C2=A1Hola,_se=C3=B1or!?=", "¡Hola, señor!", false}, 126 | {"=?UTF-8?Q?Fran=C3=A7ois-J=C3=A9r=C3=B4me?=", "François-Jérôme", false}, 127 | {"=?UTF-8?q?ascii?=", "ascii", false}, 128 | {"=?utf-8?B?QW5kcsOp?=", "André", false}, 129 | {"=?ISO-8859-1?Q?Rapha=EBl_Dupont?=", "Raphaël Dupont", false}, 130 | {"=?utf-8?b?IkFudG9uaW8gSm9zw6kiIDxqb3NlQGV4YW1wbGUub3JnPg==?=", `"Antonio José" `, false}, 131 | {"=?UTF-8?A?Test?=", "", true}, 132 | {"=?UTF-8?Q?A=B?=", "", true}, 133 | {"=?UTF-8?Q?=A?=", "", true}, 134 | {"=?UTF-8?A?A?=", "", true}, 135 | } 136 | 137 | for _, test := range tests { 138 | dec := new(WordDecoder) 139 | s, err := dec.Decode(test.src) 140 | if test.hasErr && err == nil { 141 | t.Errorf("Decode(%q) should return an error", test.src) 142 | continue 143 | } 144 | if !test.hasErr && err != nil { 145 | t.Errorf("Decode(%q): %v", test.src, err) 146 | continue 147 | } 148 | if s != test.exp { 149 | t.Errorf("Decode(%q) = %q, want %q", test.src, s, test.exp) 150 | } 151 | } 152 | } 153 | 154 | func TestDecodeHeader(t *testing.T) { 155 | tests := []struct { 156 | src, exp string 157 | }{ 158 | {"=?UTF-8?Q?=C2=A1Hola,_se=C3=B1or!?=", "¡Hola, señor!"}, 159 | {"=?UTF-8?Q?Fran=C3=A7ois-J=C3=A9r=C3=B4me?=", "François-Jérôme"}, 160 | {"=?UTF-8?q?ascii?=", "ascii"}, 161 | {"=?utf-8?B?QW5kcsOp?=", "André"}, 162 | {"=?ISO-8859-1?Q?Rapha=EBl_Dupont?=", "Raphaël Dupont"}, 163 | {"Jean", "Jean"}, 164 | {"=?utf-8?b?IkFudG9uaW8gSm9zw6kiIDxqb3NlQGV4YW1wbGUub3JnPg==?=", `"Antonio José" `}, 165 | {"=?UTF-8?A?Test?=", "=?UTF-8?A?Test?="}, 166 | {"=?UTF-8?Q?A=B?=", "=?UTF-8?Q?A=B?="}, 167 | {"=?UTF-8?Q?=A?=", "=?UTF-8?Q?=A?="}, 168 | {"=?UTF-8?A?A?=", "=?UTF-8?A?A?="}, 169 | // Incomplete words 170 | {"=?", "=?"}, 171 | {"=?UTF-8?", "=?UTF-8?"}, 172 | {"=?UTF-8?=", "=?UTF-8?="}, 173 | {"=?UTF-8?Q", "=?UTF-8?Q"}, 174 | {"=?UTF-8?Q?", "=?UTF-8?Q?"}, 175 | {"=?UTF-8?Q?=", "=?UTF-8?Q?="}, 176 | {"=?UTF-8?Q?A", "=?UTF-8?Q?A"}, 177 | {"=?UTF-8?Q?A?", "=?UTF-8?Q?A?"}, 178 | // Tests from RFC 2047 179 | {"=?ISO-8859-1?Q?a?=", "a"}, 180 | {"=?ISO-8859-1?Q?a?= b", "a b"}, 181 | {"=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=", "ab"}, 182 | {"=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=", "ab"}, 183 | {"=?ISO-8859-1?Q?a?= \r\n\t =?ISO-8859-1?Q?b?=", "ab"}, 184 | {"=?ISO-8859-1?Q?a_b?=", "a b"}, 185 | } 186 | 187 | for _, test := range tests { 188 | dec := new(WordDecoder) 189 | s, err := dec.DecodeHeader(test.src) 190 | if err != nil { 191 | t.Errorf("DecodeHeader(%q): %v", test.src, err) 192 | } 193 | if s != test.exp { 194 | t.Errorf("DecodeHeader(%q) = %q, want %q", test.src, s, test.exp) 195 | } 196 | } 197 | } 198 | 199 | func TestCharsetDecoder(t *testing.T) { 200 | tests := []struct { 201 | src string 202 | want string 203 | charsets []string 204 | content []string 205 | }{ 206 | {"=?utf-8?b?Q2Fmw6k=?=", "Café", nil, nil}, 207 | {"=?ISO-8859-1?Q?caf=E9?=", "café", nil, nil}, 208 | {"=?US-ASCII?Q?foo_bar?=", "foo bar", nil, nil}, 209 | {"=?utf-8?Q?=?=", "=?utf-8?Q?=?=", nil, nil}, 210 | {"=?utf-8?Q?=A?=", "=?utf-8?Q?=A?=", nil, nil}, 211 | { 212 | "=?ISO-8859-15?Q?f=F5=F6?= =?windows-1252?Q?b=E0r?=", 213 | "f\xf5\xf6b\xe0r", 214 | []string{"iso-8859-15", "windows-1252"}, 215 | []string{"f\xf5\xf6", "b\xe0r"}, 216 | }, 217 | } 218 | 219 | for _, test := range tests { 220 | i := 0 221 | dec := &WordDecoder{ 222 | CharsetReader: func(charset string, input io.Reader) (io.Reader, error) { 223 | if charset != test.charsets[i] { 224 | t.Errorf("DecodeHeader(%q), got charset %q, want %q", test.src, charset, test.charsets[i]) 225 | } 226 | content, err := ioutil.ReadAll(input) 227 | if err != nil { 228 | t.Errorf("DecodeHeader(%q), error in reader: %v", test.src, err) 229 | } 230 | got := string(content) 231 | if got != test.content[i] { 232 | t.Errorf("DecodeHeader(%q), got content %q, want %q", test.src, got, test.content[i]) 233 | } 234 | i++ 235 | 236 | return strings.NewReader(got), nil 237 | }, 238 | } 239 | got, err := dec.DecodeHeader(test.src) 240 | if err != nil { 241 | t.Errorf("DecodeHeader(%q): %v", test.src, err) 242 | } 243 | if got != test.want { 244 | t.Errorf("DecodeHeader(%q) = %q, want %q", test.src, got, test.want) 245 | } 246 | } 247 | } 248 | 249 | func TestCharsetDecoderError(t *testing.T) { 250 | dec := &WordDecoder{ 251 | CharsetReader: func(charset string, input io.Reader) (io.Reader, error) { 252 | return nil, errors.New("Test error") 253 | }, 254 | } 255 | 256 | if _, err := dec.DecodeHeader("=?charset?Q?foo?="); err == nil { 257 | t.Error("DecodeHeader should return an error") 258 | } 259 | } 260 | 261 | func BenchmarkQEncodeWord(b *testing.B) { 262 | for i := 0; i < b.N; i++ { 263 | QEncoding.Encode("UTF-8", "¡Hola, señor!") 264 | } 265 | } 266 | 267 | func BenchmarkQDecodeWord(b *testing.B) { 268 | dec := new(WordDecoder) 269 | 270 | for i := 0; i < b.N; i++ { 271 | dec.Decode("=?utf-8?q?=C2=A1Hola,_se=C3=B1or!?=") 272 | } 273 | } 274 | 275 | func BenchmarkQDecodeHeader(b *testing.B) { 276 | dec := new(WordDecoder) 277 | 278 | for i := 0; i < b.N; i++ { 279 | dec.Decode("=?utf-8?q?=C2=A1Hola,_se=C3=B1or!?=") 280 | } 281 | } 282 | --------------------------------------------------------------------------------