15

I'm trying to figure out how to build multipart/mime envelopes for emails in Go. The following code generates correctly-nested bodies - but the boundaries are not inserted correctly.

You can see a demo on https://play.golang.org/p/XLc4DQFObRn

package main

import (
    "bytes"
    "fmt"
    "io"
    "log"
    "math/rand"
    "mime/multipart"
    "mime/quotedprintable"
    "net/textproto"
)

//  multipart/mixed
//  |- multipart/related
//  |  |- multipart/alternative
//  |  |  |- text/plain
//  |  |  `- text/html
//  |  `- inlines..
//  `- attachments..


func main() {

    body := &bytes.Buffer{}
    writer := multipart.NewWriter(body)

    var part io.Writer
    var err error

    // Text Content
    part, err = writer.CreatePart(textproto.MIMEHeader{"Content-Type": {"multipart/alternative"}})
    if err != nil {
        log.Fatal(err)
    }

    childWriter := multipart.NewWriter(part)

    var subpart io.Writer
    for _, contentType := range []string{"text/plain", "text/html"} {
        subpart, err = CreateQuoteTypePart(childWriter, contentType)
        if err != nil {
            log.Fatal(err)
        }
        _, err := subpart.Write([]byte("This is a line of text that needs to be wrapped by quoted-printable before it goes to far.\r\n\r\n"))
        if err != nil {
            log.Fatal(err)
        }
    }

    // Attachments
    filename := fmt.Sprintf("File_%d.jpg", rand.Int31())
    part, err = writer.CreatePart(textproto.MIMEHeader{"Content-Type": {"application/octet-stream"}, "Content-Disposition": {"attachment; filename=" + filename}})
    if err != nil {
        log.Fatal(err)
    }
    part.Write([]byte("AABBCCDDEEFF"))

    writer.Close()

    fmt.Print(`From: Bob <bob@example.com>
To: Alice <alias@example.com>
Subject: Formatted text mail
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=`)
    fmt.Println(writer.Boundary())
    fmt.Println(body.String())

}

// https://github.com/domodwyer/mailyak/blob/master/attachments.go#L142
func CreateQuoteTypePart(writer *multipart.Writer, contentType string) (part io.Writer, err error) {
    header := textproto.MIMEHeader{
        "Content-Type":              []string{contentType},
        "Content-Transfer-Encoding": []string{"quoted-printable"},
    }

    part, err = writer.CreatePart(header)
    if err != nil {
        return
    }
    part = quotedprintable.NewWriter(part)
    return
}

I want to stick to answers from the standard library (stdlib) and avoid third party attempts to wing it.

Old Pro
  • 24,624
  • 7
  • 58
  • 106
Xeoncross
  • 55,620
  • 80
  • 262
  • 364

1 Answers1

18

Unfortunately, the standard library support for writing multi-part MIME messages has a bad API for nesting. The problem is that you have to set the boundary string in the header before creating the writer, but the generated boundary string is obviously not available before creating the writer. So you have to set the boundary strings explicitly.

Here is my solution (runnable in the Go Playground), simplified for brevity. I have chosen to use the outer writer's boundary to set the inner one, and added labels to make it easier to keep track when reading the output.

package main

import ("bytes"; "fmt"; "io"; "log"; "math/rand"; "mime/multipart"; "net/textproto")

//  multipart/mixed
//  |- multipart/related
//  |  |- multipart/alternative
//  |  |  |- text/plain
//  |  |  `- text/html
//  |  `- inlines..
//  `- attachments..

func main() {
    mixedContent := &bytes.Buffer{}
    mixedWriter := multipart.NewWriter(mixedContent)

    // related content, inside mixed
    var newBoundary = "RELATED-" + mixedWriter.Boundary()
    mixedWriter.SetBoundary(first70("MIXED-" + mixedWriter.Boundary()))

    relatedWriter, newBoundary := nestedMultipart(mixedWriter, "multipart/related", newBoundary)
    altWriter, newBoundary := nestedMultipart(relatedWriter, "multipart/alternative", "ALTERNATIVE-" + newBoundary)

    // Actual content alternatives (finally!)
    var childContent io.Writer

    childContent, _ = altWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/plain"}})
    childContent.Write([]byte("This is a line of text\r\n\r\n"))
    childContent, _ = altWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"text/html"}})
    childContent.Write([]byte("<html>HTML goes here\r\n</html>\r\n"))
    altWriter.Close()

    relatedWriter.Close()

    // Attachments
    filename := fmt.Sprintf("File_%d.jpg", rand.Int31())
    var fileContent io.Writer

    fileContent, _ = mixedWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {"application/octet-stream"}, "Content-Disposition": {"attachment; filename=" + filename}})
    fileContent.Write([]byte("AABBCCDDEEFF"))

    mixedWriter.Close()

    fmt.Print(`From: Bob <bob@example.com>
To: Alice <alias@example.com>
Subject: Formatted text mail
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary=`)
    fmt.Print(mixedWriter.Boundary(), "\n\n")
    fmt.Println(mixedContent.String())

}

func nestedMultipart(enclosingWriter *multipart.Writer, contentType, boundary string) (nestedWriter *multipart.Writer, newBoundary string) {

    var contentBuffer io.Writer
    var err error

    boundary = first70(boundary)
    contentWithBoundary := contentType + "; boundary=\"" + boundary + "\""
    contentBuffer, err = enclosingWriter.CreatePart(textproto.MIMEHeader{"Content-Type": {contentWithBoundary}})
    if err != nil {
        log.Fatal(err)
    }

    nestedWriter = multipart.NewWriter(contentBuffer)
    newBoundary = nestedWriter.Boundary()
    nestedWriter.SetBoundary(boundary)
    return
}

func first70(str string) string {
    if len(str) > 70 {
        return string(str[0:69])
    }
    return str
}
Aditya Kresna Permana
  • 11,869
  • 8
  • 42
  • 48
Old Pro
  • 24,624
  • 7
  • 58
  • 106