summaryrefslogtreecommitdiffstats
path: root/vendor/gopkg.in/gomail.v2
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/gopkg.in/gomail.v2')
-rw-r--r--vendor/gopkg.in/gomail.v2/.travis.yml9
-rw-r--r--vendor/gopkg.in/gomail.v2/CHANGELOG.md20
-rw-r--r--vendor/gopkg.in/gomail.v2/CONTRIBUTING.md20
-rw-r--r--vendor/gopkg.in/gomail.v2/LICENSE20
-rw-r--r--vendor/gopkg.in/gomail.v2/README.md92
-rw-r--r--vendor/gopkg.in/gomail.v2/auth.go49
-rw-r--r--vendor/gopkg.in/gomail.v2/auth_test.go100
-rw-r--r--vendor/gopkg.in/gomail.v2/doc.go5
-rw-r--r--vendor/gopkg.in/gomail.v2/example_test.go223
-rw-r--r--vendor/gopkg.in/gomail.v2/message.go322
-rw-r--r--vendor/gopkg.in/gomail.v2/message_test.go745
-rw-r--r--vendor/gopkg.in/gomail.v2/mime.go21
-rw-r--r--vendor/gopkg.in/gomail.v2/mime_go14.go25
-rw-r--r--vendor/gopkg.in/gomail.v2/send.go116
-rw-r--r--vendor/gopkg.in/gomail.v2/send_test.go80
-rw-r--r--vendor/gopkg.in/gomail.v2/smtp.go202
-rw-r--r--vendor/gopkg.in/gomail.v2/smtp_test.go292
-rw-r--r--vendor/gopkg.in/gomail.v2/writeto.go306
18 files changed, 2647 insertions, 0 deletions
diff --git a/vendor/gopkg.in/gomail.v2/.travis.yml b/vendor/gopkg.in/gomail.v2/.travis.yml
new file mode 100644
index 000000000..48915e737
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/.travis.yml
@@ -0,0 +1,9 @@
+language: go
+
+go:
+ - 1.2
+ - 1.3
+ - 1.4
+ - 1.5
+ - 1.6
+ - tip
diff --git a/vendor/gopkg.in/gomail.v2/CHANGELOG.md b/vendor/gopkg.in/gomail.v2/CHANGELOG.md
new file mode 100644
index 000000000..a797ab4c0
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/CHANGELOG.md
@@ -0,0 +1,20 @@
+# Change Log
+All notable changes to this project will be documented in this file.
+This project adheres to [Semantic Versioning](http://semver.org/).
+
+## [2.0.0] - 2015-09-02
+
+- Mailer has been removed. It has been replaced by Dialer and Sender.
+- `File` type and the `CreateFile` and `OpenFile` functions have been removed.
+- `Message.Attach` and `Message.Embed` have a new signature.
+- `Message.GetBodyWriter` has been removed. Use `Message.AddAlternativeWriter`
+instead.
+- `Message.Export` has been removed. `Message.WriteTo` can be used instead.
+- `Message.DelHeader` has been removed.
+- The `Bcc` header field is no longer sent. It is far more simpler and
+efficient: the same message is sent to all recipients instead of sending a
+different email to each Bcc address.
+- LoginAuth has been removed. `NewPlainDialer` now implements the LOGIN
+authentication mechanism when needed.
+- Go 1.2 is now required instead of Go 1.3. No external dependency are used when
+using Go 1.5.
diff --git a/vendor/gopkg.in/gomail.v2/CONTRIBUTING.md b/vendor/gopkg.in/gomail.v2/CONTRIBUTING.md
new file mode 100644
index 000000000..d5601c257
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/CONTRIBUTING.md
@@ -0,0 +1,20 @@
+Thank you for contributing to Gomail! Here are a few guidelines:
+
+## Bugs
+
+If you think you found a bug, create an issue and supply the minimum amount
+of code triggering the bug so it can be reproduced.
+
+
+## Fixing a bug
+
+If you want to fix a bug, you can send a pull request. It should contains a
+new test or update an existing one to cover that bug.
+
+
+## New feature proposal
+
+If you think Gomail lacks a feature, you can open an issue or send a pull
+request. I want to keep Gomail code and API as simple as possible so please
+describe your needs so we can discuss whether this feature should be added to
+Gomail or not.
diff --git a/vendor/gopkg.in/gomail.v2/LICENSE b/vendor/gopkg.in/gomail.v2/LICENSE
new file mode 100644
index 000000000..5f5c12af7
--- /dev/null
+++ b/vendor/gopkg.in/gomail.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/vendor/gopkg.in/gomail.v2/README.md b/vendor/gopkg.in/gomail.v2/README.md
new file mode 100644
index 000000000..b3be9e146
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/README.md
@@ -0,0 +1,92 @@
+# Gomail
+[![Build Status](https://travis-ci.org/go-gomail/gomail.svg?branch=v2)](https://travis-ci.org/go-gomail/gomail) [![Code Coverage](http://gocover.io/_badge/gopkg.in/gomail.v2)](http://gocover.io/gopkg.in/gomail.v2) [![Documentation](https://godoc.org/gopkg.in/gomail.v2?status.svg)](https://godoc.org/gopkg.in/gomail.v2)
+
+## Introduction
+
+Gomail is a simple and efficient package to send emails. It is well tested and
+documented.
+
+Gomail can only send emails using an SMTP server. But the API is flexible and it
+is easy to implement other methods for sending emails using a local Postfix, an
+API, etc.
+
+It is versioned using [gopkg.in](https://gopkg.in) so I promise
+there will never be backward incompatible changes within each version.
+
+It requires Go 1.2 or newer. With Go 1.5, no external dependencies are used.
+
+
+## Features
+
+Gomail supports:
+- Attachments
+- Embedded images
+- HTML and text templates
+- Automatic encoding of special characters
+- SSL and TLS
+- Sending multiple emails with the same SMTP connection
+
+
+## Documentation
+
+https://godoc.org/gopkg.in/gomail.v2
+
+
+## Download
+
+ go get gopkg.in/gomail.v2
+
+
+## Examples
+
+See the [examples in the documentation](https://godoc.org/gopkg.in/gomail.v2#example-package).
+
+
+## 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`:
+
+ package main
+
+ import (
+ "crypto/tls"
+
+ "gopkg.in/gomail.v2"
+ )
+
+ func main() {
+ d := gomail.NewDialer("smtp.example.com", 587, "user", "123456")
+ d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
+
+ // Send emails using d.
+ }
+
+Note, however, that this is insecure and should not be used in production.
+
+
+## Contribute
+
+Contributions are more than welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for
+more info.
+
+
+## Change log
+
+See [CHANGELOG.md](CHANGELOG.md).
+
+
+## License
+
+[MIT](LICENSE)
+
+
+## Contact
+
+You can ask questions on the [Gomail
+thread](https://groups.google.com/d/topic/golang-nuts/jMxZHzvvEVg/discussion)
+in the Go mailing-list.
diff --git a/vendor/gopkg.in/gomail.v2/auth.go b/vendor/gopkg.in/gomail.v2/auth.go
new file mode 100644
index 000000000..d28b83ab7
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/auth.go
@@ -0,0 +1,49 @@
+package gomail
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "net/smtp"
+)
+
+// loginAuth is an smtp.Auth that implements the LOGIN authentication mechanism.
+type loginAuth struct {
+ username string
+ password string
+ host string
+}
+
+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
+ }
+
+ switch {
+ case bytes.Equal(fromServer, []byte("Username:")):
+ return []byte(a.username), nil
+ case bytes.Equal(fromServer, []byte("Password:")):
+ return []byte(a.password), nil
+ default:
+ return nil, fmt.Errorf("gomail: unexpected server challenge: %s", fromServer)
+ }
+}
diff --git a/vendor/gopkg.in/gomail.v2/auth_test.go b/vendor/gopkg.in/gomail.v2/auth_test.go
new file mode 100644
index 000000000..428ef3467
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/auth_test.go
@@ -0,0 +1,100 @@
+package gomail
+
+import (
+ "net/smtp"
+ "testing"
+)
+
+const (
+ testUser = "user"
+ testPwd = "pwd"
+ testHost = "smtp.example.com"
+)
+
+type authTest struct {
+ auths []string
+ challenges []string
+ tls bool
+ wantData []string
+ wantError bool
+}
+
+func TestNoAdvertisement(t *testing.T) {
+ testLoginAuth(t, &authTest{
+ auths: []string{},
+ tls: false,
+ wantError: true,
+ })
+}
+
+func TestNoAdvertisementTLS(t *testing.T) {
+ testLoginAuth(t, &authTest{
+ auths: []string{},
+ challenges: []string{"Username:", "Password:"},
+ tls: true,
+ wantData: []string{"", testUser, testPwd},
+ })
+}
+
+func TestLogin(t *testing.T) {
+ testLoginAuth(t, &authTest{
+ auths: []string{"PLAIN", "LOGIN"},
+ challenges: []string{"Username:", "Password:"},
+ tls: false,
+ wantData: []string{"", testUser, testPwd},
+ })
+}
+
+func TestLoginTLS(t *testing.T) {
+ testLoginAuth(t, &authTest{
+ auths: []string{"LOGIN"},
+ challenges: []string{"Username:", "Password:"},
+ tls: true,
+ wantData: []string{"", testUser, testPwd},
+ })
+}
+
+func testLoginAuth(t *testing.T, test *authTest) {
+ auth := &loginAuth{
+ username: testUser,
+ password: testPwd,
+ host: testHost,
+ }
+ server := &smtp.ServerInfo{
+ Name: testHost,
+ TLS: test.tls,
+ Auth: test.auths,
+ }
+ proto, toServer, err := auth.Start(server)
+ if err != nil && !test.wantError {
+ t.Fatalf("loginAuth.Start(): %v", err)
+ }
+ if err != nil && test.wantError {
+ return
+ }
+ if proto != "LOGIN" {
+ t.Errorf("invalid protocol, got %q, want LOGIN", proto)
+ }
+
+ i := 0
+ got := string(toServer)
+ if got != test.wantData[i] {
+ t.Errorf("Invalid response, got %q, want %q", got, test.wantData[i])
+ }
+
+ for _, challenge := range test.challenges {
+ i++
+ if i >= len(test.wantData) {
+ t.Fatalf("unexpected challenge: %q", challenge)
+ }
+
+ toServer, err = auth.Next([]byte(challenge), true)
+ if err != nil {
+ t.Fatalf("loginAuth.Auth(): %v", err)
+ }
+ got = string(toServer)
+ if got != test.wantData[i] {
+ t.Errorf("Invalid response, got %q, want %q", got, test.wantData[i])
+ }
+ }
+}
diff --git a/vendor/gopkg.in/gomail.v2/doc.go b/vendor/gopkg.in/gomail.v2/doc.go
new file mode 100644
index 000000000..a8f5091f5
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/doc.go
@@ -0,0 +1,5 @@
+// Package gomail provides a simple interface to compose emails and to mail them
+// efficiently.
+//
+// More info on Github: https://github.com/go-gomail/gomail
+package gomail
diff --git a/vendor/gopkg.in/gomail.v2/example_test.go b/vendor/gopkg.in/gomail.v2/example_test.go
new file mode 100644
index 000000000..90008abe8
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/example_test.go
@@ -0,0 +1,223 @@
+package gomail_test
+
+import (
+ "fmt"
+ "html/template"
+ "io"
+ "log"
+ "time"
+
+ "gopkg.in/gomail.v2"
+)
+
+func Example() {
+ m := gomail.NewMessage()
+ m.SetHeader("From", "alex@example.com")
+ m.SetHeader("To", "bob@example.com", "cora@example.com")
+ m.SetAddressHeader("Cc", "dan@example.com", "Dan")
+ m.SetHeader("Subject", "Hello!")
+ m.SetBody("text/html", "Hello <b>Bob</b> and <i>Cora</i>!")
+ m.Attach("/home/Alex/lolcat.jpg")
+
+ d := gomail.NewDialer("smtp.example.com", 587, "user", "123456")
+
+ // Send the email to Bob, Cora and Dan.
+ if err := d.DialAndSend(m); err != nil {
+ panic(err)
+ }
+}
+
+// A daemon that listens to a channel and sends all incoming messages.
+func Example_daemon() {
+ ch := make(chan *gomail.Message)
+
+ go func() {
+ d := gomail.NewDialer("smtp.example.com", 587, "user", "123456")
+
+ var s gomail.SendCloser
+ var err error
+ open := false
+ for {
+ select {
+ case m, ok := <-ch:
+ if !ok {
+ return
+ }
+ if !open {
+ if s, err = d.Dial(); err != nil {
+ panic(err)
+ }
+ open = true
+ }
+ if err := gomail.Send(s, m); err != nil {
+ log.Print(err)
+ }
+ // Close the connection to the SMTP server if no email was sent in
+ // the last 30 seconds.
+ case <-time.After(30 * time.Second):
+ if open {
+ if err := s.Close(); err != nil {
+ panic(err)
+ }
+ open = false
+ }
+ }
+ }
+ }()
+
+ // Use the channel in your program to send emails.
+
+ // Close the channel to stop the mail daemon.
+ close(ch)
+}
+
+// Efficiently send a customized newsletter to a list of recipients.
+func Example_newsletter() {
+ // The list of recipients.
+ var list []struct {
+ Name string
+ Address string
+ }
+
+ d := gomail.NewDialer("smtp.example.com", 587, "user", "123456")
+ s, err := d.Dial()
+ if err != nil {
+ panic(err)
+ }
+
+ m := gomail.NewMessage()
+ for _, r := range list {
+ m.SetHeader("From", "no-reply@example.com")
+ m.SetAddressHeader("To", r.Address, r.Name)
+ m.SetHeader("Subject", "Newsletter #1")
+ m.SetBody("text/html", fmt.Sprintf("Hello %s!", r.Name))
+
+ if err := gomail.Send(s, m); err != nil {
+ log.Printf("Could not send email to %q: %v", r.Address, err)
+ }
+ m.Reset()
+ }
+}
+
+// Send an email using a local SMTP server.
+func Example_noAuth() {
+ m := gomail.NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.SetHeader("Subject", "Hello!")
+ m.SetBody("text/plain", "Hello!")
+
+ d := gomail.Dialer{Host: "localhost", Port: 587}
+ if err := d.DialAndSend(m); err != nil {
+ panic(err)
+ }
+}
+
+// Send an email using an API or postfix.
+func Example_noSMTP() {
+ m := gomail.NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.SetHeader("Subject", "Hello!")
+ m.SetBody("text/plain", "Hello!")
+
+ s := gomail.SendFunc(func(from string, to []string, msg io.WriterTo) error {
+ // Implements you email-sending function, for example by calling
+ // an API, or running postfix, etc.
+ fmt.Println("From:", from)
+ fmt.Println("To:", to)
+ return nil
+ })
+
+ if err := gomail.Send(s, m); err != nil {
+ panic(err)
+ }
+ // Output:
+ // From: from@example.com
+ // To: [to@example.com]
+}
+
+var m *gomail.Message
+
+func ExampleSetCopyFunc() {
+ m.Attach("foo.txt", gomail.SetCopyFunc(func(w io.Writer) error {
+ _, err := w.Write([]byte("Content of foo.txt"))
+ return err
+ }))
+}
+
+func ExampleSetHeader() {
+ h := map[string][]string{"Content-ID": {"<foo@bar.mail>"}}
+ m.Attach("foo.jpg", gomail.SetHeader(h))
+}
+
+func ExampleRename() {
+ m.Attach("/tmp/0000146.jpg", gomail.Rename("picture.jpg"))
+}
+
+func ExampleMessage_AddAlternative() {
+ m.SetBody("text/plain", "Hello!")
+ m.AddAlternative("text/html", "<p>Hello!</p>")
+}
+
+func ExampleMessage_AddAlternativeWriter() {
+ t := template.Must(template.New("example").Parse("Hello {{.}}!"))
+ m.AddAlternativeWriter("text/plain", func(w io.Writer) error {
+ return t.Execute(w, "Bob")
+ })
+}
+
+func ExampleMessage_Attach() {
+ m.Attach("/tmp/image.jpg")
+}
+
+func ExampleMessage_Embed() {
+ m.Embed("/tmp/image.jpg")
+ m.SetBody("text/html", `<img src="cid:image.jpg" alt="My image" />`)
+}
+
+func ExampleMessage_FormatAddress() {
+ m.SetHeader("To", m.FormatAddress("bob@example.com", "Bob"), m.FormatAddress("cora@example.com", "Cora"))
+}
+
+func ExampleMessage_FormatDate() {
+ m.SetHeaders(map[string][]string{
+ "X-Date": {m.FormatDate(time.Now())},
+ })
+}
+
+func ExampleMessage_SetAddressHeader() {
+ m.SetAddressHeader("To", "bob@example.com", "Bob")
+}
+
+func ExampleMessage_SetBody() {
+ m.SetBody("text/plain", "Hello!")
+}
+
+func ExampleMessage_SetDateHeader() {
+ m.SetDateHeader("X-Date", time.Now())
+}
+
+func ExampleMessage_SetHeader() {
+ m.SetHeader("Subject", "Hello!")
+}
+
+func ExampleMessage_SetHeaders() {
+ m.SetHeaders(map[string][]string{
+ "From": {m.FormatAddress("alex@example.com", "Alex")},
+ "To": {"bob@example.com", "cora@example.com"},
+ "Subject": {"Hello"},
+ })
+}
+
+func ExampleSetCharset() {
+ m = gomail.NewMessage(gomail.SetCharset("ISO-8859-1"))
+}
+
+func ExampleSetEncoding() {
+ m = gomail.NewMessage(gomail.SetEncoding(gomail.Base64))
+}
+
+func ExampleSetPartEncoding() {
+ m.SetBody("text/plain", "Hello!", gomail.SetPartEncoding(gomail.Unencoded))
+}
diff --git a/vendor/gopkg.in/gomail.v2/message.go b/vendor/gopkg.in/gomail.v2/message.go
new file mode 100644
index 000000000..4bffb1e7f
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/message.go
@@ -0,0 +1,322 @@
+package gomail
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "path/filepath"
+ "time"
+)
+
+// Message represents an email.
+type Message struct {
+ header header
+ parts []*part
+ attachments []*file
+ embedded []*file
+ charset string
+ encoding Encoding
+ hEncoder mimeEncoder
+ buf bytes.Buffer
+}
+
+type header map[string][]string
+
+type part struct {
+ contentType string
+ copier func(io.Writer) error
+ encoding Encoding
+}
+
+// NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding
+// by default.
+func NewMessage(settings ...MessageSetting) *Message {
+ m := &Message{
+ header: make(header),
+ charset: "UTF-8",
+ encoding: QuotedPrintable,
+ }
+
+ m.applySettings(settings)
+
+ if m.encoding == Base64 {
+ m.hEncoder = bEncoding
+ } else {
+ m.hEncoder = qEncoding
+ }
+
+ return m
+}
+
+// Reset resets the message so it can be reused. The message keeps its previous
+// settings so it is in the same state that after a call to NewMessage.
+func (m *Message) Reset() {
+ for k := range m.header {
+ delete(m.header, k)
+ }
+ m.parts = nil
+ m.attachments = nil
+ m.embedded = nil
+}
+
+func (m *Message) applySettings(settings []MessageSetting) {
+ for _, s := range settings {
+ s(m)
+ }
+}
+
+// A MessageSetting can be used as an argument in NewMessage to configure an
+// email.
+type MessageSetting func(m *Message)
+
+// SetCharset is a message setting to set the charset of the email.
+func SetCharset(charset string) MessageSetting {
+ return func(m *Message) {
+ m.charset = charset
+ }
+}
+
+// SetEncoding is a message setting to set the encoding of the email.
+func SetEncoding(enc Encoding) MessageSetting {
+ return func(m *Message) {
+ m.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 (m *Message) SetHeader(field string, value ...string) {
+ m.encodeHeader(value)
+ m.header[field] = value
+}
+
+func (m *Message) encodeHeader(values []string) {
+ for i := range values {
+ values[i] = m.encodeString(values[i])
+ }
+}
+
+func (m *Message) encodeString(value string) string {
+ return m.hEncoder.Encode(m.charset, value)
+}
+
+// SetHeaders sets the message headers.
+func (m *Message) SetHeaders(h map[string][]string) {
+ for k, v := range h {
+ m.SetHeader(k, v...)
+ }
+}
+
+// SetAddressHeader sets an address to the given header field.
+func (m *Message) SetAddressHeader(field, address, name string) {
+ m.header[field] = []string{m.FormatAddress(address, name)}
+}
+
+// FormatAddress formats an address and a name as a valid RFC 5322 address.
+func (m *Message) FormatAddress(address, name string) string {
+ if name == "" {
+ return address
+ }
+
+ enc := m.encodeString(name)
+ if enc == name {
+ m.buf.WriteByte('"')
+ for i := 0; i < len(name); i++ {
+ b := name[i]
+ if b == '\\' || b == '"' {
+ m.buf.WriteByte('\\')
+ }
+ m.buf.WriteByte(b)
+ }
+ m.buf.WriteByte('"')
+ } else if hasSpecials(name) {
+ m.buf.WriteString(bEncoding.Encode(m.charset, name))
+ } else {
+ m.buf.WriteString(enc)
+ }
+ m.buf.WriteString(" <")
+ m.buf.WriteString(address)
+ m.buf.WriteByte('>')
+
+ addr := m.buf.String()
+ m.buf.Reset()
+ return addr
+}
+
+func hasSpecials(text string) bool {
+ for i := 0; i < len(text); i++ {
+ switch c := text[i]; c {
+ case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"':
+ return true
+ }
+ }
+
+ return false
+}
+
+// SetDateHeader sets a date to the given header field.
+func (m *Message) SetDateHeader(field string, date time.Time) {
+ m.header[field] = []string{m.FormatDate(date)}
+}
+
+// FormatDate formats a date as a valid RFC 5322 date.
+func (m *Message) FormatDate(date time.Time) string {
+ return date.Format(time.RFC1123Z)
+}
+
+// GetHeader gets a header field.
+func (m *Message) GetHeader(field string) []string {
+ return m.header[field]
+}
+
+// SetBody sets the body of the message. It replaces any content previously set
+// by SetBody, AddAlternative or AddAlternativeWriter.
+func (m *Message) SetBody(contentType, body string, settings ...PartSetting) {
+ m.parts = []*part{m.newPart(contentType, newCopier(body), settings)}
+}
+
+// AddAlternative adds an alternative part to the message.
+//
+// It is commonly used to send HTML emails that default to the plain text
+// version for backward compatibility. AddAlternative appends the new part to
+// the end of the message. So the plain text part should be added before the
+// HTML part. See http://en.wikipedia.org/wiki/MIME#Alternative
+func (m *Message) AddAlternative(contentType, body string, settings ...PartSetting) {
+ m.AddAlternativeWriter(contentType, newCopier(body), settings...)
+}
+
+func newCopier(s string) func(io.Writer) error {
+ return func(w io.Writer) error {
+ _, err := io.WriteString(w, s)
+ return err
+ }
+}
+
+// AddAlternativeWriter adds an alternative part to the message. It can be
+// useful with the text/template or html/template packages.
+func (m *Message) AddAlternativeWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
+ m.parts = append(m.parts, m.newPart(contentType, f, settings))
+}
+
+func (m *Message) newPart(contentType string, f func(io.Writer) error, settings []PartSetting) *part {
+ p := &part{
+ contentType: contentType,
+ copier: f,
+ encoding: m.encoding,
+ }
+
+ for _, s := range settings {
+ s(p)
+ }
+
+ return p
+}
+
+// A PartSetting can be used as an argument in Message.SetBody,
+// Message.AddAlternative or Message.AddAlternativeWriter to configure the part
+// added to a message.
+type PartSetting func(*part)
+
+// SetPartEncoding sets the encoding of the part added to the message. By
+// default, parts use the same encoding than the message.
+func SetPartEncoding(e Encoding) PartSetting {
+ return PartSetting(func(p *part) {
+ p.encoding = e
+ })
+}
+
+type file struct {
+ Name string
+ Header map[string][]string
+ CopyFunc func(w io.Writer) error
+}
+
+func (f *file) setHeader(field, value string) {
+ f.Header[field] = []string{value}
+}
+
+// A FileSetting can be used as an argument in Message.Attach or Message.Embed.
+type FileSetting func(*file)
+
+// SetHeader is a file setting to set the MIME header of the message part that
+// contains the file content.
+//
+// Mandatory headers are automatically added if they are not set when sending
+// the email.
+func SetHeader(h map[string][]string) FileSetting {
+ return func(f *file) {
+ for k, v := range h {
+ f.Header[k] = v
+ }
+ }
+}
+
+// Rename is a file setting to set the name of the attachment if the name is
+// different than the filename on disk.
+func Rename(name string) FileSetting {
+ return func(f *file) {
+ f.Name = name
+ }
+}
+
+// SetCopyFunc is a file setting to replace the function that runs when the
+// message is sent. It should copy the content of the file to the io.Writer.
+//
+// The default copy function opens the file with the given filename, and copy
+// its content to the io.Writer.
+func SetCopyFunc(f func(io.Writer) error) FileSetting {
+ return func(fi *file) {
+ fi.CopyFunc = f
+ }
+}
+
+func (m *Message) appendFile(list []*file, name string, settings []FileSetting) []*file {
+ f := &file{
+ Name: filepath.Base(name),
+ Header: make(map[string][]string),
+ CopyFunc: func(w io.Writer) error {
+ h, err := os.Open(name)
+ if err != nil {
+ return err
+ }
+ if _, err := io.Copy(w, h); err != nil {
+ h.Close()
+ return err
+ }
+ return h.Close()
+ },
+ }
+
+ for _, s := range settings {
+ s(f)
+ }
+
+ if list == nil {
+ return []*file{f}
+ }
+
+ return append(list, f)
+}
+
+// Attach attaches the files to the email.
+func (m *Message) Attach(filename string, settings ...FileSetting) {
+ m.attachments = m.appendFile(m.attachments, filename, settings)
+}
+
+// Embed embeds the images to the email.
+func (m *Message) Embed(filename string, settings ...FileSetting) {
+ m.embedded = m.appendFile(m.embedded, filename, settings)
+}
diff --git a/vendor/gopkg.in/gomail.v2/message_test.go b/vendor/gopkg.in/gomail.v2/message_test.go
new file mode 100644
index 000000000..acceff2a6
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/message_test.go
@@ -0,0 +1,745 @@
+package gomail
+
+import (
+ "bytes"
+ "encoding/base64"
+ "io"
+ "io/ioutil"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+func init() {
+ now = func() time.Time {
+ return time.Date(2014, 06, 25, 17, 46, 0, 0, time.UTC)
+ }
+}
+
+type message struct {
+ from string
+ to []string
+ content string
+}
+
+func TestMessage(t *testing.T) {
+ m := NewMessage()
+ m.SetAddressHeader("From", "from@example.com", "Señor From")
+ m.SetHeader("To", m.FormatAddress("to@example.com", "Señor To"), "tobis@example.com")
+ m.SetAddressHeader("Cc", "cc@example.com", "A, B")
+ m.SetAddressHeader("X-To", "ccbis@example.com", "à, b")
+ m.SetDateHeader("X-Date", now())
+ m.SetHeader("X-Date-2", m.FormatDate(now()))
+ m.SetHeader("Subject", "¡Hola, señor!")
+ m.SetHeaders(map[string][]string{
+ "X-Headers": {"Test", "Café"},
+ })
+ m.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, m, 0, want)
+}
+
+func TestCustomMessage(t *testing.T) {
+ m := NewMessage(SetCharset("ISO-8859-1"), SetEncoding(Base64))
+ m.SetHeaders(map[string][]string{
+ "From": {"from@example.com"},
+ "To": {"to@example.com"},
+ "Subject": {"Café"},
+ })
+ m.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, m, 0, want)
+}
+
+func TestUnencodedMessage(t *testing.T) {
+ m := NewMessage(SetEncoding(Unencoded))
+ m.SetHeaders(map[string][]string{
+ "From": {"from@example.com"},
+ "To": {"to@example.com"},
+ "Subject": {"Café"},
+ })
+ m.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, m, 0, want)
+}
+
+func TestRecipients(t *testing.T) {
+ m := NewMessage()
+ m.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!"},
+ })
+ m.SetBody("text/plain", "Test message")
+
+ want := &message{
+ from: "from@example.com",
+ to: []string{"to@example.com", "cc@example.com", "bcc1@example.com", "bcc2@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",
+ }
+
+ testMessage(t, m, 0, want)
+}
+
+func TestAlternative(t *testing.T) {
+ m := NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.SetBody("text/plain", "¡Hola, señor!")
+ m.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;\r\n" +
+ " 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, m, 1, want)
+}
+
+func TestPartSetting(t *testing.T) {
+ m := NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.SetBody("text/plain; format=flowed", "¡Hola, señor!", SetPartEncoding(Unencoded))
+ m.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;\r\n" +
+ " boundary=_BOUNDARY_1_\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: text/plain; format=flowed; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: 8bit\r\n" +
+ "\r\n" +
+ "¡Hola, señor!\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, m, 1, want)
+}
+
+func TestBodyWriter(t *testing.T) {
+ m := NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.AddAlternativeWriter("text/plain", func(w io.Writer) error {
+ _, err := w.Write([]byte("Test message"))
+ return err
+ })
+ m.AddAlternativeWriter("text/html", func(w io.Writer) error {
+ _, err := w.Write([]byte("Test HTML"))
+ return err
+ })
+
+ 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;\r\n" +
+ " 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 message\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: text/html; charset=UTF-8\r\n" +
+ "Content-Transfer-Encoding: quoted-printable\r\n" +
+ "\r\n" +
+ "Test HTML\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, m, 1, want)
+}
+
+func TestAttachmentOnly(t *testing.T) {
+ m := NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.Attach(mockCopyFile("/tmp/test.pdf"))
+
+ 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, m, 0, want)
+}
+
+func TestAttachment(t *testing.T) {
+ m := NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.SetBody("text/plain", "Test")
+ m.Attach(mockCopyFile("/tmp/test.pdf"))
+
+ 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;\r\n" +
+ " 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 of test.pdf")) + "\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, m, 1, want)
+}
+
+func TestRename(t *testing.T) {
+ m := NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.SetBody("text/plain", "Test")
+ name, copy := mockCopyFile("/tmp/test.pdf")
+ rename := Rename("another.pdf")
+ m.Attach(name, copy, rename)
+
+ 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;\r\n" +
+ " 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=\"another.pdf\"\r\n" +
+ "Content-Disposition: attachment; filename=\"another.pdf\"\r\n" +
+ "Content-Transfer-Encoding: base64\r\n" +
+ "\r\n" +
+ base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")) + "\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, m, 1, want)
+}
+
+func TestAttachmentsOnly(t *testing.T) {
+ m := NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.Attach(mockCopyFile("/tmp/test.pdf"))
+ m.Attach(mockCopyFile("/tmp/test.zip"))
+
+ 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;\r\n" +
+ " 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 of test.pdf")) + "\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 of test.zip")) + "\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, m, 1, want)
+}
+
+func TestAttachments(t *testing.T) {
+ m := NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.SetBody("text/plain", "Test")
+ m.Attach(mockCopyFile("/tmp/test.pdf"))
+ m.Attach(mockCopyFile("/tmp/test.zip"))
+
+ 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;\r\n" +
+ " 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 of test.pdf")) + "\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 of test.zip")) + "\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, m, 1, want)
+}
+
+func TestEmbedded(t *testing.T) {
+ m := NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.Embed(mockCopyFileWithHeader(m, "image1.jpg", map[string][]string{"Content-ID": {"<test-content-id>"}}))
+ m.Embed(mockCopyFile("image2.jpg"))
+ m.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;\r\n" +
+ " 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 of image1.jpg")) + "\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 of image2.jpg")) + "\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, m, 1, want)
+}
+
+func TestFullMessage(t *testing.T) {
+ m := NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.SetBody("text/plain", "¡Hola, señor!")
+ m.AddAlternative("text/html", "¡<b>Hola</b>, <i>señor</i>!</h1>")
+ m.Attach(mockCopyFile("test.pdf"))
+ m.Embed(mockCopyFile("image.jpg"))
+
+ 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;\r\n" +
+ " boundary=_BOUNDARY_1_\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_1_\r\n" +
+ "Content-Type: multipart/related;\r\n" +
+ " boundary=_BOUNDARY_2_\r\n" +
+ "\r\n" +
+ "--_BOUNDARY_2_\r\n" +
+ "Content-Type: multipart/alternative;\r\n" +
+ " 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 of image.jpg")) + "\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 of test.pdf")) + "\r\n" +
+ "--_BOUNDARY_1_--\r\n",
+ }
+
+ testMessage(t, m, 3, want)
+
+ 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 reset",
+ }
+ m.Reset()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.SetBody("text/plain", "Test reset")
+ testMessage(t, m, 0, want)
+}
+
+func TestQpLineLength(t *testing.T) {
+ m := NewMessage()
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.SetBody("text/plain",
+ 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", 72)+"à\r\n"+
+ strings.Repeat("0", 75)+"\r\n"+
+ strings.Repeat("0", 76)+"\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", 75) + "=\r\n0\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) + "=\r\n=C3=A0\r\n" +
+ strings.Repeat("0", 72) + "=C3=\r\n=A0\r\n" +
+ strings.Repeat("0", 75) + "\r\n" +
+ strings.Repeat("0", 75) + "=\r\n0\r\n",
+ }
+
+ testMessage(t, m, 0, want)
+}
+
+func TestBase64LineLength(t *testing.T) {
+ m := NewMessage(SetCharset("UTF-8"), SetEncoding(Base64))
+ m.SetHeader("From", "from@example.com")
+ m.SetHeader("To", "to@example.com")
+ m.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, m, 0, want)
+}
+
+func TestEmptyName(t *testing.T) {
+ m := NewMessage()
+ m.SetAddressHeader("From", "from@example.com", "")
+
+ want := &message{
+ from: "from@example.com",
+ content: "From: from@example.com\r\n",
+ }
+
+ testMessage(t, m, 0, want)
+}
+
+func TestEmptyHeader(t *testing.T) {
+ m := NewMessage()
+ m.SetHeaders(map[string][]string{
+ "From": {"from@example.com"},
+ "X-Empty": nil,
+ })
+
+ want := &message{
+ from: "from@example.com",
+ content: "From: from@example.com\r\n" +
+ "X-Empty:\r\n",
+ }
+
+ testMessage(t, m, 0, want)
+}
+
+func testMessage(t *testing.T, m *Message, bCount int, want *message) {
+ err := Send(stubSendMail(t, bCount, want), m)
+ if err != nil {
+ t.Error(err)
+ }
+}
+
+func stubSendMail(t *testing.T, bCount int, want *message) SendFunc {
+ return func(from string, to []string, m io.WriterTo) error {
+ 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],
+ )
+ }
+ }
+
+ buf := new(bytes.Buffer)
+ _, err := m.WriteTo(buf)
+ if err != nil {
+ t.Error(err)
+ }
+ got := buf.String()
+ 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)
+ }
+ }
+
+ 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, m string) []string {
+ if matches := boundaryRegExp.FindAllStringSubmatch(m, 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 mockCopyFile(name string) (string, FileSetting) {
+ return name, SetCopyFunc(func(w io.Writer) error {
+ _, err := w.Write([]byte("Content of " + filepath.Base(name)))
+ return err
+ })
+}
+
+func mockCopyFileWithHeader(m *Message, name string, h map[string][]string) (string, FileSetting, FileSetting) {
+ name, f := mockCopyFile(name)
+ return name, f, SetHeader(h)
+}
+
+func BenchmarkFull(b *testing.B) {
+ discardFunc := SendFunc(func(from string, to []string, m io.WriterTo) error {
+ _, err := m.WriteTo(ioutil.Discard)
+ return err
+ })
+
+ m := NewMessage()
+ b.ResetTimer()
+ for n := 0; n < b.N; n++ {
+ m.SetAddressHeader("From", "from@example.com", "Señor From")
+ m.SetHeaders(map[string][]string{
+ "To": {"to@example.com"},
+ "Cc": {"cc@example.com"},
+ "Bcc": {"bcc1@example.com", "bcc2@example.com"},
+ "Subject": {"¡Hola, señor!"},
+ })
+ m.SetBody("text/plain", "¡Hola, señor!")
+ m.AddAlternative("text/html", "<p>¡Hola, señor!</p>")
+ m.Attach(mockCopyFile("benchmark.txt"))
+ m.Embed(mockCopyFile("benchmark.jpg"))
+
+ if err := Send(discardFunc, m); err != nil {
+ panic(err)
+ }
+ m.Reset()
+ }
+}
diff --git a/vendor/gopkg.in/gomail.v2/mime.go b/vendor/gopkg.in/gomail.v2/mime.go
new file mode 100644
index 000000000..194d4a769
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/mime.go
@@ -0,0 +1,21 @@
+// +build go1.5
+
+package gomail
+
+import (
+ "mime"
+ "mime/quotedprintable"
+ "strings"
+)
+
+var newQPWriter = quotedprintable.NewWriter
+
+type mimeEncoder struct {
+ mime.WordEncoder
+}
+
+var (
+ bEncoding = mimeEncoder{mime.BEncoding}
+ qEncoding = mimeEncoder{mime.QEncoding}
+ lastIndexByte = strings.LastIndexByte
+)
diff --git a/vendor/gopkg.in/gomail.v2/mime_go14.go b/vendor/gopkg.in/gomail.v2/mime_go14.go
new file mode 100644
index 000000000..3dc26aa2a
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/mime_go14.go
@@ -0,0 +1,25 @@
+// +build !go1.5
+
+package gomail
+
+import "gopkg.in/alexcesaro/quotedprintable.v3"
+
+var newQPWriter = quotedprintable.NewWriter
+
+type mimeEncoder struct {
+ quotedprintable.WordEncoder
+}
+
+var (
+ bEncoding = mimeEncoder{quotedprintable.BEncoding}
+ qEncoding = mimeEncoder{quotedprintable.QEncoding}
+ lastIndexByte = func(s string, c byte) int {
+ for i := len(s) - 1; i >= 0; i-- {
+
+ if s[i] == c {
+ return i
+ }
+ }
+ return -1
+ }
+)
diff --git a/vendor/gopkg.in/gomail.v2/send.go b/vendor/gopkg.in/gomail.v2/send.go
new file mode 100644
index 000000000..9115ebe72
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/send.go
@@ -0,0 +1,116 @@
+package gomail
+
+import (
+ "errors"
+ "fmt"
+ "io"
+ "net/mail"
+)
+
+// Sender is the interface that wraps the Send method.
+//
+// Send sends an email to the given addresses.
+type Sender interface {
+ Send(from string, to []string, msg io.WriterTo) error
+}
+
+// SendCloser is the interface that groups the Send and Close methods.
+type SendCloser interface {
+ Sender
+ Close() error
+}
+
+// A SendFunc is a function that sends emails to the given addresses.
+//
+// The SendFunc type is an adapter to allow the use of ordinary functions as
+// email senders. If f is a function with the appropriate signature, SendFunc(f)
+// is a Sender object that calls f.
+type SendFunc func(from string, to []string, msg io.WriterTo) error
+
+// Send calls f(from, to, msg).
+func (f SendFunc) Send(from string, to []string, msg io.WriterTo) error {
+ return f(from, to, msg)
+}
+
+// Send sends emails using the given Sender.
+func Send(s Sender, msg ...*Message) error {
+ for i, m := range msg {
+ if err := send(s, m); err != nil {
+ return fmt.Errorf("gomail: could not send email %d: %v", i+1, err)
+ }
+ }
+
+ return nil
+}
+
+func send(s Sender, m *Message) error {
+ from, err := m.getFrom()
+ if err != nil {
+ return err
+ }
+
+ to, err := m.getRecipients()
+ if err != nil {
+ return err
+ }
+
+ if err := s.Send(from, to, m); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (m *Message) getFrom() (string, error) {
+ from := m.header["Sender"]
+ if len(from) == 0 {
+ from = m.header["From"]
+ if len(from) == 0 {
+ return "", errors.New(`gomail: invalid message, "From" field is absent`)
+ }
+ }
+
+ return parseAddress(from[0])
+}
+
+func (m *Message) getRecipients() ([]string, error) {
+ n := 0
+ for _, field := range []string{"To", "Cc", "Bcc"} {
+ if addresses, ok := m.header[field]; ok {
+ n += len(addresses)
+ }
+ }
+ list := make([]string, 0, n)
+
+ for _, field := range []string{"To", "Cc", "Bcc"} {
+ if addresses, ok := m.header[field]; ok {
+ for _, a := range addresses {
+ addr, err := parseAddress(a)
+ if err != nil {
+ return nil, err
+ }
+ list = addAddress(list, addr)
+ }
+ }
+ }
+
+ return list, nil
+}
+
+func addAddress(list []string, addr string) []string {
+ for _, a := range list {
+ if addr == a {
+ return list
+ }
+ }
+
+ return append(list, addr)
+}
+
+func parseAddress(field string) (string, error) {
+ addr, err := mail.ParseAddress(field)
+ if err != nil {
+ return "", fmt.Errorf("gomail: invalid address %q: %v", field, err)
+ }
+ return addr.Address, nil
+}
diff --git a/vendor/gopkg.in/gomail.v2/send_test.go b/vendor/gopkg.in/gomail.v2/send_test.go
new file mode 100644
index 000000000..ba59cd3dc
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/send_test.go
@@ -0,0 +1,80 @@
+package gomail
+
+import (
+ "bytes"
+ "io"
+ "reflect"
+ "testing"
+)
+
+const (
+ testTo1 = "to1@example.com"
+ testTo2 = "to2@example.com"
+ testFrom = "from@example.com"
+ testBody = "Test message"
+ testMsg = "To: " + testTo1 + ", " + testTo2 + "\r\n" +
+ "From: " + testFrom + "\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" +
+ testBody
+)
+
+type mockSender SendFunc
+
+func (s mockSender) Send(from string, to []string, msg io.WriterTo) error {
+ return s(from, to, msg)
+}
+
+type mockSendCloser struct {
+ mockSender
+ close func() error
+}
+
+func (s *mockSendCloser) Close() error {
+ return s.close()
+}
+
+func TestSend(t *testing.T) {
+ s := &mockSendCloser{
+ mockSender: stubSend(t, testFrom, []string{testTo1, testTo2}, testMsg),
+ close: func() error {
+ t.Error("Close() should not be called in Send()")
+ return nil
+ },
+ }
+ if err := Send(s, getTestMessage()); err != nil {
+ t.Errorf("Send(): %v", err)
+ }
+}
+
+func getTestMessage() *Message {
+ m := NewMessage()
+ m.SetHeader("From", testFrom)
+ m.SetHeader("To", testTo1, testTo2)
+ m.SetBody("text/plain", testBody)
+
+ return m
+}
+
+func stubSend(t *testing.T, wantFrom string, wantTo []string, wantBody string) mockSender {
+ return func(from string, to []string, msg io.WriterTo) error {
+ if from != wantFrom {
+ t.Errorf("invalid from, got %q, want %q", from, wantFrom)
+ }
+ if !reflect.DeepEqual(to, wantTo) {
+ t.Errorf("invalid to, got %v, want %v", to, wantTo)
+ }
+
+ buf := new(bytes.Buffer)
+ _, err := msg.WriteTo(buf)
+ if err != nil {
+ t.Fatal(err)
+ }
+ compareBodies(t, buf.String(), wantBody)
+
+ return nil
+ }
+}
diff --git a/vendor/gopkg.in/gomail.v2/smtp.go b/vendor/gopkg.in/gomail.v2/smtp.go
new file mode 100644
index 000000000..2aa49c8b6
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/smtp.go
@@ -0,0 +1,202 @@
+package gomail
+
+import (
+ "crypto/tls"
+ "fmt"
+ "io"
+ "net"
+ "net/smtp"
+ "strings"
+ "time"
+)
+
+// A Dialer is a dialer to an SMTP server.
+type Dialer struct {
+ // Host represents the host of the SMTP server.
+ Host string
+ // Port represents the port of the SMTP server.
+ Port int
+ // Username is the username to use to authenticate to the SMTP server.
+ Username string
+ // Password is the password to use to authenticate to the SMTP server.
+ Password string
+ // Auth represents the authentication mechanism used to authenticate to the
+ // SMTP server.
+ Auth smtp.Auth
+ // SSL defines whether an SSL connection is used. It should be false in
+ // most cases since the authentication mechanism should use the STARTTLS
+ // extension instead.
+ SSL bool
+ // TSLConfig represents the TLS configuration used for the TLS (when the
+ // STARTTLS extension is used) or SSL connection.
+ TLSConfig *tls.Config
+ // LocalName is the hostname sent to the SMTP server with the HELO command.
+ // By default, "localhost" is sent.
+ LocalName string
+}
+
+// NewDialer returns a new SMTP Dialer. The given parameters are used to connect
+// to the SMTP server.
+func NewDialer(host string, port int, username, password string) *Dialer {
+ return &Dialer{
+ Host: host,
+ Port: port,
+ Username: username,
+ Password: password,
+ SSL: port == 465,
+ }
+}
+
+// NewPlainDialer returns a new SMTP Dialer. The given parameters are used to
+// connect to the SMTP server.
+//
+// Deprecated: Use NewDialer instead.
+func NewPlainDialer(host string, port int, username, password string) *Dialer {
+ return NewDialer(host, port, username, password)
+}
+
+// Dial dials and authenticates to an SMTP server. The returned SendCloser
+// should be closed when done using it.
+func (d *Dialer) Dial() (SendCloser, error) {
+ conn, err := netDialTimeout("tcp", addr(d.Host, d.Port), 10*time.Second)
+ if err != nil {
+ return nil, err
+ }
+
+ if d.SSL {
+ conn = tlsClient(conn, d.tlsConfig())
+ }
+
+ c, err := smtpNewClient(conn, d.Host)
+ if err != nil {
+ return nil, err
+ }
+
+ if d.LocalName != "" {
+ if err := c.Hello(d.LocalName); err != nil {
+ return nil, err
+ }
+ }
+
+ if !d.SSL {
+ if ok, _ := c.Extension("STARTTLS"); ok {
+ if err := c.StartTLS(d.tlsConfig()); err != nil {
+ c.Close()
+ return nil, err
+ }
+ }
+ }
+
+ if d.Auth == nil && d.Username != "" {
+ if ok, auths := c.Extension("AUTH"); ok {
+ if strings.Contains(auths, "CRAM-MD5") {
+ d.Auth = smtp.CRAMMD5Auth(d.Username, d.Password)
+ } else if strings.Contains(auths, "LOGIN") &&
+ !strings.Contains(auths, "PLAIN") {
+ d.Auth = &loginAuth{
+ username: d.Username,
+ password: d.Password,
+ host: d.Host,
+ }
+ } else {
+ d.Auth = smtp.PlainAuth("", d.Username, d.Password, d.Host)
+ }
+ }
+ }
+
+ if d.Auth != nil {
+ if err = c.Auth(d.Auth); err != nil {
+ c.Close()
+ return nil, err
+ }
+ }
+
+ return &smtpSender{c, d}, nil
+}
+
+func (d *Dialer) tlsConfig() *tls.Config {
+ if d.TLSConfig == nil {
+ return &tls.Config{ServerName: d.Host}
+ }
+ return d.TLSConfig
+}
+
+func addr(host string, port int) string {
+ return fmt.Sprintf("%s:%d", host, port)
+}
+
+// DialAndSend opens a connection to the SMTP server, sends the given emails and
+// closes the connection.
+func (d *Dialer) DialAndSend(m ...*Message) error {
+ s, err := d.Dial()
+ if err != nil {
+ return err
+ }
+ defer s.Close()
+
+ return Send(s, m...)
+}
+
+type smtpSender struct {
+ smtpClient
+ d *Dialer
+}
+
+func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
+ if err := c.Mail(from); err != nil {
+ if err == io.EOF {
+ // This is probably due to a timeout, so reconnect and try again.
+ sc, derr := c.d.Dial()
+ if derr == nil {
+ if s, ok := sc.(*smtpSender); ok {
+ *c = *s
+ return c.Send(from, to, msg)
+ }
+ }
+ }
+ return err
+ }
+
+ for _, addr := range to {
+ if err := c.Rcpt(addr); err != nil {
+ return err
+ }
+ }
+
+ w, err := c.Data()
+ if err != nil {
+ return err
+ }
+
+ if _, err = msg.WriteTo(w); err != nil {
+ w.Close()
+ return err
+ }
+
+ return w.Close()
+}
+
+func (c *smtpSender) Close() error {
+ return c.Quit()
+}
+
+// Stubbed out for tests.
+var (
+ netDialTimeout = net.DialTimeout
+ tlsClient = tls.Client
+ smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) {
+ return smtp.NewClient(conn, host)
+ }
+)
+
+type smtpClient interface {
+ Hello(string) error
+ 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/vendor/gopkg.in/gomail.v2/smtp_test.go b/vendor/gopkg.in/gomail.v2/smtp_test.go
new file mode 100644
index 000000000..b6f91555b
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/smtp_test.go
@@ -0,0 +1,292 @@
+package gomail
+
+import (
+ "bytes"
+ "crypto/tls"
+ "io"
+ "net"
+ "net/smtp"
+ "reflect"
+ "testing"
+ "time"
+)
+
+const (
+ testPort = 587
+ testSSLPort = 465
+)
+
+var (
+ testConn = &net.TCPConn{}
+ testTLSConn = &tls.Conn{}
+ testConfig = &tls.Config{InsecureSkipVerify: true}
+ testAuth = smtp.PlainAuth("", testUser, testPwd, testHost)
+)
+
+func TestDialer(t *testing.T) {
+ d := NewDialer(testHost, testPort, "user", "pwd")
+ testSendMail(t, d, []string{
+ "Extension STARTTLS",
+ "StartTLS",
+ "Extension AUTH",
+ "Auth",
+ "Mail " + testFrom,
+ "Rcpt " + testTo1,
+ "Rcpt " + testTo2,
+ "Data",
+ "Write message",
+ "Close writer",
+ "Quit",
+ "Close",
+ })
+}
+
+func TestDialerSSL(t *testing.T) {
+ d := NewDialer(testHost, testSSLPort, "user", "pwd")
+ testSendMail(t, d, []string{
+ "Extension AUTH",
+ "Auth",
+ "Mail " + testFrom,
+ "Rcpt " + testTo1,
+ "Rcpt " + testTo2,
+ "Data",
+ "Write message",
+ "Close writer",
+ "Quit",
+ "Close",
+ })
+}
+
+func TestDialerConfig(t *testing.T) {
+ d := NewDialer(testHost, testPort, "user", "pwd")
+ d.LocalName = "test"
+ d.TLSConfig = testConfig
+ testSendMail(t, d, []string{
+ "Hello test",
+ "Extension STARTTLS",
+ "StartTLS",
+ "Extension AUTH",
+ "Auth",
+ "Mail " + testFrom,
+ "Rcpt " + testTo1,
+ "Rcpt " + testTo2,
+ "Data",
+ "Write message",
+ "Close writer",
+ "Quit",
+ "Close",
+ })
+}
+
+func TestDialerSSLConfig(t *testing.T) {
+ d := NewDialer(testHost, testSSLPort, "user", "pwd")
+ d.LocalName = "test"
+ d.TLSConfig = testConfig
+ testSendMail(t, d, []string{
+ "Hello test",
+ "Extension AUTH",
+ "Auth",
+ "Mail " + testFrom,
+ "Rcpt " + testTo1,
+ "Rcpt " + testTo2,
+ "Data",
+ "Write message",
+ "Close writer",
+ "Quit",
+ "Close",
+ })
+}
+
+func TestDialerNoAuth(t *testing.T) {
+ d := &Dialer{
+ Host: testHost,
+ Port: testPort,
+ }
+ testSendMail(t, d, []string{
+ "Extension STARTTLS",
+ "StartTLS",
+ "Mail " + testFrom,
+ "Rcpt " + testTo1,
+ "Rcpt " + testTo2,
+ "Data",
+ "Write message",
+ "Close writer",
+ "Quit",
+ "Close",
+ })
+}
+
+func TestDialerTimeout(t *testing.T) {
+ d := &Dialer{
+ Host: testHost,
+ Port: testPort,
+ }
+ testSendMailTimeout(t, d, []string{
+ "Extension STARTTLS",
+ "StartTLS",
+ "Mail " + testFrom,
+ "Extension STARTTLS",
+ "StartTLS",
+ "Mail " + testFrom,
+ "Rcpt " + testTo1,
+ "Rcpt " + testTo2,
+ "Data",
+ "Write message",
+ "Close writer",
+ "Quit",
+ "Close",
+ })
+}
+
+type mockClient struct {
+ t *testing.T
+ i int
+ want []string
+ addr string
+ config *tls.Config
+ timeout bool
+}
+
+func (c *mockClient) Hello(localName string) error {
+ c.do("Hello " + localName)
+ return nil
+}
+
+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 {
+ if !reflect.DeepEqual(a, testAuth) {
+ c.t.Errorf("Invalid auth, got %#v, want %#v", a, testAuth)
+ }
+ c.do("Auth")
+ return nil
+}
+
+func (c *mockClient) Mail(from string) error {
+ c.do("Mail " + from)
+ if c.timeout {
+ c.timeout = false
+ return io.EOF
+ }
+ 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: testMsg}, 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
+ buf bytes.Buffer
+}
+
+func (w *mockWriter) Write(p []byte) (int, error) {
+ if w.buf.Len() == 0 {
+ w.c.do("Write message")
+ }
+ w.buf.Write(p)
+ return len(p), nil
+}
+
+func (w *mockWriter) Close() error {
+ compareBodies(w.c.t, w.buf.String(), w.want)
+ w.c.do("Close writer")
+ return nil
+}
+
+func testSendMail(t *testing.T, d *Dialer, want []string) {
+ doTestSendMail(t, d, want, false)
+}
+
+func testSendMailTimeout(t *testing.T, d *Dialer, want []string) {
+ doTestSendMail(t, d, want, true)
+}
+
+func doTestSendMail(t *testing.T, d *Dialer, want []string, timeout bool) {
+ testClient := &mockClient{
+ t: t,
+ want: want,
+ addr: addr(d.Host, d.Port),
+ config: d.TLSConfig,
+ timeout: timeout,
+ }
+
+ netDialTimeout = func(network, address string, d time.Duration) (net.Conn, error) {
+ if network != "tcp" {
+ t.Errorf("Invalid network, got %q, want tcp", network)
+ }
+ if address != testClient.addr {
+ t.Errorf("Invalid address, got %q, want %q",
+ address, testClient.addr)
+ }
+ return testConn, nil
+ }
+
+ tlsClient = func(conn net.Conn, config *tls.Config) *tls.Conn {
+ if conn != testConn {
+ t.Errorf("Invalid conn, got %#v, want %#v", conn, testConn)
+ }
+ assertConfig(t, config, testClient.config)
+ return testTLSConn
+ }
+
+ smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) {
+ if host != testHost {
+ t.Errorf("Invalid host, got %q, want %q", host, testHost)
+ }
+ return testClient, nil
+ }
+
+ if err := d.DialAndSend(getTestMessage()); err != nil {
+ t.Error(err)
+ }
+}
+
+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)
+ }
+}
diff --git a/vendor/gopkg.in/gomail.v2/writeto.go b/vendor/gopkg.in/gomail.v2/writeto.go
new file mode 100644
index 000000000..9fb6b86e8
--- /dev/null
+++ b/vendor/gopkg.in/gomail.v2/writeto.go
@@ -0,0 +1,306 @@
+package gomail
+
+import (
+ "encoding/base64"
+ "errors"
+ "io"
+ "mime"
+ "mime/multipart"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+// WriteTo implements io.WriterTo. It dumps the whole message into w.
+func (m *Message) WriteTo(w io.Writer) (int64, error) {
+ mw := &messageWriter{w: w}
+ mw.writeMessage(m)
+ return mw.n, mw.err
+}
+
+func (w *messageWriter) writeMessage(m *Message) {
+ if _, ok := m.header["Mime-Version"]; !ok {
+ w.writeString("Mime-Version: 1.0\r\n")
+ }
+ if _, ok := m.header["Date"]; !ok {
+ w.writeHeader("Date", m.FormatDate(now()))
+ }
+ w.writeHeaders(m.header)
+
+ if m.hasMixedPart() {
+ w.openMultipart("mixed")
+ }
+
+ if m.hasRelatedPart() {
+ w.openMultipart("related")
+ }
+
+ if m.hasAlternativePart() {
+ w.openMultipart("alternative")
+ }
+ for _, part := range m.parts {
+ w.writePart(part, m.charset)
+ }
+ if m.hasAlternativePart() {
+ w.closeMultipart()
+ }
+
+ w.addFiles(m.embedded, false)
+ if m.hasRelatedPart() {
+ w.closeMultipart()
+ }
+
+ w.addFiles(m.attachments, true)
+ if m.hasMixedPart() {
+ w.closeMultipart()
+ }
+}
+
+func (m *Message) hasMixedPart() bool {
+ return (len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1
+}
+
+func (m *Message) hasRelatedPart() bool {
+ return (len(m.parts) > 0 && len(m.embedded) > 0) || len(m.embedded) > 1
+}
+
+func (m *Message) hasAlternativePart() bool {
+ return len(m.parts) > 1
+}
+
+type messageWriter struct {
+ w io.Writer
+ n int64
+ writers [3]*multipart.Writer
+ partWriter io.Writer
+ depth uint8
+ err error
+}
+
+func (w *messageWriter) openMultipart(mimeType string) {
+ mw := multipart.NewWriter(w)
+ contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary()
+ w.writers[w.depth] = mw
+
+ if w.depth == 0 {
+ w.writeHeader("Content-Type", contentType)
+ w.writeString("\r\n")
+ } else {
+ w.createPart(map[string][]string{
+ "Content-Type": {contentType},
+ })
+ }
+ w.depth++
+}
+
+func (w *messageWriter) createPart(h map[string][]string) {
+ w.partWriter, w.err = 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) writePart(p *part, charset string) {
+ w.writeHeaders(map[string][]string{
+ "Content-Type": {p.contentType + "; charset=" + charset},
+ "Content-Transfer-Encoding": {string(p.encoding)},
+ })
+ w.writeBody(p.copier, p.encoding)
+}
+
+func (w *messageWriter) addFiles(files []*file, isAttachment bool) {
+ for _, f := range files {
+ if _, ok := f.Header["Content-Type"]; !ok {
+ mediaType := mime.TypeByExtension(filepath.Ext(f.Name))
+ if mediaType == "" {
+ mediaType = "application/octet-stream"
+ }
+ f.setHeader("Content-Type", mediaType+`; name="`+f.Name+`"`)
+ }
+
+ if _, ok := f.Header["Content-Transfer-Encoding"]; !ok {
+ f.setHeader("Content-Transfer-Encoding", string(Base64))
+ }
+
+ if _, ok := f.Header["Content-Disposition"]; !ok {
+ var disp string
+ if isAttachment {
+ disp = "attachment"
+ } else {
+ disp = "inline"
+ }
+ f.setHeader("Content-Disposition", disp+`; filename="`+f.Name+`"`)
+ }
+
+ if !isAttachment {
+ if _, ok := f.Header["Content-ID"]; !ok {
+ f.setHeader("Content-ID", "<"+f.Name+">")
+ }
+ }
+ w.writeHeaders(f.Header)
+ w.writeBody(f.CopyFunc, Base64)
+ }
+}
+
+func (w *messageWriter) Write(p []byte) (int, error) {
+ if w.err != nil {
+ return 0, errors.New("gomail: cannot write as writer is in error")
+ }
+
+ var n int
+ n, w.err = w.w.Write(p)
+ w.n += int64(n)
+ return n, w.err
+}
+
+func (w *messageWriter) writeString(s string) {
+ n, _ := io.WriteString(w.w, s)
+ w.n += int64(n)
+}
+
+func (w *messageWriter) writeHeader(k string, v ...string) {
+ w.writeString(k)
+ if len(v) == 0 {
+ w.writeString(":\r\n")
+ return
+ }
+ w.writeString(": ")
+
+ // Max header line length is 78 characters in RFC 5322 and 76 characters
+ // in RFC 2047. So for the sake of simplicity we use the 76 characters
+ // limit.
+ charsLeft := 76 - len(k) - len(": ")
+
+ for i, s := range v {
+ // If the line is already too long, insert a newline right away.
+ if charsLeft < 1 {
+ if i == 0 {
+ w.writeString("\r\n ")
+ } else {
+ w.writeString(",\r\n ")
+ }
+ charsLeft = 75
+ } else if i != 0 {
+ w.writeString(", ")
+ charsLeft -= 2
+ }
+
+ // While the header content is too long, fold it by inserting a newline.
+ for len(s) > charsLeft {
+ s = w.writeLine(s, charsLeft)
+ charsLeft = 75
+ }
+ w.writeString(s)
+ if i := lastIndexByte(s, '\n'); i != -1 {
+ charsLeft = 75 - (len(s) - i - 1)
+ } else {
+ charsLeft -= len(s)
+ }
+ }
+ w.writeString("\r\n")
+}
+
+func (w *messageWriter) writeLine(s string, charsLeft int) string {
+ // If there is already a newline before the limit. Write the line.
+ if i := strings.IndexByte(s, '\n'); i != -1 && i < charsLeft {
+ w.writeString(s[:i+1])
+ return s[i+1:]
+ }
+
+ for i := charsLeft - 1; i >= 0; i-- {
+ if s[i] == ' ' {
+ w.writeString(s[:i])
+ w.writeString("\r\n ")
+ return s[i+1:]
+ }
+ }
+
+ // We could not insert a newline cleanly so look for a space or a newline
+ // even if it is after the limit.
+ for i := 75; i < len(s); i++ {
+ if s[i] == ' ' {
+ w.writeString(s[:i])
+ w.writeString("\r\n ")
+ return s[i+1:]
+ }
+ if s[i] == '\n' {
+ w.writeString(s[:i+1])
+ return s[i+1:]
+ }
+ }
+
+ // Too bad, no space or newline in the whole string. Just write everything.
+ w.writeString(s)
+ return ""
+}
+
+func (w *messageWriter) writeHeaders(h map[string][]string) {
+ if w.depth == 0 {
+ for k, v := range h {
+ if k != "Bcc" {
+ w.writeHeader(k, v...)
+ }
+ }
+ } else {
+ w.createPart(h)
+ }
+}
+
+func (w *messageWriter) writeBody(f func(io.Writer) error, enc Encoding) {
+ var subWriter io.Writer
+ if w.depth == 0 {
+ w.writeString("\r\n")
+ subWriter = w.w
+ } else {
+ subWriter = w.partWriter
+ }
+
+ if enc == Base64 {
+ wc := base64.NewEncoder(base64.StdEncoding, newBase64LineWriter(subWriter))
+ w.err = f(wc)
+ wc.Close()
+ } else if enc == Unencoded {
+ w.err = f(subWriter)
+ } else {
+ wc := newQPWriter(subWriter)
+ w.err = f(wc)
+ wc.Close()
+ }
+}
+
+// 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
+}
+
+// Stubbed out for testing.
+var now = time.Now