Prechádzať zdrojové kódy

添加原gomail老包用于mail工具类

wanghuidong 4 rokov pred
rodič
commit
2bfa5b7247

+ 2 - 0
go.mod

@@ -16,3 +16,5 @@ require (
 	gorm.io/driver/mysql v1.0.3
 	gorm.io/gorm v1.20.8
 )
+
+replace github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df => ./gomail

+ 0 - 0
gomail/5s5s.txt


+ 20 - 0
gomail/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.

+ 20 - 0
gomail/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.

+ 20 - 0
gomail/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.

+ 92 - 0
gomail/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.

+ 49 - 0
gomail/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)
+	}
+}

+ 100 - 0
gomail/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])
+		}
+	}
+}

+ 5 - 0
gomail/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

+ 223 - 0
gomail/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))
+}

+ 3 - 0
gomail/go.mod

@@ -0,0 +1,3 @@
+module gomail
+
+go 1.13

+ 350 - 0
gomail/message.go

@@ -0,0 +1,350 @@
+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)
+}
+
+func (m *Message) appendFile_new(list []*file, fb []byte, settings []FileSetting) []*file {
+	f := &file{
+		Name:   filepath.Base(""),
+		Header: make(map[string][]string),
+		CopyFunc: func(w io.Writer) error {
+			h := bytes.NewReader(fb)
+			if _, err := io.Copy(w, h); err != nil {
+				return err
+			}
+			return nil
+		},
+	}
+
+	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)
+}
+
+func (m *Message) Attach_new(fb []byte, settings ...FileSetting) {
+	m.attachments = m.appendFile_new(m.attachments, fb, settings)
+}
+
+// Embed embeds the images to the email.
+func (m *Message) Embed(filename string, settings ...FileSetting) {
+	m.embedded = m.appendFile(m.embedded, filename, settings)
+}

+ 745 - 0
gomail/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()
+	}
+}

+ 21 - 0
gomail/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
+)

+ 25 - 0
gomail/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
+	}
+)

+ 116 - 0
gomail/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
+}

+ 80 - 0
gomail/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
+	}
+}

+ 203 - 0
gomail/smtp.go

@@ -0,0 +1,203 @@
+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())
+	}
+
+	conn.SetDeadline(time.Now().Add(20 * time.Second))
+	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
+}

+ 292 - 0
gomail/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)
+	}
+}

+ 306 - 0
gomail/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