TBR: PROTOTYPE(discovery): update third_party
Change-Id: Ibaef4fca7a916e88975266acc1d528769dc2f489
diff --git a/go/src/gopkg.in/alexcesaro/quotedprintable.v2/LICENSE b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/LICENSE
new file mode 100644
index 0000000..5f5c12a
--- /dev/null
+++ b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/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/go/src/gopkg.in/alexcesaro/quotedprintable.v2/README.google b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/README.google
new file mode 100644
index 0000000..9ace3f7
--- /dev/null
+++ b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/README.google
@@ -0,0 +1,10 @@
+URL: https://github.com/alexcesaro/quotedprintable/archive/9b4a113f96b3263b5223fe9c5fd1d1115811b343.zip
+Version: 44d228784e161379153583e31bcc32d9a25796e3
+License: The MIT License (MIT)
+License File: LICENSE
+
+Description:
+A Go package concerning quoted-printable encoding.
+
+Local Modifications:
+No modifications.
diff --git a/go/src/gopkg.in/alexcesaro/quotedprintable.v2/README.md b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/README.md
new file mode 100644
index 0000000..afc82b0
--- /dev/null
+++ b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/README.md
@@ -0,0 +1,20 @@
+# quotedprintable
+
+## Introduction
+
+Package quotedprintable implements quoted-printable and message header encoding
+as specified by RFC 2045 and RFC 2047.
+
+It is similar to the package currently being developed in the standard library: https://codereview.appspot.com/132680044/
+
+It requires Go 1.3 or newer.
+
+Some links:
+ - [Post on golang-dev](https://groups.google.com/d/topic/golang-dev/PK_ICQNJTmg/discussion)
+ - [issue on the bug traker](https://code.google.com/p/go/issues/detail?id=4943)
+ - [the former code review](https://codereview.appspot.com/101330049/)
+
+
+## Documentation
+
+https://godoc.org/gopkg.in/alexcesaro/quotedprintable.v2
diff --git a/go/src/gopkg.in/alexcesaro/quotedprintable.v2/header.go b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/header.go
new file mode 100644
index 0000000..cd16f2c
--- /dev/null
+++ b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/header.go
@@ -0,0 +1,407 @@
+// Copyright 2014 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.
+
+// This file defines encoding and decoding functions for encoded-words
+// as defined in RFC 2047.
+
+package quotedprintable
+
+import (
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "strings"
+ "sync"
+ "unicode/utf8"
+
+ "gopkg.in/alexcesaro/quotedprintable.v2/internal"
+)
+
+// Encoding represents the encoding used in encoded-words. It must be one of the
+// two encodings defined in RFC 2047 ("B" or "Q" encoding).
+type Encoding string
+
+const (
+ // Q represents the Q-encoding defined in RFC 2047.
+ Q Encoding = "Q"
+ // B represents the Base64 encoding defined in RFC 2045.
+ B Encoding = "B"
+)
+
+// A HeaderEncoder is an RFC 2047 encoded-word encoder.
+type HeaderEncoder struct {
+ charset string
+ encoding Encoding
+ splitWords bool
+}
+
+const maxEncodedWordLen = 75 // As defined in RFC 2047, section 2
+
+// NewHeaderEncoder returns a new HeaderEncoder to encode strings in the
+// specified charset.
+func (enc Encoding) NewHeaderEncoder(charset string) *HeaderEncoder {
+ // We automatically split encoded-words only when the charset is UTF-8
+ // because since multi-octet character must not be split across adjacent
+ // encoded-words (see RFC 2047, section 5) there is no way to split words
+ // without knowing how the charset works.
+ splitWords := strings.ToUpper(charset) == "UTF-8"
+
+ return &HeaderEncoder{charset, enc, splitWords}
+}
+
+// Encode encodes a string to be used as a MIME header value.
+func (e *HeaderEncoder) Encode(s string) string {
+ return e.encodeWord(s)
+}
+
+// NeedsEncoding returns whether the given header content needs to be encoded
+// into an encoded-words.
+func NeedsEncoding(s string) bool {
+ for i := 0; i < len(s); i++ {
+ b := s[i]
+ if (b > '~' || b < ' ') && b != '\t' {
+ return true
+ }
+ }
+
+ return false
+}
+
+type bufPool struct {
+ sync.Pool
+}
+
+func (b *bufPool) GetBuffer() *bytes.Buffer {
+ return bufFree.Get().(*bytes.Buffer)
+}
+
+func (b *bufPool) PutBuffer(buf *bytes.Buffer) {
+ if buf.Len() > 1024 {
+ return
+ }
+ buf.Reset()
+ b.Put(buf)
+}
+
+var bufFree = &bufPool{
+ sync.Pool{
+ New: func() interface{} {
+ return new(bytes.Buffer)
+ },
+ },
+}
+
+// encodeWord encodes a string into an encoded-word.
+func (e *HeaderEncoder) encodeWord(s string) string {
+ buf := bufFree.GetBuffer()
+ defer bufFree.PutBuffer(buf)
+
+ e.openWord(buf)
+
+ switch {
+ case e.encoding == B:
+ e.encodeWordB(buf, s)
+ default:
+ e.encodeWordQ(buf, s)
+ }
+
+ e.closeWord(buf)
+ return buf.String()
+}
+
+func (e *HeaderEncoder) encodeWordB(buf *bytes.Buffer, s string) {
+ maxLen := maxEncodedWordLen - buf.Len() - 2
+ if !e.splitWords || base64.StdEncoding.EncodedLen(len(s)) <= maxLen {
+ buf.WriteString(base64.StdEncoding.EncodeToString([]byte(s)))
+ return
+ }
+
+ v := []byte(s)
+ var n, last, runeSize int
+ for i := 0; i < len(s); i += runeSize {
+ runeSize = getRuneSize(s, i)
+
+ if base64.StdEncoding.EncodedLen(n+runeSize) <= maxLen {
+ n += runeSize
+ } else {
+ buf.WriteString(base64.StdEncoding.EncodeToString(v[last:i]))
+ e.splitWord(buf)
+ last = i
+ n = runeSize
+ }
+ }
+ buf.WriteString(base64.StdEncoding.EncodeToString(v[last:]))
+}
+
+func (e *HeaderEncoder) encodeWordQ(buf *bytes.Buffer, s string) {
+ if !e.splitWords {
+ for i := 0; i < len(s); i++ {
+ writeQ(buf, s[i])
+ }
+ return
+ }
+
+ var runeSize int
+ n := buf.Len()
+ for i := 0; i < len(s); i += runeSize {
+ b := s[i]
+ var encLen int
+ if b >= ' ' && b <= '~' && b != '=' && b != '?' && b != '_' {
+ encLen, runeSize = 1, 1
+ } else {
+ runeSize = getRuneSize(s, i)
+ encLen = 3 * runeSize
+ }
+
+ // We remove 2 to let spaces for closing chars "?="
+ if n+encLen > maxEncodedWordLen-2 {
+ n = e.splitWord(buf)
+ }
+ writeQString(buf, s[i:i+runeSize])
+ n += encLen
+ }
+}
+
+func (e *HeaderEncoder) openWord(buf *bytes.Buffer) int {
+ n := buf.Len()
+ buf.WriteString("=?")
+ buf.WriteString(e.charset)
+ buf.WriteByte('?')
+ buf.WriteString(string(e.encoding))
+ buf.WriteByte('?')
+
+ return buf.Len() - n
+}
+
+func (e *HeaderEncoder) closeWord(buf *bytes.Buffer) {
+ buf.WriteString("?=")
+}
+
+func (e *HeaderEncoder) splitWord(buf *bytes.Buffer) int {
+ e.closeWord(buf)
+ buf.WriteString("\r\n ")
+ return e.openWord(buf)
+}
+
+func getRuneSize(s string, i int) int {
+ runeSize := 1
+ for i+runeSize < len(s) && !utf8.RuneStart(s[i+runeSize]) {
+ runeSize++
+ }
+
+ return runeSize
+}
+
+func writeQString(buf *bytes.Buffer, s string) {
+ for i := 0; i < len(s); i++ {
+ writeQ(buf, s[i])
+ }
+}
+
+func writeQ(buf *bytes.Buffer, b byte) {
+ switch {
+ case b == ' ':
+ buf.WriteByte('_')
+ case b >= '!' && b <= '~' && b != '=' && b != '?' && b != '_':
+ buf.WriteByte(b)
+ default:
+ enc := make([]byte, 3)
+ internal.EncodeByte(enc, b)
+ buf.Write(enc)
+ }
+}
+
+// DecodeHeader decodes a MIME header by decoding all encoded-words of the
+// header. The returned text is encoded in the returned charset. Text is not
+// necessarily encoded in UTF-8. Returned charset is always upper case. This
+// function does not support decoding headers with multiple encoded-words
+// using different charsets.
+func DecodeHeader(header string) (text, charset string, err error) {
+ buf := bufFree.GetBuffer()
+ defer bufFree.PutBuffer(buf)
+
+ for {
+ i := strings.IndexByte(header, '=')
+ if i == -1 {
+ break
+ }
+ if i > 0 {
+ buf.WriteString(header[:i])
+ header = header[i:]
+ }
+
+ word := getEncodedWord(header)
+ if word == "" {
+ buf.WriteByte('=')
+ header = header[1:]
+ continue
+ }
+
+ for {
+ dec, wordCharset, err := decodeWord(word)
+ if err != nil {
+ buf.WriteString(word)
+ header = header[len(word):]
+ break
+ }
+ if charset == "" {
+ charset = wordCharset
+ } else if charset != wordCharset {
+ return "", "", fmt.Errorf("quotedprintable: multiple charsets in header are not supported: %q and %q used", charset, wordCharset)
+ }
+ buf.Write(dec)
+ header = header[len(word):]
+
+ // White-space and newline characters separating two encoded-words
+ // must be deleted.
+ var j int
+ for j = 0; j < len(header); j++ {
+ b := header[j]
+ if b != ' ' && b != '\t' && b != '\n' && b != '\r' {
+ break
+ }
+ }
+ if j == 0 {
+ // If there are no white-space characters following the current
+ // encoded-word there is nothing special to do.
+ break
+ }
+ word = getEncodedWord(header[j:])
+ if word == "" {
+ break
+ }
+ header = header[j:]
+ }
+ }
+ buf.WriteString(header)
+
+ return buf.String(), charset, nil
+}
+
+func getEncodedWord(s string) string {
+ if len(s) < 2 {
+ return ""
+ }
+ if s[0] != '=' {
+ return ""
+ }
+ if s[1] != '?' {
+ return ""
+ }
+
+ n := 2
+ for {
+ if n >= len(s) {
+ return ""
+ }
+
+ b := s[n]
+ if (b < '0' || b > '9') &&
+ (b < 'A' || b > 'Z') &&
+ (b < 'a' || b > 'z') &&
+ b != '-' {
+ break
+ }
+
+ n++
+ }
+ if s[n] != '?' {
+ return ""
+ }
+
+ if n+2 >= len(s) {
+ return ""
+ }
+ b := s[n+1]
+ if b != 'Q' && b != 'B' && b != 'q' && b != 'b' {
+ return ""
+ }
+ if s[n+2] != '?' {
+ return ""
+ }
+
+ n = n + 3
+ for {
+ if n >= len(s) {
+ return ""
+ }
+
+ if s[n] < ' ' || s[n] > '~' {
+ return ""
+ }
+ if s[n] == '?' {
+ n++
+ break
+ }
+ n++
+ }
+ if n >= len(s) || s[n] != '=' {
+ return ""
+ }
+
+ return s[0 : n+1]
+}
+
+func decodeWord(s string) (text []byte, charset string, err error) {
+ fields := strings.Split(s, "?")
+ if len(fields) != 5 || fields[0] != "=" || fields[4] != "=" || len(fields[2]) != 1 {
+ return []byte(s), "", nil
+ }
+
+ charset, enc, src := fields[1], fields[2], fields[3]
+
+ var dec []byte
+ switch Encoding(strings.ToUpper(enc)) {
+ case B:
+ if dec, err = base64.StdEncoding.DecodeString(src); err != nil {
+ return dec, charset, err
+ }
+ case Q:
+ if dec, err = qDecode(src); err != nil {
+ return dec, charset, err
+ }
+ default:
+ return []byte(""), charset, fmt.Errorf("quotedprintable: RFC 2047 encoding not supported: %q", enc)
+ }
+
+ return dec, strings.ToUpper(charset), nil
+}
+
+// qDecode decodes a Q encoded string.
+func qDecode(s string) ([]byte, error) {
+ length := len(s)
+ for i := 0; i < len(s); i++ {
+ if s[i] == '=' {
+ length -= 2
+ i += 2
+ }
+ }
+ dec := make([]byte, length)
+
+ 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 []byte(""), io.ErrUnexpectedEOF
+ }
+ buf, err := internal.ReadHexByte([]byte(s[i+1:]))
+ if err != nil {
+ return []byte(""), err
+ }
+ dec[n] = buf
+ i += 2
+ case (c >= ' ' && c <= '~') || c == '\n' || c == '\r' || c == '\t':
+ dec[n] = c
+ default:
+ return []byte(""), fmt.Errorf("quotedprintable: invalid unescaped byte %#02x in Q encoded string at byte %d", c, i)
+ }
+ n++
+ }
+
+ return dec, nil
+}
diff --git a/go/src/gopkg.in/alexcesaro/quotedprintable.v2/header_test.go b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/header_test.go
new file mode 100644
index 0000000..8612ccb
--- /dev/null
+++ b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/header_test.go
@@ -0,0 +1,115 @@
+// Copyright 2014 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 quotedprintable
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+)
+
+func ExampleEncodeHeader() {
+ enc := Q.NewHeaderEncoder("UTF-8")
+ fmt.Println(enc.Encode("Coffee"))
+ fmt.Println(enc.Encode("Café"))
+ // Output:
+ // =?UTF-8?Q?Coffee?=
+ // =?UTF-8?Q?Caf=C3=A9?=
+}
+
+func ExampleNewHeaderEncoder() {
+ e := B.NewHeaderEncoder("UTF-8")
+ fmt.Printf(e.Encode("Caf\xc3"))
+ // Output: =?UTF-8?B?Q2Fmww==?=
+}
+
+func ExampleDecodeHeader() {
+ // text is not encoded in UTF-8 but in ISO-8859-1
+ text, charset, err := DecodeHeader("=?ISO-8859-1?Q?Caf=C3?=")
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("Text: %q, charset: %q", text, charset)
+ // Output: Text: "Caf\xc3", charset: "ISO-8859-1"
+}
+
+func TestEncodeHeader(t *testing.T) {
+ utf8, iso88591 := "UTF-8", "iso-8859-1"
+ tests := []struct {
+ charset string
+ encoding Encoding
+ src, exp string
+ }{
+ {utf8, Q, "François-Jérôme", "=?UTF-8?Q?Fran=C3=A7ois-J=C3=A9r=C3=B4me?="},
+ {utf8, B, "André", "=?UTF-8?B?QW5kcsOp?="},
+ {iso88591, Q, "Rapha\xebl Dupont", "=?iso-8859-1?Q?Rapha=EBl_Dupont?="},
+ {utf8, Q, "A", "=?UTF-8?Q?A?="},
+ {utf8, Q, "An 'encoded-word' may not be more than 75 characters long, including 'charset', 'encoding', 'encoded-text', and delimiters. ©", "=?UTF-8?Q?An_'encoded-word'_may_not_be_more_than_75_characters_long,_incl?=\r\n =?UTF-8?Q?uding_'charset',_'encoding',_'encoded-text',_and_delimiters._?=\r\n =?UTF-8?Q?=C2=A9?="},
+ {utf8, Q, strings.Repeat("0", 62) + "é", "=?UTF-8?Q?" + strings.Repeat("0", 62) + "?=\r\n =?UTF-8?Q?=C3=A9?="},
+ {utf8, B, strings.Repeat("é", 23), "=?UTF-8?B?w6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6k=?=\r\n =?UTF-8?B?w6k=?="},
+ }
+
+ for _, test := range tests {
+ e := test.encoding.NewHeaderEncoder(test.charset)
+ if s := e.Encode(test.src); s != test.exp {
+ t.Errorf("Encode(%q) = %q, want %q", test.src, s, test.exp)
+ }
+ }
+}
+
+func TestDecodeHeader(t *testing.T) {
+ tests := []struct {
+ src, exp, charset string
+ isError bool
+ }{
+ {"=?UTF-8?Q?Fran=C3=A7ois-J=C3=A9r=C3=B4me?=", "François-Jérôme", "UTF-8", false},
+ {"=?UTF-8?q?ascii?=", "ascii", "UTF-8", false},
+ {"=?utf-8?B?QW5kcsOp?=", "André", "UTF-8", false},
+ {"=?ISO-8859-1?Q?Rapha=EBl_Dupont?=", "Rapha\xebl Dupont", "ISO-8859-1", false},
+ {"Jean", "Jean", "", false},
+ {"=?UTF-8?A?Test?=", "=?UTF-8?A?Test?=", "", false},
+ {"=?UTF-8?Q?A=B?=", "=?UTF-8?Q?A=B?=", "", false},
+ {"=?UTF-8?Q?=A?=", "=?UTF-8?Q?=A?=", "", false},
+ {"=?UTF-8?A?A?=", "=?UTF-8?A?A?=", "", false},
+ // Tests from RFC 2047
+ {"=?ISO-8859-1?Q?a?=", "a", "ISO-8859-1", false},
+ {"=?ISO-8859-1?Q?a?= b", "a b", "ISO-8859-1", false},
+ {"=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=", "ab", "ISO-8859-1", false},
+ {"=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=", "ab", "ISO-8859-1", false},
+ {"=?ISO-8859-1?Q?a?= \r\n\t =?ISO-8859-1?Q?b?=", "ab", "ISO-8859-1", false},
+ {"=?ISO-8859-1?Q?a_b?=", "a b", "ISO-8859-1", false},
+ {"=?ISO-8859-1?Q?a?= =?ISO-8859-2?Q?_b?=", "", "", true},
+ }
+
+ for _, test := range tests {
+ s, charset, err := DecodeHeader(test.src)
+ if test.isError && err == nil {
+ t.Errorf("DecodeHeader(%q) should return an error", test.src)
+ }
+ if !test.isError && err != nil {
+ t.Errorf("DecodeHeader(%q): %v", test.src, err)
+ }
+ if s != test.exp || charset != test.charset {
+ t.Errorf("DecodeHeader(%q) = %q (charset=%q), want %q (charset=%q)", test.src, s, charset, test.exp, test.charset)
+ }
+ }
+}
+
+var testHeader = "¡Hola, señor!"
+
+func BenchmarkQEncode(b *testing.B) {
+ enc := Q.NewHeaderEncoder("UTF-8")
+ for i := 0; i < b.N; i++ {
+ enc.Encode(testHeader)
+ }
+}
+
+func BenchmarkBEncode(b *testing.B) {
+ encoder := &HeaderEncoder{charset: "UTF-8", encoding: B, splitWords: true}
+
+ for i := 0; i < b.N; i++ {
+ encoder.Encode(testHeader)
+ }
+}
diff --git a/go/src/gopkg.in/alexcesaro/quotedprintable.v2/internal/quotedprintable.go b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/internal/quotedprintable.go
new file mode 100644
index 0000000..8b08bd0
--- /dev/null
+++ b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/internal/quotedprintable.go
@@ -0,0 +1,43 @@
+// Copyright 2014 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 internal contains quoted-printable internals shared by mime and
+// mime/quotedprintable.
+package internal
+
+import "fmt"
+
+// EncodeByte encodes a byte using the quoted-printable encoding.
+func EncodeByte(dst []byte, b byte) {
+ dst[0] = '='
+ dst[1] = upperhex[b>>4]
+ dst[2] = upperhex[b&0x0f]
+}
+
+const upperhex = "0123456789ABCDEF"
+
+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 quoted-printable hex byte %#02x", b)
+}
+
+// ReadHexByte returns the byte represented by an hexadecimal byte slice of length 2.
+func ReadHexByte(v []byte) (b byte, err error) {
+ var hb, lb byte
+ if hb, err = fromHex(v[0]); err != nil {
+ return 0, err
+ }
+ if lb, err = fromHex(v[1]); err != nil {
+ return 0, err
+ }
+ return hb<<4 | lb, nil
+}
diff --git a/go/src/gopkg.in/alexcesaro/quotedprintable.v2/quotedprintable.go b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/quotedprintable.go
new file mode 100644
index 0000000..2a48283
--- /dev/null
+++ b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/quotedprintable.go
@@ -0,0 +1,223 @@
+// Copyright 2014 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.
+
+// This file defines quoted-printable decoders and encoders, as specified in RFC
+// 2045.
+// Deviations:
+// 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 and decoders.
+
+// Package quotedprintable implements quoted-printable and message header
+// encoding as specified by RFC 2045 and RFC 2047.
+package quotedprintable
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "io"
+
+ "gopkg.in/alexcesaro/quotedprintable.v2/internal"
+)
+
+// Encode encodes src into at most MaxEncodedLen(len(src)) bytes to dst,
+// returning the actual number of bytes written to dst.
+func Encode(dst, src []byte) int {
+ n := 0
+ for i, c := range src {
+ switch {
+ case c != '=' && (c >= '!' && c <= '~' || c == '\n' || c == '\r'):
+ dst[n] = c
+ n++
+ case c == ' ' || c == '\t':
+ if isLastChar(i, src) {
+ internal.EncodeByte(dst[n:], c)
+ n += 3
+ } else {
+ dst[n] = c
+ n++
+ }
+ default:
+ internal.EncodeByte(dst[n:], c)
+ n += 3
+ }
+ }
+
+ return n
+}
+
+// isLastChar returns true if byte i is the last character of the line.
+func isLastChar(i int, src []byte) bool {
+ return i == len(src)-1 ||
+ (i < len(src)-1 && src[i+1] == '\n') ||
+ (i < len(src)-2 && src[i+1] == '\r' && src[i+2] == '\n')
+}
+
+// EncodeToString returns the quoted-printable encoding of src.
+func EncodeToString(src []byte) string {
+ dbuf := make([]byte, MaxEncodedLen(len(src)))
+ n := Encode(dbuf, src)
+ return string(dbuf[:n])
+}
+
+// MaxEncodedLen returns the maximum length of an encoding of n source bytes.
+func MaxEncodedLen(n int) int { return 3 * n }
+
+// NewEncoder returns a new quoted-printable stream encoder. Data written to the
+// returned writer will be encoded and then written to w.
+func NewEncoder(w io.Writer) io.Writer {
+ return &encoder{w}
+}
+
+type encoder struct {
+ w io.Writer
+}
+
+func (e *encoder) Write(p []byte) (int, error) {
+ dbuf := make([]byte, MaxEncodedLen(len(p)))
+ n := Encode(dbuf, p)
+ n, err := e.w.Write(dbuf[:n])
+ if err != nil {
+ nn := 0
+ for i := 0; i < n; i++ {
+ if dbuf[i] == '=' {
+ if i+2 >= n {
+ break
+ }
+ i += 2
+ }
+ nn++
+ }
+ return nn, err
+ }
+
+ return len(p), nil
+}
+
+// Decode decodes src into at most MaxDecodedLen(len(src)) bytes to dst,
+// returning the actual number of bytes written to dst.
+func Decode(dst, src []byte) (n int, err error) {
+ var eol, trimLen, eolLen int
+ for i := 0; i < len(src); i++ {
+ if i == eol {
+ eol = bytes.IndexByte(src[i:], '\n') + i + 1
+ if eol == i {
+ eol = len(src)
+ eolLen = 0
+ } else if eol-2 >= i && src[eol-2] == '\r' {
+ eolLen = 2
+ } else {
+ eolLen = 1
+ }
+
+ // Count the number of bytes to trim
+ trimLen = 0
+ for {
+ if trimLen == eol-eolLen-i {
+ break
+ }
+
+ switch src[eol-eolLen-trimLen-1] {
+ case '\n', '\r', ' ', '\t':
+ trimLen++
+ continue
+ case '=':
+ if trimLen > 0 {
+ trimLen += eolLen + 1
+ eolLen = 0
+ err = fmt.Errorf("quotedprintable: invalid bytes after =: %q", src[eol-trimLen+1:eol])
+ } else {
+ trimLen = eolLen + 1
+ eolLen = 0
+ }
+ }
+ break
+ }
+ }
+
+ // Skip trimmable bytes
+ if trimLen > 0 && i == eol-trimLen-eolLen {
+ if err != nil {
+ return n, err
+ }
+
+ i += trimLen - 1
+ continue
+ }
+
+ switch c := src[i]; {
+ case c == '=':
+ if i+2 >= len(src) {
+ return n, io.ErrUnexpectedEOF
+ }
+ b, convErr := internal.ReadHexByte(src[i+1:])
+ if convErr != nil {
+ return n, convErr
+ }
+ dst[n] = b
+ n++
+ i += 2
+ case (c >= ' ' && c <= '~') || c == '\n' || c == '\r' || c == '\t':
+ dst[n] = c
+ n++
+ default:
+ return n, fmt.Errorf("quotedprintable: invalid unescaped byte 0x%02x in quoted-printable body", c)
+ }
+ }
+
+ return n, nil
+}
+
+// MaxDecodedLen returns the maximum length of a decoding of n source bytes.
+func MaxDecodedLen(n int) int { return n }
+
+// DecodeString returns the bytes represented by the quoted-printable string s.
+func DecodeString(s string) ([]byte, error) {
+ dbuf := make([]byte, MaxDecodedLen(len(s)))
+ n, err := Decode(dbuf, []byte(s))
+ return dbuf[:n], err
+}
+
+// NewDecoder returns a new quoted-printable stream decoder.
+func NewDecoder(r io.Reader) io.Reader {
+ return &qpReader{br: bufio.NewReader(r)}
+}
+
+type qpReader struct {
+ br *bufio.Reader
+ line []byte
+ eof bool
+ err error
+}
+
+func (q *qpReader) Read(p []byte) (int, error) {
+ n := 0
+ for n < len(p) {
+ if len(q.line) == 0 {
+ if q.err != nil {
+ return n, q.err
+ } else if q.eof {
+ return n, io.EOF
+ }
+
+ q.line, q.err = q.br.ReadSlice('\n')
+ if q.err == io.EOF {
+ q.eof = true
+ } else if q.err != nil {
+ return n, q.err
+ }
+
+ var nn int
+ nn, q.err = Decode(q.line, q.line)
+ q.line = q.line[:nn]
+ }
+
+ nn := copy(p[n:], q.line)
+ n += nn
+ q.line = q.line[nn:]
+ }
+
+ return n, nil
+}
diff --git a/go/src/gopkg.in/alexcesaro/quotedprintable.v2/quotedprintable_test.go b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/quotedprintable_test.go
new file mode 100644
index 0000000..bb5a710
--- /dev/null
+++ b/go/src/gopkg.in/alexcesaro/quotedprintable.v2/quotedprintable_test.go
@@ -0,0 +1,313 @@
+// Copyright 2014 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 quotedprintable
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "os/exec"
+ "regexp"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+)
+
+func ExampleEncodeToString() {
+ data := []byte("¡Hola, señor!")
+ str := EncodeToString(data)
+ fmt.Println(str)
+ // Output:
+ // =C2=A1Hola, se=C3=B1or!
+}
+
+func ExampleDecodeString() {
+ str := "=C2=A1Hola, se=C3=B1or!"
+ data, err := DecodeString(str)
+ if err != nil {
+ fmt.Println("error:", err)
+ return
+ }
+ fmt.Printf("%s\n", data)
+ // Output:
+ // ¡Hola, señor!
+}
+
+func ExampleNewEncoder() {
+ input := []byte("Café")
+ encoder := NewEncoder(os.Stdout)
+ encoder.Write(input)
+ // Output:
+ // Caf=C3=A9
+}
+
+func TestQuotedPrintable(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 quoted-printable body"},
+ {in: "foo bar\xff", want: "foo bar", err: "quotedprintable: invalid unescaped byte 0xff in quoted-printable 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 quoted-printable 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, NewDecoder(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 TestQPExhaustive(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, NewDecoder(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 quoted-printable 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 quoted-printable hex byte 0x0a: 1190
+quotedprintable: invalid quoted-printable hex byte 0x0d: 3325
+quotedprintable: invalid quoted-printable hex byte 0x20: 3325
+quotedprintable: invalid quoted-printable hex byte 0x3d: 810
+unexpected EOF: 1502`
+ if got != want {
+ t.Errorf("Got:\n%s\nWant:\n%s", got, want)
+ }
+}
+
+func TestEncodeToString(t *testing.T) {
+ tests := []struct {
+ in, want string
+ }{
+ {in: "", want: ""},
+ {in: "foo bar", want: "foo bar"},
+ {in: "foo bar=", want: "foo bar=3D"},
+ {in: "foo bar\n", want: "foo bar\n"},
+ {in: "foo bar\r\n", want: "foo bar\r\n"},
+ {in: "foo bar ", want: "foo bar=20"},
+ {in: "foo bar ", want: "foo bar =20"},
+ {in: "foo bar \n", want: "foo bar=20\n"},
+ {in: "foo bar \n", want: "foo bar =20\n"},
+ {in: "foo bar \n ", want: "foo bar =20\n=20"},
+ {in: "résumé", want: "r=C3=A9sum=C3=A9"},
+ {in: "\t !\"#$%&'()*+,-./ :;<>?@[\\]^_`{|}~", want: "\t !\"#$%&'()*+,-./ :;<>?@[\\]^_`{|}~"},
+ }
+
+ for _, tt := range tests {
+ got := EncodeToString([]byte(tt.in))
+ if got != tt.want {
+ t.Errorf("EncodeToString(%q), got %q; want %q", tt.in, got, tt.want)
+ }
+ }
+}
+
+type brokenWriter struct {
+ errorByte int
+ *bytes.Buffer
+}
+
+func (w *brokenWriter) Write(p []byte) (int, error) {
+ for i, b := range p {
+ if i == w.errorByte {
+ return i, errors.New("Broken writer error")
+ }
+ w.WriteByte(b)
+ }
+
+ return len(p), nil
+}
+
+func newBrokenWriter(l int) *brokenWriter {
+ return &brokenWriter{l, new(bytes.Buffer)}
+}
+
+func TestEncoder(t *testing.T) {
+ tests := []struct {
+ in, want string
+ errorByte, n int
+ isError bool
+ }{
+ {in: "a", want: "", errorByte: 0, n: 0, isError: true},
+ {in: "a", want: "a", errorByte: 1, n: 1, isError: false},
+ {in: "=", want: "=", errorByte: 1, n: 0, isError: true},
+ {in: "=", want: "=3", errorByte: 2, n: 0, isError: true},
+ {in: "=", want: "=3D", errorByte: 3, n: 1, isError: false},
+ {in: "==", want: "=3D", errorByte: 3, n: 1, isError: true},
+ {in: "==", want: "=3D=", errorByte: 4, n: 1, isError: true},
+ {in: "==", want: "=3D=3", errorByte: 5, n: 1, isError: true},
+ {in: " \r\n", want: "=20\r", errorByte: 4, n: 2, isError: true},
+ }
+
+ for _, tt := range tests {
+ w := newBrokenWriter(tt.errorByte)
+ n, err := NewEncoder(w).Write([]byte(tt.in))
+ if tt.isError && (err == nil) {
+ t.Errorf("NewEncoder.Write(%q) with error at byte %d should return an error", tt.in, tt.errorByte)
+ } else if !tt.isError && (err != nil) {
+ t.Errorf("NewEncoder.Write(%q) with error at byte %d should not return an error", tt.in, tt.errorByte)
+ }
+ if w.String() != tt.want {
+ t.Errorf("NewEncoder.Write(%q) with error at byte %d, got %q; want %q", tt.in, tt.errorByte, w.String(), tt.want)
+ }
+ if n != tt.n {
+ t.Errorf("NewEncoder.Write(%q) with error at byte %d, got n=%d; want n=%d", tt.in, tt.errorByte, n, tt.n)
+ }
+ }
+}
diff --git a/go/src/gopkg.in/gomail.v1/LICENSE b/go/src/gopkg.in/gomail.v1/LICENSE
new file mode 100644
index 0000000..5f5c12a
--- /dev/null
+++ b/go/src/gopkg.in/gomail.v1/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/go/src/gopkg.in/gomail.v1/README.google b/go/src/gopkg.in/gomail.v1/README.google
new file mode 100644
index 0000000..46fcc63
--- /dev/null
+++ b/go/src/gopkg.in/gomail.v1/README.google
@@ -0,0 +1,10 @@
+URL: https://github.com/go-gomail/gomail/archive/11b919ab4933936a28fb6aeda5c6523091266f37.zip
+Version: 11b919ab4933936a28fb6aeda5c6523091266f37
+License: The MIT License (MIT)
+License File: LICENSE
+
+Description:
+The best way to send emails in Go.
+
+Local Modifications:
+No modifications.
diff --git a/go/src/gopkg.in/gomail.v1/README.md b/go/src/gopkg.in/gomail.v1/README.md
new file mode 100644
index 0000000..70f7acf
--- /dev/null
+++ b/go/src/gopkg.in/gomail.v1/README.md
@@ -0,0 +1,97 @@
+# Gomail
+
+## Introduction
+
+Gomail is a very simple and powerful package to send emails.
+
+It requires Go 1.3 or newer.
+
+
+## Features
+
+ * Dead-simple API
+ * Highly flexible
+ * Backward compatibility promise
+ * Supports HTML and text templates
+ * Attachments
+ * Embedded images
+ * SSL/TLS support
+ * Automatic encoding of special characters
+ * Well-documented
+ * High test coverage
+
+
+## Documentation
+
+https://godoc.org/gopkg.in/gomail.v1
+
+
+## Download
+
+ go get gopkg.in/gomail.v1
+
+
+## Example
+
+```go
+package main
+
+import (
+ "gopkg.in/gomail.v1"
+)
+
+func main() {
+ msg := gomail.NewMessage()
+ msg.SetHeader("From", "alex@example.com")
+ msg.SetHeader("To", "bob@example.com", "cora@example.com")
+ msg.SetAddressHeader("Cc", "dan@example.com", "Dan")
+ msg.SetHeader("Subject", "Hello!")
+ msg.SetBody("text/html", "Hello <b>Bob</b> and <i>Cora</i>!")
+
+ f, err := gomail.OpenFile("/home/Alex/lolcat.jpg")
+ if err != nil {
+ panic(err)
+ }
+ msg.Attach(f)
+
+ // Send the email to Bob, Cora and Dan
+ mailer := gomail.NewMailer("smtp.example.com", "user", "123456", 587)
+ if err := mailer.Send(msg); err != nil {
+ panic(err)
+ }
+}
+```
+
+
+## FAQ
+
+### x509: certificate signed by unknown authority
+
+If you get this error it means the certificate used by the SMTP server is not
+considered valid by the client running Gomail. As a quick workaround you can
+bypass the verification of the server's certificate chain and host name by using
+`SetTLSConfig`:
+
+ mailer := gomail.NewMailer("smtp.example.com", "user", "123456", 587, gomail.SetTLSConfig(&tls.Config{InsecureSkipVerify: true}))
+
+Note, however, that this is insecure and should not be used in production.
+
+### 504 5.7.4 Unrecognized authentication type
+
+If you get this error, you should try using the LOGIN authentication mechanism:
+
+ addr := "smtp.example.com:587"
+ auth := gomail.LoginAuth("username", "password", "smtp.example.com")
+ mailer := gomail.NewCustomMailer(addr, auth)
+
+See [issue #16](https://github.com/go-gomail/gomail/issues/16).
+
+
+## Contact
+
+You are more than welcome to open issues and send pull requests if you find a
+bug or need a new feature.
+
+You can also ask questions on the [Gomail
+thread](https://groups.google.com/d/topic/golang-nuts/ywPpNlmSt6U/discussion)
+in the Go mailing-list or via Twitter [@alexandrecesaro](https://twitter.com/alexandrecesaro).
diff --git a/go/src/gopkg.in/gomail.v1/export.go b/go/src/gopkg.in/gomail.v1/export.go
new file mode 100644
index 0000000..1f56cd3
--- /dev/null
+++ b/go/src/gopkg.in/gomail.v1/export.go
@@ -0,0 +1,262 @@
+package gomail
+
+import (
+ "bytes"
+ "encoding/base64"
+ "io"
+ "mime/multipart"
+ "net/mail"
+ "time"
+
+ "gopkg.in/alexcesaro/quotedprintable.v2"
+)
+
+// Export converts the message into a net/mail.Message.
+func (msg *Message) Export() *mail.Message {
+ w := newMessageWriter(msg)
+
+ if msg.hasMixedPart() {
+ w.openMultipart("mixed")
+ }
+
+ if msg.hasRelatedPart() {
+ w.openMultipart("related")
+ }
+
+ if msg.hasAlternativePart() {
+ w.openMultipart("alternative")
+ }
+ for _, part := range msg.parts {
+ h := make(map[string][]string)
+ h["Content-Type"] = []string{part.contentType + "; charset=" + msg.charset}
+ h["Content-Transfer-Encoding"] = []string{string(msg.encoding)}
+
+ w.write(h, part.body.Bytes(), msg.encoding)
+ }
+ if msg.hasAlternativePart() {
+ w.closeMultipart()
+ }
+
+ w.addFiles(msg.embedded, false)
+ if msg.hasRelatedPart() {
+ w.closeMultipart()
+ }
+
+ w.addFiles(msg.attachments, true)
+ if msg.hasMixedPart() {
+ w.closeMultipart()
+ }
+
+ return w.export()
+}
+
+func (msg *Message) hasMixedPart() bool {
+ return (len(msg.parts) > 0 && len(msg.attachments) > 0) || len(msg.attachments) > 1
+}
+
+func (msg *Message) hasRelatedPart() bool {
+ return (len(msg.parts) > 0 && len(msg.embedded) > 0) || len(msg.embedded) > 1
+}
+
+func (msg *Message) hasAlternativePart() bool {
+ return len(msg.parts) > 1
+}
+
+// messageWriter helps converting the message into a net/mail.Message
+type messageWriter struct {
+ header map[string][]string
+ buf *bytes.Buffer
+ writers [3]*multipart.Writer
+ partWriter io.Writer
+ depth uint8
+}
+
+func newMessageWriter(msg *Message) *messageWriter {
+ // We copy the header so Export does not modify the message
+ header := make(map[string][]string, len(msg.header)+2)
+ for k, v := range msg.header {
+ header[k] = v
+ }
+
+ if _, ok := header["Mime-Version"]; !ok {
+ header["Mime-Version"] = []string{"1.0"}
+ }
+ if _, ok := header["Date"]; !ok {
+ header["Date"] = []string{msg.FormatDate(now())}
+ }
+
+ return &messageWriter{header: header, buf: new(bytes.Buffer)}
+}
+
+// Stubbed out for testing.
+var now = time.Now
+
+func (w *messageWriter) openMultipart(mimeType string) {
+ w.writers[w.depth] = multipart.NewWriter(w.buf)
+ contentType := "multipart/" + mimeType + "; boundary=" + w.writers[w.depth].Boundary()
+
+ if w.depth == 0 {
+ w.header["Content-Type"] = []string{contentType}
+ } else {
+ h := make(map[string][]string)
+ h["Content-Type"] = []string{contentType}
+ w.createPart(h)
+ }
+ w.depth++
+}
+
+func (w *messageWriter) createPart(h map[string][]string) {
+ // No need to check the error since the underlying writer is a bytes.Buffer
+ w.partWriter, _ = w.writers[w.depth-1].CreatePart(h)
+}
+
+func (w *messageWriter) closeMultipart() {
+ if w.depth > 0 {
+ w.writers[w.depth-1].Close()
+ w.depth--
+ }
+}
+
+func (w *messageWriter) addFiles(files []*File, isAttachment bool) {
+ for _, f := range files {
+ h := make(map[string][]string)
+ h["Content-Type"] = []string{f.MimeType + "; name=\"" + f.Name + "\""}
+ h["Content-Transfer-Encoding"] = []string{string(Base64)}
+ if isAttachment {
+ h["Content-Disposition"] = []string{"attachment; filename=\"" + f.Name + "\""}
+ } else {
+ h["Content-Disposition"] = []string{"inline; filename=\"" + f.Name + "\""}
+ if f.ContentID != "" {
+ h["Content-ID"] = []string{"<" + f.ContentID + ">"}
+ } else {
+ h["Content-ID"] = []string{"<" + f.Name + ">"}
+ }
+ }
+
+ w.write(h, f.Content, Base64)
+ }
+}
+
+func (w *messageWriter) write(h map[string][]string, body []byte, enc Encoding) {
+ w.writeHeader(h)
+ w.writeBody(body, enc)
+}
+
+func (w *messageWriter) writeHeader(h map[string][]string) {
+ if w.depth == 0 {
+ for field, value := range h {
+ w.header[field] = value
+ }
+ } else {
+ w.createPart(h)
+ }
+}
+
+func (w *messageWriter) writeBody(body []byte, enc Encoding) {
+ var subWriter io.Writer
+ if w.depth == 0 {
+ subWriter = w.buf
+ } else {
+ subWriter = w.partWriter
+ }
+
+ // The errors returned by writers are not checked since these writers cannot
+ // return errors.
+ if enc == Base64 {
+ writer := base64.NewEncoder(base64.StdEncoding, newBase64LineWriter(subWriter))
+ writer.Write(body)
+ writer.Close()
+ } else if enc == Unencoded {
+ subWriter.Write(body)
+ } else {
+ writer := quotedprintable.NewEncoder(newQpLineWriter(subWriter))
+ writer.Write(body)
+ }
+}
+
+func (w *messageWriter) export() *mail.Message {
+ return &mail.Message{Header: w.header, Body: w.buf}
+}
+
+// As required by RFC 2045, 6.7. (page 21) for quoted-printable, and
+// RFC 2045, 6.8. (page 25) for base64.
+const maxLineLen = 76
+
+// base64LineWriter limits text encoded in base64 to 76 characters per line
+type base64LineWriter struct {
+ w io.Writer
+ lineLen int
+}
+
+func newBase64LineWriter(w io.Writer) *base64LineWriter {
+ return &base64LineWriter{w: w}
+}
+
+func (w *base64LineWriter) Write(p []byte) (int, error) {
+ n := 0
+ for len(p)+w.lineLen > maxLineLen {
+ w.w.Write(p[:maxLineLen-w.lineLen])
+ w.w.Write([]byte("\r\n"))
+ p = p[maxLineLen-w.lineLen:]
+ n += maxLineLen - w.lineLen
+ w.lineLen = 0
+ }
+
+ w.w.Write(p)
+ w.lineLen += len(p)
+
+ return n + len(p), nil
+}
+
+// qpLineWriter limits text encoded in quoted-printable to 76 characters per
+// line
+type qpLineWriter struct {
+ w io.Writer
+ lineLen int
+}
+
+func newQpLineWriter(w io.Writer) *qpLineWriter {
+ return &qpLineWriter{w: w}
+}
+
+func (w *qpLineWriter) Write(p []byte) (int, error) {
+ n := 0
+ for len(p) > 0 {
+ // If the text is not over the limit, write everything
+ if len(p) < maxLineLen-w.lineLen {
+ w.w.Write(p)
+ w.lineLen += len(p)
+ return n + len(p), nil
+ }
+
+ i := bytes.IndexAny(p[:maxLineLen-w.lineLen+2], "\n")
+ // If there is a newline before the limit, write the end of the line
+ if i != -1 && (i != maxLineLen-w.lineLen+1 || p[i-1] == '\r') {
+ w.w.Write(p[:i+1])
+ p = p[i+1:]
+ n += i + 1
+ w.lineLen = 0
+ continue
+ }
+
+ // Quoted-printable text must not be cut between an equal sign and the
+ // two following characters
+ var toWrite int
+ if maxLineLen-w.lineLen-2 >= 0 && p[maxLineLen-w.lineLen-2] == '=' {
+ toWrite = maxLineLen - w.lineLen - 2
+ } else if p[maxLineLen-w.lineLen-1] == '=' {
+ toWrite = maxLineLen - w.lineLen - 1
+ } else {
+ toWrite = maxLineLen - w.lineLen
+ }
+
+ // Insert the newline where it is needed
+ w.w.Write(p[:toWrite])
+ w.w.Write([]byte("=\r\n"))
+ p = p[toWrite:]
+ n += toWrite
+ w.lineLen = 0
+ }
+
+ return n, nil
+}
diff --git a/go/src/gopkg.in/gomail.v1/gomail.go b/go/src/gopkg.in/gomail.v1/gomail.go
new file mode 100644
index 0000000..734d881
--- /dev/null
+++ b/go/src/gopkg.in/gomail.v1/gomail.go
@@ -0,0 +1,334 @@
+// Package gomail provides a simple interface to send emails.
+//
+// More info on Github: https://github.com/go-gomail/gomail
+package gomail
+
+import (
+ "bytes"
+ "io"
+ "io/ioutil"
+ "mime"
+ "path/filepath"
+ "sync"
+ "time"
+
+ "gopkg.in/alexcesaro/quotedprintable.v2"
+)
+
+// Message represents an email.
+type Message struct {
+ header header
+ parts []part
+ attachments []*File
+ embedded []*File
+ charset string
+ encoding Encoding
+ hEncoder *quotedprintable.HeaderEncoder
+}
+
+type header map[string][]string
+
+type part struct {
+ contentType string
+ body *bytes.Buffer
+}
+
+// NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding
+// by default.
+func NewMessage(settings ...MessageSetting) *Message {
+ msg := &Message{
+ header: make(header),
+ charset: "UTF-8",
+ encoding: QuotedPrintable,
+ }
+
+ msg.applySettings(settings)
+
+ var e quotedprintable.Encoding
+ if msg.encoding == Base64 {
+ e = quotedprintable.B
+ } else {
+ e = quotedprintable.Q
+ }
+ msg.hEncoder = e.NewHeaderEncoder(msg.charset)
+
+ return msg
+}
+
+func (msg *Message) applySettings(settings []MessageSetting) {
+ for _, s := range settings {
+ s(msg)
+ }
+}
+
+// A MessageSetting can be used as an argument in NewMessage to configure an
+// email.
+type MessageSetting func(msg *Message)
+
+// SetCharset is a message setting to set the charset of the email.
+//
+// Example:
+//
+// msg := gomail.NewMessage(SetCharset("ISO-8859-1"))
+func SetCharset(charset string) MessageSetting {
+ return func(msg *Message) {
+ msg.charset = charset
+ }
+}
+
+// SetEncoding is a message setting to set the encoding of the email.
+//
+// Example:
+//
+// msg := gomail.NewMessage(SetEncoding(gomail.Base64))
+func SetEncoding(enc Encoding) MessageSetting {
+ return func(msg *Message) {
+ msg.encoding = enc
+ }
+}
+
+// Encoding represents a MIME encoding scheme like quoted-printable or base64.
+type Encoding string
+
+const (
+ // QuotedPrintable represents the quoted-printable encoding as defined in
+ // RFC 2045.
+ QuotedPrintable Encoding = "quoted-printable"
+ // Base64 represents the base64 encoding as defined in RFC 2045.
+ Base64 Encoding = "base64"
+ // Unencoded can be used to avoid encoding the body of an email. The headers
+ // will still be encoded using quoted-printable encoding.
+ Unencoded Encoding = "8bit"
+)
+
+// SetHeader sets a value to the given header field.
+func (msg *Message) SetHeader(field string, value ...string) {
+ for i := range value {
+ value[i] = encodeHeader(msg.hEncoder, value[i])
+ }
+ msg.header[field] = value
+}
+
+// SetHeaders sets the message headers.
+//
+// Example:
+//
+// msg.SetHeaders(map[string][]string{
+// "From": {"alex@example.com"},
+// "To": {"bob@example.com", "cora@example.com"},
+// "Subject": {"Hello"},
+// })
+func (msg *Message) SetHeaders(h map[string][]string) {
+ for k, v := range h {
+ msg.SetHeader(k, v...)
+ }
+}
+
+// SetAddressHeader sets an address to the given header field.
+func (msg *Message) SetAddressHeader(field, address, name string) {
+ msg.header[field] = []string{msg.FormatAddress(address, name)}
+}
+
+// FormatAddress formats an address and a name as a valid RFC 5322 address.
+func (msg *Message) FormatAddress(address, name string) string {
+ buf := getBuffer()
+ defer putBuffer(buf)
+
+ if !quotedprintable.NeedsEncoding(name) {
+ quote(buf, name)
+ } else {
+ var n string
+ if hasSpecials(name) {
+ n = encodeHeader(quotedprintable.B.NewHeaderEncoder(msg.charset), name)
+ } else {
+ n = encodeHeader(msg.hEncoder, name)
+ }
+ buf.WriteString(n)
+ }
+ buf.WriteString(" <")
+ buf.WriteString(address)
+ buf.WriteByte('>')
+
+ return buf.String()
+}
+
+// SetDateHeader sets a date to the given header field.
+func (msg *Message) SetDateHeader(field string, date time.Time) {
+ msg.header[field] = []string{msg.FormatDate(date)}
+}
+
+// FormatDate formats a date as a valid RFC 5322 date.
+func (msg *Message) FormatDate(date time.Time) string {
+ return date.Format(time.RFC1123Z)
+}
+
+// GetHeader gets a header field.
+func (msg *Message) GetHeader(field string) []string {
+ return msg.header[field]
+}
+
+// DelHeader deletes a header field.
+func (msg *Message) DelHeader(field string) {
+ delete(msg.header, field)
+}
+
+// SetBody sets the body of the message.
+func (msg *Message) SetBody(contentType, body string) {
+ msg.parts = []part{
+ part{
+ contentType: contentType,
+ body: bytes.NewBufferString(body),
+ },
+ }
+}
+
+// AddAlternative adds an alternative body to the message. Commonly used to
+// send HTML emails that default to the plain text version for backward
+// compatibility.
+//
+// Example:
+//
+// msg.SetBody("text/plain", "Hello!")
+// msg.AddAlternative("text/html", "<p>Hello!</p>")
+//
+// More info: http://en.wikipedia.org/wiki/MIME#Alternative
+func (msg *Message) AddAlternative(contentType, body string) {
+ msg.parts = append(msg.parts,
+ part{
+ contentType: contentType,
+ body: bytes.NewBufferString(body),
+ },
+ )
+}
+
+// GetBodyWriter gets a writer that writes to the body. It can be useful with
+// the templates from packages text/template or html/template.
+//
+// Example:
+//
+// w := msg.GetBodyWriter("text/plain")
+// t := template.Must(template.New("example").Parse("Hello {{.}}!"))
+// t.Execute(w, "Bob")
+func (msg *Message) GetBodyWriter(contentType string) io.Writer {
+ buf := new(bytes.Buffer)
+ msg.parts = append(msg.parts,
+ part{
+ contentType: contentType,
+ body: buf,
+ },
+ )
+
+ return buf
+}
+
+// A File represents a file that can be attached or embedded in an email.
+type File struct {
+ Name string
+ MimeType string
+ Content []byte
+ ContentID string
+}
+
+// OpenFile opens a file on disk to create a gomail.File.
+func OpenFile(filename string) (*File, error) {
+ content, err := readFile(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ f := CreateFile(filepath.Base(filename), content)
+
+ return f, nil
+}
+
+// CreateFile creates a gomail.File from the given name and content.
+func CreateFile(name string, content []byte) *File {
+ mimeType := mime.TypeByExtension(filepath.Ext(name))
+ if mimeType == "" {
+ mimeType = "application/octet-stream"
+ }
+
+ return &File{
+ Name: name,
+ MimeType: mimeType,
+ Content: content,
+ }
+}
+
+// Attach attaches the files to the email.
+func (msg *Message) Attach(f ...*File) {
+ if msg.attachments == nil {
+ msg.attachments = f
+ } else {
+ msg.attachments = append(msg.attachments, f...)
+ }
+}
+
+// Embed embeds the images to the email.
+//
+// Example:
+//
+// f, err := gomail.OpenFile("/tmp/image.jpg")
+// if err != nil {
+// panic(err)
+// }
+// msg.Embed(f)
+// msg.SetBody("text/html", `<img src="cid:image.jpg" alt="My image" />`)
+func (msg *Message) Embed(image ...*File) {
+ if msg.embedded == nil {
+ msg.embedded = image
+ } else {
+ msg.embedded = append(msg.embedded, image...)
+ }
+}
+
+// Stubbed out for testing.
+var readFile = ioutil.ReadFile
+
+func quote(buf *bytes.Buffer, text string) {
+ buf.WriteByte('"')
+ for i := 0; i < len(text); i++ {
+ if text[i] == '\\' || text[i] == '"' {
+ buf.WriteByte('\\')
+ }
+ buf.WriteByte(text[i])
+ }
+ buf.WriteByte('"')
+}
+
+func hasSpecials(text string) bool {
+ for i := 0; i < len(text); i++ {
+ switch c := text[i]; c {
+ case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"':
+ return true
+ }
+ }
+
+ return false
+}
+
+func encodeHeader(enc *quotedprintable.HeaderEncoder, value string) string {
+ if !quotedprintable.NeedsEncoding(value) {
+ return value
+ }
+
+ return enc.Encode(value)
+}
+
+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/go/src/gopkg.in/gomail.v1/gomail_test.go b/go/src/gopkg.in/gomail.v1/gomail_test.go
new file mode 100644
index 0000000..087b7c9
--- /dev/null
+++ b/go/src/gopkg.in/gomail.v1/gomail_test.go
@@ -0,0 +1,639 @@
+package gomail
+
+import (
+ "encoding/base64"
+ "net/smtp"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+type message struct {
+ from string
+ to []string
+ content string
+}
+
+func TestMessage(t *testing.T) {
+ msg := NewMessage()
+ msg.SetAddressHeader("From", "from@example.com", "Señor From")
+ msg.SetHeader("To", msg.FormatAddress("to@example.com", "Señor To"), "tobis@example.com")
+ msg.SetAddressHeader("Cc", "cc@example.com", "A, B")
+ msg.SetAddressHeader("X-To", "ccbis@example.com", "à, b")
+ msg.SetDateHeader("X-Date", stubNow())
+ msg.SetHeader("X-Date-2", msg.FormatDate(stubNow()))
+ msg.SetHeader("Subject", "¡Hola, señor!")
+ msg.SetHeaders(map[string][]string{
+ "X-Headers": {"Test", "Café"},
+ })
+ msg.SetBody("text/plain", "¡Hola, señor!")
+
+ want := message{
+ from: "from@example.com",
+ to: []string{
+ "to@example.com",
+ "tobis@example.com",
+ "cc@example.com",
+ },
+ content: "From: =?UTF-8?Q?Se=C3=B1or_From?= <from@example.com>\r\n" +
+ "To: =?UTF-8?Q?Se=C3=B1or_To?= <to@example.com>, tobis@example.com\r\n" +
+ "Cc: \"A, B\" <cc@example.com>\r\n" +
+ "X-To: =?UTF-8?B?w6AsIGI=?= <ccbis@example.com>\r\n" +
+ "X-Date: Wed, 25 Jun 2014 17:46:00 +0000\r\n" +
+ "X-Date-2: Wed, 25 Jun 2014 17:46:00 +0000\r\n" +
+ "X-Headers: Test, =?UTF-8?Q?Caf=C3=A9?=\r\n" +
+ "Subject: =?UTF-8?Q?=C2=A1Hola,_se=C3=B1or!?=\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "=C2=A1Hola, se=C3=B1or!",
+ }
+
+ testMessage(t, msg, 0, want)
+}
+
+func TestBodyWriter(t *testing.T) {
+ msg := NewMessage()
+ msg.SetHeader("From", "from@example.com")
+ msg.SetHeader("To", "to@example.com")
+ w := msg.GetBodyWriter("text/plain")
+ w.Write([]byte("Test message"))
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "Test message",
+ }
+
+ testMessage(t, msg, 0, want)
+}
+
+func TestCustomMessage(t *testing.T) {
+ msg := NewMessage(SetCharset("ISO-8859-1"), SetEncoding(Base64))
+ msg.SetHeaders(map[string][]string{
+ "From": {"from@example.com"},
+ "To": {"to@example.com"},
+ "Subject": {"Café"},
+ })
+ msg.SetBody("text/html", "¡Hola, señor!")
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Subject: =?ISO-8859-1?B?Q2Fmw6k=?=\r\n" +
+ "Content-Type: text/html; charset=ISO-8859-1\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ "wqFIb2xhLCBzZcOxb3Ih",
+ }
+
+ testMessage(t, msg, 0, want)
+}
+
+func TestUnencodedMessage(t *testing.T) {
+ msg := NewMessage(SetEncoding(Unencoded))
+ msg.SetHeaders(map[string][]string{
+ "From": {"from@example.com"},
+ "To": {"to@example.com"},
+ "Subject": {"Café"},
+ })
+ msg.SetBody("text/html", "¡Hola, señor!")
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Subject: =?UTF-8?Q?Caf=C3=A9?=\r\n" +
+ "Content-Type: text/html; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: 8bit\r\n" +
+ "\r\n" +
+ "¡Hola, señor!",
+ }
+
+ testMessage(t, msg, 0, want)
+}
+
+func TestRecipients(t *testing.T) {
+ msg := NewMessage()
+ msg.SetHeaders(map[string][]string{
+ "From": {"from@example.com"},
+ "To": {"to@example.com"},
+ "Cc": {"cc@example.com"},
+ "Bcc": {"bcc1@example.com", "bcc2@example.com"},
+ "Subject": {"Hello!"},
+ })
+ msg.SetBody("text/plain", "Test message")
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com", "cc@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Cc: cc@example.com\r\n" +
+ "Subject: Hello!\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "Test message",
+ }
+ wantBcc1 := message{
+ from: "from@example.com",
+ to: []string{"bcc1@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Cc: cc@example.com\r\n" +
+ "Bcc: bcc1@example.com\r\n" +
+ "Subject: Hello!\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "Test message",
+ }
+ wantBcc2 := message{
+ from: "from@example.com",
+ to: []string{"bcc2@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Cc: cc@example.com\r\n" +
+ "Bcc: bcc2@example.com\r\n" +
+ "Subject: Hello!\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "Test message",
+ }
+
+ testMessage(t, msg, 0, want, wantBcc1, wantBcc2)
+}
+
+func TestAlternative(t *testing.T) {
+ msg := NewMessage()
+ msg.SetHeader("From", "from@example.com")
+ msg.SetHeader("To", "to@example.com")
+ msg.SetBody("text/plain", "¡Hola, señor!")
+ msg.AddAlternative("text/html", "¡<b>Hola</b>, <i>señor</i>!</h1>")
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Content-Type: multipart/alternative; boundary=_BOUNDARY_1_\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "=C2=A1Hola, se=C3=B1or!\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: text/html; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "=C2=A1<b>Hola</b>, <i>se=C3=B1or</i>!</h1>\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, msg, 1, want)
+}
+
+func TestAttachmentOnly(t *testing.T) {
+ readFile = func(filename string) ([]byte, error) {
+ return []byte("Content of " + filepath.Base(filename)), nil
+ }
+
+ msg := NewMessage()
+ msg.SetHeader("From", "from@example.com")
+ msg.SetHeader("To", "to@example.com")
+ f, err := OpenFile("/tmp/test.pdf")
+ if err != nil {
+ panic(err)
+ }
+ msg.Attach(f)
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Content-Type: application/pdf; name=\"test.pdf\"\r\n" +
+ "Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")),
+ }
+
+ testMessage(t, msg, 0, want)
+}
+
+func TestAttachment(t *testing.T) {
+ msg := NewMessage()
+ msg.SetHeader("From", "from@example.com")
+ msg.SetHeader("To", "to@example.com")
+ msg.SetBody("text/plain", "Test")
+ msg.Attach(CreateFile("test.pdf", []byte("Content")))
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "Test\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: application/pdf; name=\"test.pdf\"\r\n" +
+ "Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ base64.StdEncoding.EncodeToString([]byte("Content")) + "\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, msg, 1, want)
+}
+
+func TestAttachmentsOnly(t *testing.T) {
+ msg := NewMessage()
+ msg.SetHeader("From", "from@example.com")
+ msg.SetHeader("To", "to@example.com")
+ msg.Attach(CreateFile("test.pdf", []byte("Content 1")))
+ msg.Attach(CreateFile("test.zip", []byte("Content 2")))
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: application/pdf; name=\"test.pdf\"\r\n" +
+ "Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ base64.StdEncoding.EncodeToString([]byte("Content 1")) + "\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: application/zip; name=\"test.zip\"\r\n" +
+ "Content-Disposition: attachment; filename=\"test.zip\"\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ base64.StdEncoding.EncodeToString([]byte("Content 2")) + "\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, msg, 1, want)
+}
+
+func TestAttachments(t *testing.T) {
+ msg := NewMessage()
+ msg.SetHeader("From", "from@example.com")
+ msg.SetHeader("To", "to@example.com")
+ msg.SetBody("text/plain", "Test")
+ msg.Attach(CreateFile("test.pdf", []byte("Content 1")))
+ msg.Attach(CreateFile("test.zip", []byte("Content 2")))
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "Test\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: application/pdf; name=\"test.pdf\"\r\n" +
+ "Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ base64.StdEncoding.EncodeToString([]byte("Content 1")) + "\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: application/zip; name=\"test.zip\"\r\n" +
+ "Content-Disposition: attachment; filename=\"test.zip\"\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ base64.StdEncoding.EncodeToString([]byte("Content 2")) + "\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, msg, 1, want)
+}
+
+func TestEmbedded(t *testing.T) {
+ msg := NewMessage()
+ msg.SetHeader("From", "from@example.com")
+ msg.SetHeader("To", "to@example.com")
+ f := CreateFile("image1.jpg", []byte("Content 1"))
+ f.ContentID = "test-content-id"
+ msg.Embed(f)
+ msg.Embed(CreateFile("image2.jpg", []byte("Content 2")))
+ msg.SetBody("text/plain", "Test")
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Content-Type: multipart/related; boundary=_BOUNDARY_1_\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "Test\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: image/jpeg; name=\"image1.jpg\"\r\n" +
+ "Content-Disposition: inline; filename=\"image1.jpg\"\r\n" +
+ "Content-ID: <test-content-id>\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ base64.StdEncoding.EncodeToString([]byte("Content 1")) + "\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: image/jpeg; name=\"image2.jpg\"\r\n" +
+ "Content-Disposition: inline; filename=\"image2.jpg\"\r\n" +
+ "Content-ID: <image2.jpg>\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ base64.StdEncoding.EncodeToString([]byte("Content 2")) + "\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, msg, 1, want)
+}
+
+func TestFullMessage(t *testing.T) {
+ msg := NewMessage()
+ msg.SetHeader("From", "from@example.com")
+ msg.SetHeader("To", "to@example.com")
+ msg.SetBody("text/plain", "¡Hola, señor!")
+ msg.AddAlternative("text/html", "¡<b>Hola</b>, <i>señor</i>!</h1>")
+ msg.Attach(CreateFile("test.pdf", []byte("Content 1")))
+ msg.Embed(CreateFile("image.jpg", []byte("Content 2")))
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: multipart/related; boundary=_BOUNDARY_2_\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_2_\r\n" +
+ "Content-Type: multipart/alternative; boundary=_BOUNDARY_3_\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_3_\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "=C2=A1Hola, se=C3=B1or!\r\n" +
+ "--_BOUNDARY_3_\r\n" +
+ "Content-Type: text/html; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "=C2=A1<b>Hola</b>, <i>se=C3=B1or</i>!</h1>\r\n" +
+ "--_BOUNDARY_3_--\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_2_\r\n" +
+ "Content-Type: image/jpeg; name=\"image.jpg\"\r\n" +
+ "Content-Disposition: inline; filename=\"image.jpg\"\r\n" +
+ "Content-ID: <image.jpg>\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ base64.StdEncoding.EncodeToString([]byte("Content 2")) + "\r\n" +
+ "--_BOUNDARY_2_--\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: application/pdf; name=\"test.pdf\"\r\n" +
+ "Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ base64.StdEncoding.EncodeToString([]byte("Content 1")) + "\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, msg, 3, want)
+}
+
+func TestQpLineLength(t *testing.T) {
+ msg := NewMessage()
+ msg.SetHeader("From", "from@example.com")
+ msg.SetHeader("To", "to@example.com")
+ msg.SetBody("text/plain",
+ strings.Repeat("0", 77)+"\r\n"+
+ strings.Repeat("0", 76)+"à\r\n"+
+ strings.Repeat("0", 75)+"à\r\n"+
+ strings.Repeat("0", 74)+"à\r\n"+
+ strings.Repeat("0", 73)+"à\r\n"+
+ strings.Repeat("0", 76)+"\r\n"+
+ strings.Repeat("0", 77)+"\n")
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ strings.Repeat("0", 76) + "=\r\n0\r\n" +
+ strings.Repeat("0", 76) + "=\r\n=C3=A0\r\n" +
+ strings.Repeat("0", 75) + "=\r\n=C3=A0\r\n" +
+ strings.Repeat("0", 74) + "=\r\n=C3=A0\r\n" +
+ strings.Repeat("0", 73) + "=C3=\r\n=A0\r\n" +
+ strings.Repeat("0", 76) + "\r\n" +
+ strings.Repeat("0", 76) + "=\r\n0\n",
+ }
+
+ testMessage(t, msg, 0, want)
+}
+
+func TestBase64LineLength(t *testing.T) {
+ msg := NewMessage(SetCharset("UTF-8"), SetEncoding(Base64))
+ msg.SetHeader("From", "from@example.com")
+ msg.SetHeader("To", "to@example.com")
+ msg.SetBody("text/plain", strings.Repeat("0", 58))
+
+ want := message{
+ from: "from@example.com",
+ to: []string{"to@example.com"},
+ content: "From: from@example.com\r\n" +
+ "To: to@example.com\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ strings.Repeat("MDAw", 19) + "\r\nMA==",
+ }
+
+ testMessage(t, msg, 0, want)
+}
+
+func testMessage(t *testing.T, msg *Message, bCount int, emails ...message) {
+ now = stubNow
+ mailer := NewMailer("host", "username", "password", 587, SetSendMail(stubSendMail(t, bCount, emails...)))
+
+ err := mailer.Send(msg)
+ if err != nil {
+ t.Error(err)
+ }
+}
+
+func stubNow() time.Time {
+ return time.Date(2014, 06, 25, 17, 46, 0, 0, time.UTC)
+}
+
+func stubSendMail(t *testing.T, bCount int, emails ...message) SendMailFunc {
+ i := 0
+ return func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
+ if i > len(emails) {
+ t.Fatalf("Only %d mails should be sent", len(emails))
+ }
+ want := emails[i]
+
+ if addr != "host:587" {
+ t.Fatalf("Invalid address, got %q, want host:587", addr)
+ }
+
+ if from != want.from {
+ t.Fatalf("Invalid from, got %q, want %q", from, want.from)
+ }
+
+ if len(to) != len(want.to) {
+ t.Fatalf("Invalid recipient count, \ngot %d: %q\nwant %d: %q",
+ len(to), to,
+ len(want.to), want.to,
+ )
+ }
+ for i := range want.to {
+ if to[i] != want.to[i] {
+ t.Fatalf("Invalid recipient, got %q, want %q",
+ to[i], want.to[i],
+ )
+ }
+ }
+
+ got := string(msg)
+ wantMsg := string("Mime-Version: 1.0\r\n" +
+ "Date: Wed, 25 Jun 2014 17:46:00 +0000\r\n" +
+ want.content)
+ if bCount > 0 {
+ boundaries := getBoundaries(t, bCount, got)
+ for i, b := range boundaries {
+ wantMsg = strings.Replace(wantMsg, "_BOUNDARY_"+strconv.Itoa(i+1)+"_", b, -1)
+ }
+ }
+ i++
+
+ compareBodies(t, got, wantMsg)
+
+ return nil
+ }
+}
+
+func compareBodies(t *testing.T, got, want string) {
+ // We cannot do a simple comparison since the ordering of headers' fields
+ // is random.
+ gotLines := strings.Split(got, "\r\n")
+ wantLines := strings.Split(want, "\r\n")
+
+ // We only test for too many lines, missing lines are tested after
+ if len(gotLines) > len(wantLines) {
+ t.Fatalf("Message has too many lines, \ngot %d:\n%s\nwant %d:\n%s", len(gotLines), got, len(wantLines), want)
+ }
+
+ isInHeader := true
+ headerStart := 0
+ for i, line := range wantLines {
+ if line == gotLines[i] {
+ if line == "" {
+ isInHeader = false
+ } else if !isInHeader && len(line) > 2 && line[:2] == "--" {
+ isInHeader = true
+ headerStart = i + 1
+ }
+ continue
+ }
+
+ if !isInHeader {
+ missingLine(t, line, got, want)
+ }
+
+ isMissing := true
+ for j := headerStart; j < len(gotLines); j++ {
+ if gotLines[j] == "" {
+ break
+ }
+ if gotLines[j] == line {
+ isMissing = false
+ break
+ }
+ }
+ if isMissing {
+ missingLine(t, line, got, want)
+ }
+ }
+}
+
+func missingLine(t *testing.T, line, got, want string) {
+ t.Fatalf("Missing line %q\ngot:\n%s\nwant:\n%s", line, got, want)
+}
+
+func getBoundaries(t *testing.T, count int, msg string) []string {
+ if matches := boundaryRegExp.FindAllStringSubmatch(msg, count); matches != nil {
+ boundaries := make([]string, count)
+ for i, match := range matches {
+ boundaries[i] = match[1]
+ }
+ return boundaries
+ }
+
+ t.Fatal("Boundary not found in body")
+ return []string{""}
+}
+
+var boundaryRegExp = regexp.MustCompile("boundary=(\\w+)")
+
+func BenchmarkFull(b *testing.B) {
+ emptyFunc := func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
+ return nil
+ }
+
+ for n := 0; n < b.N; n++ {
+ msg := NewMessage()
+ msg.SetAddressHeader("From", "from@example.com", "Señor From")
+ msg.SetHeaders(map[string][]string{
+ "To": {"to@example.com"},
+ "Cc": {"cc@example.com"},
+ "Bcc": {"bcc1@example.com", "bcc2@example.com"},
+ "Subject": {"¡Hola, señor!"},
+ })
+ msg.SetBody("text/plain", "¡Hola, señor!")
+ msg.AddAlternative("text/html", "<p>¡Hola, señor!</p>")
+ msg.Attach(CreateFile("benchmark.txt", []byte("Benchmark")))
+ msg.Embed(CreateFile("benchmark.jpg", []byte("Benchmark")))
+
+ mailer := NewMailer("host", "username", "password", 587, SetSendMail(emptyFunc))
+ if err := mailer.Send(msg); err != nil {
+ panic(err)
+ }
+ }
+}
diff --git a/go/src/gopkg.in/gomail.v1/login.go b/go/src/gopkg.in/gomail.v1/login.go
new file mode 100644
index 0000000..ee4b3b4
--- /dev/null
+++ b/go/src/gopkg.in/gomail.v1/login.go
@@ -0,0 +1,54 @@
+package gomail
+
+import (
+ "errors"
+ "fmt"
+ "net/smtp"
+ "strings"
+)
+
+type loginAuth struct {
+ username string
+ password string
+ host string
+}
+
+// LoginAuth returns an Auth that implements the LOGIN authentication mechanism.
+func LoginAuth(username, password, host string) smtp.Auth {
+ return &loginAuth{username, password, host}
+}
+
+func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
+ if !server.TLS {
+ advertised := false
+ for _, mechanism := range server.Auth {
+ if mechanism == "LOGIN" {
+ advertised = true
+ break
+ }
+ }
+ if !advertised {
+ return "", nil, errors.New("gomail: unencrypted connection")
+ }
+ }
+ if server.Name != a.host {
+ return "", nil, errors.New("gomail: wrong host name")
+ }
+ return "LOGIN", nil, nil
+}
+
+func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
+ if !more {
+ return nil, nil
+ }
+
+ command := strings.ToLower(strings.TrimSuffix(string(fromServer), ":"))
+ switch command {
+ case "username":
+ return []byte(fmt.Sprintf("%s", a.username)), nil
+ case "password":
+ return []byte(fmt.Sprintf("%s", a.password)), nil
+ default:
+ return nil, fmt.Errorf("gomail: unexpected server challenge: %s", command)
+ }
+}
diff --git a/go/src/gopkg.in/gomail.v1/login_test.go b/go/src/gopkg.in/gomail.v1/login_test.go
new file mode 100644
index 0000000..64e1762
--- /dev/null
+++ b/go/src/gopkg.in/gomail.v1/login_test.go
@@ -0,0 +1,66 @@
+package gomail
+
+import (
+ "net/smtp"
+ "testing"
+)
+
+type output struct {
+ proto string
+ data []string
+ err error
+}
+
+const (
+ testUser = "user"
+ testPwd = "pwd"
+)
+
+func TestPlainAuth(t *testing.T) {
+ tests := []struct {
+ serverProtos []string
+ serverChallenges []string
+ proto string
+ data []string
+ }{
+ {
+ serverProtos: []string{"LOGIN"},
+ serverChallenges: []string{"Username:", "Password:"},
+ proto: "LOGIN",
+ data: []string{"", testUser, testPwd},
+ },
+ }
+
+ for _, test := range tests {
+ auth := LoginAuth(testUser, testPwd, testHost)
+ server := &smtp.ServerInfo{
+ Name: testHost,
+ TLS: true,
+ Auth: test.serverProtos,
+ }
+ proto, toServer, err := auth.Start(server)
+ if err != nil {
+ t.Fatalf("Start error: %v", err)
+ }
+ if proto != test.proto {
+ t.Errorf("Invalid protocol, got %q, want %q", proto, test.proto)
+ }
+
+ i := 0
+ got := string(toServer)
+ if got != test.data[i] {
+ t.Errorf("Invalid response, got %q, want %q", got, test.data[i])
+ }
+ for _, challenge := range test.serverChallenges {
+ toServer, err = auth.Next([]byte(challenge), true)
+ if err != nil {
+ t.Fatalf("Auth error: %v", err)
+ }
+ i++
+ got = string(toServer)
+ if got != test.data[i] {
+ t.Errorf("Invalid response, got %q, want %q", got, test.data[i])
+ }
+ }
+ }
+}
diff --git a/go/src/gopkg.in/gomail.v1/mailer.go b/go/src/gopkg.in/gomail.v1/mailer.go
new file mode 100644
index 0000000..99ab619
--- /dev/null
+++ b/go/src/gopkg.in/gomail.v1/mailer.go
@@ -0,0 +1,205 @@
+package gomail
+
+import (
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net"
+ "net/mail"
+ "net/smtp"
+ "strings"
+)
+
+// A Mailer represents an SMTP server.
+type Mailer struct {
+ addr string
+ host string
+ config *tls.Config
+ auth smtp.Auth
+ send SendMailFunc
+}
+
+// A MailerSetting can be used in a mailer constructor to configure it.
+type MailerSetting func(m *Mailer)
+
+// SetSendMail allows to set the email-sending function of a mailer.
+//
+// Example:
+//
+// myFunc := func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
+// // Implement your email-sending function similar to smtp.SendMail
+// }
+// mailer := gomail.NewMailer("host", "user", "pwd", 465, SetSendMail(myFunc))
+func SetSendMail(s SendMailFunc) MailerSetting {
+ return func(m *Mailer) {
+ m.send = s
+ }
+}
+
+// SetTLSConfig allows to set the TLS configuration used to connect the SMTP
+// server.
+func SetTLSConfig(c *tls.Config) MailerSetting {
+ return func(m *Mailer) {
+ m.config = c
+ }
+}
+
+// A SendMailFunc is a function to send emails with the same signature than
+// smtp.SendMail.
+type SendMailFunc func(addr string, a smtp.Auth, from string, to []string, msg []byte) error
+
+// NewMailer returns a mailer. The given parameters are used to connect to the
+// SMTP server via a PLAIN authentication mechanism.
+func NewMailer(host string, username string, password string, port int, settings ...MailerSetting) *Mailer {
+ return NewCustomMailer(
+ fmt.Sprintf("%s:%d", host, port),
+ smtp.PlainAuth("", username, password, host),
+ settings...,
+ )
+}
+
+// NewCustomMailer creates a mailer with the given authentication mechanism.
+//
+// Example:
+//
+// gomail.NewCustomMailer("host:587", smtp.CRAMMD5Auth("username", "secret"))
+func NewCustomMailer(addr string, auth smtp.Auth, settings ...MailerSetting) *Mailer {
+ // Error is not handled here to preserve backward compatibility
+ host, port, _ := net.SplitHostPort(addr)
+
+ m := &Mailer{
+ addr: addr,
+ host: host,
+ auth: auth,
+ }
+
+ for _, s := range settings {
+ s(m)
+ }
+
+ if m.config == nil {
+ m.config = &tls.Config{ServerName: host}
+ }
+ if m.send == nil {
+ m.send = m.getSendMailFunc(port == "465")
+ }
+
+ return m
+}
+
+// Send sends the emails to all the recipients of the message.
+func (m *Mailer) Send(msg *Message) error {
+ message := msg.Export()
+
+ from, err := getFrom(message)
+ if err != nil {
+ return err
+ }
+ recipients, bcc, err := getRecipients(message)
+ if err != nil {
+ return err
+ }
+
+ h := flattenHeader(message, "")
+ body, err := ioutil.ReadAll(message.Body)
+ if err != nil {
+ return err
+ }
+
+ mail := append(h, body...)
+ if err := m.send(m.addr, m.auth, from, recipients, mail); err != nil {
+ return err
+ }
+
+ for _, to := range bcc {
+ h = flattenHeader(message, to)
+ mail = append(h, body...)
+ if err := m.send(m.addr, m.auth, from, []string{to}, mail); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func flattenHeader(msg *mail.Message, bcc string) []byte {
+ buf := getBuffer()
+ defer putBuffer(buf)
+
+ for field, value := range msg.Header {
+ if field != "Bcc" {
+ buf.WriteString(field)
+ buf.WriteString(": ")
+ buf.WriteString(strings.Join(value, ", "))
+ buf.WriteString("\r\n")
+ } else if bcc != "" {
+ for _, to := range value {
+ if strings.Contains(to, bcc) {
+ buf.WriteString(field)
+ buf.WriteString(": ")
+ buf.WriteString(to)
+ buf.WriteString("\r\n")
+ }
+ }
+ }
+ }
+ buf.WriteString("\r\n")
+
+ return buf.Bytes()
+}
+
+func getFrom(msg *mail.Message) (string, error) {
+ from := msg.Header.Get("Sender")
+ if from == "" {
+ from = msg.Header.Get("From")
+ if from == "" {
+ return "", errors.New("mailer: invalid message, \"From\" field is absent")
+ }
+ }
+
+ return parseAddress(from)
+}
+
+func getRecipients(msg *mail.Message) (recipients, bcc []string, err error) {
+ for _, field := range []string{"Bcc", "To", "Cc"} {
+ if addresses, ok := msg.Header[field]; ok {
+ for _, addr := range addresses {
+ switch field {
+ case "Bcc":
+ bcc, err = addAdress(bcc, addr)
+ default:
+ recipients, err = addAdress(recipients, addr)
+ }
+ if err != nil {
+ return recipients, bcc, err
+ }
+ }
+ }
+ }
+
+ return recipients, bcc, nil
+}
+
+func addAdress(list []string, addr string) ([]string, error) {
+ addr, err := parseAddress(addr)
+ if err != nil {
+ return list, err
+ }
+ for _, a := range list {
+ if addr == a {
+ return list, nil
+ }
+ }
+
+ return append(list, addr), nil
+}
+
+func parseAddress(field string) (string, error) {
+ a, err := mail.ParseAddress(field)
+ if a == nil {
+ return "", err
+ }
+
+ return a.Address, err
+}
diff --git a/go/src/gopkg.in/gomail.v1/send.go b/go/src/gopkg.in/gomail.v1/send.go
new file mode 100644
index 0000000..77aa6a2
--- /dev/null
+++ b/go/src/gopkg.in/gomail.v1/send.go
@@ -0,0 +1,102 @@
+package gomail
+
+import (
+ "crypto/tls"
+ "io"
+ "net"
+ "net/smtp"
+)
+
+func (m *Mailer) getSendMailFunc(ssl bool) SendMailFunc {
+ return func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
+ var c smtpClient
+ var err error
+ if ssl {
+ c, err = sslDial(addr, m.host, m.config)
+ } else {
+ c, err = starttlsDial(addr, m.config)
+ }
+ if err != nil {
+ return err
+ }
+ defer c.Close()
+
+ if a != nil {
+ if ok, _ := c.Extension("AUTH"); ok {
+ if err = c.Auth(a); err != nil {
+ return err
+ }
+ }
+ }
+
+ if err = c.Mail(from); err != nil {
+ return err
+ }
+
+ for _, addr := range to {
+ if err = c.Rcpt(addr); err != nil {
+ return err
+ }
+ }
+
+ w, err := c.Data()
+ if err != nil {
+ return err
+ }
+ _, err = w.Write(msg)
+ if err != nil {
+ return err
+ }
+ err = w.Close()
+ if err != nil {
+ return err
+ }
+
+ return c.Quit()
+ }
+}
+
+func sslDial(addr, host string, config *tls.Config) (smtpClient, error) {
+ conn, err := initTLS("tcp", addr, config)
+ if err != nil {
+ return nil, err
+ }
+
+ return newClient(conn, host)
+}
+
+func starttlsDial(addr string, config *tls.Config) (smtpClient, error) {
+ c, err := initSMTP(addr)
+ if err != nil {
+ return c, err
+ }
+
+ if ok, _ := c.Extension("STARTTLS"); ok {
+ return c, c.StartTLS(config)
+ }
+
+ return c, nil
+}
+
+var initSMTP = func(addr string) (smtpClient, error) {
+ return smtp.Dial(addr)
+}
+
+var initTLS = func(network, addr string, config *tls.Config) (*tls.Conn, error) {
+ return tls.Dial(network, addr, config)
+}
+
+var newClient = func(conn net.Conn, host string) (smtpClient, error) {
+ return smtp.NewClient(conn, host)
+}
+
+type smtpClient interface {
+ Extension(string) (bool, string)
+ StartTLS(*tls.Config) error
+ Auth(smtp.Auth) error
+ Mail(string) error
+ Rcpt(string) error
+ Data() (io.WriteCloser, error)
+ Quit() error
+ Close() error
+}
diff --git a/go/src/gopkg.in/gomail.v1/send_test.go b/go/src/gopkg.in/gomail.v1/send_test.go
new file mode 100644
index 0000000..7f074bc
--- /dev/null
+++ b/go/src/gopkg.in/gomail.v1/send_test.go
@@ -0,0 +1,245 @@
+package gomail
+
+import (
+ "crypto/tls"
+ "io"
+ "net"
+ "net/smtp"
+ "testing"
+)
+
+var (
+ testAddr = "smtp.example.com:587"
+ testSSLAddr = "smtp.example.com:465"
+ testTLSConn = &tls.Conn{}
+ testConfig = &tls.Config{InsecureSkipVerify: true}
+ testHost = "smtp.example.com"
+ testAuth = smtp.PlainAuth("", "user", "pwd", "smtp.example.com")
+ testFrom = "from@example.com"
+ testTo = []string{"to1@example.com", "to2@example.com"}
+ testBody = "Test message"
+)
+
+const wantMsg = "To: to1@example.com, to2@example.com\r\n" +
+ "From: from@example.com\r\n" +
+ "Mime-Version: 1.0\r\n" +
+ "Date: Wed, 25 Jun 2014 17:46:00 +0000\r\n" +
+ "Content-Type: text/plain; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "Test message"
+
+func TestDefaultSendMail(t *testing.T) {
+ testSendMail(t, testAddr, nil, []string{
+ "Extension STARTTLS",
+ "StartTLS",
+ "Extension AUTH",
+ "Auth",
+ "Mail " + testFrom,
+ "Rcpt " + testTo[0],
+ "Rcpt " + testTo[1],
+ "Data",
+ "Write message",
+ "Close writer",
+ "Quit",
+ "Close",
+ })
+}
+
+func TestSSLSendMail(t *testing.T) {
+ testSendMail(t, testSSLAddr, nil, []string{
+ "Extension AUTH",
+ "Auth",
+ "Mail " + testFrom,
+ "Rcpt " + testTo[0],
+ "Rcpt " + testTo[1],
+ "Data",
+ "Write message",
+ "Close writer",
+ "Quit",
+ "Close",
+ })
+}
+
+func TestTLSConfigSendMail(t *testing.T) {
+ testSendMail(t, testAddr, testConfig, []string{
+ "Extension STARTTLS",
+ "StartTLS",
+ "Extension AUTH",
+ "Auth",
+ "Mail " + testFrom,
+ "Rcpt " + testTo[0],
+ "Rcpt " + testTo[1],
+ "Data",
+ "Write message",
+ "Close writer",
+ "Quit",
+ "Close",
+ })
+}
+
+func TestTLSConfigSSLSendMail(t *testing.T) {
+ testSendMail(t, testSSLAddr, testConfig, []string{
+ "Extension AUTH",
+ "Auth",
+ "Mail " + testFrom,
+ "Rcpt " + testTo[0],
+ "Rcpt " + testTo[1],
+ "Data",
+ "Write message",
+ "Close writer",
+ "Quit",
+ "Close",
+ })
+}
+
+type mockClient struct {
+ t *testing.T
+ i int
+ want []string
+ addr string
+ auth smtp.Auth
+ config *tls.Config
+}
+
+func (c *mockClient) Extension(ext string) (bool, string) {
+ c.do("Extension " + ext)
+ return true, ""
+}
+
+func (c *mockClient) StartTLS(config *tls.Config) error {
+ assertConfig(c.t, config, c.config)
+ c.do("StartTLS")
+ return nil
+}
+
+func (c *mockClient) Auth(a smtp.Auth) error {
+ assertAuth(c.t, a, c.auth)
+ c.do("Auth")
+ return nil
+}
+
+func (c *mockClient) Mail(from string) error {
+ c.do("Mail " + from)
+ return nil
+}
+
+func (c *mockClient) Rcpt(to string) error {
+ c.do("Rcpt " + to)
+ return nil
+}
+
+func (c *mockClient) Data() (io.WriteCloser, error) {
+ c.do("Data")
+ return &mockWriter{c: c, want: wantMsg}, nil
+}
+
+func (c *mockClient) Quit() error {
+ c.do("Quit")
+ return nil
+}
+
+func (c *mockClient) Close() error {
+ c.do("Close")
+ return nil
+}
+
+func (c *mockClient) do(cmd string) {
+ if c.i >= len(c.want) {
+ c.t.Fatalf("Invalid command %q", cmd)
+ }
+
+ if cmd != c.want[c.i] {
+ c.t.Fatalf("Invalid command, got %q, want %q", cmd, c.want[c.i])
+ }
+ c.i++
+}
+
+type mockWriter struct {
+ want string
+ c *mockClient
+}
+
+func (w *mockWriter) Write(p []byte) (int, error) {
+ w.c.do("Write message")
+ compareBodies(w.c.t, string(p), w.want)
+ return len(p), nil
+}
+
+func (w *mockWriter) Close() error {
+ w.c.do("Close writer")
+ return nil
+}
+
+func testSendMail(t *testing.T, addr string, config *tls.Config, want []string) {
+ testClient := &mockClient{
+ t: t,
+ want: want,
+ addr: addr,
+ auth: testAuth,
+ config: config,
+ }
+
+ initSMTP = func(addr string) (smtpClient, error) {
+ assertAddr(t, addr, testClient.addr)
+ return testClient, nil
+ }
+
+ initTLS = func(network, addr string, config *tls.Config) (*tls.Conn, error) {
+ if network != "tcp" {
+ t.Errorf("Invalid network, got %q, want tcp", network)
+ }
+ assertAddr(t, addr, testClient.addr)
+ assertConfig(t, config, testClient.config)
+ return testTLSConn, nil
+ }
+
+ newClient = func(conn net.Conn, host string) (smtpClient, error) {
+ if conn != testTLSConn {
+ t.Error("Invalid TLS connection used")
+ }
+ if host != testHost {
+ t.Errorf("Invalid host, got %q, want %q", host, testHost)
+ }
+ return testClient, nil
+ }
+
+ msg := NewMessage()
+ msg.SetHeader("From", testFrom)
+ msg.SetHeader("To", testTo...)
+ msg.SetBody("text/plain", testBody)
+
+ var settings []MailerSetting
+ if config != nil {
+ settings = []MailerSetting{SetTLSConfig(config)}
+ }
+
+ mailer := NewCustomMailer(addr, testAuth, settings...)
+ if err := mailer.Send(msg); err != nil {
+ t.Error(err)
+ }
+}
+
+func assertAuth(t *testing.T, got, want smtp.Auth) {
+ if got != want {
+ t.Errorf("Invalid auth, got %#v, want %#v", got, want)
+ }
+}
+
+func assertAddr(t *testing.T, got, want string) {
+ if got != want {
+ t.Errorf("Invalid addr, got %q, want %q", got, want)
+ }
+}
+
+func assertConfig(t *testing.T, got, want *tls.Config) {
+ if want == nil {
+ want = &tls.Config{ServerName: testHost}
+ }
+ if got.ServerName != want.ServerName {
+ t.Errorf("Invalid field ServerName in config, got %q, want %q", got.ServerName, want.ServerName)
+ }
+ if got.InsecureSkipVerify != want.InsecureSkipVerify {
+ t.Errorf("Invalid field InsecureSkipVerify in config, got %v, want %v", got.InsecureSkipVerify, want.InsecureSkipVerify)
+ }
+}