Add AttachFromIOFS and EmbedFromIOFS functions

Introduce new methods AttachFromIOFS and EmbedFromIOFS to handle attachments and embeds from a general file system (fs.FS). Updated tests to cover these new functionalities and modified error messages for consistency. Updated README to reflect support for fs.FS.
This commit is contained in:
Winni Neessen 2024-11-19 10:52:54 +01:00
parent b137fe4611
commit 101a35f7d3
Signed by: wneessen
GPG key ID: 385AC9889632126E
3 changed files with 184 additions and 12 deletions

View file

@ -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

64
msg.go
View file

@ -1963,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 := fileFromIOFS(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
} }
@ -2109,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 := fileFromIOFS(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
} }
@ -2684,22 +2722,26 @@ func (m *Msg) addDefaultHeader() {
// References: // References:
// - https://datatracker.ietf.org/doc/html/rfc2183 // - https://datatracker.ietf.org/doc/html/rfc2183
func fileFromIOFS(name string, iofs fs.FS) (*File, error) { func fileFromIOFS(name string, iofs fs.FS) (*File, error) {
if iofs == nil {
return nil, errors.New("fs.FS is nil")
}
_, err := iofs.Open(name) _, 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 := iofs.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()
}, },

View file

@ -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{}