diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 29cddc63..1ccb6301 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -120,6 +120,10 @@ "ImportPath": "google.golang.org/api/googleapi", "Rev": "d3edb0282bde692467788c50070a9211afe75cf3" }, + { + "ImportPath": "gopkg.in/alexcesaro/quotedprintable.v3", + "Rev": "2caba252f4dc53eaf6b553000885530023f54623" + }, { "ImportPath": "gopkg.in/gomail.v2", "Comment": "2.0.0-2-gb1e5552", diff --git a/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE new file mode 100644 index 00000000..5f5c12af --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Alexandre Cesaro + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/README.md b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/README.md new file mode 100644 index 00000000..98ddf829 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/README.md @@ -0,0 +1,16 @@ +# quotedprintable + +## Introduction + +Package quotedprintable implements quoted-printable and message header encoding +as specified by RFC 2045 and RFC 2047. + +It is a copy of the Go 1.5 package `mime/quotedprintable`. It also includes +the new functions of package `mime` concerning RFC 2047. + +This code has minor changes with the standard library code in order to work +with Go 1.0 and newer. + +## Documentation + +https://godoc.org/gopkg.in/alexcesaro/quotedprintable.v3 diff --git a/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go new file mode 100644 index 00000000..cfd02617 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go @@ -0,0 +1,279 @@ +package quotedprintable + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + "strings" + "unicode" + "unicode/utf8" +) + +// A WordEncoder is a RFC 2047 encoded-word encoder. +type WordEncoder byte + +const ( + // BEncoding represents Base64 encoding scheme as defined by RFC 2045. + BEncoding = WordEncoder('b') + // QEncoding represents the Q-encoding scheme as defined by RFC 2047. + QEncoding = WordEncoder('q') +) + +var ( + errInvalidWord = errors.New("mime: invalid RFC 2047 encoded-word") +) + +// Encode returns the encoded-word form of s. If s is ASCII without special +// characters, it is returned unchanged. The provided charset is the IANA +// charset name of s. It is case insensitive. +func (e WordEncoder) Encode(charset, s string) string { + if !needsEncoding(s) { + return s + } + return e.encodeWord(charset, s) +} + +func needsEncoding(s string) bool { + for _, b := range s { + if (b < ' ' || b > '~') && b != '\t' { + return true + } + } + return false +} + +// encodeWord encodes a string into an encoded-word. +func (e WordEncoder) encodeWord(charset, s string) string { + buf := getBuffer() + defer putBuffer(buf) + + buf.WriteString("=?") + buf.WriteString(charset) + buf.WriteByte('?') + buf.WriteByte(byte(e)) + buf.WriteByte('?') + + if e == BEncoding { + w := base64.NewEncoder(base64.StdEncoding, buf) + io.WriteString(w, s) + w.Close() + } else { + enc := make([]byte, 3) + for i := 0; i < len(s); i++ { + b := s[i] + switch { + case b == ' ': + buf.WriteByte('_') + case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_': + buf.WriteByte(b) + default: + enc[0] = '=' + enc[1] = upperhex[b>>4] + enc[2] = upperhex[b&0x0f] + buf.Write(enc) + } + } + } + buf.WriteString("?=") + return buf.String() +} + +const upperhex = "0123456789ABCDEF" + +// A WordDecoder decodes MIME headers containing RFC 2047 encoded-words. +type WordDecoder struct { + // CharsetReader, if non-nil, defines a function to generate + // charset-conversion readers, converting from the provided + // charset into UTF-8. + // Charsets are always lower-case. utf-8, iso-8859-1 and us-ascii charsets + // are handled by default. + // One of the the CharsetReader's result values must be non-nil. + CharsetReader func(charset string, input io.Reader) (io.Reader, error) +} + +// Decode decodes an encoded-word. If word is not a valid RFC 2047 encoded-word, +// word is returned unchanged. +func (d *WordDecoder) Decode(word string) (string, error) { + fields := strings.Split(word, "?") // TODO: remove allocation? + if len(fields) != 5 || fields[0] != "=" || fields[4] != "=" || len(fields[2]) != 1 { + return "", errInvalidWord + } + + content, err := decode(fields[2][0], fields[3]) + if err != nil { + return "", err + } + + buf := getBuffer() + defer putBuffer(buf) + + if err := d.convert(buf, fields[1], content); err != nil { + return "", err + } + + return buf.String(), nil +} + +// DecodeHeader decodes all encoded-words of the given string. It returns an +// error if and only if CharsetReader of d returns an error. +func (d *WordDecoder) DecodeHeader(header string) (string, error) { + // If there is no encoded-word, returns before creating a buffer. + i := strings.Index(header, "=?") + if i == -1 { + return header, nil + } + + buf := getBuffer() + defer putBuffer(buf) + + buf.WriteString(header[:i]) + header = header[i:] + + betweenWords := false + for { + start := strings.Index(header, "=?") + if start == -1 { + break + } + cur := start + len("=?") + + i := strings.Index(header[cur:], "?") + if i == -1 { + break + } + charset := header[cur : cur+i] + cur += i + len("?") + + if len(header) < cur+len("Q??=") { + break + } + encoding := header[cur] + cur++ + + if header[cur] != '?' { + break + } + cur++ + + j := strings.Index(header[cur:], "?=") + if j == -1 { + break + } + text := header[cur : cur+j] + end := cur + j + len("?=") + + content, err := decode(encoding, text) + if err != nil { + betweenWords = false + buf.WriteString(header[:start+2]) + header = header[start+2:] + continue + } + + // Write characters before the encoded-word. White-space and newline + // characters separating two encoded-words must be deleted. + if start > 0 && (!betweenWords || hasNonWhitespace(header[:start])) { + buf.WriteString(header[:start]) + } + + if err := d.convert(buf, charset, content); err != nil { + return "", err + } + + header = header[end:] + betweenWords = true + } + + if len(header) > 0 { + buf.WriteString(header) + } + + return buf.String(), nil +} + +func decode(encoding byte, text string) ([]byte, error) { + switch encoding { + case 'B', 'b': + return base64.StdEncoding.DecodeString(text) + case 'Q', 'q': + return qDecode(text) + } + return nil, errInvalidWord +} + +func (d *WordDecoder) convert(buf *bytes.Buffer, charset string, content []byte) error { + switch { + case strings.EqualFold("utf-8", charset): + buf.Write(content) + case strings.EqualFold("iso-8859-1", charset): + for _, c := range content { + buf.WriteRune(rune(c)) + } + case strings.EqualFold("us-ascii", charset): + for _, c := range content { + if c >= utf8.RuneSelf { + buf.WriteRune(unicode.ReplacementChar) + } else { + buf.WriteByte(c) + } + } + default: + if d.CharsetReader == nil { + return fmt.Errorf("mime: unhandled charset %q", charset) + } + r, err := d.CharsetReader(strings.ToLower(charset), bytes.NewReader(content)) + if err != nil { + return err + } + if _, err = buf.ReadFrom(r); err != nil { + return err + } + } + return nil +} + +// hasNonWhitespace reports whether s (assumed to be ASCII) contains at least +// one byte of non-whitespace. +func hasNonWhitespace(s string) bool { + for _, b := range s { + switch b { + // Encoded-words can only be separated by linear white spaces which does + // not include vertical tabs (\v). + case ' ', '\t', '\n', '\r': + default: + return true + } + } + return false +} + +// qDecode decodes a Q encoded string. +func qDecode(s string) ([]byte, error) { + dec := make([]byte, len(s)) + n := 0 + for i := 0; i < len(s); i++ { + switch c := s[i]; { + case c == '_': + dec[n] = ' ' + case c == '=': + if i+2 >= len(s) { + return nil, errInvalidWord + } + b, err := readHexByte(s[i+1], s[i+2]) + if err != nil { + return nil, err + } + dec[n] = b + i += 2 + case (c <= '~' && c >= ' ') || c == '\n' || c == '\r' || c == '\t': + dec[n] = c + default: + return nil, errInvalidWord + } + n++ + } + + return dec[:n], nil +} diff --git a/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/encodedword_test.go b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/encodedword_test.go new file mode 100644 index 00000000..368794fe --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/encodedword_test.go @@ -0,0 +1,281 @@ +package quotedprintable + +import ( + "bytes" + "errors" + "fmt" + "io" + "io/ioutil" + "strings" + "testing" +) + +func ExampleWordEncoder_Encode() { + fmt.Println(QEncoding.Encode("utf-8", "¡Hola, señor!")) + fmt.Println(QEncoding.Encode("utf-8", "Hello!")) + fmt.Println(BEncoding.Encode("UTF-8", "¡Hola, señor!")) + fmt.Println(QEncoding.Encode("ISO-8859-1", "Caf\xE9")) + // Output: + // =?utf-8?q?=C2=A1Hola,_se=C3=B1or!?= + // Hello! + // =?UTF-8?b?wqFIb2xhLCBzZcOxb3Ih?= + // =?ISO-8859-1?q?Caf=E9?= +} + +func ExampleWordDecoder_Decode() { + dec := new(WordDecoder) + header, err := dec.Decode("=?utf-8?q?=C2=A1Hola,_se=C3=B1or!?=") + if err != nil { + panic(err) + } + fmt.Println(header) + + dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { + switch charset { + case "x-case": + // Fake character set for example. + // Real use would integrate with packages such + // as code.google.com/p/go-charset + content, err := ioutil.ReadAll(input) + if err != nil { + return nil, err + } + return bytes.NewReader(bytes.ToUpper(content)), nil + } + return nil, fmt.Errorf("unhandled charset %q", charset) + } + header, err = dec.Decode("=?x-case?q?hello!?=") + if err != nil { + panic(err) + } + fmt.Println(header) + // Output: + // ¡Hola, señor! + // HELLO! +} + +func ExampleWordDecoder_DecodeHeader() { + dec := new(WordDecoder) + header, err := dec.DecodeHeader("=?utf-8?q?=C3=89ric?= , =?utf-8?q?Ana=C3=AFs?= ") + if err != nil { + panic(err) + } + fmt.Println(header) + + header, err = dec.DecodeHeader("=?utf-8?q?=C2=A1Hola,?= =?utf-8?q?_se=C3=B1or!?=") + if err != nil { + panic(err) + } + fmt.Println(header) + + dec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) { + switch charset { + case "x-case": + // Fake character set for example. + // Real use would integrate with packages such + // as code.google.com/p/go-charset + content, err := ioutil.ReadAll(input) + if err != nil { + return nil, err + } + return bytes.NewReader(bytes.ToUpper(content)), nil + } + return nil, fmt.Errorf("unhandled charset %q", charset) + } + header, err = dec.DecodeHeader("=?x-case?q?hello_?= =?x-case?q?world!?=") + if err != nil { + panic(err) + } + fmt.Println(header) + // Output: + // Éric , Anaïs + // ¡Hola, señor! + // HELLO WORLD! +} + +func TestEncodeWord(t *testing.T) { + utf8, iso88591 := "utf-8", "iso-8859-1" + tests := []struct { + enc WordEncoder + charset string + src, exp string + }{ + {QEncoding, utf8, "François-Jérôme", "=?utf-8?q?Fran=C3=A7ois-J=C3=A9r=C3=B4me?="}, + {BEncoding, utf8, "Café", "=?utf-8?b?Q2Fmw6k=?="}, + {QEncoding, iso88591, "La Seleção", "=?iso-8859-1?q?La_Sele=C3=A7=C3=A3o?="}, + {QEncoding, utf8, "", ""}, + {QEncoding, utf8, "A", "A"}, + {QEncoding, iso88591, "a", "a"}, + {QEncoding, utf8, "123 456", "123 456"}, + {QEncoding, utf8, "\t !\"#$%&'()*+,-./ :;<>?@[\\]^_`{|}~", "\t !\"#$%&'()*+,-./ :;<>?@[\\]^_`{|}~"}, + } + + for _, test := range tests { + if s := test.enc.Encode(test.charset, test.src); s != test.exp { + t.Errorf("Encode(%q) = %q, want %q", test.src, s, test.exp) + } + } +} + +func TestDecodeWord(t *testing.T) { + tests := []struct { + src, exp string + hasErr bool + }{ + {"=?UTF-8?Q?=C2=A1Hola,_se=C3=B1or!?=", "¡Hola, señor!", false}, + {"=?UTF-8?Q?Fran=C3=A7ois-J=C3=A9r=C3=B4me?=", "François-Jérôme", false}, + {"=?UTF-8?q?ascii?=", "ascii", false}, + {"=?utf-8?B?QW5kcsOp?=", "André", false}, + {"=?ISO-8859-1?Q?Rapha=EBl_Dupont?=", "Raphaël Dupont", false}, + {"=?utf-8?b?IkFudG9uaW8gSm9zw6kiIDxqb3NlQGV4YW1wbGUub3JnPg==?=", `"Antonio José" `, false}, + {"=?UTF-8?A?Test?=", "", true}, + {"=?UTF-8?Q?A=B?=", "", true}, + {"=?UTF-8?Q?=A?=", "", true}, + {"=?UTF-8?A?A?=", "", true}, + } + + for _, test := range tests { + dec := new(WordDecoder) + s, err := dec.Decode(test.src) + if test.hasErr && err == nil { + t.Errorf("Decode(%q) should return an error", test.src) + continue + } + if !test.hasErr && err != nil { + t.Errorf("Decode(%q): %v", test.src, err) + continue + } + if s != test.exp { + t.Errorf("Decode(%q) = %q, want %q", test.src, s, test.exp) + } + } +} + +func TestDecodeHeader(t *testing.T) { + tests := []struct { + src, exp string + }{ + {"=?UTF-8?Q?=C2=A1Hola,_se=C3=B1or!?=", "¡Hola, señor!"}, + {"=?UTF-8?Q?Fran=C3=A7ois-J=C3=A9r=C3=B4me?=", "François-Jérôme"}, + {"=?UTF-8?q?ascii?=", "ascii"}, + {"=?utf-8?B?QW5kcsOp?=", "André"}, + {"=?ISO-8859-1?Q?Rapha=EBl_Dupont?=", "Raphaël Dupont"}, + {"Jean", "Jean"}, + {"=?utf-8?b?IkFudG9uaW8gSm9zw6kiIDxqb3NlQGV4YW1wbGUub3JnPg==?=", `"Antonio José" `}, + {"=?UTF-8?A?Test?=", "=?UTF-8?A?Test?="}, + {"=?UTF-8?Q?A=B?=", "=?UTF-8?Q?A=B?="}, + {"=?UTF-8?Q?=A?=", "=?UTF-8?Q?=A?="}, + {"=?UTF-8?A?A?=", "=?UTF-8?A?A?="}, + // Incomplete words + {"=?", "=?"}, + {"=?UTF-8?", "=?UTF-8?"}, + {"=?UTF-8?=", "=?UTF-8?="}, + {"=?UTF-8?Q", "=?UTF-8?Q"}, + {"=?UTF-8?Q?", "=?UTF-8?Q?"}, + {"=?UTF-8?Q?=", "=?UTF-8?Q?="}, + {"=?UTF-8?Q?A", "=?UTF-8?Q?A"}, + {"=?UTF-8?Q?A?", "=?UTF-8?Q?A?"}, + // Tests from RFC 2047 + {"=?ISO-8859-1?Q?a?=", "a"}, + {"=?ISO-8859-1?Q?a?= b", "a b"}, + {"=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=", "ab"}, + {"=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=", "ab"}, + {"=?ISO-8859-1?Q?a?= \r\n\t =?ISO-8859-1?Q?b?=", "ab"}, + {"=?ISO-8859-1?Q?a_b?=", "a b"}, + } + + for _, test := range tests { + dec := new(WordDecoder) + s, err := dec.DecodeHeader(test.src) + if err != nil { + t.Errorf("DecodeHeader(%q): %v", test.src, err) + } + if s != test.exp { + t.Errorf("DecodeHeader(%q) = %q, want %q", test.src, s, test.exp) + } + } +} + +func TestCharsetDecoder(t *testing.T) { + tests := []struct { + src string + want string + charsets []string + content []string + }{ + {"=?utf-8?b?Q2Fmw6k=?=", "Café", nil, nil}, + {"=?ISO-8859-1?Q?caf=E9?=", "café", nil, nil}, + {"=?US-ASCII?Q?foo_bar?=", "foo bar", nil, nil}, + {"=?utf-8?Q?=?=", "=?utf-8?Q?=?=", nil, nil}, + {"=?utf-8?Q?=A?=", "=?utf-8?Q?=A?=", nil, nil}, + { + "=?ISO-8859-15?Q?f=F5=F6?= =?windows-1252?Q?b=E0r?=", + "f\xf5\xf6b\xe0r", + []string{"iso-8859-15", "windows-1252"}, + []string{"f\xf5\xf6", "b\xe0r"}, + }, + } + + for _, test := range tests { + i := 0 + dec := &WordDecoder{ + CharsetReader: func(charset string, input io.Reader) (io.Reader, error) { + if charset != test.charsets[i] { + t.Errorf("DecodeHeader(%q), got charset %q, want %q", test.src, charset, test.charsets[i]) + } + content, err := ioutil.ReadAll(input) + if err != nil { + t.Errorf("DecodeHeader(%q), error in reader: %v", test.src, err) + } + got := string(content) + if got != test.content[i] { + t.Errorf("DecodeHeader(%q), got content %q, want %q", test.src, got, test.content[i]) + } + i++ + + return strings.NewReader(got), nil + }, + } + got, err := dec.DecodeHeader(test.src) + if err != nil { + t.Errorf("DecodeHeader(%q): %v", test.src, err) + } + if got != test.want { + t.Errorf("DecodeHeader(%q) = %q, want %q", test.src, got, test.want) + } + } +} + +func TestCharsetDecoderError(t *testing.T) { + dec := &WordDecoder{ + CharsetReader: func(charset string, input io.Reader) (io.Reader, error) { + return nil, errors.New("Test error") + }, + } + + if _, err := dec.DecodeHeader("=?charset?Q?foo?="); err == nil { + t.Error("DecodeHeader should return an error") + } +} + +func BenchmarkQEncodeWord(b *testing.B) { + for i := 0; i < b.N; i++ { + QEncoding.Encode("UTF-8", "¡Hola, señor!") + } +} + +func BenchmarkQDecodeWord(b *testing.B) { + dec := new(WordDecoder) + + for i := 0; i < b.N; i++ { + dec.Decode("=?utf-8?q?=C2=A1Hola,_se=C3=B1or!?=") + } +} + +func BenchmarkQDecodeHeader(b *testing.B) { + dec := new(WordDecoder) + + for i := 0; i < b.N; i++ { + dec.Decode("=?utf-8?q?=C2=A1Hola,_se=C3=B1or!?=") + } +} diff --git a/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/pool.go b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/pool.go new file mode 100644 index 00000000..24283c52 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/pool.go @@ -0,0 +1,26 @@ +// +build go1.3 + +package quotedprintable + +import ( + "bytes" + "sync" +) + +var bufPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +func getBuffer() *bytes.Buffer { + return bufPool.Get().(*bytes.Buffer) +} + +func putBuffer(buf *bytes.Buffer) { + if buf.Len() > 1024 { + return + } + buf.Reset() + bufPool.Put(buf) +} diff --git a/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go new file mode 100644 index 00000000..d335b4ab --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go @@ -0,0 +1,24 @@ +// +build !go1.3 + +package quotedprintable + +import "bytes" + +var ch = make(chan *bytes.Buffer, 32) + +func getBuffer() *bytes.Buffer { + select { + case buf := <-ch: + return buf + default: + } + return new(bytes.Buffer) +} + +func putBuffer(buf *bytes.Buffer) { + buf.Reset() + select { + case ch <- buf: + default: + } +} diff --git a/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/reader.go b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/reader.go new file mode 100644 index 00000000..955edca2 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/reader.go @@ -0,0 +1,121 @@ +// Package quotedprintable implements quoted-printable encoding as specified by +// RFC 2045. +package quotedprintable + +import ( + "bufio" + "bytes" + "fmt" + "io" +) + +// Reader is a quoted-printable decoder. +type Reader struct { + br *bufio.Reader + rerr error // last read error + line []byte // to be consumed before more of br +} + +// NewReader returns a quoted-printable reader, decoding from r. +func NewReader(r io.Reader) *Reader { + return &Reader{ + br: bufio.NewReader(r), + } +} + +func fromHex(b byte) (byte, error) { + switch { + case b >= '0' && b <= '9': + return b - '0', nil + case b >= 'A' && b <= 'F': + return b - 'A' + 10, nil + // Accept badly encoded bytes. + case b >= 'a' && b <= 'f': + return b - 'a' + 10, nil + } + return 0, fmt.Errorf("quotedprintable: invalid hex byte 0x%02x", b) +} + +func readHexByte(a, b byte) (byte, error) { + var hb, lb byte + var err error + if hb, err = fromHex(a); err != nil { + return 0, err + } + if lb, err = fromHex(b); err != nil { + return 0, err + } + return hb<<4 | lb, nil +} + +func isQPDiscardWhitespace(r rune) bool { + switch r { + case '\n', '\r', ' ', '\t': + return true + } + return false +} + +var ( + crlf = []byte("\r\n") + lf = []byte("\n") + softSuffix = []byte("=") +) + +// Read reads and decodes quoted-printable data from the underlying reader. +func (r *Reader) Read(p []byte) (n int, err error) { + // Deviations from RFC 2045: + // 1. in addition to "=\r\n", "=\n" is also treated as soft line break. + // 2. it will pass through a '\r' or '\n' not preceded by '=', consistent + // with other broken QP encoders & decoders. + for len(p) > 0 { + if len(r.line) == 0 { + if r.rerr != nil { + return n, r.rerr + } + r.line, r.rerr = r.br.ReadSlice('\n') + + // Does the line end in CRLF instead of just LF? + hasLF := bytes.HasSuffix(r.line, lf) + hasCR := bytes.HasSuffix(r.line, crlf) + wholeLine := r.line + r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace) + if bytes.HasSuffix(r.line, softSuffix) { + rightStripped := wholeLine[len(r.line):] + r.line = r.line[:len(r.line)-1] + if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) { + r.rerr = fmt.Errorf("quotedprintable: invalid bytes after =: %q", rightStripped) + } + } else if hasLF { + if hasCR { + r.line = append(r.line, '\r', '\n') + } else { + r.line = append(r.line, '\n') + } + } + continue + } + b := r.line[0] + + switch { + case b == '=': + if len(r.line[1:]) < 2 { + return n, io.ErrUnexpectedEOF + } + b, err = readHexByte(r.line[1], r.line[2]) + if err != nil { + return n, err + } + r.line = r.line[2:] // 2 of the 3; other 1 is done below + case b == '\t' || b == '\r' || b == '\n': + break + case b < ' ' || b > '~': + return n, fmt.Errorf("quotedprintable: invalid unescaped byte 0x%02x in body", b) + } + p[0] = b + p = p[1:] + r.line = r.line[1:] + n++ + } + return n, nil +} diff --git a/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/reader_test.go b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/reader_test.go new file mode 100644 index 00000000..01ba8f75 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/reader_test.go @@ -0,0 +1,200 @@ +package quotedprintable + +import ( + "bufio" + "bytes" + "errors" + "flag" + "fmt" + "io" + "os/exec" + "regexp" + "sort" + "strings" + "testing" + "time" +) + +func TestReader(t *testing.T) { + tests := []struct { + in, want string + err interface{} + }{ + {in: "", want: ""}, + {in: "foo bar", want: "foo bar"}, + {in: "foo bar=3D", want: "foo bar="}, + {in: "foo bar=3d", want: "foo bar="}, // lax. + {in: "foo bar=\n", want: "foo bar"}, + {in: "foo bar\n", want: "foo bar\n"}, // somewhat lax. + {in: "foo bar=0", want: "foo bar", err: io.ErrUnexpectedEOF}, + {in: "foo bar=0D=0A", want: "foo bar\r\n"}, + {in: " A B \r\n C ", want: " A B\r\n C"}, + {in: " A B =\r\n C ", want: " A B C"}, + {in: " A B =\n C ", want: " A B C"}, // lax. treating LF as CRLF + {in: "foo=\nbar", want: "foobar"}, + {in: "foo\x00bar", want: "foo", err: "quotedprintable: invalid unescaped byte 0x00 in body"}, + {in: "foo bar\xff", want: "foo bar", err: "quotedprintable: invalid unescaped byte 0xff in body"}, + + // Equal sign. + {in: "=3D30\n", want: "=30\n"}, + {in: "=00=FF0=\n", want: "\x00\xff0"}, + + // Trailing whitespace + {in: "foo \n", want: "foo\n"}, + {in: "foo \n\nfoo =\n\nfoo=20\n\n", want: "foo\n\nfoo \nfoo \n\n"}, + + // Tests that we allow bare \n and \r through, despite it being strictly + // not permitted per RFC 2045, Section 6.7 Page 22 bullet (4). + {in: "foo\nbar", want: "foo\nbar"}, + {in: "foo\rbar", want: "foo\rbar"}, + {in: "foo\r\nbar", want: "foo\r\nbar"}, + + // Different types of soft line-breaks. + {in: "foo=\r\nbar", want: "foobar"}, + {in: "foo=\nbar", want: "foobar"}, + {in: "foo=\rbar", want: "foo", err: "quotedprintable: invalid hex byte 0x0d"}, + {in: "foo=\r\r\r \nbar", want: "foo", err: `quotedprintable: invalid bytes after =: "\r\r\r \n"`}, + + // Example from RFC 2045: + {in: "Now's the time =\n" + "for all folk to come=\n" + " to the aid of their country.", + want: "Now's the time for all folk to come to the aid of their country."}, + } + for _, tt := range tests { + var buf bytes.Buffer + _, err := io.Copy(&buf, NewReader(strings.NewReader(tt.in))) + if got := buf.String(); got != tt.want { + t.Errorf("for %q, got %q; want %q", tt.in, got, tt.want) + } + switch verr := tt.err.(type) { + case nil: + if err != nil { + t.Errorf("for %q, got unexpected error: %v", tt.in, err) + } + case string: + if got := fmt.Sprint(err); got != verr { + t.Errorf("for %q, got error %q; want %q", tt.in, got, verr) + } + case error: + if err != verr { + t.Errorf("for %q, got error %q; want %q", tt.in, err, verr) + } + } + } + +} + +func everySequence(base, alpha string, length int, fn func(string)) { + if len(base) == length { + fn(base) + return + } + for i := 0; i < len(alpha); i++ { + everySequence(base+alpha[i:i+1], alpha, length, fn) + } +} + +var useQprint = flag.Bool("qprint", false, "Compare against the 'qprint' program.") + +var badSoftRx = regexp.MustCompile(`=([^\r\n]+?\n)|([^\r\n]+$)|(\r$)|(\r[^\n]+\n)|( \r\n)`) + +func TestExhaustive(t *testing.T) { + if *useQprint { + _, err := exec.LookPath("qprint") + if err != nil { + t.Fatalf("Error looking for qprint: %v", err) + } + } + + var buf bytes.Buffer + res := make(map[string]int) + everySequence("", "0A \r\n=", 6, func(s string) { + if strings.HasSuffix(s, "=") || strings.Contains(s, "==") { + return + } + buf.Reset() + _, err := io.Copy(&buf, NewReader(strings.NewReader(s))) + if err != nil { + errStr := err.Error() + if strings.Contains(errStr, "invalid bytes after =:") { + errStr = "invalid bytes after =" + } + res[errStr]++ + if strings.Contains(errStr, "invalid hex byte ") { + if strings.HasSuffix(errStr, "0x20") && (strings.Contains(s, "=0 ") || strings.Contains(s, "=A ") || strings.Contains(s, "= ")) { + return + } + if strings.HasSuffix(errStr, "0x3d") && (strings.Contains(s, "=0=") || strings.Contains(s, "=A=")) { + return + } + if strings.HasSuffix(errStr, "0x0a") || strings.HasSuffix(errStr, "0x0d") { + // bunch of cases; since whitespace at the end of a line before \n is removed. + return + } + } + if strings.Contains(errStr, "unexpected EOF") { + return + } + if errStr == "invalid bytes after =" && badSoftRx.MatchString(s) { + return + } + t.Errorf("decode(%q) = %v", s, err) + return + } + if *useQprint { + cmd := exec.Command("qprint", "-d") + cmd.Stdin = strings.NewReader(s) + stderr, err := cmd.StderrPipe() + if err != nil { + panic(err) + } + qpres := make(chan interface{}, 2) + go func() { + br := bufio.NewReader(stderr) + s, _ := br.ReadString('\n') + if s != "" { + qpres <- errors.New(s) + if cmd.Process != nil { + // It can get stuck on invalid input, like: + // echo -n "0000= " | qprint -d + cmd.Process.Kill() + } + } + }() + go func() { + want, err := cmd.Output() + if err == nil { + qpres <- want + } + }() + select { + case got := <-qpres: + if want, ok := got.([]byte); ok { + if string(want) != buf.String() { + t.Errorf("go decode(%q) = %q; qprint = %q", s, want, buf.String()) + } + } else { + t.Logf("qprint -d(%q) = %v", s, got) + } + case <-time.After(5 * time.Second): + t.Logf("qprint timeout on %q", s) + } + } + res["OK"]++ + }) + var outcomes []string + for k, v := range res { + outcomes = append(outcomes, fmt.Sprintf("%v: %d", k, v)) + } + sort.Strings(outcomes) + got := strings.Join(outcomes, "\n") + want := `OK: 21576 +invalid bytes after =: 3397 +quotedprintable: invalid hex byte 0x0a: 1400 +quotedprintable: invalid hex byte 0x0d: 2700 +quotedprintable: invalid hex byte 0x20: 2490 +quotedprintable: invalid hex byte 0x3d: 440 +unexpected EOF: 3122` + if got != want { + t.Errorf("Got:\n%s\nWant:\n%s", got, want) + } +} diff --git a/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/writer.go b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/writer.go new file mode 100644 index 00000000..43359d51 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/writer.go @@ -0,0 +1,166 @@ +package quotedprintable + +import "io" + +const lineMaxLen = 76 + +// A Writer is a quoted-printable writer that implements io.WriteCloser. +type Writer struct { + // Binary mode treats the writer's input as pure binary and processes end of + // line bytes as binary data. + Binary bool + + w io.Writer + i int + line [78]byte + cr bool +} + +// NewWriter returns a new Writer that writes to w. +func NewWriter(w io.Writer) *Writer { + return &Writer{w: w} +} + +// Write encodes p using quoted-printable encoding and writes it to the +// underlying io.Writer. It limits line length to 76 characters. The encoded +// bytes are not necessarily flushed until the Writer is closed. +func (w *Writer) Write(p []byte) (n int, err error) { + for i, b := range p { + switch { + // Simple writes are done in batch. + case b >= '!' && b <= '~' && b != '=': + continue + case isWhitespace(b) || !w.Binary && (b == '\n' || b == '\r'): + continue + } + + if i > n { + if err := w.write(p[n:i]); err != nil { + return n, err + } + n = i + } + + if err := w.encode(b); err != nil { + return n, err + } + n++ + } + + if n == len(p) { + return n, nil + } + + if err := w.write(p[n:]); err != nil { + return n, err + } + + return len(p), nil +} + +// Close closes the Writer, flushing any unwritten data to the underlying +// io.Writer, but does not close the underlying io.Writer. +func (w *Writer) Close() error { + if err := w.checkLastByte(); err != nil { + return err + } + + return w.flush() +} + +// write limits text encoded in quoted-printable to 76 characters per line. +func (w *Writer) write(p []byte) error { + for _, b := range p { + if b == '\n' || b == '\r' { + // If the previous byte was \r, the CRLF has already been inserted. + if w.cr && b == '\n' { + w.cr = false + continue + } + + if b == '\r' { + w.cr = true + } + + if err := w.checkLastByte(); err != nil { + return err + } + if err := w.insertCRLF(); err != nil { + return err + } + continue + } + + if w.i == lineMaxLen-1 { + if err := w.insertSoftLineBreak(); err != nil { + return err + } + } + + w.line[w.i] = b + w.i++ + w.cr = false + } + + return nil +} + +func (w *Writer) encode(b byte) error { + if lineMaxLen-1-w.i < 3 { + if err := w.insertSoftLineBreak(); err != nil { + return err + } + } + + w.line[w.i] = '=' + w.line[w.i+1] = upperhex[b>>4] + w.line[w.i+2] = upperhex[b&0x0f] + w.i += 3 + + return nil +} + +// checkLastByte encodes the last buffered byte if it is a space or a tab. +func (w *Writer) checkLastByte() error { + if w.i == 0 { + return nil + } + + b := w.line[w.i-1] + if isWhitespace(b) { + w.i-- + if err := w.encode(b); err != nil { + return err + } + } + + return nil +} + +func (w *Writer) insertSoftLineBreak() error { + w.line[w.i] = '=' + w.i++ + + return w.insertCRLF() +} + +func (w *Writer) insertCRLF() error { + w.line[w.i] = '\r' + w.line[w.i+1] = '\n' + w.i += 2 + + return w.flush() +} + +func (w *Writer) flush() error { + if _, err := w.w.Write(w.line[:w.i]); err != nil { + return err + } + + w.i = 0 + return nil +} + +func isWhitespace(b byte) bool { + return b == ' ' || b == '\t' +} diff --git a/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/writer_test.go b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/writer_test.go new file mode 100644 index 00000000..ed850728 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/alexcesaro/quotedprintable.v3/writer_test.go @@ -0,0 +1,154 @@ +package quotedprintable + +import ( + "bytes" + "io/ioutil" + "strings" + "testing" +) + +func TestWriter(t *testing.T) { + testWriter(t, false) +} + +func TestWriterBinary(t *testing.T) { + testWriter(t, true) +} + +func testWriter(t *testing.T, binary bool) { + tests := []struct { + in, want, wantB string + }{ + {in: "", want: ""}, + {in: "foo bar", want: "foo bar"}, + {in: "foo bar=", want: "foo bar=3D"}, + {in: "foo bar\r", want: "foo bar\r\n", wantB: "foo bar=0D"}, + {in: "foo bar\r\r", want: "foo bar\r\n\r\n", wantB: "foo bar=0D=0D"}, + {in: "foo bar\n", want: "foo bar\r\n", wantB: "foo bar=0A"}, + {in: "foo bar\r\n", want: "foo bar\r\n", wantB: "foo bar=0D=0A"}, + {in: "foo bar\r\r\n", want: "foo bar\r\n\r\n", wantB: "foo bar=0D=0D=0A"}, + {in: "foo bar ", want: "foo bar=20"}, + {in: "foo bar\t", want: "foo bar=09"}, + {in: "foo bar ", want: "foo bar =20"}, + {in: "foo bar \n", want: "foo bar=20\r\n", wantB: "foo bar =0A"}, + {in: "foo bar \r", want: "foo bar=20\r\n", wantB: "foo bar =0D"}, + {in: "foo bar \r\n", want: "foo bar=20\r\n", wantB: "foo bar =0D=0A"}, + {in: "foo bar \n", want: "foo bar =20\r\n", wantB: "foo bar =0A"}, + {in: "foo bar \n ", want: "foo bar =20\r\n=20", wantB: "foo bar =0A=20"}, + {in: "¡Hola Señor!", want: "=C2=A1Hola Se=C3=B1or!"}, + { + in: "\t !\"#$%&'()*+,-./ :;<>?@[\\]^_`{|}~", + want: "\t !\"#$%&'()*+,-./ :;<>?@[\\]^_`{|}~", + }, + { + in: strings.Repeat("a", 75), + want: strings.Repeat("a", 75), + }, + { + in: strings.Repeat("a", 76), + want: strings.Repeat("a", 75) + "=\r\na", + }, + { + in: strings.Repeat("a", 72) + "=", + want: strings.Repeat("a", 72) + "=3D", + }, + { + in: strings.Repeat("a", 73) + "=", + want: strings.Repeat("a", 73) + "=\r\n=3D", + }, + { + in: strings.Repeat("a", 74) + "=", + want: strings.Repeat("a", 74) + "=\r\n=3D", + }, + { + in: strings.Repeat("a", 75) + "=", + want: strings.Repeat("a", 75) + "=\r\n=3D", + }, + { + in: strings.Repeat(" ", 73), + want: strings.Repeat(" ", 72) + "=20", + }, + { + in: strings.Repeat(" ", 74), + want: strings.Repeat(" ", 73) + "=\r\n=20", + }, + { + in: strings.Repeat(" ", 75), + want: strings.Repeat(" ", 74) + "=\r\n=20", + }, + { + in: strings.Repeat(" ", 76), + want: strings.Repeat(" ", 75) + "=\r\n=20", + }, + { + in: strings.Repeat(" ", 77), + want: strings.Repeat(" ", 75) + "=\r\n =20", + }, + } + + for _, tt := range tests { + buf := new(bytes.Buffer) + w := NewWriter(buf) + + want := tt.want + if binary { + w.Binary = true + if tt.wantB != "" { + want = tt.wantB + } + } + + if _, err := w.Write([]byte(tt.in)); err != nil { + t.Errorf("Write(%q): %v", tt.in, err) + continue + } + if err := w.Close(); err != nil { + t.Errorf("Close(): %v", err) + continue + } + got := buf.String() + if got != want { + t.Errorf("Write(%q), got:\n%q\nwant:\n%q", tt.in, got, want) + } + } +} + +func TestRoundTrip(t *testing.T) { + buf := new(bytes.Buffer) + w := NewWriter(buf) + if _, err := w.Write(testMsg); err != nil { + t.Fatalf("Write: %v", err) + } + if err := w.Close(); err != nil { + t.Fatalf("Close: %v", err) + } + + r := NewReader(buf) + gotBytes, err := ioutil.ReadAll(r) + if err != nil { + t.Fatalf("Error while reading from Reader: %v", err) + } + got := string(gotBytes) + if got != string(testMsg) { + t.Errorf("Encoding and decoding changed the message, got:\n%s", got) + } +} + +// From http://fr.wikipedia.org/wiki/Quoted-Printable +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" + + "\r\n" + + "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" + + "\r\n" + + "L'encodage Quoted-Printable permet de remédier à ce problème, en procédant de la manière suivante :\r\n" + + "\r\n" + + "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" + + "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" + + "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") + +func BenchmarkWriter(b *testing.B) { + for i := 0; i < b.N; i++ { + w := NewWriter(ioutil.Discard) + w.Write(testMsg) + w.Close() + } +}