diff --git a/app/inbucket/mime.go b/app/inbucket/mime.go new file mode 100644 index 0000000..8992e08 --- /dev/null +++ b/app/inbucket/mime.go @@ -0,0 +1,81 @@ +package inbucket + +import ( + "bytes" + "fmt" + "io" + "mime" + "mime/multipart" + "net/mail" +) + +const MIME_BUF_BYTES = 1024 + +type MIMEMessage struct { + text string + html string +} + +func ParseMIMEMessage(mailMsg *mail.Message) (*MIMEMessage, error) { + mimeMsg := new(MIMEMessage) + + // Parse top-level multipart + ctype := mailMsg.Header.Get("Content-Type") + mediatype, params, err := mime.ParseMediaType(ctype) + if err != nil { + return nil, err + } + switch mediatype { + case "multipart/alternative": + // Good + default: + return nil, fmt.Errorf("Unknown mediatype: %v", mediatype) + } + boundary := params["boundary"] + if boundary == "" { + return nil, fmt.Errorf("Unable to locate boundary param in Content-Type header") + } + + // Buffer used by Part.Read to hand us chunks of data + readBytes := make([]byte, MIME_BUF_BYTES) + + // Loop over MIME parts + mr := multipart.NewReader(mailMsg.Body, boundary) + for { + part, err := mr.NextPart() + if err != nil { + if err == io.EOF { + // This is a clean end-of-message signal + break + } + return nil, err + } + mediatype, params, err = mime.ParseMediaType(part.Header.Get("Content-Type")) + if err != nil { + return nil, err + } + if mediatype == "text/plain" && mimeMsg.text == "" { + // First text section, we'll have that, but first we have to deal with + // the odd way Part lets us read data. + var buf bytes.Buffer + for { + n, err := part.Read(readBytes) + if err != nil { + if err == io.EOF { + // Clean end of part signal + break + } + return nil, err + } + // Extra data in readBytes is not cleared, so we must respect + // the value returned in 'n' + buf.Write(readBytes[:n]) + } + + mimeMsg.text = buf.String() + fmt.Printf("text: '%v'\n", mimeMsg.text) + } + } + + return mimeMsg, nil +} diff --git a/app/inbucket/mime_test.go b/app/inbucket/mime_test.go new file mode 100644 index 0000000..52c8df4 --- /dev/null +++ b/app/inbucket/mime_test.go @@ -0,0 +1,28 @@ +package inbucket + +import ( + "bufio" + "net/mail" + "os" + "testing" +) + +func TestSomething(t *testing.T) { + // Open test email for parsing + raw, err := os.Open("../../test-data/html-mime-attach.raw") + if err != nil { + t.Fatalf("Failed to open test data: %v", err) + } + + // Parse email into a mail.Message object like we do + reader := bufio.NewReader(raw) + msg, err := mail.ReadMessage(reader) + if err != nil { + t.Fatalf("Failed to read message: %v", err) + } + + _, err = ParseMIMEMessage(msg) + if err != nil { + t.Fatalf("Failed to parse mime: %v", err) + } +} diff --git a/test-data/html-mime-attach.raw b/test-data/html-mime-attach.raw new file mode 100644 index 0000000..72573a3 --- /dev/null +++ b/test-data/html-mime-attach.raw @@ -0,0 +1,54 @@ +From: James Hillyerd +Content-Type: multipart/alternative; boundary="Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6" +Subject: MIME test 1 +Date: Sat, 13 Oct 2012 15:33:07 -0700 +Message-Id: <4E2E5A48-1A2C-4450-8663-D41B451DA93E@makita.skynet> +To: greg@nobody.com +Mime-Version: 1.0 (Apple Message framework v1283) +X-Mailer: Apple Mail (2.1283) + + +--Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6 +Content-Transfer-Encoding: 7bit +Content-Type: text/plain; + charset=us-ascii + +Test of HTML section +--Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6 +Content-Type: multipart/related; + type="text/html"; + boundary="Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5" + + +--Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 +Content-Transfer-Encoding: 7bit +Content-Type: text/html; + charset=us-ascii + +Test of HTML section +--Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 +Content-Transfer-Encoding: base64 +Content-Disposition: inline; + filename=favicon.png +Content-Type: image/png; + x-unix-mode=0644; + name="favicon.png" +Content-Id: <8B8481A2-25CA-4886-9B5A-8EB9115DD064@skynet> + +iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ +bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN +bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd +HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap +XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb +yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB +VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 +lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 +NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU +d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 +pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D +cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu +QmCC + +--Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5-- + +--Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6--