// IsAttachment returns true, if the given header defines an attachment. // First it checks, if the Content-Disposition header defines an attachement. // If this test is false, the Content-Type header is checked. // // Valid Attachment-Headers: // // Content-Disposition: attachment; filename="frog.jpg" // Content-Type: attachment; filename="frog.jpg" // func IsAttachment(header mail.Header) bool { mediatype, _, _ := mime.ParseMediaType(header.Get("Content-Disposition")) if strings.ToLower(mediatype) == "attachment" { return true } mediatype, _, _ = mime.ParseMediaType(header.Get("Content-Type")) if strings.ToLower(mediatype) == "attachment" { return true } return false }
// ParseMIME reads a MIME document from the provided reader and parses it into // tree of MIMEPart objects. func ParseMIME(reader *bufio.Reader) (MIMEPart, error) { tr := textproto.NewReader(reader) header, err := tr.ReadMIMEHeader() if err != nil { if !strings.HasPrefix(err.Error(), "malformed MIME header") { return nil, err } } mediatype, params, err := mime.ParseMediaType(header.Get("Content-Type")) if err != nil && mime.IsOkPMTError(err) != nil { return nil, err } root := &memMIMEPart{header: header, contentType: mediatype} if strings.HasPrefix(mediatype, "multipart/") { boundary := params["boundary"] err = parseParts(root, reader, boundary) if err != nil { return nil, err } } else { // Content is text or data, decode it content, err := decodeSection(header.Get("Content-Transfer-Encoding"), reader) if err != nil { return nil, err } root.content = content } return root, nil }
func (p *Part) parseContentDisposition() { v := p.Header.Get("Content-Disposition") var err error p.disposition, p.dispositionParams, err = mime.ParseMediaType(v) if err != nil && mime.IsOkPMTError(err) != nil { p.dispositionParams = emptyParams } }
// Returns a MIME message with only one Attachment, the parsed original mail body. func binMIME(mailMsg *mail.Message) (*MIMEBody, error) { // Root Node of our tree ctype := mailMsg.Header.Get("Content-Type") mediatype, mparams, err := mime.ParseMediaType(ctype) if err != nil { mediatype = "attachment" } m := &MIMEBody{ header: mailMsg.Header, Root: NewMIMEPart(nil, mediatype), IsTextFromHTML: false, } p := NewMIMEPart(nil, mediatype) p.content, err = decodeSection(mailMsg.Header.Get("Content-Transfer-Encoding"), mailMsg.Body) if err != nil { return nil, err } // get set headers p.header = make(textproto.MIMEHeader, 4) // Figure out our disposition, filename disposition, dparams, err := mime.ParseMediaType(mailMsg.Header.Get("Content-Disposition")) if err == nil { // Disposition is optional p.disposition = disposition p.fileName = DecodeHeader(dparams["filename"]) } if p.fileName == "" && mparams["name"] != "" { p.fileName = DecodeHeader(mparams["name"]) } if p.fileName == "" && mparams["file"] != "" { p.fileName = DecodeHeader(mparams["file"]) } if p.charset == "" { p.charset = mparams["charset"] } p.header.Set("Content-Type", mailMsg.Header.Get("Content-Type")) p.header.Set("Content-Disposition", mailMsg.Header.Get("Content-Disposition")) m.Attachments = append(m.Attachments, p) return m, err }
// IsPlain returns true, if the the mime headers define a valid // 'text/plain' or 'text/html part'. Ff emptyContentTypeIsPlain is set // to true, a missing Content-Type header will result in a positive // plain part detection. func IsPlain(header mail.Header, emptyContentTypeIsPlain bool) bool { ctype := header.Get("Content-Type") if ctype == "" && emptyContentTypeIsPlain { return true } mediatype, _, err := mime.ParseMediaType(ctype) if err != nil { return false } switch mediatype { case "text/plain", "text/html": return true } return false }
// IsMultipartMessage returns true if the message has a recognized multipart Content-Type // header. You don't need to check this before calling ParseMIMEBody, it can handle // non-multipart messages. func IsMultipartMessage(mailMsg *mail.Message) bool { // Parse top-level multipart ctype := mailMsg.Header.Get("Content-Type") mediatype, _, err := mime.ParseMediaType(ctype) if err != nil && mime.IsOkPMTError(err) != nil { return false } switch mediatype { case "multipart/alternative", "multipart/mixed", "multipart/related", "multipart/signed": return true default: if strings.HasPrefix(mediatype, "multipart/") { // according to rfc2046#section-5.1.7 all other multipart should // be treated as multipart/mixed return true } } return false }
// parseParts recursively parses a mime multipart document. func parseParts(parent *memMIMEPart, reader io.Reader, boundary string) error { var prevSibling *memMIMEPart // Loop over MIME parts mr := multipart.NewReader(reader, boundary) for { // mrp is golang's built in mime-part mrp, err := mr.NextPart() if err != nil { if err == io.EOF { // This is a clean end-of-message signal break } else if strings.HasPrefix(err.Error(), "malformed MIME header") { // ignore this type of error and continue to process and valid MIME header //log.Println("debug: malformed MIME header - ignore it", len(mrp.Header)) } else if strings.HasSuffix(err.Error(), "EOF") { //log.Println("debug: type of EOF failure:", err) if mrp == nil { //log.Println("debug: next part is empty") break } } else { return err } } if len(mrp.Header) == 0 { // // Empty header probably means the part didn't using the correct trailing "--" // // syntax to close its boundary. We will let this slide if this this the // // last MIME part. // if _, err := mr.NextPart(); err != nil { // if err == io.EOF || strings.HasSuffix(err.Error(), "EOF") { // // This is what we were hoping for // break // } else { // return fmt.Errorf("Error at boundary %v: %v", boundary, err) // } // } // return fmt.Errorf("Empty header at boundary %v", boundary) if errEOF := mr.CheckNextPart(); errEOF != nil { if errEOF == io.EOF || strings.HasSuffix(errEOF.Error(), "EOF") { // This is what we were hoping for. And to remain the ability // to detect the empty MIME header caused by improper boundary // ending. break } } // empty header field inside mime part body should not treat as error as // MIME is allowed to have empty header and straight to the body content. mrp.Header.Add("Content-Type", default_content_type) } ctype := mrp.Header.Get("Content-Type") if ctype == "" { //return fmt.Errorf("Missing Content-Type at boundary %v", boundary) // can not find Content-Type header does not mean error //log.Println("debug: can not found content-type - use default") mrp.Header.Add("Content-Type", default_content_type) ctype = mrp.Header.Get("Content-Type") } mediatype, mparams, err := mime.ParseMediaType(ctype) if err != nil && mime.IsOkPMTError(err) != nil { //log.Println("debug: parse parts media type error") return err } // Insert ourselves into tree, p is enmime's mime-part p := NewMIMEPart(parent, mediatype) p.header = mrp.Header if prevSibling != nil { prevSibling.nextSibling = p } else { parent.firstChild = p } prevSibling = p // Figure out our disposition, filename disposition, dparams, err := mime.ParseMediaType(mrp.Header.Get("Content-Disposition")) if err == nil || mime.IsOkPMTError(err) == nil { // Disposition is optional p.disposition = disposition p.fileName = DecodeHeader(dparams["filename"]) } if p.fileName == "" && mparams["name"] != "" { p.fileName = DecodeHeader(mparams["name"]) } if p.fileName == "" && mparams["file"] != "" { p.fileName = DecodeHeader(mparams["file"]) } if p.charset == "" { p.charset = mparams["charset"] } boundary := mparams["boundary"] if boundary != "" && !strings.HasPrefix(mediatype, "text/") { // Content is another multipart err = parseParts(p, mrp, boundary) if err != nil { return err } } else { // Content is text or data, decode it d := mrp.Header.Get("Content-Transfer-Encoding") if mediatype == "message/rfc822" { switch strings.ToLower(d) { case "7bit", "8bit", "binary": default: d = "" // force no decoding } } data, err := decodeSection(d, mrp) if err != nil { return err } p.content = data } } return nil }
// ParseMIMEBody parses the body of the message object into a tree of MIMEPart objects, // each of which is aware of its content type, filename and headers. If the part was // encoded in quoted-printable or base64, it is decoded before being stored in the // MIMEPart object. func ParseMIMEBody(mailMsg *mail.Message) (*MIMEBody, error) { var gerr error mimeMsg := &MIMEBody{ IsTextFromHTML: false, header: mailMsg.Header, } if !IsMultipartMessage(mailMsg) { // Attachment only? if IsBinaryBody(mailMsg) { return binMIME(mailMsg) } // Parse as text only bodyBytes, err := decodeSection(mailMsg.Header.Get("Content-Transfer-Encoding"), mailMsg.Body) if err != nil { return nil, fmt.Errorf("Error decoding text-only message: %v", err) } // Handle plain ASCII text, content-type unspecified mimeMsg.Text = string(bodyBytes) // Check for HTML at top-level, eat errors quietly ctype := mailMsg.Header.Get("Content-Type") if ctype != "" { if mediatype, mparams, err := mime.ParseMediaType(ctype); err == nil || mime.IsOkPMTError(err) == nil { /* *Content-Type: text/plain;\t charset="hz-gb-2312" */ if mparams["charset"] != "" { // Convert plain text to UTF8 if content type specified a charset newStr, err := ConvertToUTF8String(mparams["charset"], bodyBytes) if err != nil && newStr == "" { return nil, err } else { if err != nil { gerr = err } mimeMsg.Text = newStr } } else if mediatype == "text/html" { // charset is empty, look in html body for charset charset, err := charsetFromHTMLString(mimeMsg.Text) if charset != "" && err == nil { newStr, err := ConvertToUTF8String(charset, bodyBytes) if err != nil && newStr == "" { return nil, err } else { if err != nil { gerr = err } mimeMsg.Text = newStr } } } if mediatype == "text/html" { mimeMsg.HTML = mimeMsg.Text // Empty Text will trigger html2text conversion below mimeMsg.Text = "" } } } } else { // Parse top-level multipart ctype := mailMsg.Header.Get("Content-Type") mediatype, params, err := mime.ParseMediaType(ctype) if err != nil && mime.IsOkPMTError(err) != nil { return nil, fmt.Errorf("Unable to parse media type: %v", err) } if !strings.HasPrefix(mediatype, "multipart/") { 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") } // Root Node of our tree root := NewMIMEPart(nil, mediatype) mimeMsg.Root = root err = parseParts(root, mailMsg.Body, boundary) if err != nil { return nil, err } // Locate text body if mediatype == "multipart/altern" { log.Println("surPrise: should not be here go.enmime/mime.go") match := BreadthMatchFirst(root, func(p MIMEPart) bool { return p.ContentType() == "text/plain" && p.Disposition() != "attachment" }) if match != nil { if match.Charset() != "" { newStr, err := ConvertToUTF8String(match.Charset(), match.Content()) if err != nil { if newStr == "" { return nil, err } else { gerr = err } } mimeMsg.Text += newStr } else { mimeMsg.Text += string(match.Content()) } } } else { // multipart is of a mixed type match := DepthMatchAll(root, func(p MIMEPart) bool { return p.ContentType() == "text/plain" && p.Disposition() != "attachment" }) for i, m := range match { if i > 0 { mimeMsg.Text += "\n--\n" } if m.Charset() != "" { newStr, err := ConvertToUTF8String(m.Charset(), m.Content()) if err != nil { if newStr == "" { return nil, err } else { gerr = err } } mimeMsg.Text += newStr } else { mimeMsg.Text += string(m.Content()) } } } // Locate HTML body match := BreadthMatchFirst(root, func(p MIMEPart) bool { return p.ContentType() == "text/html" && p.Disposition() != "attachment" }) if match != nil { if match.Charset() != "" { newStr, err := ConvertToUTF8String(match.Charset(), match.Content()) if err != nil { if newStr == "" { return nil, err } else { gerr = err } } mimeMsg.HTML += newStr } else { mimeMsg.HTML = string(match.Content()) } } // Locate attachments mimeMsg.Attachments = BreadthMatchAll(root, func(p MIMEPart) bool { return p.Disposition() == "attachment" || p.ContentType() == "application/octet-stream" }) // Locate inlines mimeMsg.Inlines = BreadthMatchAll(root, func(p MIMEPart) bool { return p.Disposition() == "inline" }) // Locate others parts not handled in "Attachments" and "inlines" mimeMsg.OtherParts = BreadthMatchAll(root, func(p MIMEPart) bool { if strings.HasPrefix(p.ContentType(), "multipart/") { return false } if p.Disposition() != "" { return false } if p.ContentType() == "application/octet-stream" { return false } return p.ContentType() != "text/plain" && p.ContentType() != "text/html" }) } // Down-convert HTML to text if necessary if mimeMsg.Text == "" && mimeMsg.HTML != "" { mimeMsg.IsTextFromHTML = true var err error if mimeMsg.Text, err = html2text.FromString(mimeMsg.HTML); err != nil { // Fail gently mimeMsg.Text = "" return mimeMsg, err } } return mimeMsg, gerr }