mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-24 22:50:49 +01:00
Merge pull request #376 from wneessen/feature/embed-fs-to-io-fs
Support attachments/embeds via io/fs.FS
This commit is contained in:
commit
f37ab2457b
3 changed files with 190 additions and 17 deletions
|
@ -52,7 +52,7 @@ Here are some highlights of go-mail's featureset:
|
||||||
* [X] RFC5322 compliant mail address validation
|
* [X] RFC5322 compliant mail address validation
|
||||||
* [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, Priority, etc.)
|
* [X] Support for common mail header field generation (Message-ID, Date, Bulk-Precedence, Priority, etc.)
|
||||||
* [X] Concurrency-safe reusing the same SMTP connection to send multiple mails
|
* [X] Concurrency-safe reusing the same SMTP connection to send multiple mails
|
||||||
* [X] Support for attachments and inline embeds (from file system, `io.Reader` or `embed.FS`)
|
* [X] Support for attachments and inline embeds (from file system, `io.Reader`, `embed.FS` or `fs.FS`)
|
||||||
* [X] Support for different encodings
|
* [X] Support for different encodings
|
||||||
* [X] Middleware support for 3rd-party libraries to alter mail messages
|
* [X] Middleware support for 3rd-party libraries to alter mail messages
|
||||||
* [X] Support sending mails via a local sendmail command
|
* [X] Support sending mails via a local sendmail command
|
||||||
|
|
75
msg.go
75
msg.go
|
@ -12,6 +12,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
ht "html/template"
|
ht "html/template"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"mime"
|
"mime"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"os"
|
"os"
|
||||||
|
@ -1962,9 +1963,28 @@ func (m *Msg) AttachTextTemplate(
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc2183
|
// - https://datatracker.ietf.org/doc/html/rfc2183
|
||||||
func (m *Msg) AttachFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error {
|
func (m *Msg) AttachFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error {
|
||||||
if fs == nil {
|
if fs == nil {
|
||||||
return fmt.Errorf("embed.FS must not be nil")
|
return errors.New("embed.FS must not be nil")
|
||||||
}
|
}
|
||||||
file, err := fileFromEmbedFS(name, fs)
|
return m.AttachFromIOFS(name, *fs, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttachFromIOFS attaches a file from a generic file system to the message.
|
||||||
|
//
|
||||||
|
// This function retrieves a file by name from an fs.FS instance, processes it, and appends it to the
|
||||||
|
// message's attachment collection. Additional file options can be provided for further customization.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - name: The name of the file to retrieve from the file system.
|
||||||
|
// - iofs: The file system (must not be nil).
|
||||||
|
// - opts: Optional file options to customize the attachment process.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if the file cannot be retrieved, the fs.FS is nil, or any other issue occurs.
|
||||||
|
func (m *Msg) AttachFromIOFS(name string, iofs fs.FS, opts ...FileOption) error {
|
||||||
|
if iofs == nil {
|
||||||
|
return errors.New("fs.FS must not be nil")
|
||||||
|
}
|
||||||
|
file, err := fileFromIOFS(name, iofs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -2108,9 +2128,28 @@ func (m *Msg) EmbedTextTemplate(
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc2183
|
// - https://datatracker.ietf.org/doc/html/rfc2183
|
||||||
func (m *Msg) EmbedFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error {
|
func (m *Msg) EmbedFromEmbedFS(name string, fs *embed.FS, opts ...FileOption) error {
|
||||||
if fs == nil {
|
if fs == nil {
|
||||||
return fmt.Errorf("embed.FS must not be nil")
|
return errors.New("embed.FS must not be nil")
|
||||||
}
|
}
|
||||||
file, err := fileFromEmbedFS(name, fs)
|
return m.EmbedFromIOFS(name, *fs, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbedFromIOFS embeds a file from a generic file system into the message.
|
||||||
|
//
|
||||||
|
// This function retrieves a file by name from an fs.FS instance, processes it, and appends it to the
|
||||||
|
// message's embed collection. Additional file options can be provided for further customization.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - name: The name of the file to retrieve from the file system.
|
||||||
|
// - iofs: The file system (must not be nil).
|
||||||
|
// - opts: Optional file options to customize the embedding process.
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - An error if the file cannot be retrieved, the fs.FS is nil, or any other issue occurs.
|
||||||
|
func (m *Msg) EmbedFromIOFS(name string, iofs fs.FS, opts ...FileOption) error {
|
||||||
|
if iofs == nil {
|
||||||
|
return errors.New("fs.FS must not be nil")
|
||||||
|
}
|
||||||
|
file, err := fileFromIOFS(name, iofs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -2666,15 +2705,15 @@ func (m *Msg) addDefaultHeader() {
|
||||||
m.SetGenHeader(HeaderMIMEVersion, string(m.mimever))
|
m.SetGenHeader(HeaderMIMEVersion, string(m.mimever))
|
||||||
}
|
}
|
||||||
|
|
||||||
// fileFromEmbedFS returns a File pointer from a given file in the provided embed.FS.
|
// fileFromIOFS returns a File pointer from a given file in the provided fs.FS.
|
||||||
//
|
//
|
||||||
// This method retrieves a file from the embedded filesystem (embed.FS) and returns a File structure
|
// This method retrieves a file from the provided io/fs (fs.FS) and returns a File structure
|
||||||
// that can be used as an attachment or embed in the email message. The file's content is read when
|
// that can be used as an attachment or embed in the email message. The file's content is read when
|
||||||
// writing to an io.Writer, and the file is identified by its base name.
|
// writing to an io.Writer, and the file is identified by its base name.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - name: The name of the file to retrieve from the embedded filesystem.
|
// - name: The name of the file to retrieve from the embedded filesystem.
|
||||||
// - fs: A pointer to the embed.FS from which the file will be opened.
|
// - fs: An instance that satisfies the fs.FS interface
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - A pointer to the File structure representing the embedded file.
|
// - A pointer to the File structure representing the embedded file.
|
||||||
|
@ -2682,23 +2721,27 @@ func (m *Msg) addDefaultHeader() {
|
||||||
//
|
//
|
||||||
// References:
|
// References:
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc2183
|
// - https://datatracker.ietf.org/doc/html/rfc2183
|
||||||
func fileFromEmbedFS(name string, fs *embed.FS) (*File, error) {
|
func fileFromIOFS(name string, iofs fs.FS) (*File, error) {
|
||||||
_, err := fs.Open(name)
|
if iofs == nil {
|
||||||
|
return nil, errors.New("fs.FS is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := iofs.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open file from embed.FS: %w", err)
|
return nil, fmt.Errorf("failed to open file from fs.FS: %w", err)
|
||||||
}
|
}
|
||||||
return &File{
|
return &File{
|
||||||
Name: filepath.Base(name),
|
Name: filepath.Base(name),
|
||||||
Header: make(map[string][]string),
|
Header: make(map[string][]string),
|
||||||
Writer: func(writer io.Writer) (int64, error) {
|
Writer: func(writer io.Writer) (int64, error) {
|
||||||
file, err := fs.Open(name)
|
file, ferr := iofs.Open(name)
|
||||||
if err != nil {
|
if ferr != nil {
|
||||||
return 0, err
|
return 0, fmt.Errorf("failed to open file from fs.FS: %w", ferr)
|
||||||
}
|
}
|
||||||
numBytes, err := io.Copy(writer, file)
|
numBytes, ferr := io.Copy(writer, file)
|
||||||
if err != nil {
|
if ferr != nil {
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
return numBytes, fmt.Errorf("failed to copy file to io.Writer: %w", err)
|
return numBytes, fmt.Errorf("failed to copy file from fs.FS to io.Writer: %w", ferr)
|
||||||
}
|
}
|
||||||
return numBytes, file.Close()
|
return numBytes, file.Close()
|
||||||
},
|
},
|
||||||
|
|
130
msg_test.go
130
msg_test.go
|
@ -4970,6 +4970,75 @@ func TestMsg_AttachFromEmbedFS(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMsg_AttachFromIOFS(t *testing.T) {
|
||||||
|
t.Run("AttachFromIOFS successful", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
if err := message.AttachFromIOFS("testdata/attachment.txt", efs,
|
||||||
|
WithFileName("attachment.txt")); err != nil {
|
||||||
|
t.Fatalf("failed to attach from embed FS: %s", err)
|
||||||
|
}
|
||||||
|
attachments := message.GetAttachments()
|
||||||
|
if len(attachments) != 1 {
|
||||||
|
t.Fatalf("failed to retrieve attachments list")
|
||||||
|
}
|
||||||
|
if attachments[0] == nil {
|
||||||
|
t.Fatal("expected attachment to be not nil")
|
||||||
|
}
|
||||||
|
if attachments[0].Name != "attachment.txt" {
|
||||||
|
t.Errorf("expected attachment name to be %s, got: %s", "attachment.txt", attachments[0].Name)
|
||||||
|
}
|
||||||
|
messageBuf := bytes.NewBuffer(nil)
|
||||||
|
_, err := attachments[0].Writer(messageBuf)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("writer func failed: %s", err)
|
||||||
|
}
|
||||||
|
got := strings.TrimSpace(messageBuf.String())
|
||||||
|
if !strings.EqualFold(got, "This is a test attachment") {
|
||||||
|
t.Errorf("expected message body to be %s, got: %s", "This is a test attachment", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AttachFromIOFS with invalid path", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
err := message.AttachFromIOFS("testdata/invalid.txt", efs, WithFileName("attachment.txt"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AttachFromIOFS with nil embed FS", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
err := message.AttachFromIOFS("testdata/invalid.txt", nil, WithFileName("attachment.txt"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("AttachFromIOFS with fs.FS fails on copy", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
if err := message.AttachFromIOFS("testdata/attachment.txt", efs); err != nil {
|
||||||
|
t.Fatalf("failed to attach file from fs.FS: %s", err)
|
||||||
|
}
|
||||||
|
attachments := message.GetAttachments()
|
||||||
|
if len(attachments) != 1 {
|
||||||
|
t.Fatalf("failed to get attachments, expected 1, got: %d", len(attachments))
|
||||||
|
}
|
||||||
|
_, err := attachments[0].Writer(failReadWriteSeekCloser{})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("writer func expected to fail, but didn't")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMsg_EmbedFile(t *testing.T) {
|
func TestMsg_EmbedFile(t *testing.T) {
|
||||||
t.Run("EmbedFile with file", func(t *testing.T) {
|
t.Run("EmbedFile with file", func(t *testing.T) {
|
||||||
message := NewMsg()
|
message := NewMsg()
|
||||||
|
@ -5435,6 +5504,58 @@ func TestMsg_EmbedFromEmbedFS(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMsg_EmbedFromIOFS(t *testing.T) {
|
||||||
|
t.Run("EmbedFromIOFS successful", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
if err := message.EmbedFromIOFS("testdata/embed.txt", efs,
|
||||||
|
WithFileName("embed.txt")); err != nil {
|
||||||
|
t.Fatalf("failed to embed from embed FS: %s", err)
|
||||||
|
}
|
||||||
|
embeds := message.GetEmbeds()
|
||||||
|
if len(embeds) != 1 {
|
||||||
|
t.Fatalf("failed to retrieve embeds list")
|
||||||
|
}
|
||||||
|
if embeds[0] == nil {
|
||||||
|
t.Fatal("expected embed to be not nil")
|
||||||
|
}
|
||||||
|
if embeds[0].Name != "embed.txt" {
|
||||||
|
t.Errorf("expected embed name to be %s, got: %s", "embed.txt", embeds[0].Name)
|
||||||
|
}
|
||||||
|
messageBuf := bytes.NewBuffer(nil)
|
||||||
|
_, err := embeds[0].Writer(messageBuf)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("writer func failed: %s", err)
|
||||||
|
}
|
||||||
|
got := strings.TrimSpace(messageBuf.String())
|
||||||
|
if !strings.EqualFold(got, "This is a test embed") {
|
||||||
|
t.Errorf("expected message body to be %s, got: %s", "This is a test embed", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("EmbedFromIOFS with invalid path", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
err := message.EmbedFromIOFS("testdata/invalid.txt", efs, WithFileName("embed.txt"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("EmbedFromIOFS with nil embed FS", func(t *testing.T) {
|
||||||
|
message := NewMsg()
|
||||||
|
if message == nil {
|
||||||
|
t.Fatal("message is nil")
|
||||||
|
}
|
||||||
|
err := message.EmbedFromIOFS("testdata/invalid.txt", nil, WithFileName("embed.txt"))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMsg_Reset(t *testing.T) {
|
func TestMsg_Reset(t *testing.T) {
|
||||||
message := NewMsg()
|
message := NewMsg()
|
||||||
if message == nil {
|
if message == nil {
|
||||||
|
@ -6440,6 +6561,15 @@ func TestMsg_addDefaultHeader(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMsg_fileFromIOFS(t *testing.T) {
|
||||||
|
t.Run("file from fs.FS where fs is nil ", func(t *testing.T) {
|
||||||
|
_, err := fileFromIOFS("testfile.txt", nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for fs.FS that is nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// uppercaseMiddleware is a middleware type that transforms the subject to uppercase.
|
// uppercaseMiddleware is a middleware type that transforms the subject to uppercase.
|
||||||
type uppercaseMiddleware struct{}
|
type uppercaseMiddleware struct{}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue