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.
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.
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 (--).
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.
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.
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With