Initial commit
This commit is contained in:
commit
707e81ecee
|
@ -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.
|
|
@ -0,0 +1,70 @@
|
|||
# Gomail
|
||||
|
||||
## Introduction
|
||||
|
||||
Package gomail provides a simple interface to send emails.
|
||||
|
||||
It requires Go 1.2 or newer.
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
* Dead-simple API
|
||||
* Highly flexible
|
||||
* Backward compatibility promise
|
||||
* Supports HTML and text templates
|
||||
* Attachments
|
||||
* Embedded images
|
||||
* Automatic encoding of special characters
|
||||
* Well-documented
|
||||
* High test coverage
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
https://godoc.org/gopkg.in/gomail.v1
|
||||
|
||||
|
||||
## Download
|
||||
|
||||
go get gopkg.in/gomail.v1
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"gopkg.in/gomail.v1"
|
||||
)
|
||||
|
||||
func main() {
|
||||
msg := gomail.NewMessage()
|
||||
msg.SetHeader("From", "alex@example.com")
|
||||
msg.SetHeader("To", "bob@example.com", "cora@example.com")
|
||||
msg.SetAddressHeader("Cc", "dan@example.com", "Dan")
|
||||
msg.SetHeader("Subject", "Hello!")
|
||||
msg.SetBody("text/html", "Hello <b>Bob</b> and <i>Cora</i>!")
|
||||
|
||||
f, err := gomail.OpenFile("/home/Alex/lolcat.jpg")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
msg.Attach(f)
|
||||
|
||||
// Send the email to Bob, Cora and Dan
|
||||
mailer := gomail.NewMailer("smtp.example.com", "user", "123456", 25)
|
||||
if err := mailer.Send(msg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
## Contact
|
||||
|
||||
You are more than welcome to open issues and send pull requests if you find a
|
||||
bug or need a new feature.
|
||||
|
||||
You can also ask questions on the [Gomail
|
||||
thread](https://groups.google.com/d/topic/golang-nuts/ywPpNlmSt6U/discussion)
|
||||
in the Go mailing-list or via Twitter [@alexandrecesaro](https://twitter.com/alexandrecesaro).
|
|
@ -0,0 +1,259 @@
|
|||
package gomail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/mail"
|
||||
"time"
|
||||
|
||||
"gopkg.in/alexcesaro/quotedprintable.v1"
|
||||
)
|
||||
|
||||
// Export converts the message into a net/mail.Message.
|
||||
func (msg *Message) Export() *mail.Message {
|
||||
w := newMessageWriter(msg)
|
||||
|
||||
if msg.hasMixedPart() {
|
||||
w.openMultipart("mixed")
|
||||
}
|
||||
|
||||
if msg.hasRelatedPart() {
|
||||
w.openMultipart("related")
|
||||
}
|
||||
|
||||
if msg.hasAlternativePart() {
|
||||
w.openMultipart("alternative")
|
||||
}
|
||||
for _, part := range msg.parts {
|
||||
h := make(map[string][]string)
|
||||
h["Content-Type"] = []string{part.contentType + "; charset=" + msg.charset}
|
||||
if msg.encoding == Base64 {
|
||||
h["Content-Transfer-Encoding"] = []string{string(Base64)}
|
||||
} else {
|
||||
h["Content-Transfer-Encoding"] = []string{string(QuotedPrintable)}
|
||||
}
|
||||
|
||||
w.write(h, part.body.Bytes(), msg.encoding)
|
||||
}
|
||||
if msg.hasAlternativePart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
|
||||
w.addFiles(msg.embedded, false)
|
||||
if msg.hasRelatedPart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
|
||||
w.addFiles(msg.attachments, true)
|
||||
if msg.hasMixedPart() {
|
||||
w.closeMultipart()
|
||||
}
|
||||
|
||||
return w.export()
|
||||
}
|
||||
|
||||
func (msg *Message) hasMixedPart() bool {
|
||||
return (len(msg.parts) > 0 && len(msg.attachments) > 0) || len(msg.attachments) > 1
|
||||
}
|
||||
|
||||
func (msg *Message) hasRelatedPart() bool {
|
||||
return (len(msg.parts) > 0 && len(msg.embedded) > 0) || len(msg.embedded) > 1
|
||||
}
|
||||
|
||||
func (msg *Message) hasAlternativePart() bool {
|
||||
return len(msg.parts) > 1
|
||||
}
|
||||
|
||||
// messageWriter helps converting the message into a net/mail.Message
|
||||
type messageWriter struct {
|
||||
header map[string][]string
|
||||
buf *bytes.Buffer
|
||||
writers [3]*multipart.Writer
|
||||
partWriter io.Writer
|
||||
depth uint8
|
||||
}
|
||||
|
||||
func newMessageWriter(msg *Message) *messageWriter {
|
||||
// We copy the header so Export does not modify the message
|
||||
header := make(map[string][]string, len(msg.header)+2)
|
||||
for k, v := range msg.header {
|
||||
header[k] = v
|
||||
}
|
||||
|
||||
if _, ok := header["Mime-Version"]; !ok {
|
||||
header["Mime-Version"] = []string{"1.0"}
|
||||
}
|
||||
if _, ok := header["Date"]; !ok {
|
||||
header["Date"] = []string{msg.FormatDate(now())}
|
||||
}
|
||||
|
||||
return &messageWriter{header: header, buf: new(bytes.Buffer)}
|
||||
}
|
||||
|
||||
// Stubbed out for testing.
|
||||
var now = time.Now
|
||||
|
||||
func (w *messageWriter) openMultipart(mimeType string) {
|
||||
w.writers[w.depth] = multipart.NewWriter(w.buf)
|
||||
contentType := "multipart/" + mimeType + "; boundary=" + w.writers[w.depth].Boundary()
|
||||
|
||||
if w.depth == 0 {
|
||||
w.header["Content-Type"] = []string{contentType}
|
||||
} else {
|
||||
h := make(map[string][]string)
|
||||
h["Content-Type"] = []string{contentType}
|
||||
w.createPart(h)
|
||||
}
|
||||
w.depth++
|
||||
}
|
||||
|
||||
func (w *messageWriter) createPart(h map[string][]string) {
|
||||
// No need to check the error since the underlying writer is a bytes.Buffer
|
||||
w.partWriter, _ = 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 {
|
||||
h := make(map[string][]string)
|
||||
h["Content-Type"] = []string{f.MimeType + "; name=\"" + f.Name + "\""}
|
||||
h["Content-Transfer-Encoding"] = []string{string(Base64)}
|
||||
if isAttachment {
|
||||
h["Content-Disposition"] = []string{"attachment; filename=\"" + f.Name + "\""}
|
||||
} else {
|
||||
h["Content-Disposition"] = []string{"inline; filename=\"" + f.Name + "\""}
|
||||
h["Content-ID"] = []string{"<" + f.Name + ">"}
|
||||
}
|
||||
|
||||
w.write(h, f.Content, Base64)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) write(h map[string][]string, body []byte, enc Encoding) {
|
||||
w.writeHeader(h)
|
||||
w.writeBody(body, enc)
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeHeader(h map[string][]string) {
|
||||
if w.depth == 0 {
|
||||
for field, value := range h {
|
||||
w.header[field] = value
|
||||
}
|
||||
} else {
|
||||
w.createPart(h)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) writeBody(body []byte, enc Encoding) {
|
||||
var subWriter io.Writer
|
||||
if w.depth == 0 {
|
||||
subWriter = w.buf
|
||||
} else {
|
||||
subWriter = w.partWriter
|
||||
}
|
||||
|
||||
// The errors returned by writers are not checked since these writers cannot
|
||||
// return errors.
|
||||
if enc == Base64 {
|
||||
writer := base64.NewEncoder(base64.StdEncoding, newBase64LineWriter(subWriter))
|
||||
writer.Write(body)
|
||||
writer.Close()
|
||||
} else {
|
||||
writer := quotedprintable.NewEncoder(newQpLineWriter(subWriter))
|
||||
writer.Write(body)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *messageWriter) export() *mail.Message {
|
||||
return &mail.Message{Header: w.header, Body: w.buf}
|
||||
}
|
||||
|
||||
// As defined in RFC 5322, 2.1.1.
|
||||
const maxLineLen = 78
|
||||
|
||||
// base64LineWriter limits text encoded in base64 to 78 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
|
||||
}
|
||||
|
||||
// qpLineWriter limits text encoded in quoted-printable to 78 characters per
|
||||
// line
|
||||
type qpLineWriter struct {
|
||||
w io.Writer
|
||||
lineLen int
|
||||
}
|
||||
|
||||
func newQpLineWriter(w io.Writer) *qpLineWriter {
|
||||
return &qpLineWriter{w: w}
|
||||
}
|
||||
|
||||
func (w *qpLineWriter) Write(p []byte) (int, error) {
|
||||
n := 0
|
||||
for len(p) > 0 {
|
||||
// If the text is not over the limit, write everything
|
||||
if len(p) < maxLineLen-w.lineLen {
|
||||
w.w.Write(p)
|
||||
w.lineLen += len(p)
|
||||
return n + len(p), nil
|
||||
}
|
||||
|
||||
i := bytes.IndexAny(p[:maxLineLen-w.lineLen+2], "\n")
|
||||
// If there is a newline before the limit, write the end of the line
|
||||
if i != -1 && (i != maxLineLen-w.lineLen+1 || p[i-1] == '\r') {
|
||||
w.w.Write(p[:i+1])
|
||||
p = p[i+1:]
|
||||
n += i + 1
|
||||
w.lineLen = 0
|
||||
continue
|
||||
}
|
||||
|
||||
// Quoted-printable text must not be cut between an equal sign and the
|
||||
// two following characters
|
||||
var toWrite int
|
||||
if maxLineLen-w.lineLen-2 >= 0 && p[maxLineLen-w.lineLen-2] == '=' {
|
||||
toWrite = maxLineLen - w.lineLen - 2
|
||||
} else if p[maxLineLen-w.lineLen-1] == '=' {
|
||||
toWrite = maxLineLen - w.lineLen - 1
|
||||
} else {
|
||||
toWrite = maxLineLen - w.lineLen
|
||||
}
|
||||
|
||||
// Insert the newline where it is needed
|
||||
w.w.Write(p[:toWrite])
|
||||
w.w.Write([]byte("=\r\n"))
|
||||
p = p[toWrite:]
|
||||
n += toWrite
|
||||
w.lineLen = 0
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
// Package gomail provides a simple interface to send emails.
|
||||
//
|
||||
// More info on Github: https://github.com/go-gomail/gomail
|
||||
package gomail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gopkg.in/alexcesaro/quotedprintable.v1"
|
||||
)
|
||||
|
||||
// Message represents an email.
|
||||
type Message struct {
|
||||
header header
|
||||
parts []part
|
||||
attachments []*File
|
||||
embedded []*File
|
||||
charset string
|
||||
encoding Encoding
|
||||
hEncoder *quotedprintable.HeaderEncoder
|
||||
}
|
||||
|
||||
type header map[string][]string
|
||||
|
||||
type part struct {
|
||||
contentType string
|
||||
body *bytes.Buffer
|
||||
}
|
||||
|
||||
// NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding
|
||||
// by default.
|
||||
func NewMessage(settings ...MessageSetting) *Message {
|
||||
msg := &Message{
|
||||
header: make(header),
|
||||
charset: "UTF-8",
|
||||
encoding: QuotedPrintable,
|
||||
}
|
||||
|
||||
msg.applySettings(settings)
|
||||
|
||||
var e quotedprintable.Encoding
|
||||
if msg.encoding == Base64 {
|
||||
e = quotedprintable.B
|
||||
} else {
|
||||
e = quotedprintable.Q
|
||||
}
|
||||
msg.hEncoder = e.NewHeaderEncoder(msg.charset)
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
func (msg *Message) applySettings(settings []MessageSetting) {
|
||||
for _, s := range settings {
|
||||
s(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// A MessageSetting can be used as an argument in NewMessage to configure an
|
||||
// email.
|
||||
type MessageSetting func(msg *Message)
|
||||
|
||||
// SetCharset is a message setting to set the charset of the email.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// msg := NewMessage(SetCharset("ISO-8859-1"))
|
||||
func SetCharset(charset string) MessageSetting {
|
||||
return func(msg *Message) {
|
||||
msg.charset = charset
|
||||
}
|
||||
}
|
||||
|
||||
// SetEncoding is a message setting to set the encoding of the email.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// msg := NewMessage(SetEncoding(gomail.Base64))
|
||||
func SetEncoding(enc Encoding) MessageSetting {
|
||||
return func(msg *Message) {
|
||||
msg.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"
|
||||
)
|
||||
|
||||
// SetHeader sets a value to the given header field.
|
||||
func (msg *Message) SetHeader(field string, value ...string) {
|
||||
for i := range value {
|
||||
value[i] = msg.encodeHeader(value[i])
|
||||
}
|
||||
msg.header[field] = value
|
||||
}
|
||||
|
||||
func (msg *Message) encodeHeader(value string) string {
|
||||
return msg.hEncoder.Encode(value)
|
||||
}
|
||||
|
||||
// SetHeaders sets the message headers.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// msg.SetHeaders(map[string][]string{
|
||||
// "From": {"alex@example.com"},
|
||||
// "To": {"bob@example.com", "cora@example.com"},
|
||||
// "Subject": {"Hello"},
|
||||
// })
|
||||
func (msg *Message) SetHeaders(h map[string][]string) {
|
||||
for k, v := range h {
|
||||
msg.SetHeader(k, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// SetAddressHeader sets an address to the given header field.
|
||||
func (msg *Message) SetAddressHeader(field, address, name string) {
|
||||
msg.header[field] = []string{msg.FormatAddress(address, name)}
|
||||
}
|
||||
|
||||
// FormatAddress formats an address and a name as a valid RFC 5322 address.
|
||||
func (msg *Message) FormatAddress(address, name string) string {
|
||||
return msg.encodeHeader(name) + " <" + address + ">"
|
||||
}
|
||||
|
||||
// SetDateHeader sets a date to the given header field.
|
||||
func (msg *Message) SetDateHeader(field string, date time.Time) {
|
||||
msg.header[field] = []string{msg.FormatDate(date)}
|
||||
}
|
||||
|
||||
// FormatDate formats a date as a valid RFC 5322 date.
|
||||
func (msg *Message) FormatDate(date time.Time) string {
|
||||
return date.Format(time.RFC822)
|
||||
}
|
||||
|
||||
// GetHeader gets a header field.
|
||||
func (msg *Message) GetHeader(field string) []string {
|
||||
return msg.header[field]
|
||||
}
|
||||
|
||||
// DelHeader deletes a header field.
|
||||
func (msg *Message) DelHeader(field string) {
|
||||
delete(msg.header, field)
|
||||
}
|
||||
|
||||
// SetBody sets the body of the message.
|
||||
func (msg *Message) SetBody(contentType, body string) {
|
||||
msg.parts = []part{
|
||||
part{
|
||||
contentType: contentType,
|
||||
body: bytes.NewBufferString(body),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// AddAlternative adds an alternative body to the message. Commonly used to
|
||||
// send HTML emails that default to the plain text version for backward
|
||||
// compatibility.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// msg.SetBody("text/plain", "Hello!")
|
||||
// msg.AddAlternative("text/html", "<p>Hello!</p>")
|
||||
//
|
||||
// More info: http://en.wikipedia.org/wiki/MIME#Alternative
|
||||
func (msg *Message) AddAlternative(contentType, body string) {
|
||||
msg.parts = append(msg.parts,
|
||||
part{
|
||||
contentType: contentType,
|
||||
body: bytes.NewBufferString(body),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// GetBodyWriter gets a writer that writes to the body. It can be useful with
|
||||
// the templates from packages text/template or html/template.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// w := msg.GetBodyWriter("text/plain")
|
||||
// t := template.Must(template.New("example").Parse("Hello {{.}}!"))
|
||||
// t.Execute(w, "Bob")
|
||||
func (msg *Message) GetBodyWriter(contentType string) io.Writer {
|
||||
buf := new(bytes.Buffer)
|
||||
msg.parts = append(msg.parts,
|
||||
part{
|
||||
contentType: contentType,
|
||||
body: buf,
|
||||
},
|
||||
)
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// A File represents a file that can be attached or embedded in an email.
|
||||
type File struct {
|
||||
Name string
|
||||
MimeType string
|
||||
Content []byte
|
||||
}
|
||||
|
||||
// OpenFile opens a file on disk to create a gomail.File.
|
||||
func OpenFile(filename string) (*File, error) {
|
||||
content, err := readFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
f := CreateFile(filepath.Base(filename), content)
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// CreateFile creates a gomail.File from the given name and content.
|
||||
func CreateFile(name string, content []byte) *File {
|
||||
mimeType := mime.TypeByExtension(filepath.Ext(name))
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
return &File{
|
||||
Name: name,
|
||||
MimeType: mimeType,
|
||||
Content: content,
|
||||
}
|
||||
}
|
||||
|
||||
// Attach attaches the files to the email.
|
||||
func (msg *Message) Attach(f ...*File) {
|
||||
if msg.attachments == nil {
|
||||
msg.attachments = f
|
||||
} else {
|
||||
msg.attachments = append(msg.attachments, f...)
|
||||
}
|
||||
}
|
||||
|
||||
// Embed embeds the images to the email.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// f, err := gomail.OpenFile("/tmp/image.jpg")
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// msg.Embed(f)
|
||||
// msg.SetBody("text/html", `<img src="cid:image.jpg" alt="My image" />`)
|
||||
func (msg *Message) Embed(image ...*File) {
|
||||
if msg.embedded == nil {
|
||||
msg.embedded = image
|
||||
} else {
|
||||
msg.embedded = append(msg.attachments, image...)
|
||||
}
|
||||
}
|
||||
|
||||
// Stubbed out for testing.
|
||||
var readFile = ioutil.ReadFile
|
|
@ -0,0 +1,600 @@
|
|||
package gomail
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/smtp"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type message struct {
|
||||
from string
|
||||
to []string
|
||||
content string
|
||||
}
|
||||
|
||||
func TestMessage(t *testing.T) {
|
||||
msg := NewMessage()
|
||||
msg.SetAddressHeader("From", "from@example.com", "Señor From")
|
||||
msg.SetHeader("To", msg.FormatAddress("to@example.com", "Señor To"), "tobis@example.com")
|
||||
msg.SetDateHeader("X-Date", stubNow())
|
||||
msg.SetHeader("X-Date-2", msg.FormatDate(stubNow()))
|
||||
msg.SetHeader("Subject", "¡Hola, señor!")
|
||||
msg.SetHeaders(map[string][]string{
|
||||
"X-Headers": {"Test", "Café"},
|
||||
})
|
||||
msg.SetBody("text/plain", "¡Hola, señor!")
|
||||
|
||||
want := message{
|
||||
from: "=?UTF-8?Q?Se=C3=B1or_From?= <from@example.com>",
|
||||
to: []string{
|
||||
"=?UTF-8?Q?Se=C3=B1or_To?= <to@example.com>",
|
||||
"tobis@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" +
|
||||
"X-Date: 25 Jun 14 17:46 UTC\r\n" +
|
||||
"X-Date-2: 25 Jun 14 17:46 UTC\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, msg, 0, want)
|
||||
}
|
||||
|
||||
func TestBodyWriter(t *testing.T) {
|
||||
msg := NewMessage()
|
||||
msg.SetHeader("From", "from@example.com")
|
||||
msg.SetHeader("To", "to@example.com")
|
||||
w := msg.GetBodyWriter("text/plain")
|
||||
w.Write([]byte("Test message"))
|
||||
|
||||
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, msg, 0, want)
|
||||
}
|
||||
|
||||
func TestCustomMessage(t *testing.T) {
|
||||
msg := NewMessage(SetCharset("ISO-8859-1"), SetEncoding(Base64))
|
||||
msg.SetHeaders(map[string][]string{
|
||||
"From": {"from@example.com"},
|
||||
"To": {"to@example.com"},
|
||||
"Subject": {"Café"},
|
||||
})
|
||||
msg.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, msg, 0, want)
|
||||
}
|
||||
|
||||
func TestRecipients(t *testing.T) {
|
||||
msg := NewMessage()
|
||||
msg.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!"},
|
||||
})
|
||||
msg.SetBody("text/plain", "Test message")
|
||||
|
||||
want := message{
|
||||
from: "from@example.com",
|
||||
to: []string{"to@example.com", "cc@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",
|
||||
}
|
||||
wantBcc1 := message{
|
||||
from: "from@example.com",
|
||||
to: []string{"bcc1@example.com"},
|
||||
content: "From: from@example.com\r\n" +
|
||||
"To: to@example.com\r\n" +
|
||||
"Cc: cc@example.com\r\n" +
|
||||
"Bcc: bcc1@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",
|
||||
}
|
||||
wantBcc2 := message{
|
||||
from: "from@example.com",
|
||||
to: []string{"bcc2@example.com"},
|
||||
content: "From: from@example.com\r\n" +
|
||||
"To: to@example.com\r\n" +
|
||||
"Cc: cc@example.com\r\n" +
|
||||
"Bcc: bcc2@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, msg, 0, want, wantBcc1, wantBcc2)
|
||||
}
|
||||
|
||||
func TestAlternative(t *testing.T) {
|
||||
msg := NewMessage()
|
||||
msg.SetHeader("From", "from@example.com")
|
||||
msg.SetHeader("To", "to@example.com")
|
||||
msg.SetBody("text/plain", "¡Hola, señor!")
|
||||
msg.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, msg, 1, want)
|
||||
}
|
||||
|
||||
func TestAttachmentOnly(t *testing.T) {
|
||||
readFile = func(filename string) ([]byte, error) {
|
||||
return []byte("Content of " + filepath.Base(filename)), nil
|
||||
}
|
||||
|
||||
msg := NewMessage()
|
||||
msg.SetHeader("From", "from@example.com")
|
||||
msg.SetHeader("To", "to@example.com")
|
||||
f, err := OpenFile("/tmp/test.pdf")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
msg.Attach(f)
|
||||
|
||||
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, msg, 0, want)
|
||||
}
|
||||
|
||||
func TestAttachment(t *testing.T) {
|
||||
msg := NewMessage()
|
||||
msg.SetHeader("From", "from@example.com")
|
||||
msg.SetHeader("To", "to@example.com")
|
||||
msg.SetBody("text/plain", "Test")
|
||||
msg.Attach(CreateFile("test.pdf", []byte("Content")))
|
||||
|
||||
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")) + "\r\n" +
|
||||
"--_BOUNDARY_1_--\r\n",
|
||||
}
|
||||
|
||||
testMessage(t, msg, 1, want)
|
||||
}
|
||||
|
||||
func TestAttachmentsOnly(t *testing.T) {
|
||||
msg := NewMessage()
|
||||
msg.SetHeader("From", "from@example.com")
|
||||
msg.SetHeader("To", "to@example.com")
|
||||
msg.Attach(CreateFile("test.pdf", []byte("Content 1")))
|
||||
msg.Attach(CreateFile("test.zip", []byte("Content 2")))
|
||||
|
||||
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 1")) + "\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 2")) + "\r\n" +
|
||||
"--_BOUNDARY_1_--\r\n",
|
||||
}
|
||||
|
||||
testMessage(t, msg, 1, want)
|
||||
}
|
||||
|
||||
func TestAttachments(t *testing.T) {
|
||||
msg := NewMessage()
|
||||
msg.SetHeader("From", "from@example.com")
|
||||
msg.SetHeader("To", "to@example.com")
|
||||
msg.SetBody("text/plain", "Test")
|
||||
msg.Attach(CreateFile("test.pdf", []byte("Content 1")))
|
||||
msg.Attach(CreateFile("test.zip", []byte("Content 2")))
|
||||
|
||||
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 1")) + "\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 2")) + "\r\n" +
|
||||
"--_BOUNDARY_1_--\r\n",
|
||||
}
|
||||
|
||||
testMessage(t, msg, 1, want)
|
||||
}
|
||||
|
||||
func TestEmbedded(t *testing.T) {
|
||||
msg := NewMessage()
|
||||
msg.SetHeader("From", "from@example.com")
|
||||
msg.SetHeader("To", "to@example.com")
|
||||
msg.Embed(CreateFile("image.jpg", []byte("Content")))
|
||||
msg.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=\"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")) + "\r\n" +
|
||||
"--_BOUNDARY_1_--\r\n",
|
||||
}
|
||||
|
||||
testMessage(t, msg, 1, want)
|
||||
}
|
||||
|
||||
func TestFullMessage(t *testing.T) {
|
||||
msg := NewMessage()
|
||||
msg.SetHeader("From", "from@example.com")
|
||||
msg.SetHeader("To", "to@example.com")
|
||||
msg.SetBody("text/plain", "¡Hola, señor!")
|
||||
msg.AddAlternative("text/html", "¡<b>Hola</b>, <i>señor</i>!</h1>")
|
||||
msg.Attach(CreateFile("test.pdf", []byte("Content 1")))
|
||||
msg.Embed(CreateFile("image.jpg", []byte("Content 2")))
|
||||
|
||||
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 2")) + "\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 1")) + "\r\n" +
|
||||
"--_BOUNDARY_1_--\r\n",
|
||||
}
|
||||
|
||||
testMessage(t, msg, 3, want)
|
||||
}
|
||||
|
||||
func TestQpLineLength(t *testing.T) {
|
||||
msg := NewMessage()
|
||||
msg.SetHeader("From", "from@example.com")
|
||||
msg.SetHeader("To", "to@example.com")
|
||||
msg.SetBody("text/plain",
|
||||
strings.Repeat("0", 79)+"\r\n"+
|
||||
strings.Repeat("0", 78)+"à\r\n"+
|
||||
strings.Repeat("0", 77)+"à\r\n"+
|
||||
strings.Repeat("0", 76)+"à\r\n"+
|
||||
strings.Repeat("0", 75)+"à\r\n"+
|
||||
strings.Repeat("0", 78)+"\r\n"+
|
||||
strings.Repeat("0", 79)+"\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", 78) + "=\r\n0\r\n" +
|
||||
strings.Repeat("0", 78) + "=\r\n=C3=A0\r\n" +
|
||||
strings.Repeat("0", 77) + "=\r\n=C3=A0\r\n" +
|
||||
strings.Repeat("0", 76) + "=\r\n=C3=A0\r\n" +
|
||||
strings.Repeat("0", 75) + "=C3=\r\n=A0\r\n" +
|
||||
strings.Repeat("0", 78) + "\r\n" +
|
||||
strings.Repeat("0", 78) + "=\r\n0\n",
|
||||
}
|
||||
|
||||
testMessage(t, msg, 0, want)
|
||||
}
|
||||
|
||||
func TestBase64LineLength(t *testing.T) {
|
||||
msg := NewMessage(SetCharset("UTF-8"), SetEncoding(Base64))
|
||||
msg.SetHeader("From", "from@example.com")
|
||||
msg.SetHeader("To", "to@example.com")
|
||||
msg.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) + "MA\r\n==",
|
||||
}
|
||||
|
||||
testMessage(t, msg, 0, want)
|
||||
}
|
||||
|
||||
func testMessage(t *testing.T, msg *Message, bCount int, emails ...message) {
|
||||
now = stubNow
|
||||
mailer := NewMailer("host", "username", "password", 25, SetSendMail(stubSendMail(t, bCount, emails...)))
|
||||
|
||||
err := mailer.Send(msg)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func stubNow() time.Time {
|
||||
return time.Date(2014, 06, 25, 17, 46, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
func stubSendMail(t *testing.T, bCount int, emails ...message) SendMailFunc {
|
||||
i := 0
|
||||
return func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
|
||||
if i > len(emails) {
|
||||
t.Fatalf("Only %d mails should be sent", len(emails))
|
||||
}
|
||||
want := emails[i]
|
||||
|
||||
if addr != "host:25" {
|
||||
t.Fatalf("Invalid address, got %q, want host:25", addr)
|
||||
}
|
||||
|
||||
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],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
got := string(msg)
|
||||
wantMsg := string("Mime-Version: 1.0\r\n" +
|
||||
"Date: 25 Jun 14 17:46 UTC\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)
|
||||
}
|
||||
}
|
||||
i++
|
||||
|
||||
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, msg string) []string {
|
||||
if matches := boundaryRegExp.FindAllStringSubmatch(msg, 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 BenchmarkFull(b *testing.B) {
|
||||
emptyFunc := func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
msg := NewMessage()
|
||||
msg.SetAddressHeader("From", "from@example.com", "Señor From")
|
||||
msg.SetHeaders(map[string][]string{
|
||||
"To": {"to@example.com"},
|
||||
"Cc": {"cc@example.com"},
|
||||
"Bcc": {"bcc1@example.com", "bcc2@example.com"},
|
||||
"Subject": {"¡Hola, señor!"},
|
||||
})
|
||||
msg.SetBody("text/plain", "¡Hola, señor!")
|
||||
msg.AddAlternative("text/html", "<p>¡Hola, señor!</p>")
|
||||
msg.Attach(CreateFile("benchmark.txt", []byte("Benchmark")))
|
||||
msg.Embed(CreateFile("benchmark.jpg", []byte("Benchmark")))
|
||||
|
||||
mailer := NewMailer("host", "username", "password", 25, SetSendMail(emptyFunc))
|
||||
if err := mailer.Send(msg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
package gomail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Mailer represents an SMTP server.
|
||||
type Mailer struct {
|
||||
addr string
|
||||
auth smtp.Auth
|
||||
send SendMailFunc
|
||||
}
|
||||
|
||||
// A MailerSetting can be used in a mailer constructor to configure it.
|
||||
type MailerSetting func(m *Mailer)
|
||||
|
||||
// SetSendMail is an option to set the email-sending function of a mailer.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// myFunc := func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
|
||||
// // Implement your email-sending function similar to smtp.SendMail
|
||||
// }
|
||||
// mailer := NewMailer("host", "username", "password", 25, SetSendMail(myFunc))
|
||||
func SetSendMail(s SendMailFunc) MailerSetting {
|
||||
return func(m *Mailer) {
|
||||
m.send = s
|
||||
}
|
||||
}
|
||||
|
||||
// A SendMailFunc is a function to send emails with the same signature than
|
||||
// smtp.SendMail.
|
||||
type SendMailFunc func(addr string, a smtp.Auth, from string, to []string, msg []byte) error
|
||||
|
||||
// NewMailer returns a mailer. The given parameters are used to connect to the
|
||||
// SMTP server via a PLAIN authentication mechanism.
|
||||
func NewMailer(host string, username string, password string, port int, settings ...MailerSetting) *Mailer {
|
||||
return NewCustomMailer(
|
||||
fmt.Sprintf("%s:%d", host, port),
|
||||
smtp.PlainAuth("", username, password, host),
|
||||
settings...,
|
||||
)
|
||||
}
|
||||
|
||||
// NewCustomMailer creates a mailer with the given authentication mechanism.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// gomail.NewCustomMailer("host:25", smtp.CRAMMD5Auth("username", "secret"))
|
||||
func NewCustomMailer(addr string, auth smtp.Auth, settings ...MailerSetting) *Mailer {
|
||||
m := &Mailer{
|
||||
addr: addr,
|
||||
auth: auth,
|
||||
send: smtp.SendMail,
|
||||
}
|
||||
m.applySettings(settings)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Mailer) applySettings(settings []MailerSetting) {
|
||||
for _, s := range settings {
|
||||
s(m)
|
||||
}
|
||||
}
|
||||
|
||||
// Send sends the emails to all the recipients of the message.
|
||||
func (m *Mailer) Send(msg *Message) error {
|
||||
message := msg.Export()
|
||||
|
||||
from, err := getFrom(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recipients, bcc := getRecipients(message)
|
||||
|
||||
h := flattenHeader(message, "")
|
||||
body, err := ioutil.ReadAll(message.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mail := append(h, body...)
|
||||
if err := m.send(m.addr, m.auth, from, recipients, mail); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, to := range bcc {
|
||||
h = flattenHeader(message, to)
|
||||
mail = append(h, body...)
|
||||
if err := m.send(m.addr, m.auth, from, []string{to}, mail); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func flattenHeader(msg *mail.Message, bcc string) []byte {
|
||||
var buf bytes.Buffer
|
||||
for field, value := range msg.Header {
|
||||
if field != "Bcc" {
|
||||
buf.WriteString(field + ": " + strings.Join(value, ", ") + "\r\n")
|
||||
} else if bcc != "" {
|
||||
for _, to := range value {
|
||||
if strings.Contains(to, bcc) {
|
||||
buf.WriteString(field + ": " + to + "\r\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.WriteString("\r\n")
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func getFrom(msg *mail.Message) (string, error) {
|
||||
from := msg.Header.Get("Sender")
|
||||
if from == "" {
|
||||
from = msg.Header.Get("From")
|
||||
if from == "" {
|
||||
return "", errors.New("mailer: invalid message, \"From\" field is absent")
|
||||
}
|
||||
}
|
||||
|
||||
return from, nil
|
||||
}
|
||||
|
||||
func getRecipients(msg *mail.Message) (recipients, bcc []string) {
|
||||
for _, field := range []string{"Bcc", "To", "Cc"} {
|
||||
if addresses, ok := msg.Header[field]; ok {
|
||||
for _, addr := range addresses {
|
||||
switch field {
|
||||
case "Bcc":
|
||||
bcc = addAdress(bcc, addr)
|
||||
default:
|
||||
recipients = addAdress(recipients, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return recipients, bcc
|
||||
}
|
||||
|
||||
func addAdress(list []string, addr string) []string {
|
||||
for _, a := range list {
|
||||
if addr == a {
|
||||
return list
|
||||
}
|
||||
}
|
||||
|
||||
return append(list, addr)
|
||||
}
|
Loading…
Reference in New Issue