diff --git a/msg.go b/msg.go index 521cd30..bfb521d 100644 --- a/msg.go +++ b/msg.go @@ -181,6 +181,9 @@ func (m *Msg) Charset() string { // SetHeader sets a generic header field of the Msg func (m *Msg) SetHeader(h Header, v ...string) { + if m.genHeader == nil { + m.genHeader = make(map[Header][]string) + } for i, hv := range v { v[i] = m.encodeString(hv) } @@ -189,6 +192,9 @@ func (m *Msg) SetHeader(h Header, v ...string) { // SetAddrHeader sets an address related header field of the Msg func (m *Msg) SetAddrHeader(h AddrHeader, v ...string) error { + if m.addrHeader == nil { + m.addrHeader = make(map[AddrHeader][]*mail.Address) + } var al []*mail.Address for _, av := range v { a, err := mail.ParseAddress(av) @@ -807,14 +813,31 @@ func (m *Msg) WriteToSendmailWithContext(ctx context.Context, sp string, a ...st return nil } -// Read outputs the length of p into p to satisfy the io.Reader interface -func (m *Msg) Read(p []byte) (int, error) { +// 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{} - _, err := m.WriteTo(&wbuf) + _, err := m.Write(&wbuf) if err != nil { - return 0, fmt.Errorf("failed to write message to internal write buffer: %w", err) + r.err = fmt.Errorf("failed to write Msg to Reader buffer: %w", err) } - return wbuf.Read(p) + r.buf = wbuf.Bytes() + return r +} + +// 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{} + _, err := m.Write(&wbuf) + r.Reset() + r.buf = wbuf.Bytes() + r.err = err } // encodeString encodes a string based on the configured message encoder and the corresponding diff --git a/msg_test.go b/msg_test.go index 5b24251..0abb724 100644 --- a/msg_test.go +++ b/msg_test.go @@ -1718,52 +1718,29 @@ func TestMsg_multipleWrites(t *testing.T) { } } -// TestMsg_Read tests the Msg.Read method that implements the io.Reader interface -func TestMsg_Read(t *testing.T) { - tests := []struct { - name string - plen int - }{ - {"P length is bigger than the mail", 32000}, - {"P length is smaller than the mail", 128}, - } - +// TestMsg_NewReader tests the Msg.NewReader method +func TestMsg_NewReader(t *testing.T) { m := NewMsg() m.SetBodyString(TypeTextPlain, "TEST123") - wbuf := bytes.Buffer{} - _, err := m.Write(&wbuf) - if err != nil { - t.Errorf("failed to write message into temporary buffer: %s", err) + mr := m.NewReader() + if mr == nil { + t.Errorf("NewReader failed: Reader is nil") } - elen := wbuf.Len() - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := make([]byte, tt.plen) - n, err := m.Read(p) - if err != nil { - t.Errorf("failed to Read(): %s", err) - } - if n == 0 { - t.Errorf("failed to Read() - received 0 bytes of data") - } - if tt.plen >= elen && n != elen { - t.Errorf("failed to Read() - not all data received. Expected: %d, got: %d", elen, n) - } - if tt.plen < elen && n != tt.plen { - t.Errorf("failed to Read() - full length of p wasn't filled with data. Expected: %d, got: %d", - tt.plen, n) - } - }) + if mr.Error() != nil { + t.Errorf("NewReader failed: %s", mr.Error()) } } -// TestMsg_Read_ioCopy tests the Msg.Read method using io.Copy -func TestMsg_Read_ioCopy(t *testing.T) { +// 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) @@ -1772,9 +1749,9 @@ func TestMsg_Read_ioCopy(t *testing.T) { } // Then we write to wbuf2 via io.Copy - n, err := io.Copy(&wbuf2, m) + n, err := io.Copy(&wbuf2, mr) if err != nil { - t.Errorf("failed to use io.Copy on Msg: %s", err) + 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) @@ -1784,6 +1761,37 @@ func TestMsg_Read_ioCopy(t *testing.T) { } } +// 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 new file mode 100644 index 0000000..dfc5113 --- /dev/null +++ b/reader.go @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2022 Winni Neessen +// +// SPDX-License-Identifier: MIT + +package mail + +import ( + "io" +) + +// 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 +} + +// Error returns an error if the Reader err field is not nil +func (r *Reader) Error() error { + return r.err +} + +// 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) { + if r.err != nil { + return 0, r.err + } + if r.empty() { + r.Reset() + if len(p) == 0 { + return 0, nil + } + return 0, io.EOF + } + n = copy(p, r.buf[r.off:]) + r.off += n + 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() { + 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 } diff --git a/reader_test.go b/reader_test.go new file mode 100644 index 0000000..64e2193 --- /dev/null +++ b/reader_test.go @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2022 Winni Neessen +// +// SPDX-License-Identifier: MIT + +package mail + +import ( + "bytes" + "fmt" + "testing" +) + +// TestReader_Read tests the Reader.Read method that implements the io.Reader interface +func TestReader_Read(t *testing.T) { + tests := []struct { + name string + plen int + }{ + {"P length is bigger than the mail", 3200000}, + {"P length is smaller than the mail", 128}, + } + + m := NewMsg() + m.SetBodyString(TypeTextPlain, "TEST123") + wbuf := bytes.Buffer{} + _, err := m.Write(&wbuf) + if err != nil { + t.Errorf("failed to write message into temporary buffer: %s", err) + } + elen := wbuf.Len() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := make([]byte, tt.plen) + mr := m.NewReader() + n, err := mr.Read(p) + if err != nil { + t.Errorf("failed to Read(): %s", err) + } + if n == 0 { + t.Errorf("failed to Read() - received 0 bytes of data") + } + if tt.plen >= elen && n != elen { + t.Errorf("failed to Read() - not all data received. Expected: %d, got: %d", elen, n) + } + if tt.plen < elen && n != tt.plen { + t.Errorf("failed to Read() - full length of p wasn't filled with data. Expected: %d, got: %d", + tt.plen, n) + } + }) + } +} + +// TestReader_Read_error tests the Reader.Read method with an intentional error +func TestReader_Read_error(t *testing.T) { + r := Reader{err: fmt.Errorf("FAILED")} + var p []byte + _, err := r.Read(p) + if err == nil { + t.Errorf("Reader was supposed to fail, but didn't") + } +} + +// TestReader_Read_empty tests the Reader.Read method with an empty buffer +func TestReader_Read_empty(t *testing.T) { + r := Reader{buf: []byte{}} + var p []byte + _, err := r.Read(p) + if err != nil { + t.Errorf("Reader failed: %s", err) + } +}