From 05ed8313f629c36bdccd34aa0a75b646da62e4bf Mon Sep 17 00:00:00 2001 From: Yurii Rubakha Date: Tue, 18 Jul 2023 18:37:47 +0300 Subject: [PATCH 1/7] feat: attachments base64encoding direct from streams (but still in buffer) --- email.go | 61 ++++++++++++++++++++++++++++++++++++++++++++------- email_test.go | 10 +++++++-- helper.go | 31 ++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 helper.go diff --git a/email.go b/email.go index 57d1b53..ed4f22a 100644 --- a/email.go +++ b/email.go @@ -262,15 +262,11 @@ func parseMIMEParts(hs textproto.MIMEHeader, b io.Reader) ([]*part, error) { // Required parameters include an io.Reader, the desired filename for the attachment, and the Content-Type // The function will return the created Attachment for reference, as well as nil for the error, if successful. func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, err error) { - var buffer bytes.Buffer - if _, err = io.Copy(&buffer, r); err != nil { - return - } at := &Attachment{ Filename: filename, ContentType: c, Header: textproto.MIMEHeader{}, - Content: buffer.Bytes(), + Content: r, } e.Attachments = append(e.Attachments, at) return at, nil @@ -375,6 +371,15 @@ func (e *Email) categorizeAttachments() (htmlRelated, others []*Attachment) { return } +// streamToBytes is a helper for compatibility old and new logic +func streamToBytes(r io.Reader) (b []byte, err error) { + var buffer bytes.Buffer + if _, err = io.Copy(&buffer, r); err != nil { + return + } + return buffer.Bytes(), nil +} + // Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc. func (e *Email) Bytes() ([]byte, error) { // TODO: better guess buffer size @@ -472,7 +477,9 @@ func (e *Email) Bytes() ([]byte, error) { return nil, err } // Write the base64Wrapped content to the part - base64Wrap(ap, a.Content) + if err = streamBase64Wrap(ap, a.Content); err != nil { + return nil, err + } } if isMixed || isAlternative { @@ -494,7 +501,9 @@ func (e *Email) Bytes() ([]byte, error) { return nil, err } // Write the base64Wrapped content to the part - base64Wrap(ap, a.Content) + if err = streamBase64Wrap(ap, a.Content); err != nil { + return nil, err + } } if isMixed || isAlternative || isRelated { if err := w.Close(); err != nil { @@ -702,7 +711,7 @@ type Attachment struct { Filename string ContentType string Header textproto.MIMEHeader - Content []byte + Content io.Reader HTMLRelated bool } @@ -751,6 +760,42 @@ func base64Wrap(w io.Writer, b []byte) { } } +// streamBase64Wrap encodes the attachment content, provided as stream, and wraps it according to RFC 2045 standards (every 76 chars) +// The output is then written to the specified io.Writer +func streamBase64Wrap(w io.Writer, r io.Reader) error { + // 57 raw bytes per 76-byte base64 line. + const maxRaw = 57 + // Buffer for each line, including trailing CRLF. + wrBuffer := make([]byte, MaxLineLength+len("\r\n")) + copy(wrBuffer[MaxLineLength:], "\r\n") + rdBuffer := make([]byte, maxRaw, maxRaw) + var lastRead int + var err error + cr := NewChunkedReader(r, maxRaw) + for { + //Reading next 57-bytes chunk. + if lastRead, err = cr.Read(rdBuffer); err != nil && !errors.Is(err, io.EOF) { + return err + } + //In case of last chunk jump to last chunk processing + if errors.Is(err, io.EOF) { + break + } + //normal chunk processing. It's len=maxRaw exactly + base64.StdEncoding.Encode(wrBuffer, rdBuffer) + w.Write(wrBuffer) + + } + //last chunk processing. It can be 0<=size<=maxRaw + if lastRead > 0 { + out := wrBuffer[:base64.StdEncoding.EncodedLen(lastRead)] + base64.StdEncoding.Encode(out, rdBuffer[:lastRead]) + out = append(out, "\r\n"...) + w.Write(out) + } + return nil +} + // headerToBytes renders "header" to "buff". If there are multiple values for a // field, multiple "Field: value\r\n" lines will be emitted. func headerToBytes(buff io.Writer, header textproto.MIMEHeader) { diff --git a/email_test.go b/email_test.go index b6d62d2..da7a792 100644 --- a/email_test.go +++ b/email_test.go @@ -717,13 +717,19 @@ TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= if e.Attachments[0].Filename != a.Filename { t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[0].Filename, a.Filename) } - if !bytes.Equal(e.Attachments[0].Content, a.Content) { + var b1, b2 []byte + b1, _ = streamToBytes(e.Attachments[0].Content) + b2, _ = streamToBytes(a.Content) + if !bytes.Equal(b1, b2) { t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[0].Content, a.Content) } if e.Attachments[1].Filename != b.Filename { t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[1].Filename, b.Filename) } - if !bytes.Equal(e.Attachments[1].Content, b.Content) { + var b3, b4 []byte + b3, _ = streamToBytes(e.Attachments[1].Content) + b4, _ = streamToBytes(b.Content) + if !bytes.Equal(b3, b4) { t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[1].Content, b.Content) } } diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..1c13647 --- /dev/null +++ b/helper.go @@ -0,0 +1,31 @@ +package email + +import "io" + +// ChunkedReader provides reading by specified portion size. +// Only last chunk can be lesser or zero-size same time with EOF or other error +type ChunkedReader struct { + r io.Reader + chunkLen int +} + +func (cr *ChunkedReader) Read(b []byte) (int, error) { + accumulatedBytes := 0 + if len(b) < cr.chunkLen { + return 0, io.ErrShortBuffer + } + var err error + var n int + for accumulatedBytes < cr.chunkLen && err == nil { + n, err = cr.r.Read(b[accumulatedBytes:]) + accumulatedBytes += n + } + return accumulatedBytes, err +} + +func NewChunkedReader(r io.Reader, chunkLen int) *ChunkedReader { + return &ChunkedReader{ + r: r, + chunkLen: chunkLen, + } +} From 5d8735b86c3c71dbf976e603ded6d1b1b7b8bfce Mon Sep 17 00:00:00 2001 From: YuriiRubakha <132891724+YuriiRubakha@users.noreply.github.com> Date: Tue, 25 Jul 2023 16:22:02 +0300 Subject: [PATCH 2/7] ref: streamToBytes simplified Co-authored-by: Francisco Delmar Kurpiel --- email.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/email.go b/email.go index ed4f22a..5477d48 100644 --- a/email.go +++ b/email.go @@ -373,11 +373,7 @@ func (e *Email) categorizeAttachments() (htmlRelated, others []*Attachment) { // streamToBytes is a helper for compatibility old and new logic func streamToBytes(r io.Reader) (b []byte, err error) { - var buffer bytes.Buffer - if _, err = io.Copy(&buffer, r); err != nil { - return - } - return buffer.Bytes(), nil + return io.ReadAll(r) } // Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc. From 70b564eeb65084eedb1054a94f47960e7fbd370c Mon Sep 17 00:00:00 2001 From: Yurii Rubakha Date: Wed, 2 Aug 2023 13:28:20 +0300 Subject: [PATCH 3/7] ref: replaced streamToBytes with standard io.ReadAll --- email.go | 5 ----- email_test.go | 8 ++++---- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/email.go b/email.go index 5477d48..09e9a0e 100644 --- a/email.go +++ b/email.go @@ -371,11 +371,6 @@ func (e *Email) categorizeAttachments() (htmlRelated, others []*Attachment) { return } -// streamToBytes is a helper for compatibility old and new logic -func streamToBytes(r io.Reader) (b []byte, err error) { - return io.ReadAll(r) -} - // Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc. func (e *Email) Bytes() ([]byte, error) { // TODO: better guess buffer size diff --git a/email_test.go b/email_test.go index da7a792..6890c5c 100644 --- a/email_test.go +++ b/email_test.go @@ -718,8 +718,8 @@ TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[0].Filename, a.Filename) } var b1, b2 []byte - b1, _ = streamToBytes(e.Attachments[0].Content) - b2, _ = streamToBytes(a.Content) + b1, _ = io.ReadAll(e.Attachments[0].Content) + b2, _ = io.ReadAll(a.Content) if !bytes.Equal(b1, b2) { t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[0].Content, a.Content) } @@ -727,8 +727,8 @@ TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[1].Filename, b.Filename) } var b3, b4 []byte - b3, _ = streamToBytes(e.Attachments[1].Content) - b4, _ = streamToBytes(b.Content) + b3, _ = io.ReadAll(e.Attachments[1].Content) + b4, _ = io.ReadAll(b.Content) if !bytes.Equal(b3, b4) { t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[1].Content, b.Content) } From fe2a03a418d3aa10641e4d7ca0c10f80ac1367a1 Mon Sep 17 00:00:00 2001 From: Yurii Rubakha Date: Wed, 2 Aug 2023 13:31:03 +0300 Subject: [PATCH 4/7] ref: renamed helper.go to chunkedreader.go --- helper.go => cnunkedreader.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename helper.go => cnunkedreader.go (100%) diff --git a/helper.go b/cnunkedreader.go similarity index 100% rename from helper.go rename to cnunkedreader.go From 98a9c75fbfafa995ea459f4af68b555e457a61c2 Mon Sep 17 00:00:00 2001 From: YuriiRubakha <132891724+YuriiRubakha@users.noreply.github.com> Date: Wed, 2 Aug 2023 13:34:08 +0300 Subject: [PATCH 5/7] ref: redundant parameter removed Co-authored-by: Fabian Holler --- email.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/email.go b/email.go index 5477d48..1684299 100644 --- a/email.go +++ b/email.go @@ -764,7 +764,7 @@ func streamBase64Wrap(w io.Writer, r io.Reader) error { // Buffer for each line, including trailing CRLF. wrBuffer := make([]byte, MaxLineLength+len("\r\n")) copy(wrBuffer[MaxLineLength:], "\r\n") - rdBuffer := make([]byte, maxRaw, maxRaw) + rdBuffer := make([]byte, maxRaw) var lastRead int var err error cr := NewChunkedReader(r, maxRaw) From 7f77ee8a0ec08f0e4edc68bf41c963d1170121e7 Mon Sep 17 00:00:00 2001 From: Yurii Rubakha Date: Wed, 2 Aug 2023 13:39:23 +0300 Subject: [PATCH 6/7] ref: added error check while w.Write --- email.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/email.go b/email.go index a5d1756..0194f5f 100644 --- a/email.go +++ b/email.go @@ -774,7 +774,9 @@ func streamBase64Wrap(w io.Writer, r io.Reader) error { } //normal chunk processing. It's len=maxRaw exactly base64.StdEncoding.Encode(wrBuffer, rdBuffer) - w.Write(wrBuffer) + if _, err := w.Write(wrBuffer); err != nil { + return err + } } //last chunk processing. It can be 0<=size<=maxRaw @@ -782,7 +784,9 @@ func streamBase64Wrap(w io.Writer, r io.Reader) error { out := wrBuffer[:base64.StdEncoding.EncodedLen(lastRead)] base64.StdEncoding.Encode(out, rdBuffer[:lastRead]) out = append(out, "\r\n"...) - w.Write(out) + if _, err := w.Write(out); err != nil { + return err + } } return nil } From 155f1958c4e527d5739ecaa345fb45fd345a0ada Mon Sep 17 00:00:00 2001 From: Yurii Rubakha Date: Mon, 11 Sep 2023 11:31:12 +0300 Subject: [PATCH 7/7] ref: fixed typo in chunkedreader.go name --- cnunkedreader.go => chunkedreader.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cnunkedreader.go => chunkedreader.go (100%) diff --git a/cnunkedreader.go b/chunkedreader.go similarity index 100% rename from cnunkedreader.go rename to chunkedreader.go