Merge pull request #15 from wneessen/13-template-attachments

Template attachments
This commit is contained in:
Winni Neessen 2022-06-03 12:17:50 +02:00 committed by GitHub
commit 8fbcb076af
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 505 additions and 10 deletions

View file

@ -35,6 +35,7 @@ Some of the features of this library:
* [X] Support for different encodings * [X] Support for different encodings
* [X] Support sending mails via a local sendmail command * [X] Support sending mails via a local sendmail command
* [X] Message object satisfies `io.WriteTo` and `io.Reader` interfaces * [X] Message object satisfies `io.WriteTo` and `io.Reader` interfaces
* [X] Support for Go's `html/template` and `text/template` (as message body, alternative part or attachment/emebed)
go-mail works like a programatic email client and provides lots of methods and functionalities you would consider go-mail works like a programatic email client and provides lots of methods and functionalities you would consider
standard in a MUA. standard in a MUA.

2
doc.go
View file

@ -2,4 +2,4 @@
package mail package mail
// VERSION is used in the default user agent string // VERSION is used in the default user agent string
const VERSION = "0.1.9" const VERSION = "0.2.2"

150
msg.go
View file

@ -5,6 +5,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
ht "html/template"
"io" "io"
"math/rand" "math/rand"
"mime" "mime"
@ -13,6 +14,7 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"syscall" "syscall"
tt "text/template"
"time" "time"
) )
@ -389,10 +391,7 @@ func (m *Msg) GetRecipients() ([]string, error) {
// SetBodyString sets the body of the message. // SetBodyString sets the body of the message.
func (m *Msg) SetBodyString(ct ContentType, b string, o ...PartOption) { func (m *Msg) SetBodyString(ct ContentType, b string, o ...PartOption) {
buf := bytes.NewBufferString(b) buf := bytes.NewBufferString(b)
w := func(w io.Writer) (int64, error) { w := writeFuncFromBuffer(buf)
nb, err := w.Write(buf.Bytes())
return int64(nb), err
}
m.SetBodyWriter(ct, w, o...) m.SetBodyWriter(ct, w, o...)
} }
@ -403,13 +402,40 @@ func (m *Msg) SetBodyWriter(ct ContentType, w func(io.Writer) (int64, error), o
m.parts = []*Part{p} m.parts = []*Part{p}
} }
// SetBodyHTMLTemplate sets the body of the message from a given html/template.Template pointer
// The content type will be set to text/html automatically
func (m *Msg) SetBodyHTMLTemplate(t *ht.Template, d interface{}, o ...PartOption) error {
if t == nil {
return fmt.Errorf("template pointer is nil")
}
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
w := writeFuncFromBuffer(&buf)
m.SetBodyWriter(TypeTextHTML, w, o...)
return nil
}
// SetBodyTextTemplate sets the body of the message from a given text/template.Template pointer
// The content type will be set to text/plain automatically
func (m *Msg) SetBodyTextTemplate(t *tt.Template, d interface{}, o ...PartOption) error {
if t == nil {
return fmt.Errorf("template pointer is nil")
}
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
w := writeFuncFromBuffer(&buf)
m.SetBodyWriter(TypeTextPlain, w, o...)
return nil
}
// AddAlternativeString sets the alternative body of the message. // AddAlternativeString sets the alternative body of the message.
func (m *Msg) AddAlternativeString(ct ContentType, b string, o ...PartOption) { func (m *Msg) AddAlternativeString(ct ContentType, b string, o ...PartOption) {
buf := bytes.NewBufferString(b) buf := bytes.NewBufferString(b)
w := func(w io.Writer) (int64, error) { w := writeFuncFromBuffer(buf)
nb, err := w.Write(buf.Bytes())
return int64(nb), err
}
m.AddAlternativeWriter(ct, w, o...) m.AddAlternativeWriter(ct, w, o...)
} }
@ -420,6 +446,36 @@ func (m *Msg) AddAlternativeWriter(ct ContentType, w func(io.Writer) (int64, err
m.parts = append(m.parts, p) m.parts = append(m.parts, p)
} }
// AddAlternativeHTMLTemplate sets the alternative body of the message to a html/template.Template output
// The content type will be set to text/html automatically
func (m *Msg) AddAlternativeHTMLTemplate(t *ht.Template, d interface{}, o ...PartOption) error {
if t == nil {
return fmt.Errorf("template pointer is nil")
}
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
w := writeFuncFromBuffer(&buf)
m.AddAlternativeWriter(TypeTextHTML, w, o...)
return nil
}
// AddAlternativeTextTemplate sets the alternative body of the message to a text/template.Template output
// The content type will be set to text/plain automatically
func (m *Msg) AddAlternativeTextTemplate(t *tt.Template, d interface{}, o ...PartOption) error {
if t == nil {
return fmt.Errorf("template pointer is nil")
}
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
w := writeFuncFromBuffer(&buf)
m.AddAlternativeWriter(TypeTextPlain, w, o...)
return nil
}
// AttachFile adds an attachment File to the Msg // AttachFile adds an attachment File to the Msg
func (m *Msg) AttachFile(n string, o ...FileOption) { func (m *Msg) AttachFile(n string, o ...FileOption) {
f := fileFromFS(n) f := fileFromFS(n)
@ -435,6 +491,26 @@ func (m *Msg) AttachReader(n string, r io.Reader, o ...FileOption) {
m.attachments = m.appendFile(m.attachments, f, o...) m.attachments = m.appendFile(m.attachments, f, o...)
} }
// AttachHTMLTemplate adds the output of a html/template.Template pointer as File attachment to the Msg
func (m *Msg) AttachHTMLTemplate(n string, t *ht.Template, d interface{}, o ...FileOption) error {
f, err := fileFromHTMLTemplate(n, t, d)
if err != nil {
return fmt.Errorf("failed to attach template: %w", err)
}
m.attachments = m.appendFile(m.attachments, f, o...)
return nil
}
// AttachTextTemplate adds the output of a text/template.Template pointer as File attachment to the Msg
func (m *Msg) AttachTextTemplate(n string, t *tt.Template, d interface{}, o ...FileOption) error {
f, err := fileFromTextTemplate(n, t, d)
if err != nil {
return fmt.Errorf("failed to attach template: %w", err)
}
m.attachments = m.appendFile(m.attachments, f, o...)
return nil
}
// EmbedFile adds an embedded File to the Msg // EmbedFile adds an embedded File to the Msg
func (m *Msg) EmbedFile(n string, o ...FileOption) { func (m *Msg) EmbedFile(n string, o ...FileOption) {
f := fileFromFS(n) f := fileFromFS(n)
@ -450,6 +526,26 @@ func (m *Msg) EmbedReader(n string, r io.Reader, o ...FileOption) {
m.embeds = m.appendFile(m.embeds, f, o...) m.embeds = m.appendFile(m.embeds, f, o...)
} }
// EmbedHTMLTemplate adds the output of a html/template.Template pointer as embedded File to the Msg
func (m *Msg) EmbedHTMLTemplate(n string, t *ht.Template, d interface{}, o ...FileOption) error {
f, err := fileFromHTMLTemplate(n, t, d)
if err != nil {
return fmt.Errorf("failed to embed template: %w", err)
}
m.embeds = m.appendFile(m.embeds, f, o...)
return nil
}
// EmbedTextTemplate adds the output of a text/template.Template pointer as embedded File to the Msg
func (m *Msg) EmbedTextTemplate(n string, t *tt.Template, d interface{}, o ...FileOption) error {
f, err := fileFromTextTemplate(n, t, d)
if err != nil {
return fmt.Errorf("failed to embed template: %w", err)
}
m.embeds = m.appendFile(m.embeds, f, o...)
return nil
}
// Reset resets all headers, body parts and attachments/embeds of the Msg // Reset resets all headers, body parts and attachments/embeds of the Msg
// It leaves already set encodings, charsets, boundaries, etc. as is // It leaves already set encodings, charsets, boundaries, etc. as is
func (m *Msg) Reset() { func (m *Msg) Reset() {
@ -467,7 +563,7 @@ func (m *Msg) WriteTo(w io.Writer) (int64, error) {
return mw.n, mw.err return mw.n, mw.err
} }
// Write is an alias method to WriteTo due to compatiblity reasons // Write is an alias method to WriteTo due to compatibility reasons
func (m *Msg) Write(w io.Writer) (int64, error) { func (m *Msg) Write(w io.Writer) (int64, error) {
return m.WriteTo(w) return m.WriteTo(w)
} }
@ -668,6 +764,32 @@ func fileFromReader(n string, r io.Reader) *File {
} }
} }
// fileFromHTMLTemplate returns a File pointer form a given html/template.Template
func fileFromHTMLTemplate(n string, t *ht.Template, d interface{}) (*File, error) {
if t == nil {
return nil, fmt.Errorf("template pointer is nil")
}
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return nil, fmt.Errorf("failed to execute template: %w", err)
}
f := fileFromReader(n, &buf)
return f, nil
}
// fileFromTextTemplate returns a File pointer form a given text/template.Template
func fileFromTextTemplate(n string, t *tt.Template, d interface{}) (*File, error) {
if t == nil {
return nil, fmt.Errorf("template pointer is nil")
}
buf := bytes.Buffer{}
if err := t.Execute(&buf, d); err != nil {
return nil, fmt.Errorf("failed to execute template: %w", err)
}
f := fileFromReader(n, &buf)
return f, nil
}
// getEncoder creates a new mime.WordEncoder based on the encoding setting of the message // getEncoder creates a new mime.WordEncoder based on the encoding setting of the message
func getEncoder(e Encoding) mime.WordEncoder { func getEncoder(e Encoding) mime.WordEncoder {
switch e { switch e {
@ -679,3 +801,13 @@ func getEncoder(e Encoding) mime.WordEncoder {
return mime.QEncoding return mime.QEncoding
} }
} }
// writeFuncFromBuffer is a common method to convert a byte buffer into a writeFunc as
// often required by this library
func writeFuncFromBuffer(buf *bytes.Buffer) func(io.Writer) (int64, error) {
w := func(w io.Writer) (int64, error) {
nb, err := w.Write(buf.Bytes())
return int64(nb), err
}
return w
}

View file

@ -4,10 +4,12 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"fmt" "fmt"
htpl "html/template"
"io" "io"
"net/mail" "net/mail"
"strings" "strings"
"testing" "testing"
ttpl "text/template"
"time" "time"
) )
@ -1338,3 +1340,363 @@ func TestMsg_Read_ioCopy(t *testing.T) {
t.Errorf("message content of WriteTo and io.Copy differ") t.Errorf("message content of WriteTo and io.Copy differ")
} }
} }
// TestMsg_SetBodyTextTemplate tests the Msg.SetBodyTextTemplate method
func TestMsg_SetBodyTextTemplate(t *testing.T) {
tests := []struct {
name string
tpl string
ph string
sf bool
}{
{"normal text", "This is a {{.Placeholder}}", "TemplateTest", false},
{"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := struct {
Placeholder string
}{Placeholder: tt.ph}
tpl, err := ttpl.New("test").Parse(tt.tpl)
if err != nil && !tt.sf {
t.Errorf("failed to render template: %s", err)
return
}
m := NewMsg()
if err := m.SetBodyTextTemplate(tpl, data); err != nil && !tt.sf {
t.Errorf("failed to set template as body: %s", err)
}
wbuf := bytes.Buffer{}
_, err = m.WriteTo(&wbuf)
if err != nil {
t.Errorf("failed to write body to buffer: %s", err)
}
if !strings.Contains(wbuf.String(), tt.ph) && !tt.sf {
t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph)
}
m.Reset()
})
}
}
// TestMsg_SetBodyHTMLTemplate tests the Msg.SetBodyHTMLTemplate method
func TestMsg_SetBodyHTMLTemplate(t *testing.T) {
tests := []struct {
name string
tpl string
ph string
ex string
sf bool
}{
{"normal HTML", "<p>This is a {{.Placeholder}}</p>", "TemplateTest", "TemplateTest", false},
{"HTML with HTML", "<p>This is a {{.Placeholder}}</p>", "<script>alert(1)</script>",
"&lt;script&gt;alert(1)&lt;/script&gt;", false},
{"invalid tpl", "<p>This is a {{ foo .Placeholder}}</p>", "TemplateTest", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := struct {
Placeholder string
}{Placeholder: tt.ph}
tpl, err := htpl.New("test").Parse(tt.tpl)
if err != nil && !tt.sf {
t.Errorf("failed to render template: %s", err)
return
}
m := NewMsg()
if err := m.SetBodyHTMLTemplate(tpl, data); err != nil && !tt.sf {
t.Errorf("failed to set template as body: %s", err)
}
wbuf := bytes.Buffer{}
_, err = m.WriteTo(&wbuf)
if err != nil {
t.Errorf("failed to write body to buffer: %s", err)
}
if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf {
t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph)
}
m.Reset()
})
}
}
// TestMsg_AddAlternativeTextTemplate tests the Msg.AddAlternativeTextTemplate method
func TestMsg_AddAlternativeTextTemplate(t *testing.T) {
tests := []struct {
name string
tpl string
ph string
sf bool
}{
{"normal text", "This is a {{.Placeholder}}", "TemplateTest", false},
{"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := struct {
Placeholder string
}{Placeholder: tt.ph}
tpl, err := ttpl.New("test").Parse(tt.tpl)
if err != nil && !tt.sf {
t.Errorf("failed to render template: %s", err)
return
}
m := NewMsg()
m.SetBodyString(TypeTextHTML, "")
if err := m.AddAlternativeTextTemplate(tpl, data); err != nil && !tt.sf {
t.Errorf("failed to set template as alternative part: %s", err)
}
wbuf := bytes.Buffer{}
_, err = m.WriteTo(&wbuf)
if err != nil {
t.Errorf("failed to write body to buffer: %s", err)
}
if !strings.Contains(wbuf.String(), tt.ph) && !tt.sf {
t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph)
}
m.Reset()
})
}
}
// TestMsg_AddAlternativeHTMLTemplate tests the Msg.AddAlternativeHTMLTemplate method
func TestMsg_AddAlternativeHTMLTemplate(t *testing.T) {
tests := []struct {
name string
tpl string
ph string
ex string
sf bool
}{
{"normal HTML", "<p>This is a {{.Placeholder}}</p>", "TemplateTest", "TemplateTest", false},
{"HTML with HTML", "<p>This is a {{.Placeholder}}</p>", "<script>alert(1)</script>",
"&lt;script&gt;alert(1)&lt;/script&gt;", false},
{"invalid tpl", "<p>This is a {{ foo .Placeholder}}</p>", "TemplateTest", "", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := struct {
Placeholder string
}{Placeholder: tt.ph}
tpl, err := htpl.New("test").Parse(tt.tpl)
if err != nil && !tt.sf {
t.Errorf("failed to render template: %s", err)
return
}
m := NewMsg()
m.SetBodyString(TypeTextPlain, "")
if err := m.AddAlternativeHTMLTemplate(tpl, data); err != nil && !tt.sf {
t.Errorf("failed to set template as alternative part: %s", err)
}
wbuf := bytes.Buffer{}
_, err = m.WriteTo(&wbuf)
if err != nil {
t.Errorf("failed to write body to buffer: %s", err)
}
if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf {
t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph)
}
m.Reset()
})
}
}
// TestMsg_AttachTextTemplate tests the Msg.AttachTextTemplate method
func TestMsg_AttachTextTemplate(t *testing.T) {
tests := []struct {
name string
tpl string
ph string
ex string
ac int
sf bool
}{
{"normal text", "This is a {{.Placeholder}}", "TemplateTest",
"VGhpcyBpcyBhIFRlbXBsYXRlVGVzdA==", 1, false},
{"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", "", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := struct {
Placeholder string
}{Placeholder: tt.ph}
tpl, err := ttpl.New("test").Parse(tt.tpl)
if err != nil && !tt.sf {
t.Errorf("failed to render template: %s", err)
return
}
m := NewMsg()
m.SetBodyString(TypeTextPlain, "This is the body")
if err := m.AttachTextTemplate("attachment.txt", tpl, data); err != nil && !tt.sf {
t.Errorf("failed to attach template: %s", err)
}
wbuf := bytes.Buffer{}
_, err = m.WriteTo(&wbuf)
if err != nil {
t.Errorf("failed to write body to buffer: %s", err)
}
if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf {
t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph)
}
if len(m.attachments) != tt.ac {
t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ac, len(m.attachments))
}
m.Reset()
})
}
}
// TestMsg_AttachHTMLTemplate tests the Msg.AttachHTMLTemplate method
func TestMsg_AttachHTMLTemplate(t *testing.T) {
tests := []struct {
name string
tpl string
ph string
ex string
ac int
sf bool
}{
{"normal HTML", "<p>This is a {{.Placeholder}}</p>", "TemplateTest",
"PHA+VGhpcyBpcyBhIFRlbXBsYXRlVGVzdDwvcD4=", 1, false},
{"HTML with HTML", "<p>This is a {{.Placeholder}}</p>", "<script>alert(1)</script>",
"PHA+VGhpcyBpcyBhICZsdDtzY3JpcHQmZ3Q7YWxlcnQoMSkmbHQ7L3NjcmlwdCZndDs8L3A+", 1, false},
{"invalid tpl", "<p>This is a {{ foo .Placeholder}}</p>", "TemplateTest", "", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := struct {
Placeholder string
}{Placeholder: tt.ph}
tpl, err := htpl.New("test").Parse(tt.tpl)
if err != nil && !tt.sf {
t.Errorf("failed to render template: %s", err)
return
}
m := NewMsg()
m.SetBodyString(TypeTextPlain, "")
if err := m.AttachHTMLTemplate("attachment.html", tpl, data); err != nil && !tt.sf {
t.Errorf("failed to set template as alternative part: %s", err)
}
wbuf := bytes.Buffer{}
_, err = m.WriteTo(&wbuf)
if err != nil {
t.Errorf("failed to write body to buffer: %s", err)
}
if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf {
t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph)
}
if len(m.attachments) != tt.ac {
t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ac, len(m.attachments))
}
m.Reset()
})
}
}
// TestMsg_EmbedTextTemplate tests the Msg.EmbedTextTemplate method
func TestMsg_EmbedTextTemplate(t *testing.T) {
tests := []struct {
name string
tpl string
ph string
ex string
ec int
sf bool
}{
{"normal text", "This is a {{.Placeholder}}", "TemplateTest",
"VGhpcyBpcyBhIFRlbXBsYXRlVGVzdA==", 1, false},
{"invalid tpl", "This is a {{ foo .Placeholder}}", "TemplateTest", "", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := struct {
Placeholder string
}{Placeholder: tt.ph}
tpl, err := ttpl.New("test").Parse(tt.tpl)
if err != nil && !tt.sf {
t.Errorf("failed to render template: %s", err)
return
}
m := NewMsg()
m.SetBodyString(TypeTextPlain, "This is the body")
if err := m.EmbedTextTemplate("attachment.txt", tpl, data); err != nil && !tt.sf {
t.Errorf("failed to attach template: %s", err)
}
wbuf := bytes.Buffer{}
_, err = m.WriteTo(&wbuf)
if err != nil {
t.Errorf("failed to write body to buffer: %s", err)
}
if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf {
t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph)
}
if len(m.embeds) != tt.ec {
t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ec, len(m.attachments))
}
m.Reset()
})
}
}
// TestMsg_EmbedHTMLTemplate tests the Msg.EmbedHTMLTemplate method
func TestMsg_EmbedHTMLTemplate(t *testing.T) {
tests := []struct {
name string
tpl string
ph string
ex string
ec int
sf bool
}{
{"normal HTML", "<p>This is a {{.Placeholder}}</p>", "TemplateTest",
"PHA+VGhpcyBpcyBhIFRlbXBsYXRlVGVzdDwvcD4=", 1, false},
{"HTML with HTML", "<p>This is a {{.Placeholder}}</p>", "<script>alert(1)</script>",
"PHA+VGhpcyBpcyBhICZsdDtzY3JpcHQmZ3Q7YWxlcnQoMSkmbHQ7L3NjcmlwdCZndDs8L3A+", 1, false},
{"invalid tpl", "<p>This is a {{ foo .Placeholder}}</p>", "TemplateTest", "", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := struct {
Placeholder string
}{Placeholder: tt.ph}
tpl, err := htpl.New("test").Parse(tt.tpl)
if err != nil && !tt.sf {
t.Errorf("failed to render template: %s", err)
return
}
m := NewMsg()
m.SetBodyString(TypeTextPlain, "")
if err := m.EmbedHTMLTemplate("attachment.html", tpl, data); err != nil && !tt.sf {
t.Errorf("failed to set template as alternative part: %s", err)
}
wbuf := bytes.Buffer{}
_, err = m.WriteTo(&wbuf)
if err != nil {
t.Errorf("failed to write body to buffer: %s", err)
}
if !strings.Contains(wbuf.String(), tt.ex) && !tt.sf {
t.Errorf("SetBodyTextTemplate failed: Body does not contain the expected tpl placeholder: %s", tt.ph)
}
if len(m.embeds) != tt.ec {
t.Errorf("wrong number of attachments. Expected: %d, got: %d", tt.ec, len(m.attachments))
}
m.Reset()
})
}
}