diff --git a/Documentation/email-configuration.md b/Documentation/email-configuration.md new file mode 100644 index 00000000..2a5d4b04 --- /dev/null +++ b/Documentation/email-configuration.md @@ -0,0 +1,52 @@ +# Configuring Sending Emails + +Dex sends emails to a during the registration process to verify an email +address belongs to the person signing up. Currently Dex supports two ways of +sending emails, and has a third option for use during development. + +Configuration of the email provider in Dex is provided through a JSON file. All +email providers have a `type` and `id` field as well as some additional provider +specific fields. + +## SMTP + +If using SMTP the `type` field **must** be set to `smtp`. Additionally both +`host` and `port` are required. If you wish to use SMTP plain auth, then +set `auth` to `plain` and specify your username and password. + +``` +{ + "type": "smtp", + "host": "smtp.example.org", + "port": 587, + "auth": "plain", + "username": "postmaster@example.org", + "password": "foo" +} +``` + +## Mailgun + +If using Mailgun the `type` field **must** be set to `mailgun`. Additionally +`privateAPIKey`, `publicAPIKey`, and `domain` are required. + +``` +{ + "type": "mailgun", + "privateAPIKey": "key-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "publicAPIKey": "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY", + "domain": "sandboxZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ.mailgun.org" +} +``` + +## Dev + +The fake emailer should only be used in development. The fake emailer +prints emails to `stdout` rather than sending any email. If using the fake +emailer the `type` field **must** be set to `fake`. + +``` +{ + "type": "fake" +} +``` \ No newline at end of file diff --git a/Documentation/getting-started.md b/Documentation/getting-started.md index 073086f9..9ae3aa32 100644 --- a/Documentation/getting-started.md +++ b/Documentation/getting-started.md @@ -46,7 +46,7 @@ The dex overlord and workers allow multiple key secrets (separated by commas) to # Start the overlord -The overlord is responsible for creating and rotating keys and some other adminsitrative tasks. In addition, the overlord is responsible for creating the necessary database tables (and when you update, performing schema migrations), so it must be started before we do anything else. Debug logging is turned on so we can see more of what's going on. Start it up. +The overlord is responsible for creating and rotating keys and some other adminsitrative tasks. In addition, the overlord is responsible for creating the necessary database tables (and when you update, performing schema migrations), so it must be started before we do anything else. Debug logging is turned on so we can see more of what's going on. Start it up. `./bin/dex-overlord --db-url=$DEX_DB_URL --key-secrets=$DEX_KEY_SECRET --log-debug=true &` @@ -63,7 +63,12 @@ export DEX_OVERLORD_LOG_DEBUG=true # Start the dex-worker -Now start the worker: +Before starting `dex-worker` you should determine how you want verification emails to be delivered to the user. +If you just want to test dex out, you can just use the provided sample config in `static/fixtures/emailer.json.sample`. +Please review [email-configuration](https://github.com/coreos/dex/blob/master/Documentation/email-configuration.md) for details +(make sure you point `--email-cfg` to your newly configured file). + +Once you have setup your email config run `dex-worker`: `./bin/dex-worker --db-url=$DEX_DB_URL --key-secrets=$DEX_KEY_SECRET --email-cfg=static/fixtures/emailer.json.sample --log-debug=true &` @@ -75,7 +80,7 @@ Note: the issuer URL MUST have an `https` scheme in production to meet spec comp The worker and overlord are up and running, but we need to tell dex what connectors we want to use to authenticate. For this case we'll set up a local connector, where dex manages credentials and provides a UI for authentication, and a Google OIDC connector. -If you prefer to use the Google OIDC Identity Provider (IdP), just omit the second entry in the JSON connector list. Note that you must replace DEX_GOOGLE_CLIENT_{ID,SECRET} with the client ID and client Secret you got when you registered your project with the Google developer console. +If you prefer to use the Google OIDC Identity Provider (IdP), just omit the second entry in the JSON connector list. Note that you must replace DEX_GOOGLE_CLIENT_{ID,SECRET} with the client ID and client Secret you got when you registered your project with the Google developer console. ``` cat << EOF > /tmp/dex_connectors.json [ diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 0720db7d..29cddc63 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -120,6 +120,11 @@ "ImportPath": "google.golang.org/api/googleapi", "Rev": "d3edb0282bde692467788c50070a9211afe75cf3" }, + { + "ImportPath": "gopkg.in/gomail.v2", + "Comment": "2.0.0-2-gb1e5552", + "Rev": "b1e55520bf557d8a614f1e1f493ce892c1b5e97e" + }, { "ImportPath": "gopkg.in/gorp.v1", "Comment": "v1.7.1", diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/.travis.yml b/Godeps/_workspace/src/gopkg.in/gomail.v2/.travis.yml new file mode 100644 index 00000000..24edf22c --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/.travis.yml @@ -0,0 +1,8 @@ +language: go + +go: + - 1.2 + - 1.3 + - 1.4 + - 1.5 + - tip diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/CHANGELOG.md b/Godeps/_workspace/src/gopkg.in/gomail.v2/CHANGELOG.md new file mode 100644 index 00000000..a797ab4c --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/CHANGELOG.md @@ -0,0 +1,20 @@ +# Change Log +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +## [2.0.0] - 2015-09-02 + +- Mailer has been removed. It has been replaced by Dialer and Sender. +- `File` type and the `CreateFile` and `OpenFile` functions have been removed. +- `Message.Attach` and `Message.Embed` have a new signature. +- `Message.GetBodyWriter` has been removed. Use `Message.AddAlternativeWriter` +instead. +- `Message.Export` has been removed. `Message.WriteTo` can be used instead. +- `Message.DelHeader` has been removed. +- The `Bcc` header field is no longer sent. It is far more simpler and +efficient: the same message is sent to all recipients instead of sending a +different email to each Bcc address. +- LoginAuth has been removed. `NewPlainDialer` now implements the LOGIN +authentication mechanism when needed. +- Go 1.2 is now required instead of Go 1.3. No external dependency are used when +using Go 1.5. diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/CONTRIBUTING.md b/Godeps/_workspace/src/gopkg.in/gomail.v2/CONTRIBUTING.md new file mode 100644 index 00000000..d5601c25 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/CONTRIBUTING.md @@ -0,0 +1,20 @@ +Thank you for contributing to Gomail! Here are a few guidelines: + +## Bugs + +If you think you found a bug, create an issue and supply the minimum amount +of code triggering the bug so it can be reproduced. + + +## Fixing a bug + +If you want to fix a bug, you can send a pull request. It should contains a +new test or update an existing one to cover that bug. + + +## New feature proposal + +If you think Gomail lacks a feature, you can open an issue or send a pull +request. I want to keep Gomail code and API as simple as possible so please +describe your needs so we can discuss whether this feature should be added to +Gomail or not. diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/LICENSE b/Godeps/_workspace/src/gopkg.in/gomail.v2/LICENSE new file mode 100644 index 00000000..5f5c12af --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Alexandre Cesaro + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/README.md b/Godeps/_workspace/src/gopkg.in/gomail.v2/README.md new file mode 100644 index 00000000..c1692942 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/README.md @@ -0,0 +1,100 @@ +# 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 +they 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`: + + d := gomail.NewPlainDialer("smtp.example.com", "user", "123456", 587) + d.TLSConfig = &tls.Config{InsecureSkipVerify: true} + +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. + + +## Support + +If you want to support the development of Gomail, I gladly accept donations. + +I will give 100% of the money I receive to +[Enfants, Espoir Du Monde](http://www.eedm.fr/). +EEDM is a French NGO which helps children in Bangladesh, Cameroun, Haiti, India +and Madagascar. + +All its members are volunteers so its operating costs are only 1.9%. So your +money will directly help children of these countries. + +As an added bonus, your donations will also tip me by lowering my taxes :smile: + +I will send an email with the receipt of the donation to EEDM annually to all +donors. + +[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=PYQKC7VFVXCFG) diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/auth.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/auth.go new file mode 100644 index 00000000..4bcdd062 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/auth.go @@ -0,0 +1,67 @@ +package gomail + +import ( + "bytes" + "errors" + "fmt" + "net/smtp" +) + +// plainAuth is an smtp.Auth that implements the PLAIN authentication mechanism. +// It fallbacks to the LOGIN mechanism if it is the only mechanism advertised +// by the server. +type plainAuth struct { + username string + password string + host string + login bool +} + +func (a *plainAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + if server.Name != a.host { + return "", nil, errors.New("gomail: wrong host name") + } + + var plain, login bool + for _, a := range server.Auth { + switch a { + case "PLAIN": + plain = true + case "LOGIN": + login = true + } + } + + if !server.TLS && !plain && !login { + return "", nil, errors.New("gomail: unencrypted connection") + } + + if !plain && login { + a.login = true + return "LOGIN", nil, nil + } + + return "PLAIN", []byte("\x00" + a.username + "\x00" + a.password), nil +} + +func (a *plainAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if !a.login { + if more { + return nil, errors.New("gomail: unexpected server challenge") + } + return nil, nil + } + + 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/Godeps/_workspace/src/gopkg.in/gomail.v2/auth_test.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/auth_test.go new file mode 100644 index 00000000..20b47721 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/auth_test.go @@ -0,0 +1,156 @@ +package gomail + +import ( + "net/smtp" + "testing" +) + +const ( + testUser = "user" + testPwd = "pwd" + testHost = "smtp.example.com" +) + +var testAuth = &plainAuth{ + username: testUser, + password: testPwd, + host: testHost, +} + +type plainAuthTest struct { + auths []string + challenges []string + tls bool + wantProto string + wantData []string + wantError bool +} + +func TestNoAdvertisement(t *testing.T) { + testPlainAuth(t, &plainAuthTest{ + auths: []string{}, + challenges: []string{"Username:", "Password:"}, + tls: false, + wantProto: "PLAIN", + wantError: true, + }) +} + +func TestNoAdvertisementTLS(t *testing.T) { + testPlainAuth(t, &plainAuthTest{ + auths: []string{}, + challenges: []string{"Username:", "Password:"}, + tls: true, + wantProto: "PLAIN", + wantData: []string{"\x00" + testUser + "\x00" + testPwd}, + }) +} + +func TestPlain(t *testing.T) { + testPlainAuth(t, &plainAuthTest{ + auths: []string{"PLAIN"}, + challenges: []string{"Username:", "Password:"}, + tls: false, + wantProto: "PLAIN", + wantData: []string{"\x00" + testUser + "\x00" + testPwd}, + }) +} + +func TestPlainTLS(t *testing.T) { + testPlainAuth(t, &plainAuthTest{ + auths: []string{"PLAIN"}, + challenges: []string{"Username:", "Password:"}, + tls: true, + wantProto: "PLAIN", + wantData: []string{"\x00" + testUser + "\x00" + testPwd}, + }) +} + +func TestPlainAndLogin(t *testing.T) { + testPlainAuth(t, &plainAuthTest{ + auths: []string{"PLAIN", "LOGIN"}, + challenges: []string{"Username:", "Password:"}, + tls: false, + wantProto: "PLAIN", + wantData: []string{"\x00" + testUser + "\x00" + testPwd}, + }) +} + +func TestPlainAndLoginTLS(t *testing.T) { + testPlainAuth(t, &plainAuthTest{ + auths: []string{"PLAIN", "LOGIN"}, + challenges: []string{"Username:", "Password:"}, + tls: true, + wantProto: "PLAIN", + wantData: []string{"\x00" + testUser + "\x00" + testPwd}, + }) +} + +func TestLogin(t *testing.T) { + testPlainAuth(t, &plainAuthTest{ + auths: []string{"LOGIN"}, + challenges: []string{"Username:", "Password:"}, + tls: false, + wantProto: "LOGIN", + wantData: []string{"", testUser, testPwd}, + }) +} + +func TestLoginTLS(t *testing.T) { + testPlainAuth(t, &plainAuthTest{ + auths: []string{"LOGIN"}, + challenges: []string{"Username:", "Password:"}, + tls: true, + wantProto: "LOGIN", + wantData: []string{"", testUser, testPwd}, + }) +} + +func testPlainAuth(t *testing.T, test *plainAuthTest) { + auth := &plainAuth{ + 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("plainAuth.Start(): %v", err) + } + if err != nil && test.wantError { + return + } + if proto != test.wantProto { + t.Errorf("invalid protocol, got %q, want %q", proto, test.wantProto) + } + + i := 0 + got := string(toServer) + if got != test.wantData[i] { + t.Errorf("Invalid response, got %q, want %q", got, test.wantData[i]) + } + + if proto == "PLAIN" { + return + } + + 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("plainAuth.Auth(): %v", err) + } + got = string(toServer) + if got != test.wantData[i] { + t.Errorf("Invalid response, got %q, want %q", got, test.wantData[i]) + } + } +} diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/doc.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/doc.go new file mode 100644 index 00000000..a8f5091f --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/doc.go @@ -0,0 +1,5 @@ +// Package gomail provides a simple interface to compose emails and to mail them +// efficiently. +// +// More info on Github: https://github.com/go-gomail/gomail +package gomail diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/example_test.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/example_test.go new file mode 100644 index 00000000..8d9c6c29 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/example_test.go @@ -0,0 +1,215 @@ +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 Bob and Cora!") + m.Attach("/home/Alex/lolcat.jpg") + + d := gomail.NewPlainDialer("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.NewPlainDialer("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.NewPlainDialer("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": {""}} + m.Attach("foo.jpg", gomail.SetHeader(h)) +} + +func ExampleMessage_AddAlternative() { + m.SetBody("text/plain", "Hello!") + m.AddAlternative("text/html", "

