Closes #29: Allow attaching/embedding from embed.FS

- Added `EmbedFromEmbedFS()` to allow embedding from embed.FS
- Added `AttachFromEmbedFS()` to allow attaching from embed.FS
- Added `fileFromEmbedFS()` as internal method for both other m
  methods to attach/embed the embed.FS file
This commit is contained in:
Winni Neessen 2022-07-07 10:46:57 +02:00
parent 4fcd42dfad
commit 192627f6a5
Signed by: wneessen
GPG key ID: 385AC9889632126E
2 changed files with 143 additions and 0 deletions

51
msg.go
View file

@ -7,6 +7,7 @@ package mail
import (
"bytes"
"context"
"embed"
"errors"
"fmt"
ht "html/template"
@ -539,6 +540,19 @@ func (m *Msg) AttachTextTemplate(n string, t *tt.Template, d interface{}, o ...F
return nil
}
// AttachFromEmbedFS adds an attachment File from an embed.FS to the Msg
func (m *Msg) AttachFromEmbedFS(n string, f *embed.FS, o ...FileOption) error {
if f == nil {
return fmt.Errorf("embed.FS must not be nil")
}
ef, err := fileFromEmbedFS(n, f)
if err != nil {
return err
}
m.attachments = m.appendFile(m.attachments, ef, o...)
return nil
}
// EmbedFile adds an embedded File to the Msg
func (m *Msg) EmbedFile(n string, o ...FileOption) {
f := fileFromFS(n)
@ -574,6 +588,19 @@ func (m *Msg) EmbedTextTemplate(n string, t *tt.Template, d interface{}, o ...Fi
return nil
}
// EmbedFromEmbedFS adds an embedded File from an embed.FS to the Msg
func (m *Msg) EmbedFromEmbedFS(n string, f *embed.FS, o ...FileOption) error {
if f == nil {
return fmt.Errorf("embed.FS must not be nil")
}
ef, err := fileFromEmbedFS(n, f)
if err != nil {
return err
}
m.embeds = m.appendFile(m.embeds, ef, o...)
return nil
}
// Reset resets all headers, body parts and attachments/embeds of the Msg
// It leaves already set encodings, charsets, boundaries, etc. as is
func (m *Msg) Reset() {
@ -767,6 +794,30 @@ func (m *Msg) addDefaultHeader() {
m.SetHeader(HeaderMIMEVersion, string(m.mimever))
}
// fileFromEmbedFS returns a File pointer from a given file in the provided embed.FS
func fileFromEmbedFS(n string, f *embed.FS) (*File, error) {
_, err := f.Open(n)
if err != nil {
return nil, fmt.Errorf("failed to open file from embed.FS: %w", err)
}
return &File{
Name: filepath.Base(n),
Header: make(map[string][]string),
Writer: func(w io.Writer) (int64, error) {
h, err := f.Open(n)
if err != nil {
return 0, err
}
nb, err := io.Copy(w, h)
if err != nil {
_ = h.Close()
return nb, fmt.Errorf("failed to copy file to io.Writer: %w", err)
}
return nb, h.Close()
},
}, nil
}
// fileFromFS returns a File pointer from a given file in the system's file system
func fileFromFS(n string) *File {
_, err := os.Stat(n)

View file

@ -7,6 +7,7 @@ package mail
import (
"bufio"
"bytes"
"embed"
"fmt"
htpl "html/template"
"io"
@ -18,6 +19,9 @@ import (
"time"
)
//go:embed README.md
var efs embed.FS
// TestNewMsg tests the NewMsg method
func TestNewMsg(t *testing.T) {
m := NewMsg()
@ -1029,6 +1033,50 @@ func TestMsg_AttachFile(t *testing.T) {
}
}
// TestMsg_AttachFromEmbedFS tests the Msg.AttachFromEmbedFS and the WithFilename FileOption method
func TestMsg_AttachFromEmbedFS(t *testing.T) {
tests := []struct {
name string
file string
fn string
sf bool
}{
{"File: README.md", "README.md", "README.md", false},
{"File: nonexisting", "", "invalid.file", true},
}
m := NewMsg()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := m.AttachFromEmbedFS(tt.file, &efs, WithFileName(tt.fn)); err != nil && !tt.sf {
t.Errorf("AttachFromEmbedFS() failed: %s", err)
return
}
if len(m.attachments) != 1 && !tt.sf {
t.Errorf("AttachFile() failed. Number of attachments expected: %d, got: %d", 1,
len(m.attachments))
return
}
if !tt.sf {
file := m.attachments[0]
if file == nil {
t.Errorf("AttachFile() failed. Attachment file pointer is nil")
return
}
if file.Name != tt.fn {
t.Errorf("AttachFile() failed. Filename of attachment expected: %s, got: %s", tt.fn,
file.Name)
}
buf := bytes.Buffer{}
if _, err := file.Writer(&buf); err != nil {
t.Errorf("failed to execute WriterFunc: %s", err)
return
}
}
m.Reset()
})
}
}
// TestMsg_AttachFileBrokenFunc tests WriterFunc of the Msg.AttachFile method
func TestMsg_AttachFileBrokenFunc(t *testing.T) {
m := NewMsg()
@ -1125,6 +1173,50 @@ func TestMsg_EmbedFile(t *testing.T) {
}
}
// TestMsg_EmbedFromEmbedFS tests the Msg.EmbedFromEmbedFS and the WithFilename FileOption method
func TestMsg_EmbedFromEmbedFS(t *testing.T) {
tests := []struct {
name string
file string
fn string
sf bool
}{
{"File: README.md", "README.md", "README.md", false},
{"File: nonexisting", "", "invalid.file", true},
}
m := NewMsg()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := m.EmbedFromEmbedFS(tt.file, &efs, WithFileName(tt.fn)); err != nil && !tt.sf {
t.Errorf("EmbedFromEmbedFS() failed: %s", err)
return
}
if len(m.embeds) != 1 && !tt.sf {
t.Errorf("EmbedFile() failed. Number of embeds expected: %d, got: %d", 1,
len(m.embeds))
return
}
if !tt.sf {
file := m.embeds[0]
if file == nil {
t.Errorf("EmbedFile() failed. Embedded file pointer is nil")
return
}
if file.Name != tt.fn {
t.Errorf("EmbedFile() failed. Filename of embeds expected: %s, got: %s", tt.fn,
file.Name)
}
buf := bytes.Buffer{}
if _, err := file.Writer(&buf); err != nil {
t.Errorf("failed to execute WriterFunc: %s", err)
return
}
}
m.Reset()
})
}
}
// TestMsg_EmbedFileBrokenFunc tests WriterFunc of the Msg.EmbedFile method
func TestMsg_EmbedFileBrokenFunc(t *testing.T) {
m := NewMsg()