diff --git a/msg.go b/msg.go index 28ed932f..474a2164 100644 --- a/msg.go +++ b/msg.go @@ -714,11 +714,22 @@ func (m *Msg) AttachFile(n string, o ...FileOption) { } // AttachReader adds an attachment File via io.Reader to the Msg +// +// CAVEAT: For AttachReader to work it has to read all data of the io.Reader +// into memory first, so it can seek through it. Using larger amounts of +// data on the io.Reader should be avoided. For such, it is recommeded to +// either use AttachFile or AttachReadSeeker instead func (m *Msg) AttachReader(n string, r io.Reader, o ...FileOption) { f := fileFromReader(n, r) m.attachments = m.appendFile(m.attachments, f, o...) } +// AttachReadSeeker adds an attachment File via io.ReadSeeker to the Msg +func (m *Msg) AttachReadSeeker(n string, r io.ReadSeeker, o ...FileOption) { + f := fileFromReadSeeker(n, r) + m.attachments = m.appendFile(m.attachments, f, o...) +} + // AttachHTMLTemplate adds the output of a html/template.Template pointer as File attachment to the Msg func (m *Msg) AttachHTMLTemplate(n string, t *ht.Template, d interface{}, o ...FileOption) error { f, err := fileFromHTMLTemplate(n, t, d) @@ -762,11 +773,22 @@ func (m *Msg) EmbedFile(n string, o ...FileOption) { } // EmbedReader adds an embedded File from an io.Reader to the Msg +// +// CAVEAT: For AttachReader to work it has to read all data of the io.Reader +// into memory first, so it can seek through it. Using larger amounts of +// data on the io.Reader should be avoided. For such, it is recommeded to +// either use AttachFile or AttachReadSeeker instead func (m *Msg) EmbedReader(n string, r io.Reader, o ...FileOption) { f := fileFromReader(n, r) m.embeds = m.appendFile(m.embeds, f, o...) } +// EmbedReadSeeker adds an embedded File from an io.ReadSeeker to the Msg +func (m *Msg) EmbedReadSeeker(n string, r io.ReadSeeker, o ...FileOption) { + f := fileFromReadSeeker(n, r) + m.embeds = m.appendFile(m.embeds, f, o...) +} + // EmbedHTMLTemplate adds the output of a html/template.Template pointer as embedded File to the Msg func (m *Msg) EmbedHTMLTemplate(n string, t *ht.Template, d interface{}, o ...FileOption) error { f, err := fileFromHTMLTemplate(n, t, d) @@ -1114,15 +1136,37 @@ func fileFromFS(n string) *File { // fileFromReader returns a File pointer from a given io.Reader func fileFromReader(n string, r io.Reader) *File { + d, err := io.ReadAll(r) + if err != nil { + return &File{} + } + br := bytes.NewReader(d) return &File{ - Name: filepath.Base(n), + Name: n, + Header: make(map[string][]string), + Writer: func(w io.Writer) (int64, error) { + rb, cerr := io.Copy(w, br) + if cerr != nil { + return rb, cerr + } + _, cerr = br.Seek(0, io.SeekStart) + return rb, cerr + }, + } +} + +// fileFromReadSeeker returns a File pointer from a given io.ReadSeeker +func fileFromReadSeeker(n string, r io.ReadSeeker) *File { + return &File{ + Name: n, Header: make(map[string][]string), Writer: func(w io.Writer) (int64, error) { - nb, err := io.Copy(w, r) + rb, err := io.Copy(w, r) if err != nil { - return nb, err + return rb, err } - return nb, nil + _, err = r.Seek(0, io.SeekStart) + return rb, err }, } } diff --git a/msg_test.go b/msg_test.go index 1ac0ac1b..a2c4a8ef 100644 --- a/msg_test.go +++ b/msg_test.go @@ -2667,3 +2667,127 @@ func TestMsg_GetBccString(t *testing.T) { `"Toni Cc" "`, fh[0]) } } + +// TestMsg_AttachEmbedReader_consecutive tests the Msg.AttachReader and Msg.EmbedReader +// methods with consecutive calls to Msg.WriteTo to make sure the attachments are not +// lost (see Github issue #110) +func TestMsg_AttachEmbedReader_consecutive(t *testing.T) { + ts1 := "This is a test string" + ts2 := "Another test string" + m := NewMsg() + m.AttachReader("attachment.txt", bytes.NewBufferString(ts1)) + m.EmbedReader("embeded.txt", bytes.NewBufferString(ts2)) + obuf1 := &bytes.Buffer{} + obuf2 := &bytes.Buffer{} + _, err := m.WriteTo(obuf1) + if err != nil { + t.Errorf("WriteTo to first output buffer failed: %s", err) + } + _, err = m.WriteTo(obuf2) + if err != nil { + t.Errorf("WriteTo to second output buffer failed: %s", err) + } + if !strings.Contains(obuf1.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("Expected file attachment string not found in first output buffer") + } + if !strings.Contains(obuf2.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("Expected file attachment string not found in second output buffer") + } + if !strings.Contains(obuf1.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { + t.Errorf("Expected embeded file string not found in first output buffer") + } + if !strings.Contains(obuf2.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { + t.Errorf("Expected embded file string not found in second output buffer") + } +} + +// TestMsg_AttachEmbedReadSeeker_consecutive tests the Msg.AttachReadSeeker and +// Msg.EmbedReadSeeker methods with consecutive calls to Msg.WriteTo to make +// sure the attachments are not lost (see Github issue #110) +func TestMsg_AttachEmbedReadSeeker_consecutive(t *testing.T) { + ts1 := []byte("This is a test string") + ts2 := []byte("Another test string") + m := NewMsg() + m.AttachReadSeeker("attachment.txt", bytes.NewReader(ts1)) + m.EmbedReadSeeker("embeded.txt", bytes.NewReader(ts2)) + obuf1 := &bytes.Buffer{} + obuf2 := &bytes.Buffer{} + _, err := m.WriteTo(obuf1) + if err != nil { + t.Errorf("WriteTo to first output buffer failed: %s", err) + } + _, err = m.WriteTo(obuf2) + if err != nil { + t.Errorf("WriteTo to second output buffer failed: %s", err) + } + if !strings.Contains(obuf1.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("Expected file attachment string not found in first output buffer") + } + if !strings.Contains(obuf2.String(), "VGhpcyBpcyBhIHRlc3Qgc3RyaW5n") { + t.Errorf("Expected file attachment string not found in second output buffer") + } + if !strings.Contains(obuf1.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { + t.Errorf("Expected embeded file string not found in first output buffer") + } + if !strings.Contains(obuf2.String(), "QW5vdGhlciB0ZXN0IHN0cmluZw==") { + t.Errorf("Expected embded file string not found in second output buffer") + } +} + +// TestMsg_AttachReadSeeker tests the Msg.AttachReadSeeker method +func TestMsg_AttachReadSeeker(t *testing.T) { + m := NewMsg() + ts := []byte("This is a test string") + r := bytes.NewReader(ts) + m.AttachReadSeeker("testfile.txt", r) + if len(m.attachments) != 1 { + t.Errorf("AttachReadSeeker() failed. Number of attachments expected: %d, got: %d", 1, + len(m.attachments)) + return + } + file := m.attachments[0] + if file == nil { + t.Errorf("AttachReadSeeker() failed. Attachment file pointer is nil") + return + } + if file.Name != "testfile.txt" { + t.Errorf("AttachReadSeeker() failed. Expected file name: %s, got: %s", "testfile.txt", + file.Name) + } + wbuf := bytes.Buffer{} + if _, err := file.Writer(&wbuf); err != nil { + t.Errorf("execute WriterFunc failed: %s", err) + } + if wbuf.String() != string(ts) { + t.Errorf("AttachReadSeeker() failed. Expected string: %q, got: %q", ts, wbuf.String()) + } +} + +// TestMsg_EmbedReadSeeker tests the Msg.EmbedReadSeeker method +func TestMsg_EmbedReadSeeker(t *testing.T) { + m := NewMsg() + ts := []byte("This is a test string") + r := bytes.NewReader(ts) + m.EmbedReadSeeker("testfile.txt", r) + if len(m.embeds) != 1 { + t.Errorf("EmbedReadSeeker() failed. Number of attachments expected: %d, got: %d", 1, + len(m.embeds)) + return + } + file := m.embeds[0] + if file == nil { + t.Errorf("EmbedReadSeeker() failed. Embedded file pointer is nil") + return + } + if file.Name != "testfile.txt" { + t.Errorf("EmbedReadSeeker() failed. Expected file name: %s, got: %s", "testfile.txt", + file.Name) + } + wbuf := bytes.Buffer{} + if _, err := file.Writer(&wbuf); err != nil { + t.Errorf("execute WriterFunc failed: %s", err) + } + if wbuf.String() != string(ts) { + t.Errorf("EmbedReadSeeker() failed. Expected string: %q, got: %q", ts, wbuf.String()) + } +}