Skip to content

Commit

Permalink
fix: correct messages with empty/partially empty bodies (#85) (#86)
Browse files Browse the repository at this point in the history
  • Loading branch information
Darkclainer authored Aug 3, 2023
1 parent db61a74 commit e4cd94d
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 40 deletions.
53 changes: 37 additions & 16 deletions mime.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ func (m *MailYak) buildMimeWithBoundaries(w io.Writer, mb, ab string) error {
return err
}

var (
hasBody = m.html.Len() != 0 || m.plain.Len() != 0
hasAttachments = len(m.attachments) != 0
)

// if we don't have text/html body or attachments we can skip Content-Type header
// in that case next default will be assumed
// Content-type: text/plain; charset=us-ascii
// See https://datatracker.ietf.org/doc/html/rfc2045#page-14
if !hasBody && !hasAttachments {
// The RFC said that body can be ommited: https://datatracker.ietf.org/doc/html/rfc822#section-3.1
// but for example mailhog can't correctly read message without any body.
// This CRLF just separate header section.
_, err := fmt.Fprint(w, "\r\n")
return err
}
// Start our multipart/mixed part
mixed := multipart.NewWriter(w)
if err := mixed.SetBoundary(mb); err != nil {
Expand All @@ -58,18 +74,17 @@ func (m *MailYak) buildMimeWithBoundaries(w io.Writer, mb, ab string) error {
tryWrite := func() error {
fmt.Fprintf(w, "Content-Type: multipart/mixed;\r\n\tboundary=\"%s\"; charset=UTF-8\r\n\r\n", mixed.Boundary())

ctype := fmt.Sprintf("multipart/alternative;\r\n\tboundary=\"%s\"", ab)

altPart, err := mixed.CreatePart(textproto.MIMEHeader{"Content-Type": {ctype}})
if err != nil {
return err
if hasBody {
if err := m.writeAlternativePart(mixed, ab); err != nil {
return err
}
}

if err := m.writeBody(altPart, ab); err != nil {
return err
if hasAttachments {
if err := m.writeAttachments(mixed, lineSplitterBuilder{}); err != nil {
return err
}
}

return m.writeAttachments(mixed, lineSplitterBuilder{})
return nil
}

if err := tryWrite(); err != nil {
Expand All @@ -82,7 +97,6 @@ func (m *MailYak) buildMimeWithBoundaries(w io.Writer, mb, ab string) error {
// writeHeaders writes the MIME-Version, Date, Reply-To, From, To and Subject headers,
// plus any custom headers set via AddHeader().
func (m *MailYak) writeHeaders(w io.Writer) error {

if _, err := w.Write([]byte(m.fromHeader())); err != nil {
return err
}
Expand Down Expand Up @@ -133,13 +147,20 @@ func (m *MailYak) fromHeader() string {
return fmt.Sprintf("From: %s <%s>\r\n", m.fromName, m.fromAddr)
}

// writeBody writes the text/plain and text/html mime parts.
func (m *MailYak) writeBody(w io.Writer, boundary string) error {
if m.plain.Len() == 0 && m.html.Len() == 0 {
// No body to write - just skip it
return nil
func (m *MailYak) writeAlternativePart(mixed *multipart.Writer, boundary string) error {
ctype := fmt.Sprintf("multipart/alternative;\r\n\tboundary=\"%s\"", boundary)

altPart, err := mixed.CreatePart(textproto.MIMEHeader{"Content-Type": {ctype}})
if err != nil {
return err
}

return m.writeBody(altPart, boundary)
}

// writeBody writes the text/plain and text/html mime parts.
// It's incorrect to call writeBody without html or plain content.
func (m *MailYak) writeBody(w io.Writer, boundary string) error {
alt := multipart.NewWriter(w)

if err := alt.SetBoundary(boundary); err != nil {
Expand Down
86 changes: 62 additions & 24 deletions mime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"fmt"
"io"
"io/ioutil"
"mime"
"mime/multipart"
"net/mail"
"regexp"
"strings"
"testing"
Expand Down Expand Up @@ -216,14 +218,6 @@ func TestMailYakWriteBody(t *testing.T) {
wantW string
wantErr bool
}{
{
"Empty",
"",
"",
"test",
"",
false,
},
{
"HTML",
"HTML",
Expand Down Expand Up @@ -287,13 +281,14 @@ func TestMailYakBuildMime(t *testing.T) {
// Test description.
name string
// Receiver fields.
rHTML []byte
rPlain []byte
rtoAddrs []string
rsubject string
rfromAddr string
rfromName string
rreplyTo string
rHTML []byte
rPlain []byte
rtoAddrs []string
rsubject string
rfromAddr string
rfromName string
rreplyTo string
rAttachemnt string
// Expected results.
want string
wantErr bool
Expand All @@ -307,7 +302,8 @@ func TestMailYakBuildMime(t *testing.T) {
"",
"",
"",
"From: \r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: \r\nContent-Type: multipart/mixed;\r\n\tboundary=\"mixed\"; charset=UTF-8\r\n\r\n--mixed\r\nContent-Type: multipart/alternative;\r\n\tboundary=\"alt\"\r\n\r\n\r\n--mixed--\r\n",
"",
"From: \r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: \r\n\r\n",
false,
},
{
Expand All @@ -319,6 +315,7 @@ func TestMailYakBuildMime(t *testing.T) {
"",
"",
"",
"",
"From: \r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: \r\nContent-Type: multipart/mixed;\r\n\tboundary=\"mixed\"; charset=UTF-8\r\n\r\n--mixed\r\nContent-Type: multipart/alternative;\r\n\tboundary=\"alt\"\r\n\r\n--alt\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/html; charset=UTF-8\r\n\r\nHTML\r\n--alt--\r\n\r\n--mixed--\r\n",
false,
},
Expand All @@ -331,9 +328,23 @@ func TestMailYakBuildMime(t *testing.T) {
"",
"",
"",
"",
"From: \r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: \r\nContent-Type: multipart/mixed;\r\n\tboundary=\"mixed\"; charset=UTF-8\r\n\r\n--mixed\r\nContent-Type: multipart/alternative;\r\n\tboundary=\"alt\"\r\n\r\n--alt\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\nPlain\r\n--alt--\r\n\r\n--mixed--\r\n",
false,
},
{
"Attachemnt",
[]byte{},
[]byte{},
[]string{""},
"",
"",
"",
"",
"attachment",
"From: \r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: \r\nContent-Type: multipart/mixed;\r\n\tboundary=\"mixed\"; charset=UTF-8\r\n\r\n--mixed\r\nContent-Disposition: attachment;\n\tfilename=\"testAttachment\"; name=\"testAttachment\"\r\nContent-ID: <testAttachment>\r\nContent-Transfer-Encoding: base64\r\nContent-Type: text/plain; charset=utf-8;\n\tfilename=\"testAttachment\"; name=\"testAttachment\"\r\n\r\nYXR0YWNobWVudA==\r\n--mixed--\r\n",
false,
},
{
"Reply-To",
[]byte{},
Expand All @@ -343,7 +354,8 @@ func TestMailYakBuildMime(t *testing.T) {
"",
"",
"reply",
"From: \r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nReply-To: reply\r\nSubject: \r\nTo: \r\nContent-Type: multipart/mixed;\r\n\tboundary=\"mixed\"; charset=UTF-8\r\n\r\n--mixed\r\nContent-Type: multipart/alternative;\r\n\tboundary=\"alt\"\r\n\r\n\r\n--mixed--\r\n",
"",
"From: \r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nReply-To: reply\r\nSubject: \r\nTo: \r\n\r\n",
false,
},
{
Expand All @@ -355,7 +367,8 @@ func TestMailYakBuildMime(t *testing.T) {
"",
"name",
"",
"From: name <>\r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: \r\nContent-Type: multipart/mixed;\r\n\tboundary=\"mixed\"; charset=UTF-8\r\n\r\n--mixed\r\nContent-Type: multipart/alternative;\r\n\tboundary=\"alt\"\r\n\r\n\r\n--mixed--\r\n",
"",
"From: name <>\r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: \r\n\r\n",
false,
},
{
Expand All @@ -367,7 +380,8 @@ func TestMailYakBuildMime(t *testing.T) {
"addr",
"name",
"",
"From: name <addr>\r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: \r\nContent-Type: multipart/mixed;\r\n\tboundary=\"mixed\"; charset=UTF-8\r\n\r\n--mixed\r\nContent-Type: multipart/alternative;\r\n\tboundary=\"alt\"\r\n\r\n\r\n--mixed--\r\n",
"",
"From: name <addr>\r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: \r\n\r\n",
false,
},
{
Expand All @@ -379,7 +393,8 @@ func TestMailYakBuildMime(t *testing.T) {
"from",
"",
"",
"From: from\r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: \r\nContent-Type: multipart/mixed;\r\n\tboundary=\"mixed\"; charset=UTF-8\r\n\r\n--mixed\r\nContent-Type: multipart/alternative;\r\n\tboundary=\"alt\"\r\n\r\n\r\n--mixed--\r\n",
"",
"From: from\r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: \r\n\r\n",
false,
},
{
Expand All @@ -391,7 +406,8 @@ func TestMailYakBuildMime(t *testing.T) {
"",
"",
"",
"From: \r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: subject\r\nTo: \r\nContent-Type: multipart/mixed;\r\n\tboundary=\"mixed\"; charset=UTF-8\r\n\r\n--mixed\r\nContent-Type: multipart/alternative;\r\n\tboundary=\"alt\"\r\n\r\n\r\n--mixed--\r\n",
"",
"From: \r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: subject\r\nTo: \r\n\r\n",
false,
},
{
Expand All @@ -403,7 +419,8 @@ func TestMailYakBuildMime(t *testing.T) {
"",
"",
"",
"From: \r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: one,two\r\nContent-Type: multipart/mixed;\r\n\tboundary=\"mixed\"; charset=UTF-8\r\n\r\n--mixed\r\nContent-Type: multipart/alternative;\r\n\tboundary=\"alt\"\r\n\r\n\r\n--mixed--\r\n",
"",
"From: \r\nMIME-Version: 1.0\r\nDate: " + now + "\r\nSubject: \r\nTo: one,two\r\n\r\n",
false,
},
}
Expand All @@ -426,6 +443,9 @@ func TestMailYakBuildMime(t *testing.T) {
}
m.HTML().Write(tt.rHTML)
m.Plain().Write(tt.rPlain)
if tt.rAttachemnt != "" {
m.Attach("testAttachment", strings.NewReader(tt.rAttachemnt))
}

buf := &bytes.Buffer{}
err := m.buildMimeWithBoundaries(buf, "mixed", "alt")
Expand Down Expand Up @@ -563,10 +583,28 @@ func TestMailYakBuildMime_withAttachments(t *testing.T) {
}

seen := 0
mr := multipart.NewReader(buf, "mixed")
msg, err := mail.ReadMessage(buf)
if err != nil {
t.Fatalf("%q. MailYak.buildMime() error %v", tt.name, err)
}
contentTypeHeader := msg.Header.Get("Content-Type")
if contentTypeHeader == "" {
if len(tt.rattachments) != 0 {
t.Errorf("%q. MailYak.buildMime() attachments were expected, but Content-Type header wasn't set", tt.name)
}
return
}
mediaType, mediaTypeParams, err := mime.ParseMediaType(contentTypeHeader)
if err != nil {
t.Fatalf("%q. MailYak.buildMime() error %v", tt.name, err)
}
if mediaType != "multipart/mixed" {
t.Fatalf("%q. MailYak.buildMime() Content-Type media type multipart/mixed was expected, but got %q", tt.name, mediaType)
}
mr := multipart.NewReader(msg.Body, mediaTypeParams["boundary"])

// Itterate over the mime parts, look for attachments
for {
for contentTypeHeader != "" {
p, err := mr.NextPart()
if err == io.EOF {
break
Expand Down

0 comments on commit e4cd94d

Please sign in to comment.