diff --git a/message_test.go b/message_test.go index 40e44f2..9dc51fc 100644 --- a/message_test.go +++ b/message_test.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "path/filepath" "regexp" + "runtime" "strconv" "strings" "testing" @@ -150,7 +151,8 @@ func TestAlternative(t *testing.T) { to: []string{"to@example.com"}, content: "From: from@example.com\r\n" + "To: to@example.com\r\n" + - "Content-Type: multipart/alternative; boundary=_BOUNDARY_1_\r\n" + + "Content-Type: multipart/alternative;\r\n" + + " boundary=_BOUNDARY_1_\r\n" + "\r\n" + "--_BOUNDARY_1_\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" + @@ -180,7 +182,8 @@ func TestPartSetting(t *testing.T) { to: []string{"to@example.com"}, content: "From: from@example.com\r\n" + "To: to@example.com\r\n" + - "Content-Type: multipart/alternative; boundary=_BOUNDARY_1_\r\n" + + "Content-Type: multipart/alternative;\r\n" + + " boundary=_BOUNDARY_1_\r\n" + "\r\n" + "--_BOUNDARY_1_\r\n" + "Content-Type: text/plain; format=flowed; charset=UTF-8\r\n" + @@ -216,7 +219,8 @@ func TestBodyWriter(t *testing.T) { to: []string{"to@example.com"}, content: "From: from@example.com\r\n" + "To: to@example.com\r\n" + - "Content-Type: multipart/alternative; boundary=_BOUNDARY_1_\r\n" + + "Content-Type: multipart/alternative;\r\n" + + " boundary=_BOUNDARY_1_\r\n" + "\r\n" + "--_BOUNDARY_1_\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" + @@ -267,7 +271,8 @@ func TestAttachment(t *testing.T) { to: []string{"to@example.com"}, content: "From: from@example.com\r\n" + "To: to@example.com\r\n" + - "Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" + + "Content-Type: multipart/mixed;\r\n" + + " boundary=_BOUNDARY_1_\r\n" + "\r\n" + "--_BOUNDARY_1_\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" + @@ -298,7 +303,8 @@ func TestAttachmentsOnly(t *testing.T) { to: []string{"to@example.com"}, content: "From: from@example.com\r\n" + "To: to@example.com\r\n" + - "Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" + + "Content-Type: multipart/mixed;\r\n" + + " boundary=_BOUNDARY_1_\r\n" + "\r\n" + "--_BOUNDARY_1_\r\n" + "Content-Type: application/pdf; name=\"test.pdf\"\r\n" + @@ -331,7 +337,8 @@ func TestAttachments(t *testing.T) { to: []string{"to@example.com"}, content: "From: from@example.com\r\n" + "To: to@example.com\r\n" + - "Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" + + "Content-Type: multipart/mixed;\r\n" + + " boundary=_BOUNDARY_1_\r\n" + "\r\n" + "--_BOUNDARY_1_\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" + @@ -369,7 +376,8 @@ func TestEmbedded(t *testing.T) { to: []string{"to@example.com"}, content: "From: from@example.com\r\n" + "To: to@example.com\r\n" + - "Content-Type: multipart/related; boundary=_BOUNDARY_1_\r\n" + + "Content-Type: multipart/related;\r\n" + + " boundary=_BOUNDARY_1_\r\n" + "\r\n" + "--_BOUNDARY_1_\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" + @@ -410,13 +418,16 @@ func TestFullMessage(t *testing.T) { to: []string{"to@example.com"}, content: "From: from@example.com\r\n" + "To: to@example.com\r\n" + - "Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" + + "Content-Type: multipart/mixed;\r\n" + + " boundary=_BOUNDARY_1_\r\n" + "\r\n" + "--_BOUNDARY_1_\r\n" + - "Content-Type: multipart/related; boundary=_BOUNDARY_2_\r\n" + + "Content-Type: multipart/related;\r\n" + + " boundary=_BOUNDARY_2_\r\n" + "\r\n" + "--_BOUNDARY_2_\r\n" + - "Content-Type: multipart/alternative; boundary=_BOUNDARY_3_\r\n" + + "Content-Type: multipart/alternative;\r\n" + + " boundary=_BOUNDARY_3_\r\n" + "\r\n" + "--_BOUNDARY_3_\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" + @@ -530,7 +541,7 @@ func TestEmptyHeader(t *testing.T) { want := &message{ from: "from@example.com", content: "From: from@example.com\r\n" + - "X-Empty: \r\n", + "X-Empty:\r\n", } testMessage(t, m, 0, want) @@ -660,6 +671,32 @@ func mockCopyFileWithHeader(m *Message, name string, h map[string][]string) (str return name, f, SetHeader(h) } +func TestLineLength(t *testing.T) { + switch runtime.Version()[:5] { + case "go1.2", "go1.3", "go1.4", "go1.5": + t.Skip("Only pass with Go 1.6+") + } + + m := NewMessage() + m.SetAddressHeader("From", "from@example.com", "Señor From") + m.SetHeader("Subject", "{$firstname} Bienvendio a Apostólica, aquí inicia el camino de tu") + m.SetBody("text/plain", strings.Repeat("a", 100)) + + buf := new(bytes.Buffer) + m.WriteTo(buf) + n := 0 + for _, b := range buf.Bytes() { + if b == '\n' { + n = 0 + } else { + n++ + if n == 80 { + t.Errorf("A line is too long:\n%s", buf.Bytes()) + } + } + } +} + func BenchmarkFull(b *testing.B) { discardFunc := SendFunc(func(from string, to []string, m io.WriterTo) error { _, err := m.WriteTo(ioutil.Discard) diff --git a/mime.go b/mime.go index 51cba72..194d4a7 100644 --- a/mime.go +++ b/mime.go @@ -5,6 +5,7 @@ package gomail import ( "mime" "mime/quotedprintable" + "strings" ) var newQPWriter = quotedprintable.NewWriter @@ -14,6 +15,7 @@ type mimeEncoder struct { } var ( - bEncoding = mimeEncoder{mime.BEncoding} - qEncoding = mimeEncoder{mime.QEncoding} + bEncoding = mimeEncoder{mime.BEncoding} + qEncoding = mimeEncoder{mime.QEncoding} + lastIndexByte = strings.LastIndexByte ) diff --git a/mime_go14.go b/mime_go14.go index 246e2e5..3dc26aa 100644 --- a/mime_go14.go +++ b/mime_go14.go @@ -11,6 +11,15 @@ type mimeEncoder struct { } var ( - bEncoding = mimeEncoder{quotedprintable.BEncoding} - qEncoding = mimeEncoder{quotedprintable.QEncoding} + bEncoding = mimeEncoder{quotedprintable.BEncoding} + qEncoding = mimeEncoder{quotedprintable.QEncoding} + lastIndexByte = func(s string, c byte) int { + for i := len(s) - 1; i >= 0; i-- { + + if s[i] == c { + return i + } + } + return -1 + } ) diff --git a/writeto.go b/writeto.go index 490f3b1..7c05132 100644 --- a/writeto.go +++ b/writeto.go @@ -7,6 +7,7 @@ import ( "mime" "mime/multipart" "path/filepath" + "strings" "time" ) @@ -78,7 +79,7 @@ type messageWriter struct { func (w *messageWriter) openMultipart(mimeType string) { mw := multipart.NewWriter(w) - contentType := "multipart/" + mimeType + "; boundary=" + mw.Boundary() + contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary() w.writers[w.depth] = mw if w.depth == 0 { @@ -163,17 +164,87 @@ func (w *messageWriter) writeString(s string) { func (w *messageWriter) writeHeader(k string, v ...string) { w.writeString(k) + if len(v) == 0 { + w.writeString(":\r\n") + return + } w.writeString(": ") - if len(v) > 0 { - w.writeString(v[0]) - for _, s := range v[1:] { + + // Max header line length is 78 characters in RFC 5322 and 76 characters + // in RFC 2047. So for the sake of simplicity we use the 76 characters + // limit. + charsLeft := 76 - len(k) - len(": ") + + for i, s := range v { + // If the line is already too long, insert a newline right away. + if charsLeft < 1 { + if i == 0 { + w.writeString("\r\n ") + } else { + w.writeString(",\r\n ") + } + charsLeft = 75 + } else if i != 0 { w.writeString(", ") - w.writeString(s) + charsLeft -= 2 + } + + // While the header content is too long, fold it by inserting a newline. + for len(s) > charsLeft { + s = w.writeLine(s, charsLeft) + charsLeft = 75 + } + w.writeString(s) + if i := lastIndexByte(s, '\n'); i != -1 { + charsLeft = 75 - (len(s) - i - 1) + } else { + charsLeft -= len(s) } } w.writeString("\r\n") } +func (w *messageWriter) writeLine(s string, charsLeft int) string { + // If there is already a newline before the limit. Write the line. + if i := strings.IndexByte(s, '\n'); i != -1 && i < charsLeft { + w.writeString(s[:i+1]) + return s[i+1:] + } + + for i := charsLeft - 1; i >= 0; i-- { + if s[i] == ' ' { + w.writeString(s[:i]) + w.writeString("\r\n ") + return s[i+1:] + } + } + + // No space found so insert a newline if it was not the start of the + // line. + if charsLeft != 75 { + w.writeString("\r\n ") + return s + } + + // We could not insert a newline cleanly so look for a space or a newline + // even if it is after the limit. + for i := 75; i < len(s); i++ { + if s[i] == ' ' { + w.writeString(s[:i]) + w.writeString("\r\n ") + return s[i+1:] + } + if s[i] == '\n' { + w.writeString(s[:i+1]) + return s[i+1:] + } + } + + // Too bad, no space or newline in the whole string. Just write everything. + w.writeString(s) + return "" +} + func (w *messageWriter) writeHeaders(h map[string][]string) { if w.depth == 0 { for k, v := range h {