Compare commits

..

No commits in common. "f37ab2457b632cd1e15445bc7b86e1c9e22fc2a4" and "bcc32526b78cd9b641ff4423e96b7101b39a4fe8" have entirely different histories.

3 changed files with 17 additions and 190 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`, `embed.FS` or `fs.FS`) * [X] Support for attachments and inline embeds (from file system, `io.Reader` or `embed.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
View file

@ -12,7 +12,6 @@ import (
"fmt" "fmt"
ht "html/template" ht "html/template"
"io" "io"
"io/fs"
"mime" "mime"
"net/mail" "net/mail"
"os" "os"
@ -1963,28 +1962,9 @@ 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 errors.New("embed.FS must not be nil") return fmt.Errorf("embed.FS must not be nil")
} }
return m.AttachFromIOFS(name, *fs, opts...) file, err := fileFromEmbedFS(name, fs)
}
// 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
} }
@ -2128,28 +2108,9 @@ 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 errors.New("embed.FS must not be nil") return fmt.Errorf("embed.FS must not be nil")
} }
return m.EmbedFromIOFS(name, *fs, opts...) file, err := fileFromEmbedFS(name, fs)
}
// 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
} }
@ -2705,15 +2666,15 @@ func (m *Msg) addDefaultHeader() {
m.SetGenHeader(HeaderMIMEVersion, string(m.mimever)) m.SetGenHeader(HeaderMIMEVersion, string(m.mimever))
} }
// fileFromIOFS returns a File pointer from a given file in the provided fs.FS. // fileFromEmbedFS returns a File pointer from a given file in the provided embed.FS.
// //
// This method retrieves a file from the provided io/fs (fs.FS) and returns a File structure // This method retrieves a file from the embedded filesystem (embed.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: An instance that satisfies the fs.FS interface // - fs: A pointer to the embed.FS from which the file will be opened.
// //
// Returns: // Returns:
// - A pointer to the File structure representing the embedded file. // - A pointer to the File structure representing the embedded file.
@ -2721,27 +2682,23 @@ 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 fileFromEmbedFS(name string, fs *embed.FS) (*File, error) {
if iofs == nil { _, err := fs.Open(name)
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 fs.FS: %w", err) return nil, fmt.Errorf("failed to open file from embed.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, ferr := iofs.Open(name) file, err := fs.Open(name)
if ferr != nil { if err != nil {
return 0, fmt.Errorf("failed to open file from fs.FS: %w", ferr) return 0, err
} }
numBytes, ferr := io.Copy(writer, file) numBytes, err := io.Copy(writer, file)
if ferr != nil { if err != nil {
_ = file.Close() _ = file.Close()
return numBytes, fmt.Errorf("failed to copy file from fs.FS to io.Writer: %w", ferr) return numBytes, fmt.Errorf("failed to copy file to io.Writer: %w", err)
} }
return numBytes, file.Close() return numBytes, file.Close()
}, },

View file

@ -4970,75 +4970,6 @@ 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()
@ -5504,58 +5435,6 @@ 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 {
@ -6561,15 +6440,6 @@ 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{}