diff --git a/msg.go b/msg.go index 30bad37c..7ec8dbba 100644 --- a/msg.go +++ b/msg.go @@ -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) diff --git a/msg_test.go b/msg_test.go index d14a7567..ed70a73d 100644 --- a/msg_test.go +++ b/msg_test.go @@ -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()