diff --git a/msg.go b/msg.go index dbfac83..782489f 100644 --- a/msg.go +++ b/msg.go @@ -807,24 +807,31 @@ func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sp string, a ...st return nil } -// NewReader returns a Msg reader that satisfies the io.Reader interface. -// **Please note:** when creating a new reader, the current state of the Msg is taken, as -// basis for the reader. If you perform changes on Msg after creating the reader, you need -// to perform a call to Msg.UpdateReader first -func (m *Msg) NewReader() io.Reader { +// NewReader returns a Reader type that satisfies the io.Reader interface. +// +// IMPORTANT: when creating a new Reader, the current state of the Msg is taken, as +// basis for the Reader. If you perform changes on Msg after creating the Reader, these +// changes will not be reflected in the Reader. You will have to use Msg.UpdateReader +// first to update the Reader's buffer with the current Msg content +func (m *Msg) NewReader() *Reader { + r := &Reader{} wbuf := bytes.Buffer{} - _, _ = m.Write(&wbuf) - r := &reader{buf: wbuf.Bytes()} + _, err := m.Write(&wbuf) + if err != nil { + r.err = fmt.Errorf("failed to write Msg to Reader buffer: %w", err) + } + r.buf = wbuf.Bytes() return r } -// UpdateReader will update a reader with the content of the current Msg and reset the reader position -// to the start -func (m *Msg) UpdateReader(r *reader) { +// UpdateReader will update a Reader with the content of the given Msg and reset the +// Reader position to the start +func (m *Msg) UpdateReader(r *Reader) { wbuf := bytes.Buffer{} - _, _ = m.Write(&wbuf) + _, err := m.Write(&wbuf) r.Reset() r.buf = wbuf.Bytes() + r.err = err } // Read outputs the length of p into p to satisfy the io.Reader interface diff --git a/msg_test.go b/msg_test.go index 5b24251..095c215 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1784,6 +1784,86 @@ func TestMsg_Read_ioCopy(t *testing.T) { } } +// TestMsg_NewReader tests the Msg.NewReader method +func TestMsg_NewReader(t *testing.T) { + m := NewMsg() + m.SetBodyString(TypeTextPlain, "TEST123") + mr := m.NewReader() + if mr == nil { + t.Errorf("NewReader failed: Reader is nil") + } +} + +// TestMsg_NewReader_broken tests the Msg.NewReader method error handling +func TestMsg_NewReader_broken(t *testing.T) { + m := &Msg{} + mr := m.NewReader() + if mr == nil { + t.Errorf("NewReader failed: Reader is nil") + } +} + +// TestMsg_NewReader_ioCopy tests the Msg.NewReader method using io.Copy +func TestMsg_NewReader_ioCopy(t *testing.T) { + wbuf1 := bytes.Buffer{} + wbuf2 := bytes.Buffer{} + m := NewMsg() + m.SetBodyString(TypeTextPlain, "TEST123") + mr := m.NewReader() + if mr == nil { + t.Errorf("NewReader failed: Reader is nil") + } + + // First we use WriteTo to have something to compare to + _, err := m.WriteTo(&wbuf1) + if err != nil { + t.Errorf("failed to write body to buffer: %s", err) + } + + // Then we write to wbuf2 via io.Copy + n, err := io.Copy(&wbuf2, mr) + if err != nil { + t.Errorf("failed to use io.Copy on Reader: %s", err) + } + if n != int64(wbuf1.Len()) { + t.Errorf("message length of WriteTo and io.Copy differ. Expected: %d, got: %d", wbuf1.Len(), n) + } + if wbuf1.String() != wbuf2.String() { + t.Errorf("message content of WriteTo and io.Copy differ") + } +} + +// TestMsg_UpdateReader tests the Msg.UpdateReader method +func TestMsg_UpdateReader(t *testing.T) { + m := NewMsg() + m.Subject("Subject-Run 1") + mr := m.NewReader() + if mr == nil { + t.Errorf("NewReader failed: Reader is nil") + } + wbuf1 := bytes.Buffer{} + _, err := io.Copy(&wbuf1, mr) + if err != nil { + t.Errorf("io.Copy on Reader failed: %s", err) + } + if !strings.Contains(wbuf1.String(), "Subject: Subject-Run 1") { + t.Errorf("io.Copy on Reader failed. Expected to find %q but string in Subject was not found", + "Subject-Run 1") + } + + m.Subject("Subject-Run 2") + m.UpdateReader(mr) + wbuf2 := bytes.Buffer{} + _, err = io.Copy(&wbuf2, mr) + if err != nil { + t.Errorf("2nd io.Copy on Reader failed: %s", err) + } + if !strings.Contains(wbuf2.String(), "Subject: Subject-Run 2") { + t.Errorf("io.Copy on Reader failed. Expected to find %q but string in Subject was not found", + "Subject-Run 2") + } +} + // TestMsg_SetBodyTextTemplate tests the Msg.SetBodyTextTemplate method func TestMsg_SetBodyTextTemplate(t *testing.T) { tests := []struct { diff --git a/reader.go b/reader.go index 124c51d..fbb0612 100644 --- a/reader.go +++ b/reader.go @@ -4,16 +4,22 @@ package mail -import "io" +import ( + "io" +) -// reader is a type that implements the io.Reader interface for a Msg -type reader struct { +// Reader is a type that implements the io.Reader interface for a Msg +type Reader struct { buf []byte // contents are the bytes buf[off : len(buf)] off int // read at &buf[off], write at &buf[len(buf)] + err error // initalization error } // Read reads the length of p of the Msg buffer to satisfy the io.Reader interface -func (r *reader) Read(p []byte) (n int, err error) { +func (r *Reader) Read(p []byte) (n int, err error) { + if r.err != nil { + return 0, r.err + } if r.empty() { r.Reset() if len(p) == 0 { @@ -23,15 +29,15 @@ func (r *reader) Read(p []byte) (n int, err error) { } n = copy(p, r.buf[r.off:]) r.off += n - return n, nil + return n, err } // Reset resets the Reader buffer to be empty, but it retains the underlying storage // for use by future writes. -func (r *reader) Reset() { +func (r *Reader) Reset() { r.buf = r.buf[:0] r.off = 0 } // empty reports whether the unread portion of the Reader buffer is empty. -func (r *reader) empty() bool { return len(r.buf) <= r.off } +func (r *Reader) empty() bool { return len(r.buf) <= r.off }