Hello!

") +} + +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", `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)) +} diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/message.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/message.go new file mode 100644 index 00000000..2f75368b --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/message.go @@ -0,0 +1,302 @@ +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 { + header header + copier func(io.Writer) error +} + +// 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 { + 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. +func (m *Message) SetBody(contentType, body string) { + m.parts = []part{ + { + header: m.getPartHeader(contentType), + copier: func(w io.Writer) error { + _, err := io.WriteString(w, body) + return err + }, + }, + } +} + +// 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. +// +// More info: http://en.wikipedia.org/wiki/MIME#Alternative +func (m *Message) AddAlternative(contentType, body string) { + m.parts = append(m.parts, + part{ + header: m.getPartHeader(contentType), + copier: func(w io.Writer) error { + _, err := io.WriteString(w, body) + 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) { + m.parts = []part{ + { + header: m.getPartHeader(contentType), + copier: f, + }, + } +} + +func (m *Message) getPartHeader(contentType string) header { + return map[string][]string{ + "Content-Type": {contentType + "; charset=" + m.charset}, + "Content-Transfer-Encoding": {string(m.encoding)}, + } +} + +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 + } + } +} + +// 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/Godeps/_workspace/src/gopkg.in/gomail.v2/message_test.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/message_test.go new file mode 100644 index 00000000..fdd9ff9b --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/message_test.go @@ -0,0 +1,630 @@ +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?= \r\n" + + "To: =?UTF-8?q?Se=C3=B1or_To?= , tobis@example.com\r\n" + + "Cc: \"A, B\" \r\n" + + "X-To: =?UTF-8?b?w6AsIGI=?= \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 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 + }) + + want := &message{ + from: "from@example.com", + to: []string{"to@example.com"}, + content: "From: from@example.com\r\n" + + "To: to@example.com\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "Content-Transfer-Encoding: quoted-printable\r\n" + + "\r\n" + + "Test message", + } + + testMessage(t, 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", "¡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" + + "Content-Type: multipart/alternative; boundary=_BOUNDARY_1_\r\n" + + "\r\n" + + "--_BOUNDARY_1_\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "Content-Transfer-Encoding: quoted-printable\r\n" + + "\r\n" + + "=C2=A1Hola, se=C3=B1or!\r\n" + + "--_BOUNDARY_1_\r\n" + + "Content-Type: text/html; charset=UTF-8\r\n" + + "Content-Transfer-Encoding: quoted-printable\r\n" + + "\r\n" + + "=C2=A1Hola, se=C3=B1or!\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; 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 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; 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; 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": {""}})) + 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; 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: \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: \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", "¡Hola, señor!") + 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; boundary=_BOUNDARY_1_\r\n" + + "\r\n" + + "--_BOUNDARY_1_\r\n" + + "Content-Type: multipart/related; boundary=_BOUNDARY_2_\r\n" + + "\r\n" + + "--_BOUNDARY_2_\r\n" + + "Content-Type: multipart/alternative; boundary=_BOUNDARY_3_\r\n" + + "\r\n" + + "--_BOUNDARY_3_\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "Content-Transfer-Encoding: quoted-printable\r\n" + + "\r\n" + + "=C2=A1Hola, se=C3=B1or!\r\n" + + "--_BOUNDARY_3_\r\n" + + "Content-Type: text/html; charset=UTF-8\r\n" + + "Content-Transfer-Encoding: quoted-printable\r\n" + + "\r\n" + + "=C2=A1Hola, se=C3=B1or!\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: \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 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", "

¡Hola, señor!

") + m.Attach(mockCopyFile("benchmark.txt")) + m.Embed(mockCopyFile("benchmark.jpg")) + + if err := Send(discardFunc, m); err != nil { + panic(err) + } + m.Reset() + } +} diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/mime.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/mime.go new file mode 100644 index 00000000..51cba724 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/mime.go @@ -0,0 +1,19 @@ +// +build go1.5 + +package gomail + +import ( + "mime" + "mime/quotedprintable" +) + +var newQPWriter = quotedprintable.NewWriter + +type mimeEncoder struct { + mime.WordEncoder +} + +var ( + bEncoding = mimeEncoder{mime.BEncoding} + qEncoding = mimeEncoder{mime.QEncoding} +) diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/mime_go14.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/mime_go14.go new file mode 100644 index 00000000..246e2e5e --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/mime_go14.go @@ -0,0 +1,16 @@ +// +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} +) diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/send.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/send.go new file mode 100644 index 00000000..3e672650 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/send.go @@ -0,0 +1,117 @@ +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 adresses. +// +// 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) { + a, err := mail.ParseAddress(field) + if a == nil { + return "", err + } + + return a.Address, err +} diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/send_test.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/send_test.go new file mode 100644 index 00000000..ba59cd3d --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/send_test.go @@ -0,0 +1,80 @@ +package gomail + +import ( + "bytes" + "io" + "reflect" + "testing" +) + +const ( + testTo1 = "to1@example.com" + testTo2 = "to2@example.com" + testFrom = "from@example.com" + testBody = "Test message" + testMsg = "To: " + testTo1 + ", " + testTo2 + "\r\n" + + "From: " + testFrom + "\r\n" + + "Mime-Version: 1.0\r\n" + + "Date: Wed, 25 Jun 2014 17:46:00 +0000\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "Content-Transfer-Encoding: quoted-printable\r\n" + + "\r\n" + + testBody +) + +type mockSender SendFunc + +func (s mockSender) Send(from string, to []string, msg io.WriterTo) error { + return s(from, to, msg) +} + +type mockSendCloser struct { + mockSender + close func() error +} + +func (s *mockSendCloser) Close() error { + return s.close() +} + +func TestSend(t *testing.T) { + s := &mockSendCloser{ + mockSender: stubSend(t, testFrom, []string{testTo1, testTo2}, testMsg), + close: func() error { + t.Error("Close() should not be called in Send()") + return nil + }, + } + if err := Send(s, getTestMessage()); err != nil { + t.Errorf("Send(): %v", err) + } +} + +func getTestMessage() *Message { + m := NewMessage() + m.SetHeader("From", testFrom) + m.SetHeader("To", testTo1, testTo2) + m.SetBody("text/plain", testBody) + + return m +} + +func stubSend(t *testing.T, wantFrom string, wantTo []string, wantBody string) mockSender { + return func(from string, to []string, msg io.WriterTo) error { + if from != wantFrom { + t.Errorf("invalid from, got %q, want %q", from, wantFrom) + } + if !reflect.DeepEqual(to, wantTo) { + t.Errorf("invalid to, got %v, want %v", to, wantTo) + } + + buf := new(bytes.Buffer) + _, err := msg.WriteTo(buf) + if err != nil { + t.Fatal(err) + } + compareBodies(t, buf.String(), wantBody) + + return nil + } +} diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/smtp.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/smtp.go new file mode 100644 index 00000000..cf773a10 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/smtp.go @@ -0,0 +1,175 @@ +package gomail + +import ( + "crypto/tls" + "fmt" + "io" + "net" + "net/smtp" +) + +// 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 + // 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 +} + +// NewPlainDialer returns a Dialer. The given parameters are used to connect to +// the SMTP server via a PLAIN authentication mechanism. +// +// It fallbacks to the LOGIN mechanism if it is the only mechanism advertised by +// the server. +func NewPlainDialer(host string, port int, username, password string) *Dialer { + return &Dialer{ + Host: host, + Port: port, + Auth: &plainAuth{ + username: username, + password: password, + host: host, + }, + SSL: port == 465, + } +} + +// Dial dials and authenticates to an SMTP server. The returned SendCloser +// should be closed when done using it. +func (d *Dialer) Dial() (SendCloser, error) { + c, err := d.dial() + if err != nil { + return nil, err + } + + if d.Auth != nil { + if ok, _ := c.Extension("AUTH"); ok { + if err = c.Auth(d.Auth); err != nil { + c.Close() + return nil, err + } + } + } + + return &smtpSender{c}, nil +} + +func (d *Dialer) dial() (smtpClient, error) { + if d.SSL { + return d.sslDial() + } + return d.starttlsDial() +} + +func (d *Dialer) starttlsDial() (smtpClient, error) { + c, err := smtpDial(addr(d.Host, d.Port)) + if err != nil { + return nil, err + } + + if ok, _ := c.Extension("STARTTLS"); ok { + if err := c.StartTLS(d.tlsConfig()); err != nil { + c.Close() + return nil, err + } + } + + return c, nil +} + +func (d *Dialer) sslDial() (smtpClient, error) { + conn, err := tlsDial("tcp", addr(d.Host, d.Port), d.tlsConfig()) + if err != nil { + return nil, err + } + + return newClient(conn, d.Host) +} + +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 +} + +func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error { + if err := c.Mail(from); err != nil { + return err + } + + for _, addr := range to { + if err := c.Rcpt(addr); err != nil { + return err + } + } + + w, err := c.Data() + if err != nil { + return err + } + + 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 ( + smtpDial = func(addr string) (smtpClient, error) { + return smtp.Dial(addr) + } + tlsDial = tls.Dial + newClient = func(conn net.Conn, host string) (smtpClient, error) { + return smtp.NewClient(conn, host) + } +) + +type smtpClient interface { + Extension(string) (bool, string) + StartTLS(*tls.Config) error + Auth(smtp.Auth) error + Mail(string) error + Rcpt(string) error + Data() (io.WriteCloser, error) + Quit() error + Close() error +} diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/smtp_test.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/smtp_test.go new file mode 100644 index 00000000..c8503489 --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/smtp_test.go @@ -0,0 +1,254 @@ +package gomail + +import ( + "bytes" + "crypto/tls" + "io" + "net" + "net/smtp" + "reflect" + "testing" +) + +const ( + testPort = 587 + testSSLPort = 465 +) + +var ( + testTLSConn = &tls.Conn{} + testConfig = &tls.Config{InsecureSkipVerify: true} +) + +func TestDialer(t *testing.T) { + d := NewPlainDialer(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 := NewPlainDialer(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 := NewPlainDialer(testHost, testPort, "user", "pwd") + d.TLSConfig = testConfig + testSendMail(t, d, []string{ + "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 := NewPlainDialer(testHost, testSSLPort, "user", "pwd") + d.TLSConfig = testConfig + testSendMail(t, d, []string{ + "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", + }) +} + +type mockClient struct { + t *testing.T + i int + want []string + addr string + auth smtp.Auth + config *tls.Config +} + +func (c *mockClient) Extension(ext string) (bool, string) { + c.do("Extension " + ext) + return true, "" +} + +func (c *mockClient) StartTLS(config *tls.Config) error { + assertConfig(c.t, config, c.config) + c.do("StartTLS") + return nil +} + +func (c *mockClient) Auth(a smtp.Auth) error { + assertAuth(c.t, a, c.auth) + c.do("Auth") + return nil +} + +func (c *mockClient) Mail(from string) error { + c.do("Mail " + from) + return nil +} + +func (c *mockClient) Rcpt(to string) error { + c.do("Rcpt " + to) + return nil +} + +func (c *mockClient) Data() (io.WriteCloser, error) { + c.do("Data") + return &mockWriter{c: c, want: 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) { + testClient := &mockClient{ + t: t, + want: want, + addr: addr(d.Host, d.Port), + auth: testAuth, + config: d.TLSConfig, + } + + smtpDial = func(addr string) (smtpClient, error) { + assertAddr(t, addr, testClient.addr) + return testClient, nil + } + + tlsDial = func(network, addr string, config *tls.Config) (*tls.Conn, error) { + if network != "tcp" { + t.Errorf("Invalid network, got %q, want tcp", network) + } + assertAddr(t, addr, testClient.addr) + assertConfig(t, config, testClient.config) + return testTLSConn, nil + } + + newClient = func(conn net.Conn, host string) (smtpClient, error) { + if conn != testTLSConn { + t.Error("Invalid TLS connection used") + } + if host != testHost { + t.Errorf("Invalid host, got %q, want %q", host, testHost) + } + return testClient, nil + } + + if err := d.DialAndSend(getTestMessage()); err != nil { + t.Error(err) + } +} + +func assertAuth(t *testing.T, got, want smtp.Auth) { + if !reflect.DeepEqual(got, want) { + t.Errorf("Invalid auth, got %#v, want %#v", got, want) + } +} + +func assertAddr(t *testing.T, got, want string) { + if got != want { + t.Errorf("Invalid addr, got %q, want %q", got, want) + } +} + +func assertConfig(t *testing.T, got, want *tls.Config) { + if want == nil { + want = &tls.Config{ServerName: testHost} + } + if got.ServerName != want.ServerName { + t.Errorf("Invalid field ServerName in config, got %q, want %q", got.ServerName, want.ServerName) + } + if got.InsecureSkipVerify != want.InsecureSkipVerify { + t.Errorf("Invalid field InsecureSkipVerify in config, got %v, want %v", got.InsecureSkipVerify, want.InsecureSkipVerify) + } +} diff --git a/Godeps/_workspace/src/gopkg.in/gomail.v2/writeto.go b/Godeps/_workspace/src/gopkg.in/gomail.v2/writeto.go new file mode 100644 index 00000000..57a1dd7f --- /dev/null +++ b/Godeps/_workspace/src/gopkg.in/gomail.v2/writeto.go @@ -0,0 +1,242 @@ +package gomail + +import ( + "encoding/base64" + "errors" + "io" + "mime" + "mime/multipart" + "path/filepath" + "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.writeHeaders(part.header) + w.writeBody(part.copier, m.encoding) + } + 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 + "; 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) 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) writeStrings(a []string, sep string) { + if len(a) > 0 { + w.writeString(a[0]) + if len(a) == 1 { + return + } + } + for _, s := range a[1:] { + w.writeString(sep) + w.writeString(s) + } +} + +func (w *messageWriter) writeHeader(k string, v ...string) { + w.writeString(k) + w.writeString(": ") + w.writeStrings(v, ", ") + w.writeString("\r\n") +} + +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/email/interface.go b/email/interface.go index eb9ff908..05b9ed92 100644 --- a/email/interface.go +++ b/email/interface.go @@ -3,6 +3,7 @@ package email import ( "encoding/json" "errors" + "expvar" "fmt" "io" "os" @@ -14,7 +15,8 @@ const ( ) var ( - ErrorNoTemplate = errors.New("No HTML or Text template found for template name.") + counterEmailSendErr = expvar.NewInt("email.send.err") + ErrorNoTemplate = errors.New("No HTML or Text template found for template name.") ) func init() { @@ -63,7 +65,6 @@ func NewEmailerConfigFromFile(loc string) (EmailerConfig, error) { } type FakeEmailerConfig struct { - ID string `json:"id"` } func (cfg FakeEmailerConfig) EmailerType() string { @@ -71,7 +72,7 @@ func (cfg FakeEmailerConfig) EmailerType() string { } func (cfg FakeEmailerConfig) EmailerID() string { - return cfg.ID + return FakeEmailerType } func (cfg FakeEmailerConfig) Emailer() (Emailer, error) { diff --git a/email/mailgun.go b/email/mailgun.go index 6661f29f..8dd121ce 100644 --- a/email/mailgun.go +++ b/email/mailgun.go @@ -3,7 +3,6 @@ package email import ( "encoding/json" "errors" - "expvar" "github.com/coreos/dex/pkg/log" mailgun "github.com/mailgun/mailgun-go" @@ -13,16 +12,11 @@ const ( MailgunEmailerType = "mailgun" ) -var ( - counterEmailSendErr = expvar.NewInt("mailgun.send.err") -) - func init() { RegisterEmailerConfigType(MailgunEmailerType, func() EmailerConfig { return &MailgunEmailerConfig{} }) } type MailgunEmailerConfig struct { - ID string `json:"id"` PrivateAPIKey string `json:"privateAPIKey"` PublicAPIKey string `json:"publicAPIKey"` Domain string `json:"domain"` @@ -33,7 +27,7 @@ func (cfg MailgunEmailerConfig) EmailerType() string { } func (cfg MailgunEmailerConfig) EmailerID() string { - return cfg.ID + return MailgunEmailerType } func (cfg MailgunEmailerConfig) Emailer() (Emailer, error) { diff --git a/email/smtp.go b/email/smtp.go new file mode 100644 index 00000000..b8568ead --- /dev/null +++ b/email/smtp.go @@ -0,0 +1,84 @@ +package email + +import ( + "encoding/json" + "errors" + + "gopkg.in/gomail.v2" +) + +const ( + SmtpEmailerType = "smtp" +) + +func init() { + RegisterEmailerConfigType(SmtpEmailerType, func() EmailerConfig { return &SmtpEmailerConfig{} }) +} + +type SmtpEmailerConfig struct { + Host string `json:"host"` + Port int `json:"port"` + Auth string `json:"auth"` + Username string `json:"username"` + Password string `json:"password"` +} + +func (cfg SmtpEmailerConfig) EmailerType() string { + return SmtpEmailerType +} + +func (cfg SmtpEmailerConfig) EmailerID() string { + return SmtpEmailerType +} + +func (cfg SmtpEmailerConfig) Emailer() (Emailer, error) { + var dialer *gomail.Dialer + if cfg.Auth == "plain" { + dialer = gomail.NewPlainDialer(cfg.Host, cfg.Port, cfg.Username, cfg.Password) + } else { + dialer = &gomail.Dialer{ + Host: cfg.Host, + Port: cfg.Port, + } + } + return &smtpEmailer{ + dialer: dialer, + }, nil +} + +type smtpEmailerConfig SmtpEmailerConfig + +func (cfg *SmtpEmailerConfig) UnmarshalJSON(data []byte) error { + smtpCfg := smtpEmailerConfig{} + err := json.Unmarshal(data, &smtpCfg) + if err != nil { + return err + } + if smtpCfg.Host == "" { + return errors.New("must set SMTP host") + } + if smtpCfg.Port == "" { + return errors.New("must set SMTP port") + } + *cfg = SmtpEmailerConfig(smtpCfg) + return nil +} + +type smtpEmailer struct { + dialer *gomail.Dialer +} + +func (emailer *smtpEmailer) SendMail(from, subject, text, html string, to ...string) error { + msg := gomail.NewMessage() + msg.SetHeader("From", from) + msg.SetHeader("To", to...) + msg.SetHeader("Subject", subject) + msg.SetBody("text/plain", text) + msg.SetBody("text/html", html) + err := emailer.dialer.DialAndSend(msg) + if err != nil { + counterEmailSendErr.Add(1) + return err + } + return nil +} diff --git a/static/fixtures/emailer.json.sample b/static/fixtures/emailer.json.sample index 8ffc8dfc..15a5cd26 100644 --- a/static/fixtures/emailer.json.sample +++ b/static/fixtures/emailer.json.sample @@ -1,4 +1,3 @@ { - "type": "fake", - "id": "fake" + "type": "fake" } \ No newline at end of file