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