diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b5ff77..aa2bbcb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.27.0 (WIP) + +- Updated the mail attachments auto MIME type detection to use `gabriel-vasile/mimetype` for consistency and broader sniffing signatures support. + + ## v0.26.1 - Removed the wrapping of `io.EOF` error when reading files since currently `io.ReadAll` doesn't check for wrapped errors ([#6600](https://github.com/pocketbase/pocketbase/issues/6600)). diff --git a/tools/mailer/mailer.go b/tools/mailer/mailer.go index 1544da1c..c87e2e8d 100644 --- a/tools/mailer/mailer.go +++ b/tools/mailer/mailer.go @@ -1,9 +1,11 @@ package mailer import ( + "bytes" "io" "net/mail" + "github.com/gabriel-vasile/mimetype" "github.com/pocketbase/pocketbase/tools/hook" ) @@ -54,3 +56,17 @@ func addressesToStrings(addresses []mail.Address, withName bool) []string { return result } + +// detectReaderMimeType reads the first couple bytes of the reader to detect its MIME type. +// +// Returns a new combined reader from the partial read + the remaining of the original reader. +func detectReaderMimeType(r io.Reader) (io.Reader, string, error) { + readCopy := new(bytes.Buffer) + + mime, err := mimetype.DetectReader(io.TeeReader(r, readCopy)) + if err != nil { + return nil, "", err + } + + return io.MultiReader(readCopy, r), mime.String(), nil +} diff --git a/tools/mailer/mailer_test.go b/tools/mailer/mailer_test.go new file mode 100644 index 00000000..571f9ead --- /dev/null +++ b/tools/mailer/mailer_test.go @@ -0,0 +1,77 @@ +package mailer + +import ( + "fmt" + "io" + "net/mail" + "strings" + "testing" +) + +func TestAddressesToStrings(t *testing.T) { + t.Parallel() + + scenarios := []struct { + withName bool + addresses []mail.Address + expected []string + }{ + { + true, + []mail.Address{{Name: "John Doe", Address: "test1@example.com"}, {Name: "Jane Doe", Address: "test2@example.com"}}, + []string{`"John Doe" `, `"Jane Doe" `}, + }, + { + true, + []mail.Address{{Name: "John Doe", Address: "test1@example.com"}, {Address: "test2@example.com"}}, + []string{`"John Doe" `, `test2@example.com`}, + }, + { + false, + []mail.Address{{Name: "John Doe", Address: "test1@example.com"}, {Name: "Jane Doe", Address: "test2@example.com"}}, + []string{`test1@example.com`, `test2@example.com`}, + }, + } + + for _, s := range scenarios { + t.Run(fmt.Sprintf("%v_%v", s.withName, s.addresses), func(t *testing.T) { + result := addressesToStrings(s.addresses, s.withName) + + if len(s.expected) != len(result) { + t.Fatalf("Expected\n%v\ngot\n%v", s.expected, result) + } + + for k, v := range s.expected { + if v != result[k] { + t.Fatalf("Expected %d address %q, got %q", k, v, result[k]) + } + } + }) + } +} + +func TestDetectReaderMimeType(t *testing.T) { + t.Parallel() + + str := "#!/bin/node\n" + strings.Repeat("a", 10000) // ensure that it is large enough to remain after the signature sniffing + + r, mime, err := detectReaderMimeType(strings.NewReader(str)) + if err != nil { + t.Fatal(err) + } + + expectedMime := "text/javascript" + if mime != expectedMime { + t.Fatalf("Expected mime %q, got %q", expectedMime, mime) + } + + raw, err := io.ReadAll(r) + if err != nil { + t.Fatal(err) + } + rawStr := string(raw) + + if rawStr != str { + t.Fatalf("Expected content\n%s\ngot\n%s", str, rawStr) + } +} diff --git a/tools/mailer/smtp.go b/tools/mailer/smtp.go index 139bce8f..3f1f7457 100644 --- a/tools/mailer/smtp.go +++ b/tools/mailer/smtp.go @@ -116,12 +116,20 @@ func (c *SMTPClient) send(m *Message) error { // add regular attachements (if any) for name, data := range m.Attachments { - yak.Attach(name, data) + r, mime, err := detectReaderMimeType(data) + if err != nil { + return err + } + yak.AttachWithMimeType(name, r, mime) } // add inline attachments (if any) for name, data := range m.InlineAttachments { - yak.AttachInline(name, data) + r, mime, err := detectReaderMimeType(data) + if err != nil { + return err + } + yak.AttachInlineWithMimeType(name, r, mime) } // add custom headers (if any)