func selectMultiPart(msg *mail.Message, boundary, preferType string) (*mail.Message, error) { reader := multipart.NewReader(msg.Body, boundary) parts := make(map[string]*mail.Message) for part, err := reader.NextPart(); err != io.EOF; part, err = reader.NextPart() { if err != nil { return nil, err } header := mail.Header(part.Header) mediatype, _, err := mime.ParseMediaType(header.Get("Content-Type")) if err != nil { continue } if mediatype == preferType { return &mail.Message{header, part}, nil } types := strings.Split(mediatype, "/") if len(types) == 0 { continue } if _, ok := parts[types[0]]; !ok { body, err := ioutil.ReadAll(part) if err == nil { parts[types[0]] = &mail.Message{header, bytes.NewBuffer(body)} } } } types := strings.Split(preferType, "/") if part, ok := parts[types[0]]; ok { return part, nil } if part, ok := parts["multipart"]; ok { return part, nil } return nil, fmt.Errorf("No prefered part") }
func TestMailAddress(t *testing.T) { Log.SetHandler(tsthlp.TestHandler(t)) for i, str := range [][3]string{ [3]string{"=?iso-8859-2?Q?Bogl=E1rka_Tak=E1cs?= <*****@*****.**>", "Boglárka Takács", "<*****@*****.**>"}, [3]string{"=?iso-8859-2?Q?K=E1r_Oszt=E1ly_=28kar=40kobe=2Ehu=29?= <*****@*****.**>", "Kár_Osztály [email protected]", "<*****@*****.**>"}, } { mh := make(map[string][]string, 1) k := strconv.Itoa(i) mh[k] = []string{str[0]} mailHeader := mail.Header(mh) if addr, err := mailHeader.AddressList(k); err == nil { t.Errorf("address for %s: %q", k, addr) } else { t.Logf("error parsing address %s(%q): %s", k, mailHeader.Get(k), err) } if addr, err := ParseAddress(str[0]); err != nil { t.Errorf("error parsing address %q: %s", str[0], err) } else { t.Logf("address for %q: %q <%s>", str[0], addr.Name, addr.Address) } } }
func TestParseQuotedBody(t *testing.T) { header := mail.Header(map[string][]string{ "Content-Type": []string{"text/html; charset=\"iso-8859-1\""}, "Content-Transfer-Encoding": []string{"quoted-printable"}, }) html, text, multipart, err := parseBody(header, []byte(quotedEmail)) if err != nil { t.Error("error doing test! ", err) return } if multipart { t.Error("parse body returned with multipart flag set") } if len(text) > 0 { t.Errorf("an HTML only email returned with text: %q", string(text)) } if bytes.Contains(html, []byte("=3D")) { t.Errorf("parseBody did not handle quoted-printable format as expected. `=3D' still found in message:\n%q", string(html)) } // crazy Äpple to verify we parsed iso-8859-1 correctly AND did the quoted-printable fix if !bytes.HasPrefix(html, []byte("Äpple<html><head>")) || !bytes.HasSuffix(html, []byte("</html>\r\n")) { t.Errorf("parseBody did not pull out HTML correctly. Expected '<html><head>' prefix and '</html>\r\n' suffix. got:\n%q", string(html)) } }
func (h ciHeader) AddressList(key string) ([]*mail.Address, error) { header := mail.Header(h) // First try with stdlib implementation addresses, err := header.AddressList(key) if err == nil { return addresses, nil } value, found := header[key] if !found { return nil, nil } // "Forced" parsing of non-standard e-mail addresses return h.parseNonstandardAddressesList(strings.Join(value, " ")) }
// Header returns the corresponding mail header to a key. func (d Dir) Header(key string) (header mail.Header, err error) { filename, err := d.Filename(key) if err != nil { return } file, err := os.Open(filename) if err != nil { return } defer file.Close() tp := textproto.NewReader(bufio.NewReader(file)) hdr, err := tp.ReadMIMEHeader() if err != nil { return } header = mail.Header(hdr) return }
func TestParseMultiBody(t *testing.T) { header := mail.Header(map[string][]string{"Content-Type": []string{`multipart/alternative; boundary="eZDakj4l4DVQ=_?:"`}}) html, text, multipart, err := parseBody(header, []byte(multipartEmail)) if err != nil { t.Error("error doing test! ", err) return } if !multipart { t.Error("parse body did not return with multipart flag set") } if !bytes.HasPrefix(html, []byte("<!DOCTYPE html>")) || !bytes.HasSuffix(html, []byte("</html>\n")) { t.Errorf("parseBody did not pull out the HTML correctly. Expected to start with doctype and end with </html>, got:\n%q", string(html)) } if !bytes.HasPrefix(text, []byte("NBA announces")) || !bytes.HasSuffix(text, []byte("approved Ballmer.\n")) { t.Errorf("parseBody did not pull out text correctly. Expected 'NBA announces' prefix and 'Ballmer.' suffix. got:\n%q", string(text)) } }
func TestParsePlainBody(t *testing.T) { header := mail.Header(map[string][]string{"Content-Type": []string{"text/html; charset=UTF-8"}}) html, text, multipart, err := parseBody(header, []byte(htmlEmail)) if err != nil { t.Error("error doing test! ", err) return } if multipart { t.Error("parse body returned with multipart flag set") } if len(text) > 0 { t.Errorf("an HTML only email returned with text: %q", string(text)) } if !bytes.HasPrefix(html, []byte("<br />")) || !bytes.HasSuffix(html, []byte("FoxNews.com.")) { t.Errorf("parseBody did not pull out HTML correctly. Expected '<br />' prefix and 'FoxNews.com.' suffix. got:\n%q", string(html)) } }
func compose() (*mail.Message, error) { // Make sure we can re-use Stdin even after being consumed by mail.ReadMessage b := bytes.Buffer{} b.ReadFrom(os.Stdin) msg := b.String() m, err := mail.ReadMessage(bytes.NewBufferString(msg)) if err != nil { if config.ScanMessage { return nil, fmt.Errorf("ScanMessage: cannot parse message: %s", err) } // Assume there are no headers in the message m = &mail.Message{ Header: mail.Header(textproto.MIMEHeader{}), Body: bufio.NewReader(bytes.NewBufferString(msg)), } } // Make sure all required fields are set if 0 == len(m.Header["From"]) { m.Header["From"] = []string{(&mail.Address{config.Message_FromName, config.Message_From}).String()} } else if from, err := mail.ParseAddress(m.Header["From"][0]); config.ScanMessage && err == nil { // Parse and put in config; to be used by c.Mail config.Message_From = from.Address } if 0 == len(m.Header["To"]) { m.Header["To"] = config.Message_To } if 0 == len(m.Header["Date"]) { m.Header["Date"] = []string{time.Now().Format("Mon, 2 Jan 2006 15:04:05 -0700")} } if 0 == len(m.Header["Message-Id"]) { m.Header["Message-Id"] = []string{"<GOSSMTP." + generateMessageId() + "@" + config.Hostname + ">"} } return m, nil }
func (c *IMAPClient) GetMessage(id string) (*mail.Message, error) { headerResp := c.Do(fmt.Sprintf("FETCH %s %s", id, RFC822Header)) if headerResp.Error() != nil { return nil, headerResp.Error() } replys := headerResp.Replys() reader := textproto.NewReader(bufio.NewReader(bytes.NewBuffer(replys[0].content))) header, err := reader.ReadMIMEHeader() if err != nil { return nil, err } bodyResp := c.Do(fmt.Sprintf("FETCH %s %s", id, RFC822Text)) if bodyResp.Error() != nil { return nil, bodyResp.Error() } return &mail.Message{ Header: mail.Header(header), Body: bytes.NewBuffer(bodyResp.Replys()[0].content), }, nil }
func TestSelectPart(t *testing.T) { { hs := `MIME-Version: 1.0 Received: by 10.76.101.172 with HTTP; Tue, 26 Jun 2012 23:11:28 -0700 (PDT) Date: Wed, 27 Jun 2012 14:11:28 +0800 Delivered-To: [email protected] Message-ID: <*****@*****.**> Subject: test From: Googol Lee <*****@*****.**> To: =?UTF-8?B?R29vZ29sIExlZSAtIEdvb2dsZee6r+eIt+S7rO+8gemTgeihgOecn+axieWtkO+8ge+8gQ==?= <*****@*****.**> Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: base64` bd := `YTAwNyBPSyBTdWNjZXNzCgopCgotLQrmlrDnmoTnkIborrrku47lsJHmlbDkurrnmoTkuLvlvKDl iLDkuIDnu5/lpKnkuIvvvIzlubbkuI3mmK/lm6DkuLrov5nkuKrnkIborrror7TmnI3kuobliKvk urrmipvlvIPml6fop4LngrnvvIzogIzmmK/lm6DkuLrkuIDku6PkurrnmoTpgJ3ljrvjgIIK` reader := textproto.NewReader(bufio.NewReader(bytes.NewBufferString(hs))) hr, _ := reader.ReadMIMEHeader() msg := &mail.Message{ Header: mail.Header(hr), Body: bytes.NewBufferString(bd), } expect := "a007 OK Success\n\n)\n\n--\n新的理论从少数人的主张到一统天下,并不是因为这个理论说服了别人抛弃旧观点,而是因为一代人的逝去。\n" content, mediatype, charset, _ := GetBody(msg, "text/plain") if mediatype != "text/plain" { t.Errorf("mediatype expect: text/plain, got: %s", mediatype) } if charset != "UTF-8" { t.Errorf("charset expect: UTF-8, got: %s", charset) } if content != expect { t.Errorf("content expect:\n%s\ngot:\n%s", expect, content) } } { hs := `MIME-Version: 1.0 Received: by 10.76.101.172 with HTTP; Tue, 26 Jun 2012 23:11:28 -0700 (PDT) Date: Wed, 27 Jun 2012 14:11:28 +0800 Delivered-To: [email protected] Message-ID: <*****@*****.**> Subject: test From: Googol Lee <*****@*****.**> To: =?UTF-8?B?R29vZ29sIExlZSAtIEdvb2dsZee6r+eIt+S7rO+8gemTgeihgOecn+axieWtkO+8ge+8gQ==?= <*****@*****.**> Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: quoted-printable` bd := "If you believe that truth=3Dbeauty, then surely =\r\nmathematics is the most beautiful branch of philosophy." reader := textproto.NewReader(bufio.NewReader(bytes.NewBufferString(hs))) hr, _ := reader.ReadMIMEHeader() msg := &mail.Message{ Header: mail.Header(hr), Body: bytes.NewBufferString(bd), } expect := "If you believe that truth=beauty, then surely mathematics is the most beautiful branch of philosophy." content, mediatype, charset, _ := GetBody(msg, "text/plain") if mediatype != "text/plain" { t.Errorf("mediatype expect: text/plain, got: %s", mediatype) } if charset != "UTF-8" { t.Errorf("charset expect: UTF-8, got: %s", charset) } if content != expect { t.Errorf("content expect:\n%s\ngot:\n%s", expect, content) } } { hs := `MIME-Version: 1.0 Received: by 10.76.101.172 with HTTP; Tue, 26 Jun 2012 23:11:28 -0700 (PDT) Date: Wed, 27 Jun 2012 14:11:28 +0800 Delivered-To: [email protected] Message-ID: <*****@*****.**> Subject: test From: Googol Lee <*****@*****.**> To: =?UTF-8?B?R29vZ29sIExlZSAtIEdvb2dsZee6r+eIt+S7rO+8gemTgeihgOecn+axieWtkO+8ge+8gQ==?= <*****@*****.**> Content-Type: multipart/alternative; boundary=14dae9d67df4436caa04c39ae8b8` bd := `--14dae9d67df4436caa04c39ae8b8 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: base64 YWJjYWJjCgotLSAK5paw55qE55CG6K665LuO5bCR5pWw5Lq655qE5Li75byg5Yiw5LiA57uf5aSp 5LiL77yM5bm25LiN5piv5Zug5Li66L+Z5Liq55CG6K666K+05pyN5LqG5Yir5Lq65oqb5byD5pen 6KeC54K577yM6ICM5piv5Zug5Li65LiA5Luj5Lq655qE6YCd5Y6744CCCg== --14dae9d67df4436caa04c39ae8b8 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: base64 YWJjYWJjPGJyPjxicj4tLSA8YnI+5paw55qE55CG6K665LuO5bCR5pWw5Lq655qE5Li75byg5Yiw 5LiA57uf5aSp5LiL77yM5bm25LiN5piv5Zug5Li66L+Z5Liq55CG6K666K+05pyN5LqG5Yir5Lq6 5oqb5byD5pen6KeC54K577yM6ICM5piv5Zug5Li65LiA5Luj5Lq655qE6YCd5Y6744CCPGJyPgo= --14dae9d67df4436caa04c39ae8b8--` reader := textproto.NewReader(bufio.NewReader(bytes.NewBufferString(hs))) hr, _ := reader.ReadMIMEHeader() msg := &mail.Message{ Header: mail.Header(hr), Body: bytes.NewBufferString(bd), } expect := "abcabc\n\n-- \n新的理论从少数人的主张到一统天下,并不是因为这个理论说服了别人抛弃旧观点,而是因为一代人的逝去。\n" content, mediatype, charset, _ := GetBody(msg, "text/plain") if mediatype != "text/plain" { t.Errorf("mediatype expect: text/plain, got: %s", mediatype) } if charset != "UTF-8" { t.Errorf("charset expect: UTF-8, got: %s", charset) } if content != expect { t.Errorf("content expect:\n%s\ngot:\n%s", expect, content) } } { hs := `MIME-Version: 1.0 Received: by 10.76.101.172 with HTTP; Tue, 26 Jun 2012 23:11:28 -0700 (PDT) Date: Wed, 27 Jun 2012 14:11:28 +0800 Delivered-To: [email protected] Message-ID: <*****@*****.**> Subject: test From: Googol Lee <*****@*****.**> To: =?UTF-8?B?R29vZ29sIExlZSAtIEdvb2dsZee6r+eIt+S7rO+8gemTgeihgOecn+axieWtkO+8ge+8gQ==?= <*****@*****.**> Content-Type: multipart/alternative; boundary=14dae9d67df4436caa04c39ae8b8` bd := `--14dae9d67df4436caa04c39ae8b8 Content-Type: text/json; charset=UTF-8 Content-Transfer-Encoding: base64 YWJjYWJjCgotLSAK5paw55qE55CG6K665LuO5bCR5pWw5Lq655qE5Li75byg5Yiw5LiA57uf5aSp 5LiL77yM5bm25LiN5piv5Zug5Li66L+Z5Liq55CG6K666K+05pyN5LqG5Yir5Lq65oqb5byD5pen 6KeC54K577yM6ICM5piv5Zug5Li65LiA5Luj5Lq655qE6YCd5Y6744CCCg== --14dae9d67df4436caa04c39ae8b8 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: base64 YWJjYWJjPGJyPjxicj4tLSA8YnI+5paw55qE55CG6K665LuO5bCR5pWw5Lq655qE5Li75byg5Yiw 5LiA57uf5aSp5LiL77yM5bm25LiN5piv5Zug5Li66L+Z5Liq55CG6K666K+05pyN5LqG5Yir5Lq6 5oqb5byD5pen6KeC54K577yM6ICM5piv5Zug5Li65LiA5Luj5Lq655qE6YCd5Y6744CCPGJyPgo= --14dae9d67df4436caa04c39ae8b8--` reader := textproto.NewReader(bufio.NewReader(bytes.NewBufferString(hs))) hr, _ := reader.ReadMIMEHeader() msg := &mail.Message{ Header: mail.Header(hr), Body: bytes.NewBufferString(bd), } expect := "abcabc\n\n-- \n新的理论从少数人的主张到一统天下,并不是因为这个理论说服了别人抛弃旧观点,而是因为一代人的逝去。\n" content, mediatype, charset, _ := GetBody(msg, "text/plain") if mediatype != "text/json" { t.Errorf("mediatype expect: text/plain, got: %s", mediatype) } if charset != "UTF-8" { t.Errorf("charset expect: UTF-8, got: %s", charset) } if content != expect { t.Errorf("content expect:\n%s\ngot:\n%s", expect, content) } } }
// returns the content-type, params and a decoder for the body of the multipart func getCT( header map[string][]string, ) ( contentType string, params map[string]string, decoder func(io.Reader) io.Reader, err error, ) { decoder = func(r io.Reader) io.Reader { return r } contentType = mail.Header(header).Get("Content-Type") if contentType == "" { return } var nct string nct, params, err = mime.ParseMediaType(contentType) if err != nil { err = errgo.Newf("cannot parse Content-Type %s: %s", contentType, err) return } contentType = nct te := strings.ToLower(mail.Header(header).Get("Content-Transfer-Encoding")) switch te { case "": case "base64": decoder = func(r io.Reader) io.Reader { r = B64FilterReader(r, nil) if CheckEncoding { raw, e := ioutil.ReadAll(r) if e != nil { logger.Warn("msg", "cannot read data", "error", e) return bytes.NewReader(nil) } decoded := make([]byte, base64.StdEncoding.DecodedLen(len(raw))) n, e := base64.StdEncoding.Decode(decoded, raw) if e != nil { bad := string(raw[:min(20, len(raw))]) txt := e.Error() q := strings.LastIndex(txt, " ") if q >= 0 { p, e2 := strconv.Atoi(txt[q+1:]) if e2 == nil { p = max(0, p-4) q = min(len(raw), p+4) bad = string(raw[p:q]) } } logger.Error("msg", "base64 decoding", "raw", bad, "error", e) } // K-MT9461 return bytes.NewReader(decoded[:n]) } return base64.NewDecoder(base64.StdEncoding, r) } case "quoted-printable": decoder = func(r io.Reader) io.Reader { br := bufio.NewReaderSize(r, 1024) first, _ := br.Peek(1024) enc := qprintable.BinaryEncoding if len(first) > 0 { enc = qprintable.DetectEncoding(string(first)) } return qprintable.NewDecoder(enc, br) } default: logger.Warn("msg", "unknown transfer-encoding", "transfer-encoding", te) } return }
// PrependHeaderFilter writes Subject, From... headers at the beginning of the html/plain parts. func PrependHeaderFilter(inch <-chan i18nmail.MailPart, outch chan<- i18nmail.MailPart, files chan<- ArchFileItem, errch chan<- error, ctx *Context, ) { defer func() { logger.Debug("msg", "PrependHeaderFilter closes", "outch", outch) close(outch) }() ctx = prepareContext(ctx, "") mailHeader := mail.Header(make(map[string][]string, len(PrependHeaders))) headersBuf := bytes.NewBuffer(make([]byte, 0, 128)) var br *bufio.Reader for part := range inch { logger.Debug("msg", "PrependHeaderFilter receives", "seq", part.Seq, "ct", part.ContentType, "header", part.Header, "inch", inch) if len(mailHeader["From"]) == 0 || mailHeader["Subject"][0] == "" { hdrs := make([]textproto.MIMEHeader, 0, 4) parent := &part if len(parent.Header) > 0 { hdrs = append(hdrs, parent.Header) } for parent.Parent != nil { parent = parent.Parent if len(parent.Header) > 0 { hdrs = append(hdrs, parent.Header) } } if len(hdrs) > 0 { hdr := hdrs[len(hdrs)-1] logger.Info("msg", "filling mailHeader", "header", hdr) for _, k := range PrependHeaders { if v, ok := hdr[k]; ok { mailHeader[k] = v } } } } if !(part.ContentType == "text/plain" || part.ContentType == "text/html") { goto Skip } headersBuf.Reset() if err := writeHeaders(headersBuf, mailHeader, part.ContentType); err != nil { logger.Warn("msg", "error writing headers", "error", err) goto Skip } br = bufio.NewReader(part.Body) part.Body = br if part.ContentType == "text/html" { first, err := br.Peek(br.Buffered()) if err != nil { logger.Warn("msg", "cannot read", "source", br, "error", err) goto Skip } if i := bytes.Index(first, []byte("<body>")); i >= 0 { part.Body = io.MultiReader( &io.LimitedReader{R: br, N: int64(i)}, bytes.NewReader(headersBuf.Bytes()), bytes.NewReader(first[i:]), br, ) } else { part.Body = io.MultiReader(bytes.NewReader(headersBuf.Bytes()), br) } } else { part.Body = io.MultiReader(bytes.NewReader(headersBuf.Bytes()), part.Body) } Skip: outch <- part } }
func (h ciHeader) Date() (time.Time, error) { return mail.Header(h).Date() }
func analyzeEmail(n *models.EmailNode, input []byte) error { // Figure out if newlines are \n or \r\n var nl []byte firstNewline := bytes.Index(input, []byte("\n")) if firstNewline-1 >= 0 && input[firstNewline-1] == '\r' { nl = []byte("\r\n") } else { nl = []byte("\n") } doubleNl := append(nl, nl...) // Parse the header tp := textproto.NewReader(bufio.NewReader(bytes.NewReader(input))) headers, err := tp.ReadMIMEHeader() if err != nil { return err } n.Headers = mail.Header(headers) // Calculate where the parts are seperator := bytes.Index(input, doubleNl) n.HeaderPosition = [2]int{n.BasePosition, n.BasePosition + seperator + len(nl)} n.BodyPosition = [2]int{n.BasePosition + seperator + len(nl), n.BasePosition + len(input)} // Check if the node is multipart ctype := headers.Get("Content-Type") if ctype == "" { ctype = "text/plain" } media, params, err := mime.ParseMediaType(ctype) if err != nil { return err } if !strings.HasPrefix(media, "multipart/") { return nil } if _, ok := params["boundary"]; !ok { return nil } // Prepare children slice n.Children = []*models.EmailNode{} // Prepare byte slices for comparsion var ( dashBoundaryDashNl = []byte("--" + params["boundary"] + "--" + string(nl)) dashBoundaryDash = dashBoundaryDashNl[:len(dashBoundaryDashNl)-2] dashBoundaryNl = []byte("--" + params["boundary"] + string(nl)) ) // Analyze where the multipart body starts start := bytes.Index(input, dashBoundaryNl) end := bytes.Index(input, dashBoundaryDashNl) if end == -1 { end = bytes.Index(input, dashBoundaryDash) } // Trim the data to only get the body without the wrappers bodyBaseIndex := n.BasePosition + start + len(dashBoundaryNl) partsData := input[start+len(dashBoundaryNl) : end] // Split the parts into multipart body parts for _, part := range bytes.Split(partsData, dashBoundaryNl) { np := &models.EmailNode{ BasePosition: bodyBaseIndex, } if err := analyzeEmail(np, part); err != nil { return err } bodyBaseIndex += len(part) + len(dashBoundaryNl) n.Children = append(n.Children, np) } return nil }
// recursivley parse a multipart.Part // will always return a *MessageNode, even on encountering errors. You will need // good switching code to work with message nodes // If errors occur whilst creating the sub-tree, such errors will be // returned in a ChildError mapping. You may ignore such errors for the most func MessageToNode(msg *mail.Message) (*MessageNode, error) { debug("starting message to node for message: ", msg) body, backup := backupReader(msg.Body) node := &MessageNode{ Header: msg.Header, Body: backup, } ct, params, err := mime.ParseMediaType(msg.Header.Get(ContentType)) if err == nil { node.ContentType = ct node.ContentTypeParams = params if boundry, ok := params[Boundary]; ok { // parse the data as a multipart! multi := multipart.NewReader(body, boundry) child_err_occured := false child_errs := make(ChildError) node.Children = make([]*MessageNode, 0, 5) // read new Parts off the multipart parser for part, err := multi.NextPart(); err != io.EOF; part, err = multi.NextPart() { // check errors if err != nil { debug("error occured while making part: ", part, " error: ", err) // restore backup and abort parsing return node, fmt.Errorf("Aborted Multipart#NextPart at error: %v", err) } // create message so we can recurse sub_msg := &mail.Message{ Header: mail.Header(part.Header), Body: NewMarshalReader(part), } // create child node child, err := MessageToNode(sub_msg) // store any errors if err != nil { child_err_occured = true child_errs[child] = err } // store child node.Children = append(node.Children, child) } // end enumerate parts // don't need this data anymore, since we have the children node.Body = nil if child_err_occured { return node, child_errs } } // end has-boundry-parse-sections } // end decoded-mime-type if node.Body != nil { // make sure body has text export type // so the browser doesn't have to do MIME detection node.Body.MarshalAsString = IsText(node) } return node, nil }
// AddressList parses the named header field as a list of addresses. func (h Header) AddressList(key string) ([]*mail.Address, error) { return mail.Header(h).AddressList(key) }