Commit 97041dbd authored by bobbyrullo's avatar bobbyrullo

Merge pull request #129 from ecnahc515/smtp_support

Add smtp support
parents 825c3cf2 d154cad3
# Configuring Sending Emails
Dex sends emails to a during the registration process to verify an email
address belongs to the person signing up. Currently Dex supports two ways of
sending emails, and has a third option for use during development.
Configuration of the email provider in Dex is provided through a JSON file. All
email providers have a `type` and `id` field as well as some additional provider
specific fields.
## SMTP
If using SMTP the `type` field **must** be set to `smtp`. Additionally both
`host` and `port` are required. If you wish to use SMTP plain auth, then
set `auth` to `plain` and specify your username and password.
```
{
"type": "smtp",
"host": "smtp.example.org",
"port": 587,
"auth": "plain",
"username": "postmaster@example.org",
"password": "foo"
}
```
## Mailgun
If using Mailgun the `type` field **must** be set to `mailgun`. Additionally
`privateAPIKey`, `publicAPIKey`, and `domain` are required.
```
{
"type": "mailgun",
"privateAPIKey": "key-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"publicAPIKey": "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY",
"domain": "sandboxZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ.mailgun.org"
}
```
## Dev
The fake emailer should only be used in development. The fake emailer
prints emails to `stdout` rather than sending any email. If using the fake
emailer the `type` field **must** be set to `fake`.
```
{
"type": "fake"
}
```
\ No newline at end of file
......@@ -46,7 +46,7 @@ The dex overlord and workers allow multiple key secrets (separated by commas) to
# Start the overlord
The overlord is responsible for creating and rotating keys and some other adminsitrative tasks. In addition, the overlord is responsible for creating the necessary database tables (and when you update, performing schema migrations), so it must be started before we do anything else. Debug logging is turned on so we can see more of what's going on. Start it up.
The overlord is responsible for creating and rotating keys and some other adminsitrative tasks. In addition, the overlord is responsible for creating the necessary database tables (and when you update, performing schema migrations), so it must be started before we do anything else. Debug logging is turned on so we can see more of what's going on. Start it up.
`./bin/dex-overlord --db-url=$DEX_DB_URL --key-secrets=$DEX_KEY_SECRET --log-debug=true &`
......@@ -63,7 +63,12 @@ export DEX_OVERLORD_LOG_DEBUG=true
# Start the dex-worker
Now start the worker:
Before starting `dex-worker` you should determine how you want verification emails to be delivered to the user.
If you just want to test dex out, you can just use the provided sample config in `static/fixtures/emailer.json.sample`.
Please review [email-configuration](https://github.com/coreos/dex/blob/master/Documentation/email-configuration.md) for details
(make sure you point `--email-cfg` to your newly configured file).
Once you have setup your email config run `dex-worker`:
`./bin/dex-worker --db-url=$DEX_DB_URL --key-secrets=$DEX_KEY_SECRET --email-cfg=static/fixtures/emailer.json.sample --log-debug=true &`
......@@ -75,7 +80,7 @@ Note: the issuer URL MUST have an `https` scheme in production to meet spec comp
The worker and overlord are up and running, but we need to tell dex what connectors we want to use to authenticate. For this case we'll set up a local connector, where dex manages credentials and provides a UI for authentication, and a Google OIDC connector.
If you prefer to use the Google OIDC Identity Provider (IdP), just omit the second entry in the JSON connector list. Note that you must replace DEX_GOOGLE_CLIENT_{ID,SECRET} with the client ID and client Secret you got when you registered your project with the Google developer console.
If you prefer to use the Google OIDC Identity Provider (IdP), just omit the second entry in the JSON connector list. Note that you must replace DEX_GOOGLE_CLIENT_{ID,SECRET} with the client ID and client Secret you got when you registered your project with the Google developer console.
```
cat << EOF > /tmp/dex_connectors.json
[
......
......@@ -120,6 +120,11 @@
"ImportPath": "google.golang.org/api/googleapi",
"Rev": "d3edb0282bde692467788c50070a9211afe75cf3"
},
{
"ImportPath": "gopkg.in/gomail.v2",
"Comment": "2.0.0-2-gb1e5552",
"Rev": "b1e55520bf557d8a614f1e1f493ce892c1b5e97e"
},
{
"ImportPath": "gopkg.in/gorp.v1",
"Comment": "v1.7.1",
......
language: go
go:
- 1.2
- 1.3
- 1.4
- 1.5
- tip
# 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.
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.
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.
# 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)
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)
}
}
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])
}
}
}
// 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
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))
}
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)
}
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()
}
}
// +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}
)
// +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}
)
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
}
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
}
}
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
}
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)
}
}
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 (
"encoding/json"
"errors"
"expvar"
"fmt"
"io"
"os"
......@@ -14,7 +15,8 @@ const (
)
var (
ErrorNoTemplate = errors.New("No HTML or Text template found for template name.")
counterEmailSendErr = expvar.NewInt("email.send.err")
ErrorNoTemplate = errors.New("No HTML or Text template found for template name.")
)
func init() {
......@@ -63,7 +65,6 @@ func NewEmailerConfigFromFile(loc string) (EmailerConfig, error) {
}
type FakeEmailerConfig struct {
ID string `json:"id"`
}
func (cfg FakeEmailerConfig) EmailerType() string {
......@@ -71,7 +72,7 @@ func (cfg FakeEmailerConfig) EmailerType() string {
}
func (cfg FakeEmailerConfig) EmailerID() string {
return cfg.ID
return FakeEmailerType
}
func (cfg FakeEmailerConfig) Emailer() (Emailer, error) {
......
......@@ -3,7 +3,6 @@ package email
import (
"encoding/json"
"errors"
"expvar"
"github.com/coreos/dex/pkg/log"
mailgun "github.com/mailgun/mailgun-go"
......@@ -13,16 +12,11 @@ const (
MailgunEmailerType = "mailgun"
)
var (
counterEmailSendErr = expvar.NewInt("mailgun.send.err")
)
func init() {
RegisterEmailerConfigType(MailgunEmailerType, func() EmailerConfig { return &MailgunEmailerConfig{} })
}
type MailgunEmailerConfig struct {
ID string `json:"id"`
PrivateAPIKey string `json:"privateAPIKey"`
PublicAPIKey string `json:"publicAPIKey"`
Domain string `json:"domain"`
......@@ -33,7 +27,7 @@ func (cfg MailgunEmailerConfig) EmailerType() string {
}
func (cfg MailgunEmailerConfig) EmailerID() string {
return cfg.ID
return MailgunEmailerType
}
func (cfg MailgunEmailerConfig) Emailer() (Emailer, error) {
......
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
}
{
"type": "fake",
"id": "fake"
"type": "fake"
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment