Skip to content

Commit 6b83cfc

Browse files
authored
Merge pull request #2 from simplesurance/CORE-10934-attachments-streaming-new
[CORE-10934]-Redusing-memory-attachments-streaming
2 parents 943e75f + 7f77ee8 commit 6b83cfc

File tree

3 files changed

+87
-10
lines changed

3 files changed

+87
-10
lines changed

cnunkedreader.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package email
2+
3+
import "io"
4+
5+
// ChunkedReader provides reading by specified portion size.
6+
// Only last chunk can be lesser or zero-size same time with EOF or other error
7+
type ChunkedReader struct {
8+
r io.Reader
9+
chunkLen int
10+
}
11+
12+
func (cr *ChunkedReader) Read(b []byte) (int, error) {
13+
accumulatedBytes := 0
14+
if len(b) < cr.chunkLen {
15+
return 0, io.ErrShortBuffer
16+
}
17+
var err error
18+
var n int
19+
for accumulatedBytes < cr.chunkLen && err == nil {
20+
n, err = cr.r.Read(b[accumulatedBytes:])
21+
accumulatedBytes += n
22+
}
23+
return accumulatedBytes, err
24+
}
25+
26+
func NewChunkedReader(r io.Reader, chunkLen int) *ChunkedReader {
27+
return &ChunkedReader{
28+
r: r,
29+
chunkLen: chunkLen,
30+
}
31+
}

email.go

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -262,15 +262,11 @@ func parseMIMEParts(hs textproto.MIMEHeader, b io.Reader) ([]*part, error) {
262262
// Required parameters include an io.Reader, the desired filename for the attachment, and the Content-Type
263263
// The function will return the created Attachment for reference, as well as nil for the error, if successful.
264264
func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, err error) {
265-
var buffer bytes.Buffer
266-
if _, err = io.Copy(&buffer, r); err != nil {
267-
return
268-
}
269265
at := &Attachment{
270266
Filename: filename,
271267
ContentType: c,
272268
Header: textproto.MIMEHeader{},
273-
Content: buffer.Bytes(),
269+
Content: r,
274270
}
275271
e.Attachments = append(e.Attachments, at)
276272
return at, nil
@@ -472,7 +468,9 @@ func (e *Email) Bytes() ([]byte, error) {
472468
return nil, err
473469
}
474470
// Write the base64Wrapped content to the part
475-
base64Wrap(ap, a.Content)
471+
if err = streamBase64Wrap(ap, a.Content); err != nil {
472+
return nil, err
473+
}
476474
}
477475

478476
if isMixed || isAlternative {
@@ -494,7 +492,9 @@ func (e *Email) Bytes() ([]byte, error) {
494492
return nil, err
495493
}
496494
// Write the base64Wrapped content to the part
497-
base64Wrap(ap, a.Content)
495+
if err = streamBase64Wrap(ap, a.Content); err != nil {
496+
return nil, err
497+
}
498498
}
499499
if isMixed || isAlternative || isRelated {
500500
if err := w.Close(); err != nil {
@@ -702,7 +702,7 @@ type Attachment struct {
702702
Filename string
703703
ContentType string
704704
Header textproto.MIMEHeader
705-
Content []byte
705+
Content io.Reader
706706
HTMLRelated bool
707707
}
708708

@@ -751,6 +751,46 @@ func base64Wrap(w io.Writer, b []byte) {
751751
}
752752
}
753753

754+
// streamBase64Wrap encodes the attachment content, provided as stream, and wraps it according to RFC 2045 standards (every 76 chars)
755+
// The output is then written to the specified io.Writer
756+
func streamBase64Wrap(w io.Writer, r io.Reader) error {
757+
// 57 raw bytes per 76-byte base64 line.
758+
const maxRaw = 57
759+
// Buffer for each line, including trailing CRLF.
760+
wrBuffer := make([]byte, MaxLineLength+len("\r\n"))
761+
copy(wrBuffer[MaxLineLength:], "\r\n")
762+
rdBuffer := make([]byte, maxRaw)
763+
var lastRead int
764+
var err error
765+
cr := NewChunkedReader(r, maxRaw)
766+
for {
767+
//Reading next 57-bytes chunk.
768+
if lastRead, err = cr.Read(rdBuffer); err != nil && !errors.Is(err, io.EOF) {
769+
return err
770+
}
771+
//In case of last chunk jump to last chunk processing
772+
if errors.Is(err, io.EOF) {
773+
break
774+
}
775+
//normal chunk processing. It's len=maxRaw exactly
776+
base64.StdEncoding.Encode(wrBuffer, rdBuffer)
777+
if _, err := w.Write(wrBuffer); err != nil {
778+
return err
779+
}
780+
781+
}
782+
//last chunk processing. It can be 0<=size<=maxRaw
783+
if lastRead > 0 {
784+
out := wrBuffer[:base64.StdEncoding.EncodedLen(lastRead)]
785+
base64.StdEncoding.Encode(out, rdBuffer[:lastRead])
786+
out = append(out, "\r\n"...)
787+
if _, err := w.Write(out); err != nil {
788+
return err
789+
}
790+
}
791+
return nil
792+
}
793+
754794
// headerToBytes renders "header" to "buff". If there are multiple values for a
755795
// field, multiple "Field: value\r\n" lines will be emitted.
756796
func headerToBytes(buff io.Writer, header textproto.MIMEHeader) {

email_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -717,13 +717,19 @@ TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4=
717717
if e.Attachments[0].Filename != a.Filename {
718718
t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[0].Filename, a.Filename)
719719
}
720-
if !bytes.Equal(e.Attachments[0].Content, a.Content) {
720+
var b1, b2 []byte
721+
b1, _ = io.ReadAll(e.Attachments[0].Content)
722+
b2, _ = io.ReadAll(a.Content)
723+
if !bytes.Equal(b1, b2) {
721724
t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[0].Content, a.Content)
722725
}
723726
if e.Attachments[1].Filename != b.Filename {
724727
t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[1].Filename, b.Filename)
725728
}
726-
if !bytes.Equal(e.Attachments[1].Content, b.Content) {
729+
var b3, b4 []byte
730+
b3, _ = io.ReadAll(e.Attachments[1].Content)
731+
b4, _ = io.ReadAll(b.Content)
732+
if !bytes.Equal(b3, b4) {
727733
t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[1].Content, b.Content)
728734
}
729735
}

0 commit comments

Comments
 (0)