Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a nested multipart / MIME envelope for Email in Go?

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 <[email protected]>
To: Alice <[email protected]>
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.

like image 358
Xeoncross Avatar asked Dec 12 '18 23:12

Xeoncross


People also ask

Which MIME format is nested?

doc is nested, while attachment2. doc would be a top-level part. It's in Python, and they iterate through the different parts of the message to search for attachments, but skip any part that is itself a multipart.

What is multi part message in MIME format?

A multipart/mixed MIME message is composed of a mix of different data types. Each body part is delineated by a boundary. The boundary parameter is a text string used to delineate one part of the message body from another. All boundaries start with two hyphens (--).

What is nested mail?

Nested mail merge is a powerful feature that enables you to import a relational or hierarchical data source into a template document in a single statement.

What is multipart email?

A multi-part MIME message is like a package with multiple boxes within it. In your standard HTML + text message, both types of content are sent in the email. Your email client, assuming it understands MIME format, will decide which of the boxes to open and display to you.


1 Answers

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, "mulitpart/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 <[email protected]>
To: Alice <[email protected]>
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
}
like image 173
Old Pro Avatar answered Sep 29 '22 11:09

Old Pro