forked from mystiq/dex
commit
97041dbd90
25 changed files with 2601 additions and 15 deletions
52
Documentation/email-configuration.md
Normal file
52
Documentation/email-configuration.md
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
```
|
|
@ -63,7 +63,12 @@ export DEX_OVERLORD_LOG_DEBUG=true
|
||||||
|
|
||||||
# Start the dex-worker
|
# 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 &`
|
`./bin/dex-worker --db-url=$DEX_DB_URL --key-secrets=$DEX_KEY_SECRET --email-cfg=static/fixtures/emailer.json.sample --log-debug=true &`
|
||||||
|
|
||||||
|
|
5
Godeps/Godeps.json
generated
5
Godeps/Godeps.json
generated
|
@ -120,6 +120,11 @@
|
||||||
"ImportPath": "google.golang.org/api/googleapi",
|
"ImportPath": "google.golang.org/api/googleapi",
|
||||||
"Rev": "d3edb0282bde692467788c50070a9211afe75cf3"
|
"Rev": "d3edb0282bde692467788c50070a9211afe75cf3"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "gopkg.in/gomail.v2",
|
||||||
|
"Comment": "2.0.0-2-gb1e5552",
|
||||||
|
"Rev": "b1e55520bf557d8a614f1e1f493ce892c1b5e97e"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "gopkg.in/gorp.v1",
|
"ImportPath": "gopkg.in/gorp.v1",
|
||||||
"Comment": "v1.7.1",
|
"Comment": "v1.7.1",
|
||||||
|
|
8
Godeps/_workspace/src/gopkg.in/gomail.v2/.travis.yml
generated
vendored
Normal file
8
Godeps/_workspace/src/gopkg.in/gomail.v2/.travis.yml
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.2
|
||||||
|
- 1.3
|
||||||
|
- 1.4
|
||||||
|
- 1.5
|
||||||
|
- tip
|
20
Godeps/_workspace/src/gopkg.in/gomail.v2/CHANGELOG.md
generated
vendored
Normal file
20
Godeps/_workspace/src/gopkg.in/gomail.v2/CHANGELOG.md
generated
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Change Log
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [2.0.0] - 2015-09-02
|
||||||
|
|
||||||
|
- Mailer has been removed. It has been replaced by Dialer and Sender.
|
||||||
|
- `File` type and the `CreateFile` and `OpenFile` functions have been removed.
|
||||||
|
- `Message.Attach` and `Message.Embed` have a new signature.
|
||||||
|
- `Message.GetBodyWriter` has been removed. Use `Message.AddAlternativeWriter`
|
||||||
|
instead.
|
||||||
|
- `Message.Export` has been removed. `Message.WriteTo` can be used instead.
|
||||||
|
- `Message.DelHeader` has been removed.
|
||||||
|
- The `Bcc` header field is no longer sent. It is far more simpler and
|
||||||
|
efficient: the same message is sent to all recipients instead of sending a
|
||||||
|
different email to each Bcc address.
|
||||||
|
- LoginAuth has been removed. `NewPlainDialer` now implements the LOGIN
|
||||||
|
authentication mechanism when needed.
|
||||||
|
- Go 1.2 is now required instead of Go 1.3. No external dependency are used when
|
||||||
|
using Go 1.5.
|
20
Godeps/_workspace/src/gopkg.in/gomail.v2/CONTRIBUTING.md
generated
vendored
Normal file
20
Godeps/_workspace/src/gopkg.in/gomail.v2/CONTRIBUTING.md
generated
vendored
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
Thank you for contributing to Gomail! Here are a few guidelines:
|
||||||
|
|
||||||
|
## Bugs
|
||||||
|
|
||||||
|
If you think you found a bug, create an issue and supply the minimum amount
|
||||||
|
of code triggering the bug so it can be reproduced.
|
||||||
|
|
||||||
|
|
||||||
|
## Fixing a bug
|
||||||
|
|
||||||
|
If you want to fix a bug, you can send a pull request. It should contains a
|
||||||
|
new test or update an existing one to cover that bug.
|
||||||
|
|
||||||
|
|
||||||
|
## New feature proposal
|
||||||
|
|
||||||
|
If you think Gomail lacks a feature, you can open an issue or send a pull
|
||||||
|
request. I want to keep Gomail code and API as simple as possible so please
|
||||||
|
describe your needs so we can discuss whether this feature should be added to
|
||||||
|
Gomail or not.
|
20
Godeps/_workspace/src/gopkg.in/gomail.v2/LICENSE
generated
vendored
Normal file
20
Godeps/_workspace/src/gopkg.in/gomail.v2/LICENSE
generated
vendored
Normal file
|
@ -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.
|
100
Godeps/_workspace/src/gopkg.in/gomail.v2/README.md
generated
vendored
Normal file
100
Godeps/_workspace/src/gopkg.in/gomail.v2/README.md
generated
vendored
Normal file
|
@ -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)
|
67
Godeps/_workspace/src/gopkg.in/gomail.v2/auth.go
generated
vendored
Normal file
67
Godeps/_workspace/src/gopkg.in/gomail.v2/auth.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
156
Godeps/_workspace/src/gopkg.in/gomail.v2/auth_test.go
generated
vendored
Normal file
156
Godeps/_workspace/src/gopkg.in/gomail.v2/auth_test.go
generated
vendored
Normal file
|
@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
Godeps/_workspace/src/gopkg.in/gomail.v2/doc.go
generated
vendored
Normal file
5
Godeps/_workspace/src/gopkg.in/gomail.v2/doc.go
generated
vendored
Normal file
|
@ -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
|
215
Godeps/_workspace/src/gopkg.in/gomail.v2/example_test.go
generated
vendored
Normal file
215
Godeps/_workspace/src/gopkg.in/gomail.v2/example_test.go
generated
vendored
Normal file
|
@ -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 <b>Bob</b> and <i>Cora</i>!")
|
||||||
|
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": {"<foo@bar.mail>"}}
|
||||||
|
m.Attach("foo.jpg", gomail.SetHeader(h))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMessage_AddAlternative() {
|
||||||
|
m.SetBody("text/plain", "Hello!")
|
||||||
|
m.AddAlternative("text/html", "<p>Hello!</p>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMessage_AddAlternativeWriter() {
|
||||||
|
t := template.Must(template.New("example").Parse("Hello {{.}}!"))
|
||||||
|
m.AddAlternativeWriter("text/plain", func(w io.Writer) error {
|
||||||
|
return t.Execute(w, "Bob")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMessage_Attach() {
|
||||||
|
m.Attach("/tmp/image.jpg")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMessage_Embed() {
|
||||||
|
m.Embed("/tmp/image.jpg")
|
||||||
|
m.SetBody("text/html", `<img src="cid:image.jpg" alt="My image" />`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMessage_FormatAddress() {
|
||||||
|
m.SetHeader("To", m.FormatAddress("bob@example.com", "Bob"), m.FormatAddress("cora@example.com", "Cora"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMessage_FormatDate() {
|
||||||
|
m.SetHeaders(map[string][]string{
|
||||||
|
"X-Date": {m.FormatDate(time.Now())},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMessage_SetAddressHeader() {
|
||||||
|
m.SetAddressHeader("To", "bob@example.com", "Bob")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMessage_SetBody() {
|
||||||
|
m.SetBody("text/plain", "Hello!")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMessage_SetDateHeader() {
|
||||||
|
m.SetDateHeader("X-Date", time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMessage_SetHeader() {
|
||||||
|
m.SetHeader("Subject", "Hello!")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleMessage_SetHeaders() {
|
||||||
|
m.SetHeaders(map[string][]string{
|
||||||
|
"From": {m.FormatAddress("alex@example.com", "Alex")},
|
||||||
|
"To": {"bob@example.com", "cora@example.com"},
|
||||||
|
"Subject": {"Hello"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleSetCharset() {
|
||||||
|
m = gomail.NewMessage(gomail.SetCharset("ISO-8859-1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleSetEncoding() {
|
||||||
|
m = gomail.NewMessage(gomail.SetEncoding(gomail.Base64))
|
||||||
|
}
|
302
Godeps/_workspace/src/gopkg.in/gomail.v2/message.go
generated
vendored
Normal file
302
Godeps/_workspace/src/gopkg.in/gomail.v2/message.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
630
Godeps/_workspace/src/gopkg.in/gomail.v2/message_test.go
generated
vendored
Normal file
630
Godeps/_workspace/src/gopkg.in/gomail.v2/message_test.go
generated
vendored
Normal file
|
@ -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?= <from@example.com>\r\n" +
|
||||||
|
"To: =?UTF-8?q?Se=C3=B1or_To?= <to@example.com>, tobis@example.com\r\n" +
|
||||||
|
"Cc: \"A, B\" <cc@example.com>\r\n" +
|
||||||
|
"X-To: =?UTF-8?b?w6AsIGI=?= <ccbis@example.com>\r\n" +
|
||||||
|
"X-Date: Wed, 25 Jun 2014 17:46:00 +0000\r\n" +
|
||||||
|
"X-Date-2: Wed, 25 Jun 2014 17:46:00 +0000\r\n" +
|
||||||
|
"X-Headers: Test, =?UTF-8?q?Caf=C3=A9?=\r\n" +
|
||||||
|
"Subject: =?UTF-8?q?=C2=A1Hola,_se=C3=B1or!?=\r\n" +
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\r\n" +
|
||||||
|
"Content-Transfer-Encoding: quoted-printable\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"=C2=A1Hola, se=C3=B1or!",
|
||||||
|
}
|
||||||
|
|
||||||
|
testMessage(t, m, 0, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
func 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", "¡<b>Hola</b>, <i>señor</i>!</h1>")
|
||||||
|
|
||||||
|
want := &message{
|
||||||
|
from: "from@example.com",
|
||||||
|
to: []string{"to@example.com"},
|
||||||
|
content: "From: from@example.com\r\n" +
|
||||||
|
"To: to@example.com\r\n" +
|
||||||
|
"Content-Type: multipart/alternative; boundary=_BOUNDARY_1_\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"--_BOUNDARY_1_\r\n" +
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\r\n" +
|
||||||
|
"Content-Transfer-Encoding: quoted-printable\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"=C2=A1Hola, se=C3=B1or!\r\n" +
|
||||||
|
"--_BOUNDARY_1_\r\n" +
|
||||||
|
"Content-Type: text/html; charset=UTF-8\r\n" +
|
||||||
|
"Content-Transfer-Encoding: quoted-printable\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"=C2=A1<b>Hola</b>, <i>se=C3=B1or</i>!</h1>\r\n" +
|
||||||
|
"--_BOUNDARY_1_--\r\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
testMessage(t, 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": {"<test-content-id>"}}))
|
||||||
|
m.Embed(mockCopyFile("image2.jpg"))
|
||||||
|
m.SetBody("text/plain", "Test")
|
||||||
|
|
||||||
|
want := &message{
|
||||||
|
from: "from@example.com",
|
||||||
|
to: []string{"to@example.com"},
|
||||||
|
content: "From: from@example.com\r\n" +
|
||||||
|
"To: to@example.com\r\n" +
|
||||||
|
"Content-Type: multipart/related; boundary=_BOUNDARY_1_\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"--_BOUNDARY_1_\r\n" +
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\r\n" +
|
||||||
|
"Content-Transfer-Encoding: quoted-printable\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"Test\r\n" +
|
||||||
|
"--_BOUNDARY_1_\r\n" +
|
||||||
|
"Content-Type: image/jpeg; name=\"image1.jpg\"\r\n" +
|
||||||
|
"Content-Disposition: inline; filename=\"image1.jpg\"\r\n" +
|
||||||
|
"Content-ID: <test-content-id>\r\n" +
|
||||||
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
base64.StdEncoding.EncodeToString([]byte("Content of image1.jpg")) + "\r\n" +
|
||||||
|
"--_BOUNDARY_1_\r\n" +
|
||||||
|
"Content-Type: image/jpeg; name=\"image2.jpg\"\r\n" +
|
||||||
|
"Content-Disposition: inline; filename=\"image2.jpg\"\r\n" +
|
||||||
|
"Content-ID: <image2.jpg>\r\n" +
|
||||||
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
base64.StdEncoding.EncodeToString([]byte("Content of image2.jpg")) + "\r\n" +
|
||||||
|
"--_BOUNDARY_1_--\r\n",
|
||||||
|
}
|
||||||
|
|
||||||
|
testMessage(t, m, 1, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFullMessage(t *testing.T) {
|
||||||
|
m := NewMessage()
|
||||||
|
m.SetHeader("From", "from@example.com")
|
||||||
|
m.SetHeader("To", "to@example.com")
|
||||||
|
m.SetBody("text/plain", "¡Hola, señor!")
|
||||||
|
m.AddAlternative("text/html", "¡<b>Hola</b>, <i>señor</i>!</h1>")
|
||||||
|
m.Attach(mockCopyFile("test.pdf"))
|
||||||
|
m.Embed(mockCopyFile("image.jpg"))
|
||||||
|
|
||||||
|
want := &message{
|
||||||
|
from: "from@example.com",
|
||||||
|
to: []string{"to@example.com"},
|
||||||
|
content: "From: from@example.com\r\n" +
|
||||||
|
"To: to@example.com\r\n" +
|
||||||
|
"Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"--_BOUNDARY_1_\r\n" +
|
||||||
|
"Content-Type: multipart/related; boundary=_BOUNDARY_2_\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"--_BOUNDARY_2_\r\n" +
|
||||||
|
"Content-Type: multipart/alternative; boundary=_BOUNDARY_3_\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"--_BOUNDARY_3_\r\n" +
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\r\n" +
|
||||||
|
"Content-Transfer-Encoding: quoted-printable\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"=C2=A1Hola, se=C3=B1or!\r\n" +
|
||||||
|
"--_BOUNDARY_3_\r\n" +
|
||||||
|
"Content-Type: text/html; charset=UTF-8\r\n" +
|
||||||
|
"Content-Transfer-Encoding: quoted-printable\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"=C2=A1<b>Hola</b>, <i>se=C3=B1or</i>!</h1>\r\n" +
|
||||||
|
"--_BOUNDARY_3_--\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
"--_BOUNDARY_2_\r\n" +
|
||||||
|
"Content-Type: image/jpeg; name=\"image.jpg\"\r\n" +
|
||||||
|
"Content-Disposition: inline; filename=\"image.jpg\"\r\n" +
|
||||||
|
"Content-ID: <image.jpg>\r\n" +
|
||||||
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
|
"\r\n" +
|
||||||
|
base64.StdEncoding.EncodeToString([]byte("Content 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", "<p>¡Hola, señor!</p>")
|
||||||
|
m.Attach(mockCopyFile("benchmark.txt"))
|
||||||
|
m.Embed(mockCopyFile("benchmark.jpg"))
|
||||||
|
|
||||||
|
if err := Send(discardFunc, m); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
m.Reset()
|
||||||
|
}
|
||||||
|
}
|
19
Godeps/_workspace/src/gopkg.in/gomail.v2/mime.go
generated
vendored
Normal file
19
Godeps/_workspace/src/gopkg.in/gomail.v2/mime.go
generated
vendored
Normal file
|
@ -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}
|
||||||
|
)
|
16
Godeps/_workspace/src/gopkg.in/gomail.v2/mime_go14.go
generated
vendored
Normal file
16
Godeps/_workspace/src/gopkg.in/gomail.v2/mime_go14.go
generated
vendored
Normal file
|
@ -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}
|
||||||
|
)
|
117
Godeps/_workspace/src/gopkg.in/gomail.v2/send.go
generated
vendored
Normal file
117
Godeps/_workspace/src/gopkg.in/gomail.v2/send.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
80
Godeps/_workspace/src/gopkg.in/gomail.v2/send_test.go
generated
vendored
Normal file
80
Godeps/_workspace/src/gopkg.in/gomail.v2/send_test.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
175
Godeps/_workspace/src/gopkg.in/gomail.v2/smtp.go
generated
vendored
Normal file
175
Godeps/_workspace/src/gopkg.in/gomail.v2/smtp.go
generated
vendored
Normal file
|
@ -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
|
||||||
|
}
|
254
Godeps/_workspace/src/gopkg.in/gomail.v2/smtp_test.go
generated
vendored
Normal file
254
Godeps/_workspace/src/gopkg.in/gomail.v2/smtp_test.go
generated
vendored
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
242
Godeps/_workspace/src/gopkg.in/gomail.v2/writeto.go
generated
vendored
Normal file
242
Godeps/_workspace/src/gopkg.in/gomail.v2/writeto.go
generated
vendored
Normal file
|
@ -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
|
|
@ -3,6 +3,7 @@ package email
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"expvar"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
@ -14,6 +15,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
counterEmailSendErr = expvar.NewInt("email.send.err")
|
||||||
ErrorNoTemplate = errors.New("No HTML or Text template found for template name.")
|
ErrorNoTemplate = errors.New("No HTML or Text template found for template name.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -63,7 +65,6 @@ func NewEmailerConfigFromFile(loc string) (EmailerConfig, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type FakeEmailerConfig struct {
|
type FakeEmailerConfig struct {
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg FakeEmailerConfig) EmailerType() string {
|
func (cfg FakeEmailerConfig) EmailerType() string {
|
||||||
|
@ -71,7 +72,7 @@ func (cfg FakeEmailerConfig) EmailerType() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg FakeEmailerConfig) EmailerID() string {
|
func (cfg FakeEmailerConfig) EmailerID() string {
|
||||||
return cfg.ID
|
return FakeEmailerType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg FakeEmailerConfig) Emailer() (Emailer, error) {
|
func (cfg FakeEmailerConfig) Emailer() (Emailer, error) {
|
||||||
|
|
|
@ -3,7 +3,6 @@ package email
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"expvar"
|
|
||||||
|
|
||||||
"github.com/coreos/dex/pkg/log"
|
"github.com/coreos/dex/pkg/log"
|
||||||
mailgun "github.com/mailgun/mailgun-go"
|
mailgun "github.com/mailgun/mailgun-go"
|
||||||
|
@ -13,16 +12,11 @@ const (
|
||||||
MailgunEmailerType = "mailgun"
|
MailgunEmailerType = "mailgun"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
counterEmailSendErr = expvar.NewInt("mailgun.send.err")
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterEmailerConfigType(MailgunEmailerType, func() EmailerConfig { return &MailgunEmailerConfig{} })
|
RegisterEmailerConfigType(MailgunEmailerType, func() EmailerConfig { return &MailgunEmailerConfig{} })
|
||||||
}
|
}
|
||||||
|
|
||||||
type MailgunEmailerConfig struct {
|
type MailgunEmailerConfig struct {
|
||||||
ID string `json:"id"`
|
|
||||||
PrivateAPIKey string `json:"privateAPIKey"`
|
PrivateAPIKey string `json:"privateAPIKey"`
|
||||||
PublicAPIKey string `json:"publicAPIKey"`
|
PublicAPIKey string `json:"publicAPIKey"`
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
|
@ -33,7 +27,7 @@ func (cfg MailgunEmailerConfig) EmailerType() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg MailgunEmailerConfig) EmailerID() string {
|
func (cfg MailgunEmailerConfig) EmailerID() string {
|
||||||
return cfg.ID
|
return MailgunEmailerType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg MailgunEmailerConfig) Emailer() (Emailer, error) {
|
func (cfg MailgunEmailerConfig) Emailer() (Emailer, error) {
|
||||||
|
|
84
email/smtp.go
Normal file
84
email/smtp.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
{
|
{
|
||||||
"type": "fake",
|
"type": "fake"
|
||||||
"id": "fake"
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue