diff --git a/go.mod b/go.mod
index 359ae0c..d9f387d 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.24.1
 
 require (
 	github.com/gin-gonic/gin v1.10.0
+	github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
 	github.com/go-passwd/validator v0.0.0-20180902184246-0b4c967e436b
 	github.com/google/uuid v1.6.0
 	github.com/rs/zerolog v1.33.0
@@ -47,6 +48,8 @@ require (
 	golang.org/x/sys v0.30.0 // indirect
 	golang.org/x/text v0.15.0 // indirect
 	google.golang.org/protobuf v1.34.1 // indirect
+	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	modernc.org/libc v1.61.13 // indirect
 	modernc.org/mathutil v1.7.1 // indirect
diff --git a/go.sum b/go.sum
index 86317f6..eed30bf 100644
--- a/go.sum
+++ b/go.sum
@@ -22,6 +22,8 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
 github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
 github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
 github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df h1:Bao6dhmbTA1KFVxmJ6nBoMuOJit2yjEgLJpIMYpop0E=
+github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df/go.mod h1:GJr+FCSXshIwgHBtLglIg9M2l2kQSi6QjVAngtzI08Y=
 github.com/go-passwd/validator v0.0.0-20180902184246-0b4c967e436b h1:XOkaXKVHqiFDTLzzHFkZ+VJkarlqnsSxIsuzcE75tk8=
 github.com/go-passwd/validator v0.0.0-20180902184246-0b4c967e436b/go.mod h1:BOKCezpxxDZ5PLMqt+9MxZTCBeGcpUmDHDuYlkdPcI4=
 github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@@ -116,8 +118,12 @@ golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
 golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
 google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
 google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/pkg/miniauth/miniauth_test.go b/pkg/miniauth/miniauth_test.go
index 472751c..b152368 100644
--- a/pkg/miniauth/miniauth_test.go
+++ b/pkg/miniauth/miniauth_test.go
@@ -3,6 +3,7 @@ package miniauth
 import (
 	"testing"
 
+	"git.keks.cloud/kekskurse/miniauth/pkg/smtpclient"
 	"git.keks.cloud/kekskurse/miniauth/pkg/userstore"
 	"github.com/stretchr/testify/assert"
 )
@@ -172,5 +173,6 @@ func getMiniAuth(t *testing.T) Miniauth {
 	us, err := userstore.NewDummyStore()
 	assert.Nil(t, err, "[setup] should be abel to creat dummy store")
 	m := NewMiniauth(us)
+	m.smtp = smtpclient.NewDummySMTPClient()
 	return m
 }
diff --git a/pkg/smtpclient/error.go b/pkg/smtpclient/error.go
index 5b04981..4a86def 100644
--- a/pkg/smtpclient/error.go
+++ b/pkg/smtpclient/error.go
@@ -2,4 +2,8 @@ package smtpclient
 
 import "errors"
 
-var ErrCantSendMailToErrorMail = errors.New("cant send mail to error mail")
+var (
+	ErrCantSendMailToErrorMail = errors.New("cant send mail to error mail")
+	ErrCantRenderTemplate      = errors.New("cant rendet template")
+	ErrCantSendMail            = errors.New("cant send mail")
+)
diff --git a/pkg/smtpclient/smtpclient.go b/pkg/smtpclient/smtpclient.go
index 1d067a4..e390d62 100644
--- a/pkg/smtpclient/smtpclient.go
+++ b/pkg/smtpclient/smtpclient.go
@@ -6,22 +6,63 @@ import (
 	"fmt"
 	"html/template"
 	"strings"
+
+	"git.keks.cloud/kekskurse/miniauth/pkg/utils"
+	"github.com/go-gomail/gomail"
+	"github.com/rs/zerolog"
 )
 
-type SMTPClient struct{}
+type SMTPConfig struct {
+	Server   string
+	Port     int
+	Username string
+	Password string
+	From     string
+}
+
+type SMTPClient struct {
+	log    zerolog.Logger
+	config SMTPConfig
+}
 
 //go:embed template/*
 var templates embed.FS
 
 func (s SMTPClient) SendMail(templateName string, to string, data any) error {
+	log := s.log.With().Str("templateName", templateName).Str("to", to).Any("data", data).Str("func", "SendMail").Logger()
 	if to == "error@error.error" {
 		return ErrCantSendMailToErrorMail
 	}
+
+	if s.config.Server == "test" {
+		return nil
+	}
+
+	subject, txt, html, err := s.renderTemplates(templateName, data)
+	if err != nil {
+		return utils.WrapError(ErrCantRenderTemplate, err, log)
+	}
+
+	m := gomail.NewMessage()
+	m.SetHeader("From", s.config.From)
+	m.SetHeader("To", to)
+	m.SetHeader("Subject", subject)
+	m.SetBody("text/plain", txt)
+	m.AddAlternative("text/html", html)
+
+	d := gomail.NewDialer(s.config.Server, s.config.Port, s.config.Username, s.config.Password)
+
+	err = d.DialAndSend(m)
+	if err != nil {
+		return utils.WrapError(ErrCantSendMail, err, log)
+	}
+
 	return nil
 }
 
 func NewDummySMTPClient() SMTPClient {
 	s := SMTPClient{}
+	s.config.Server = "test"
 	return s
 }
 
diff --git a/vendor/github.com/go-gomail/gomail/.travis.yml b/vendor/github.com/go-gomail/gomail/.travis.yml
new file mode 100644
index 0000000..48915e7
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/.travis.yml
@@ -0,0 +1,9 @@
+language: go
+
+go:
+  - 1.2
+  - 1.3
+  - 1.4
+  - 1.5
+  - 1.6
+  - tip
diff --git a/vendor/github.com/go-gomail/gomail/CHANGELOG.md b/vendor/github.com/go-gomail/gomail/CHANGELOG.md
new file mode 100644
index 0000000..a797ab4
--- /dev/null
+++ b/vendor/github.com/go-gomail/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.
diff --git a/vendor/github.com/go-gomail/gomail/CONTRIBUTING.md b/vendor/github.com/go-gomail/gomail/CONTRIBUTING.md
new file mode 100644
index 0000000..d5601c2
--- /dev/null
+++ b/vendor/github.com/go-gomail/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.
diff --git a/vendor/github.com/go-gomail/gomail/LICENSE b/vendor/github.com/go-gomail/gomail/LICENSE
new file mode 100644
index 0000000..5f5c12a
--- /dev/null
+++ b/vendor/github.com/go-gomail/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.
diff --git a/vendor/github.com/go-gomail/gomail/README.md b/vendor/github.com/go-gomail/gomail/README.md
new file mode 100644
index 0000000..b3be9e1
--- /dev/null
+++ b/vendor/github.com/go-gomail/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.
diff --git a/vendor/github.com/go-gomail/gomail/auth.go b/vendor/github.com/go-gomail/gomail/auth.go
new file mode 100644
index 0000000..d28b83a
--- /dev/null
+++ b/vendor/github.com/go-gomail/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)
+	}
+}
diff --git a/vendor/github.com/go-gomail/gomail/doc.go b/vendor/github.com/go-gomail/gomail/doc.go
new file mode 100644
index 0000000..a8f5091
--- /dev/null
+++ b/vendor/github.com/go-gomail/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
diff --git a/vendor/github.com/go-gomail/gomail/message.go b/vendor/github.com/go-gomail/gomail/message.go
new file mode 100644
index 0000000..4bffb1e
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/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/github.com/go-gomail/gomail/mime.go b/vendor/github.com/go-gomail/gomail/mime.go
new file mode 100644
index 0000000..194d4a7
--- /dev/null
+++ b/vendor/github.com/go-gomail/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
+)
diff --git a/vendor/github.com/go-gomail/gomail/mime_go14.go b/vendor/github.com/go-gomail/gomail/mime_go14.go
new file mode 100644
index 0000000..3dc26aa
--- /dev/null
+++ b/vendor/github.com/go-gomail/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
+	}
+)
diff --git a/vendor/github.com/go-gomail/gomail/send.go b/vendor/github.com/go-gomail/gomail/send.go
new file mode 100644
index 0000000..9115ebe
--- /dev/null
+++ b/vendor/github.com/go-gomail/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
+}
diff --git a/vendor/github.com/go-gomail/gomail/smtp.go b/vendor/github.com/go-gomail/gomail/smtp.go
new file mode 100644
index 0000000..2aa49c8
--- /dev/null
+++ b/vendor/github.com/go-gomail/gomail/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/github.com/go-gomail/gomail/writeto.go b/vendor/github.com/go-gomail/gomail/writeto.go
new file mode 100644
index 0000000..9fb6b86
--- /dev/null
+++ b/vendor/github.com/go-gomail/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
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE
new file mode 100644
index 0000000..5f5c12a
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Alexandre Cesaro
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md
new file mode 100644
index 0000000..98ddf82
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/README.md
@@ -0,0 +1,16 @@
+# quotedprintable
+
+## Introduction
+
+Package quotedprintable implements quoted-printable and message header encoding
+as specified by RFC 2045 and RFC 2047.
+
+It is a copy of the Go 1.5 package `mime/quotedprintable`. It also includes
+the new functions of package `mime` concerning RFC 2047.
+
+This code has minor changes with the standard library code in order to work
+with Go 1.0 and newer. 
+
+## Documentation
+
+https://godoc.org/gopkg.in/alexcesaro/quotedprintable.v3
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go
new file mode 100644
index 0000000..cfd0261
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/encodedword.go
@@ -0,0 +1,279 @@
+package quotedprintable
+
+import (
+	"bytes"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"io"
+	"strings"
+	"unicode"
+	"unicode/utf8"
+)
+
+// A WordEncoder is a RFC 2047 encoded-word encoder.
+type WordEncoder byte
+
+const (
+	// BEncoding represents Base64 encoding scheme as defined by RFC 2045.
+	BEncoding = WordEncoder('b')
+	// QEncoding represents the Q-encoding scheme as defined by RFC 2047.
+	QEncoding = WordEncoder('q')
+)
+
+var (
+	errInvalidWord = errors.New("mime: invalid RFC 2047 encoded-word")
+)
+
+// Encode returns the encoded-word form of s. If s is ASCII without special
+// characters, it is returned unchanged. The provided charset is the IANA
+// charset name of s. It is case insensitive.
+func (e WordEncoder) Encode(charset, s string) string {
+	if !needsEncoding(s) {
+		return s
+	}
+	return e.encodeWord(charset, s)
+}
+
+func needsEncoding(s string) bool {
+	for _, b := range s {
+		if (b < ' ' || b > '~') && b != '\t' {
+			return true
+		}
+	}
+	return false
+}
+
+// encodeWord encodes a string into an encoded-word.
+func (e WordEncoder) encodeWord(charset, s string) string {
+	buf := getBuffer()
+	defer putBuffer(buf)
+
+	buf.WriteString("=?")
+	buf.WriteString(charset)
+	buf.WriteByte('?')
+	buf.WriteByte(byte(e))
+	buf.WriteByte('?')
+
+	if e == BEncoding {
+		w := base64.NewEncoder(base64.StdEncoding, buf)
+		io.WriteString(w, s)
+		w.Close()
+	} else {
+		enc := make([]byte, 3)
+		for i := 0; i < len(s); i++ {
+			b := s[i]
+			switch {
+			case b == ' ':
+				buf.WriteByte('_')
+			case b <= '~' && b >= '!' && b != '=' && b != '?' && b != '_':
+				buf.WriteByte(b)
+			default:
+				enc[0] = '='
+				enc[1] = upperhex[b>>4]
+				enc[2] = upperhex[b&0x0f]
+				buf.Write(enc)
+			}
+		}
+	}
+	buf.WriteString("?=")
+	return buf.String()
+}
+
+const upperhex = "0123456789ABCDEF"
+
+// A WordDecoder decodes MIME headers containing RFC 2047 encoded-words.
+type WordDecoder struct {
+	// CharsetReader, if non-nil, defines a function to generate
+	// charset-conversion readers, converting from the provided
+	// charset into UTF-8.
+	// Charsets are always lower-case. utf-8, iso-8859-1 and us-ascii charsets
+	// are handled by default.
+	// One of the the CharsetReader's result values must be non-nil.
+	CharsetReader func(charset string, input io.Reader) (io.Reader, error)
+}
+
+// Decode decodes an encoded-word. If word is not a valid RFC 2047 encoded-word,
+// word is returned unchanged.
+func (d *WordDecoder) Decode(word string) (string, error) {
+	fields := strings.Split(word, "?") // TODO: remove allocation?
+	if len(fields) != 5 || fields[0] != "=" || fields[4] != "=" || len(fields[2]) != 1 {
+		return "", errInvalidWord
+	}
+
+	content, err := decode(fields[2][0], fields[3])
+	if err != nil {
+		return "", err
+	}
+
+	buf := getBuffer()
+	defer putBuffer(buf)
+
+	if err := d.convert(buf, fields[1], content); err != nil {
+		return "", err
+	}
+
+	return buf.String(), nil
+}
+
+// DecodeHeader decodes all encoded-words of the given string. It returns an
+// error if and only if CharsetReader of d returns an error.
+func (d *WordDecoder) DecodeHeader(header string) (string, error) {
+	// If there is no encoded-word, returns before creating a buffer.
+	i := strings.Index(header, "=?")
+	if i == -1 {
+		return header, nil
+	}
+
+	buf := getBuffer()
+	defer putBuffer(buf)
+
+	buf.WriteString(header[:i])
+	header = header[i:]
+
+	betweenWords := false
+	for {
+		start := strings.Index(header, "=?")
+		if start == -1 {
+			break
+		}
+		cur := start + len("=?")
+
+		i := strings.Index(header[cur:], "?")
+		if i == -1 {
+			break
+		}
+		charset := header[cur : cur+i]
+		cur += i + len("?")
+
+		if len(header) < cur+len("Q??=") {
+			break
+		}
+		encoding := header[cur]
+		cur++
+
+		if header[cur] != '?' {
+			break
+		}
+		cur++
+
+		j := strings.Index(header[cur:], "?=")
+		if j == -1 {
+			break
+		}
+		text := header[cur : cur+j]
+		end := cur + j + len("?=")
+
+		content, err := decode(encoding, text)
+		if err != nil {
+			betweenWords = false
+			buf.WriteString(header[:start+2])
+			header = header[start+2:]
+			continue
+		}
+
+		// Write characters before the encoded-word. White-space and newline
+		// characters separating two encoded-words must be deleted.
+		if start > 0 && (!betweenWords || hasNonWhitespace(header[:start])) {
+			buf.WriteString(header[:start])
+		}
+
+		if err := d.convert(buf, charset, content); err != nil {
+			return "", err
+		}
+
+		header = header[end:]
+		betweenWords = true
+	}
+
+	if len(header) > 0 {
+		buf.WriteString(header)
+	}
+
+	return buf.String(), nil
+}
+
+func decode(encoding byte, text string) ([]byte, error) {
+	switch encoding {
+	case 'B', 'b':
+		return base64.StdEncoding.DecodeString(text)
+	case 'Q', 'q':
+		return qDecode(text)
+	}
+	return nil, errInvalidWord
+}
+
+func (d *WordDecoder) convert(buf *bytes.Buffer, charset string, content []byte) error {
+	switch {
+	case strings.EqualFold("utf-8", charset):
+		buf.Write(content)
+	case strings.EqualFold("iso-8859-1", charset):
+		for _, c := range content {
+			buf.WriteRune(rune(c))
+		}
+	case strings.EqualFold("us-ascii", charset):
+		for _, c := range content {
+			if c >= utf8.RuneSelf {
+				buf.WriteRune(unicode.ReplacementChar)
+			} else {
+				buf.WriteByte(c)
+			}
+		}
+	default:
+		if d.CharsetReader == nil {
+			return fmt.Errorf("mime: unhandled charset %q", charset)
+		}
+		r, err := d.CharsetReader(strings.ToLower(charset), bytes.NewReader(content))
+		if err != nil {
+			return err
+		}
+		if _, err = buf.ReadFrom(r); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// hasNonWhitespace reports whether s (assumed to be ASCII) contains at least
+// one byte of non-whitespace.
+func hasNonWhitespace(s string) bool {
+	for _, b := range s {
+		switch b {
+		// Encoded-words can only be separated by linear white spaces which does
+		// not include vertical tabs (\v).
+		case ' ', '\t', '\n', '\r':
+		default:
+			return true
+		}
+	}
+	return false
+}
+
+// qDecode decodes a Q encoded string.
+func qDecode(s string) ([]byte, error) {
+	dec := make([]byte, len(s))
+	n := 0
+	for i := 0; i < len(s); i++ {
+		switch c := s[i]; {
+		case c == '_':
+			dec[n] = ' '
+		case c == '=':
+			if i+2 >= len(s) {
+				return nil, errInvalidWord
+			}
+			b, err := readHexByte(s[i+1], s[i+2])
+			if err != nil {
+				return nil, err
+			}
+			dec[n] = b
+			i += 2
+		case (c <= '~' && c >= ' ') || c == '\n' || c == '\r' || c == '\t':
+			dec[n] = c
+		default:
+			return nil, errInvalidWord
+		}
+		n++
+	}
+
+	return dec[:n], nil
+}
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go
new file mode 100644
index 0000000..24283c5
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool.go
@@ -0,0 +1,26 @@
+// +build go1.3
+
+package quotedprintable
+
+import (
+	"bytes"
+	"sync"
+)
+
+var bufPool = sync.Pool{
+	New: func() interface{} {
+		return new(bytes.Buffer)
+	},
+}
+
+func getBuffer() *bytes.Buffer {
+	return bufPool.Get().(*bytes.Buffer)
+}
+
+func putBuffer(buf *bytes.Buffer) {
+	if buf.Len() > 1024 {
+		return
+	}
+	buf.Reset()
+	bufPool.Put(buf)
+}
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go
new file mode 100644
index 0000000..d335b4a
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/pool_go12.go
@@ -0,0 +1,24 @@
+// +build !go1.3
+
+package quotedprintable
+
+import "bytes"
+
+var ch = make(chan *bytes.Buffer, 32)
+
+func getBuffer() *bytes.Buffer {
+	select {
+	case buf := <-ch:
+		return buf
+	default:
+	}
+	return new(bytes.Buffer)
+}
+
+func putBuffer(buf *bytes.Buffer) {
+	buf.Reset()
+	select {
+	case ch <- buf:
+	default:
+	}
+}
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go
new file mode 100644
index 0000000..955edca
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/reader.go
@@ -0,0 +1,121 @@
+// Package quotedprintable implements quoted-printable encoding as specified by
+// RFC 2045.
+package quotedprintable
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"io"
+)
+
+// Reader is a quoted-printable decoder.
+type Reader struct {
+	br   *bufio.Reader
+	rerr error  // last read error
+	line []byte // to be consumed before more of br
+}
+
+// NewReader returns a quoted-printable reader, decoding from r.
+func NewReader(r io.Reader) *Reader {
+	return &Reader{
+		br: bufio.NewReader(r),
+	}
+}
+
+func fromHex(b byte) (byte, error) {
+	switch {
+	case b >= '0' && b <= '9':
+		return b - '0', nil
+	case b >= 'A' && b <= 'F':
+		return b - 'A' + 10, nil
+	// Accept badly encoded bytes.
+	case b >= 'a' && b <= 'f':
+		return b - 'a' + 10, nil
+	}
+	return 0, fmt.Errorf("quotedprintable: invalid hex byte 0x%02x", b)
+}
+
+func readHexByte(a, b byte) (byte, error) {
+	var hb, lb byte
+	var err error
+	if hb, err = fromHex(a); err != nil {
+		return 0, err
+	}
+	if lb, err = fromHex(b); err != nil {
+		return 0, err
+	}
+	return hb<<4 | lb, nil
+}
+
+func isQPDiscardWhitespace(r rune) bool {
+	switch r {
+	case '\n', '\r', ' ', '\t':
+		return true
+	}
+	return false
+}
+
+var (
+	crlf       = []byte("\r\n")
+	lf         = []byte("\n")
+	softSuffix = []byte("=")
+)
+
+// Read reads and decodes quoted-printable data from the underlying reader.
+func (r *Reader) Read(p []byte) (n int, err error) {
+	// Deviations from RFC 2045:
+	// 1. in addition to "=\r\n", "=\n" is also treated as soft line break.
+	// 2. it will pass through a '\r' or '\n' not preceded by '=', consistent
+	//    with other broken QP encoders & decoders.
+	for len(p) > 0 {
+		if len(r.line) == 0 {
+			if r.rerr != nil {
+				return n, r.rerr
+			}
+			r.line, r.rerr = r.br.ReadSlice('\n')
+
+			// Does the line end in CRLF instead of just LF?
+			hasLF := bytes.HasSuffix(r.line, lf)
+			hasCR := bytes.HasSuffix(r.line, crlf)
+			wholeLine := r.line
+			r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace)
+			if bytes.HasSuffix(r.line, softSuffix) {
+				rightStripped := wholeLine[len(r.line):]
+				r.line = r.line[:len(r.line)-1]
+				if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) {
+					r.rerr = fmt.Errorf("quotedprintable: invalid bytes after =: %q", rightStripped)
+				}
+			} else if hasLF {
+				if hasCR {
+					r.line = append(r.line, '\r', '\n')
+				} else {
+					r.line = append(r.line, '\n')
+				}
+			}
+			continue
+		}
+		b := r.line[0]
+
+		switch {
+		case b == '=':
+			if len(r.line[1:]) < 2 {
+				return n, io.ErrUnexpectedEOF
+			}
+			b, err = readHexByte(r.line[1], r.line[2])
+			if err != nil {
+				return n, err
+			}
+			r.line = r.line[2:] // 2 of the 3; other 1 is done below
+		case b == '\t' || b == '\r' || b == '\n':
+			break
+		case b < ' ' || b > '~':
+			return n, fmt.Errorf("quotedprintable: invalid unescaped byte 0x%02x in body", b)
+		}
+		p[0] = b
+		p = p[1:]
+		r.line = r.line[1:]
+		n++
+	}
+	return n, nil
+}
diff --git a/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go
new file mode 100644
index 0000000..43359d5
--- /dev/null
+++ b/vendor/gopkg.in/alexcesaro/quotedprintable.v3/writer.go
@@ -0,0 +1,166 @@
+package quotedprintable
+
+import "io"
+
+const lineMaxLen = 76
+
+// A Writer is a quoted-printable writer that implements io.WriteCloser.
+type Writer struct {
+	// Binary mode treats the writer's input as pure binary and processes end of
+	// line bytes as binary data.
+	Binary bool
+
+	w    io.Writer
+	i    int
+	line [78]byte
+	cr   bool
+}
+
+// NewWriter returns a new Writer that writes to w.
+func NewWriter(w io.Writer) *Writer {
+	return &Writer{w: w}
+}
+
+// Write encodes p using quoted-printable encoding and writes it to the
+// underlying io.Writer. It limits line length to 76 characters. The encoded
+// bytes are not necessarily flushed until the Writer is closed.
+func (w *Writer) Write(p []byte) (n int, err error) {
+	for i, b := range p {
+		switch {
+		// Simple writes are done in batch.
+		case b >= '!' && b <= '~' && b != '=':
+			continue
+		case isWhitespace(b) || !w.Binary && (b == '\n' || b == '\r'):
+			continue
+		}
+
+		if i > n {
+			if err := w.write(p[n:i]); err != nil {
+				return n, err
+			}
+			n = i
+		}
+
+		if err := w.encode(b); err != nil {
+			return n, err
+		}
+		n++
+	}
+
+	if n == len(p) {
+		return n, nil
+	}
+
+	if err := w.write(p[n:]); err != nil {
+		return n, err
+	}
+
+	return len(p), nil
+}
+
+// Close closes the Writer, flushing any unwritten data to the underlying
+// io.Writer, but does not close the underlying io.Writer.
+func (w *Writer) Close() error {
+	if err := w.checkLastByte(); err != nil {
+		return err
+	}
+
+	return w.flush()
+}
+
+// write limits text encoded in quoted-printable to 76 characters per line.
+func (w *Writer) write(p []byte) error {
+	for _, b := range p {
+		if b == '\n' || b == '\r' {
+			// If the previous byte was \r, the CRLF has already been inserted.
+			if w.cr && b == '\n' {
+				w.cr = false
+				continue
+			}
+
+			if b == '\r' {
+				w.cr = true
+			}
+
+			if err := w.checkLastByte(); err != nil {
+				return err
+			}
+			if err := w.insertCRLF(); err != nil {
+				return err
+			}
+			continue
+		}
+
+		if w.i == lineMaxLen-1 {
+			if err := w.insertSoftLineBreak(); err != nil {
+				return err
+			}
+		}
+
+		w.line[w.i] = b
+		w.i++
+		w.cr = false
+	}
+
+	return nil
+}
+
+func (w *Writer) encode(b byte) error {
+	if lineMaxLen-1-w.i < 3 {
+		if err := w.insertSoftLineBreak(); err != nil {
+			return err
+		}
+	}
+
+	w.line[w.i] = '='
+	w.line[w.i+1] = upperhex[b>>4]
+	w.line[w.i+2] = upperhex[b&0x0f]
+	w.i += 3
+
+	return nil
+}
+
+// checkLastByte encodes the last buffered byte if it is a space or a tab.
+func (w *Writer) checkLastByte() error {
+	if w.i == 0 {
+		return nil
+	}
+
+	b := w.line[w.i-1]
+	if isWhitespace(b) {
+		w.i--
+		if err := w.encode(b); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (w *Writer) insertSoftLineBreak() error {
+	w.line[w.i] = '='
+	w.i++
+
+	return w.insertCRLF()
+}
+
+func (w *Writer) insertCRLF() error {
+	w.line[w.i] = '\r'
+	w.line[w.i+1] = '\n'
+	w.i += 2
+
+	return w.flush()
+}
+
+func (w *Writer) flush() error {
+	if _, err := w.w.Write(w.line[:w.i]); err != nil {
+		return err
+	}
+
+	w.i = 0
+	return nil
+}
+
+func isWhitespace(b byte) bool {
+	return b == ' ' || b == '\t'
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index c709d8f..bded61b 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -66,6 +66,9 @@ github.com/gin-gonic/gin/binding
 github.com/gin-gonic/gin/internal/bytesconv
 github.com/gin-gonic/gin/internal/json
 github.com/gin-gonic/gin/render
+# github.com/go-gomail/gomail v0.0.0-20160411212932-81ebce5c23df
+## explicit
+github.com/go-gomail/gomail
 # github.com/go-passwd/validator v0.0.0-20180902184246-0b4c967e436b
 ## explicit
 github.com/go-passwd/validator
@@ -222,6 +225,11 @@ google.golang.org/protobuf/proto
 google.golang.org/protobuf/reflect/protoreflect
 google.golang.org/protobuf/reflect/protoregistry
 google.golang.org/protobuf/runtime/protoiface
+# gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc
+## explicit
+gopkg.in/alexcesaro/quotedprintable.v3
+# gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
+## explicit
 # gopkg.in/yaml.v3 v3.0.1
 ## explicit
 gopkg.in/yaml.v3