forked from Silverfish/proton-bridge
feat: parse most header values
This commit is contained in:
@ -68,7 +68,7 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L
|
||||
// Called from go-imap in goroutines - we need to handle panics for each function.
|
||||
defer im.panicHandler.HandlePanic()
|
||||
|
||||
m, _, _, readers, err := message.Parse(body, "", "")
|
||||
m, _, _, readers, err := message.Parse(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -182,7 +182,8 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err
|
||||
attachedPublicKeyName = "publickey - " + kr.GetIdentities()[0].Name
|
||||
}
|
||||
|
||||
message, mimeBody, plainBody, attReaders, err := message.Parse(messageReader, attachedPublicKey, attachedPublicKeyName)
|
||||
// TODO: Include public keys here!
|
||||
message, mimeBody, plainBody, attReaders, err := message.Parse(messageReader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@ -19,7 +19,6 @@ package message
|
||||
|
||||
import (
|
||||
"mime"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"time"
|
||||
@ -140,75 +139,3 @@ func GetAttachmentHeader(att *pmapi.Attachment) textproto.MIMEHeader {
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// ========= Header parsing and sanitizing functions =========
|
||||
|
||||
func parseHeader(h mail.Header) (m *pmapi.Message, err error) { //nolint[unparam]
|
||||
m = pmapi.NewMessage()
|
||||
|
||||
if subject, err := pmmime.DecodeHeader(h.Get("Subject")); err == nil {
|
||||
m.Subject = subject
|
||||
}
|
||||
if addrs, err := sanitizeAddressList(h, "From"); err == nil && len(addrs) > 0 {
|
||||
m.Sender = addrs[0]
|
||||
}
|
||||
if addrs, err := sanitizeAddressList(h, "Reply-To"); err == nil && len(addrs) > 0 {
|
||||
m.ReplyTos = addrs
|
||||
}
|
||||
if addrs, err := sanitizeAddressList(h, "To"); err == nil {
|
||||
m.ToList = addrs
|
||||
}
|
||||
if addrs, err := sanitizeAddressList(h, "Cc"); err == nil {
|
||||
m.CCList = addrs
|
||||
}
|
||||
if addrs, err := sanitizeAddressList(h, "Bcc"); err == nil {
|
||||
m.BCCList = addrs
|
||||
}
|
||||
m.Time = 0
|
||||
if t, err := h.Date(); err == nil && !t.IsZero() {
|
||||
m.Time = t.Unix()
|
||||
}
|
||||
|
||||
m.Header = h
|
||||
return
|
||||
}
|
||||
|
||||
func sanitizeAddressList(h mail.Header, field string) (addrs []*mail.Address, err error) {
|
||||
raw := h.Get(field)
|
||||
if raw == "" {
|
||||
err = mail.ErrHeaderNotPresent
|
||||
return
|
||||
}
|
||||
var decoded string
|
||||
decoded, err = pmmime.DecodeHeader(raw)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
addrs, err = mail.ParseAddressList(parseAddressComment(decoded))
|
||||
if err == nil {
|
||||
if addrs == nil {
|
||||
addrs = []*mail.Address{}
|
||||
}
|
||||
return
|
||||
}
|
||||
// Probably missing encoding error -- try to at least parse addresses in brackets.
|
||||
addrStr := h.Get(field)
|
||||
first := strings.Index(addrStr, "<")
|
||||
last := strings.LastIndex(addrStr, ">")
|
||||
if first < 0 || last < 0 || first >= last {
|
||||
return
|
||||
}
|
||||
var addrList []string
|
||||
open := first
|
||||
for open < last && 0 <= open {
|
||||
addrStr = addrStr[open:]
|
||||
close := strings.Index(addrStr, ">")
|
||||
addrList = append(addrList, addrStr[:close+1])
|
||||
addrStr = addrStr[close:]
|
||||
open = strings.Index(addrStr, "<")
|
||||
last = strings.LastIndex(addrStr, ">")
|
||||
}
|
||||
addrStr = strings.Join(addrList, ", ")
|
||||
//
|
||||
return mail.ParseAddressList(addrStr)
|
||||
}
|
||||
|
||||
@ -19,466 +19,102 @@ package message
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"mime/quotedprintable"
|
||||
"net/mail"
|
||||
"net/textproto"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ProtonMail/proton-bridge/pkg/message/parser"
|
||||
pmmime "github.com/ProtonMail/proton-bridge/pkg/mime"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-message"
|
||||
"github.com/jaytaylor/html2text"
|
||||
)
|
||||
|
||||
func parseAttachment(filename string, mediaType string, h textproto.MIMEHeader) (att *pmapi.Attachment) {
|
||||
if decoded, err := pmmime.DecodeHeader(filename); err == nil {
|
||||
filename = decoded
|
||||
}
|
||||
if filename == "" {
|
||||
ext, err := mime.ExtensionsByType(mediaType)
|
||||
if err == nil && len(ext) > 0 {
|
||||
filename = "attachment" + ext[0]
|
||||
}
|
||||
}
|
||||
|
||||
att = &pmapi.Attachment{
|
||||
Name: filename,
|
||||
MIMEType: mediaType,
|
||||
Header: h,
|
||||
}
|
||||
|
||||
headerContentID := strings.Trim(h.Get("Content-Id"), " <>")
|
||||
|
||||
if headerContentID != "" {
|
||||
att.ContentID = headerContentID
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var reEmailComment = regexp.MustCompile("[(][^)]*[)]") //nolint[gochecknoglobals]
|
||||
|
||||
// parseAddressComment removes the comments completely even though they should be allowed
|
||||
// http://tools.wordtothewise.com/rfc/822
|
||||
// NOTE: This should be supported in go>1.10 but it seems it's not ¯\_(ツ)_/¯
|
||||
func parseAddressComment(raw string) string {
|
||||
return reEmailComment.ReplaceAllString(raw, "")
|
||||
}
|
||||
|
||||
// Some clients incorrectly format messages with embedded attachments to have a format like
|
||||
// I. text/plain II. attachment III. text/plain
|
||||
// which we need to convert to a single HTML part with an embedded attachment.
|
||||
func combineParts(m *pmapi.Message, parts []io.Reader, headers []textproto.MIMEHeader, convertPlainToHTML bool, atts *[]io.Reader) (isHTML bool, err error) { //nolint[funlen]
|
||||
isHTML = true
|
||||
foundText := false
|
||||
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
part := parts[i]
|
||||
h := headers[i]
|
||||
|
||||
disp, dispParams, _ := pmmime.ParseMediaType(h.Get("Content-Disposition"))
|
||||
|
||||
d := pmmime.DecodeContentEncoding(part, h.Get("Content-Transfer-Encoding"))
|
||||
if d == nil {
|
||||
log.Warnf("Unsupported Content-Transfer-Encoding '%v'", h.Get("Content-Transfer-Encoding"))
|
||||
d = part
|
||||
}
|
||||
|
||||
contentType := h.Get("Content-Type")
|
||||
if contentType == "" {
|
||||
contentType = "text/plain"
|
||||
}
|
||||
mediaType, params, _ := pmmime.ParseMediaType(contentType)
|
||||
|
||||
if strings.HasPrefix(mediaType, "text/") && mediaType != "text/calendar" && disp != "attachment" {
|
||||
// This is text.
|
||||
var b []byte
|
||||
if b, err = ioutil.ReadAll(d); err != nil {
|
||||
continue
|
||||
}
|
||||
b, err = pmmime.DecodeCharset(b, contentType)
|
||||
if err != nil {
|
||||
log.Warn("Decode charset error: ", err)
|
||||
return false, err
|
||||
}
|
||||
contents := string(b)
|
||||
if strings.Contains(mediaType, "text/plain") && len(contents) > 0 {
|
||||
if !convertPlainToHTML {
|
||||
isHTML = false
|
||||
} else {
|
||||
contents = plaintextToHTML(contents)
|
||||
}
|
||||
} else if strings.Contains(mediaType, "text/html") && len(contents) > 0 {
|
||||
contents, err = stripHTML(contents)
|
||||
if err != nil {
|
||||
return isHTML, err
|
||||
}
|
||||
}
|
||||
m.Body = contents + m.Body
|
||||
foundText = true
|
||||
} else {
|
||||
// This is an attachment.
|
||||
filename := dispParams["filename"]
|
||||
if filename == "" {
|
||||
// Using "name" in Content-Type is discouraged.
|
||||
filename = params["name"]
|
||||
}
|
||||
if filename == "" && mediaType == "text/calendar" {
|
||||
filename = "event.ics"
|
||||
}
|
||||
|
||||
att := parseAttachment(filename, mediaType, h)
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
if d == nil {
|
||||
continue
|
||||
}
|
||||
if _, err = io.Copy(b, d); err != nil {
|
||||
continue
|
||||
}
|
||||
if foundText && att.ContentID == "" && strings.Contains(mediaType, "image") {
|
||||
// Treat this as an inline attachment even though it is not marked as one.
|
||||
hasher := sha256.New()
|
||||
_, _ = hasher.Write([]byte(att.Name + strconv.Itoa(b.Len())))
|
||||
bytes := hasher.Sum(nil)
|
||||
cid := hex.EncodeToString(bytes) + "@protonmail.com"
|
||||
|
||||
att.ContentID = cid
|
||||
embeddedHTML := makeEmbeddedImageHTML(cid, att.Name)
|
||||
m.Body = embeddedHTML + m.Body
|
||||
}
|
||||
|
||||
m.Attachments = append(m.Attachments, att)
|
||||
*atts = append(*atts, b)
|
||||
}
|
||||
}
|
||||
if isHTML {
|
||||
m.Body = addOuterHTMLTags(m.Body)
|
||||
}
|
||||
return isHTML, nil
|
||||
}
|
||||
|
||||
func checkHeaders(headers []textproto.MIMEHeader) bool {
|
||||
foundAttachment := false
|
||||
|
||||
for i := 0; i < len(headers); i++ {
|
||||
h := headers[i]
|
||||
|
||||
mediaType, _, _ := pmmime.ParseMediaType(h.Get("Content-Type"))
|
||||
|
||||
if !strings.HasPrefix(mediaType, "text/") {
|
||||
foundAttachment = true
|
||||
} else if foundAttachment {
|
||||
// This means that there is a text part after the first attachment,
|
||||
// so we will have to convert the body from plain->HTML.
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ============================== 7bit Filter ==========================
|
||||
// For every MIME part in the tree that has "8bit" or "binary" content
|
||||
// transfer encoding: transcode it to "quoted-printable".
|
||||
|
||||
type SevenBitFilter struct {
|
||||
target pmmime.VisitAcceptor
|
||||
}
|
||||
|
||||
func NewSevenBitFilter(targetAccepter pmmime.VisitAcceptor) *SevenBitFilter {
|
||||
return &SevenBitFilter{
|
||||
target: targetAccepter,
|
||||
}
|
||||
}
|
||||
|
||||
func decodePart(partReader io.Reader, header textproto.MIMEHeader) (decodedPart io.Reader) {
|
||||
decodedPart = pmmime.DecodeContentEncoding(partReader, header.Get("Content-Transfer-Encoding"))
|
||||
if decodedPart == nil {
|
||||
log.Warnf("Unsupported Content-Transfer-Encoding '%v'", header.Get("Content-Transfer-Encoding"))
|
||||
decodedPart = partReader
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (sd SevenBitFilter) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) error {
|
||||
cte := strings.ToLower(header.Get("Content-Transfer-Encoding"))
|
||||
if isFirst && pmmime.IsLeaf(header) && cte != "quoted-printable" && cte != "base64" && cte != "7bit" {
|
||||
decodedPart := decodePart(partReader, header)
|
||||
|
||||
filteredHeader := textproto.MIMEHeader{}
|
||||
for k, v := range header {
|
||||
filteredHeader[k] = v
|
||||
}
|
||||
filteredHeader.Set("Content-Transfer-Encoding", "quoted-printable")
|
||||
|
||||
filteredBuffer := &bytes.Buffer{}
|
||||
decodedSlice, _ := ioutil.ReadAll(decodedPart)
|
||||
w := quotedprintable.NewWriter(filteredBuffer)
|
||||
if _, err := w.Write(decodedSlice); err != nil {
|
||||
log.Errorf("cannot write quotedprintable from %q: %v", cte, err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
log.Errorf("cannot close quotedprintable from %q: %v", cte, err)
|
||||
}
|
||||
|
||||
_ = sd.target.Accept(filteredBuffer, filteredHeader, hasPlainSibling, true, isLast)
|
||||
} else {
|
||||
_ = sd.target.Accept(partReader, header, hasPlainSibling, isFirst, isLast)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// =================== HTML Only convertor ==================================
|
||||
// In any part of MIME tree structure, replace standalone text/html with
|
||||
// multipart/alternative containing both text/html and text/plain.
|
||||
|
||||
type HTMLOnlyConvertor struct {
|
||||
target pmmime.VisitAcceptor
|
||||
}
|
||||
|
||||
func NewHTMLOnlyConvertor(targetAccepter pmmime.VisitAcceptor) *HTMLOnlyConvertor {
|
||||
return &HTMLOnlyConvertor{
|
||||
target: targetAccepter,
|
||||
}
|
||||
}
|
||||
|
||||
func randomBoundary() string {
|
||||
buf := make([]byte, 30)
|
||||
|
||||
// We specifically use `math/rand` here to allow the generator to be seeded for test purposes.
|
||||
// The random numbers need not be cryptographically secure; we are simply generating random part boundaries.
|
||||
if _, err := rand.Read(buf); err != nil { // nolint[gosec]
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%x", buf)
|
||||
}
|
||||
|
||||
func (hoc HTMLOnlyConvertor) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSiblings bool, isFirst, isLast bool) error {
|
||||
mediaType, _, err := pmmime.ParseMediaType(header.Get("Content-Type"))
|
||||
if isFirst && err == nil && mediaType == "text/html" && !hasPlainSiblings {
|
||||
multiPartHeaders := make(textproto.MIMEHeader)
|
||||
for k, v := range header {
|
||||
multiPartHeaders[k] = v
|
||||
}
|
||||
boundary := randomBoundary()
|
||||
multiPartHeaders.Set("Content-Type", "multipart/alternative; boundary=\""+boundary+"\"")
|
||||
childCte := header.Get("Content-Transfer-Encoding")
|
||||
|
||||
_ = hoc.target.Accept(partReader, multiPartHeaders, false, true, false)
|
||||
|
||||
partData, _ := ioutil.ReadAll(partReader)
|
||||
|
||||
htmlChildHeaders := make(textproto.MIMEHeader)
|
||||
htmlChildHeaders.Set("Content-Transfer-Encoding", childCte)
|
||||
htmlChildHeaders.Set("Content-Type", "text/html")
|
||||
htmlReader := bytes.NewReader(partData)
|
||||
_ = hoc.target.Accept(htmlReader, htmlChildHeaders, false, true, false)
|
||||
|
||||
_ = hoc.target.Accept(partReader, multiPartHeaders, hasPlainSiblings, false, false)
|
||||
|
||||
plainChildHeaders := make(textproto.MIMEHeader)
|
||||
plainChildHeaders.Set("Content-Transfer-Encoding", childCte)
|
||||
plainChildHeaders.Set("Content-Type", "text/plain")
|
||||
unHtmlized, err := html2text.FromReader(bytes.NewReader(partData))
|
||||
if err != nil {
|
||||
unHtmlized = string(partData)
|
||||
}
|
||||
plainReader := strings.NewReader(unHtmlized)
|
||||
_ = hoc.target.Accept(plainReader, plainChildHeaders, false, true, true)
|
||||
|
||||
_ = hoc.target.Accept(partReader, multiPartHeaders, hasPlainSiblings, false, true)
|
||||
} else {
|
||||
_ = hoc.target.Accept(partReader, header, hasPlainSiblings, isFirst, isLast)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ======= Public Key Attacher ========
|
||||
|
||||
type PublicKeyAttacher struct {
|
||||
target pmmime.VisitAcceptor
|
||||
attachedPublicKey string
|
||||
attachedPublicKeyName string
|
||||
appendToMultipart bool
|
||||
depth int
|
||||
}
|
||||
|
||||
func NewPublicKeyAttacher(targetAccepter pmmime.VisitAcceptor, attachedPublicKey, attachedPublicKeyName string) *PublicKeyAttacher {
|
||||
return &PublicKeyAttacher{
|
||||
target: targetAccepter,
|
||||
attachedPublicKey: attachedPublicKey,
|
||||
attachedPublicKeyName: attachedPublicKeyName,
|
||||
appendToMultipart: false,
|
||||
depth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func split(input string, sliceLength int) string {
|
||||
processed := input
|
||||
result := ""
|
||||
for len(processed) > 0 {
|
||||
cutPoint := min(sliceLength, len(processed))
|
||||
part := processed[0:cutPoint]
|
||||
result = result + part + "\n"
|
||||
processed = processed[cutPoint:]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func createKeyAttachment(publicKey, publicKeyName string) (headers textproto.MIMEHeader, contents io.Reader) {
|
||||
attachmentHeaders := make(textproto.MIMEHeader)
|
||||
attachmentHeaders.Set("Content-Type", "application/pgp-key; name=\""+publicKeyName+"\"")
|
||||
attachmentHeaders.Set("Content-Transfer-Encoding", "base64")
|
||||
attachmentHeaders.Set("Content-Disposition", "attachment; filename=\""+publicKeyName+".asc.pgp\"")
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
w := base64.NewEncoder(base64.StdEncoding, buffer)
|
||||
_, _ = w.Write([]byte(publicKey))
|
||||
_ = w.Close()
|
||||
|
||||
return attachmentHeaders, strings.NewReader(split(buffer.String(), 73))
|
||||
}
|
||||
|
||||
func (pka *PublicKeyAttacher) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSiblings bool, isFirst, isLast bool) error {
|
||||
if isFirst && !pmmime.IsLeaf(header) {
|
||||
pka.depth++
|
||||
}
|
||||
if isLast && !pmmime.IsLeaf(header) {
|
||||
defer func() {
|
||||
pka.depth--
|
||||
}()
|
||||
}
|
||||
isRoot := (header.Get("From") != "")
|
||||
|
||||
// NOTE: This should also work for unspecified Content-Type (in which case us-ascii text/plain is assumed)!
|
||||
mediaType, _, err := pmmime.ParseMediaType(header.Get("Content-Type"))
|
||||
if isRoot && isFirst && err == nil && pka.attachedPublicKey != "" { //nolint[gocritic]
|
||||
if strings.HasPrefix(mediaType, "multipart/mixed") {
|
||||
pka.appendToMultipart = true
|
||||
_ = pka.target.Accept(partReader, header, hasPlainSiblings, isFirst, isLast)
|
||||
} else {
|
||||
// Create two siblings with attachment in the case toplevel is not multipart/mixed.
|
||||
multiPartHeaders := make(textproto.MIMEHeader)
|
||||
for k, v := range header {
|
||||
multiPartHeaders[k] = v
|
||||
}
|
||||
boundary := randomBoundary()
|
||||
multiPartHeaders.Set("Content-Type", "multipart/mixed; boundary=\""+boundary+"\"")
|
||||
multiPartHeaders.Del("Content-Transfer-Encoding")
|
||||
|
||||
_ = pka.target.Accept(partReader, multiPartHeaders, false, true, false)
|
||||
|
||||
originalHeader := make(textproto.MIMEHeader)
|
||||
originalHeader.Set("Content-Type", header.Get("Content-Type"))
|
||||
if header.Get("Content-Transfer-Encoding") != "" {
|
||||
originalHeader.Set("Content-Transfer-Encoding", header.Get("Content-Transfer-Encoding"))
|
||||
}
|
||||
|
||||
_ = pka.target.Accept(partReader, originalHeader, false, true, false)
|
||||
_ = pka.target.Accept(partReader, multiPartHeaders, hasPlainSiblings, false, false)
|
||||
|
||||
attachmentHeaders, attachmentReader := createKeyAttachment(pka.attachedPublicKey, pka.attachedPublicKeyName)
|
||||
|
||||
_ = pka.target.Accept(attachmentReader, attachmentHeaders, false, true, true)
|
||||
_ = pka.target.Accept(partReader, multiPartHeaders, hasPlainSiblings, false, true)
|
||||
}
|
||||
} else if isLast && pka.depth == 1 && pka.attachedPublicKey != "" {
|
||||
_ = pka.target.Accept(partReader, header, hasPlainSiblings, isFirst, false)
|
||||
attachmentHeaders, attachmentReader := createKeyAttachment(pka.attachedPublicKey, pka.attachedPublicKeyName)
|
||||
_ = pka.target.Accept(attachmentReader, attachmentHeaders, hasPlainSiblings, true, true)
|
||||
_ = pka.target.Accept(partReader, header, hasPlainSiblings, isFirst, true)
|
||||
} else {
|
||||
_ = pka.target.Accept(partReader, header, hasPlainSiblings, isFirst, isLast)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ======= Parser ==========
|
||||
func parseGoMessageHeader(h message.Header) (m *pmapi.Message, err error) {
|
||||
m = pmapi.NewMessage()
|
||||
|
||||
m.Header = make(mail.Header)
|
||||
|
||||
fields := h.Fields()
|
||||
|
||||
for fields.Next() {
|
||||
switch strings.ToLower(fields.Key()) {
|
||||
case "subject":
|
||||
if m.Subject, err = fields.Text(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Set these thingies.
|
||||
case "from":
|
||||
case "to":
|
||||
case "reply-to":
|
||||
case "cc":
|
||||
case "bcc":
|
||||
case "date":
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func ParseGoMessage(r io.Reader) (m *pmapi.Message, mimeBody string, plainContents string, atts []io.Reader, err error) {
|
||||
// Parse the message.
|
||||
func Parse(r io.Reader) (m *pmapi.Message, mimeBody, plainBody string, atts []io.Reader, err error) {
|
||||
p, err := parser.New(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Collect attachments, convert html to plaintext.
|
||||
walker := p.
|
||||
m = pmapi.NewMessage()
|
||||
|
||||
if err = parseHeader(m, p.Header()); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if m.Attachments, atts, err = collectAttachments(p); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
parts, plainParts, err := collectBodyParts(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
m.Body = strings.Join(parts, "\r\n")
|
||||
plainBody = strings.Join(plainParts, "\r\n")
|
||||
|
||||
if mimeBody, err = writeMimeBody(p); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func collectAttachments(p *parser.Parser) (atts []*pmapi.Attachment, data []io.Reader, err error) {
|
||||
w := p.
|
||||
NewWalker().
|
||||
WithContentDispositionHandler("attachment", func(p *parser.Part, _ parser.PartHandler) (err error) {
|
||||
atts = append(atts, bytes.NewReader(p.Body))
|
||||
return
|
||||
}).
|
||||
WithContentTypeHandler("text/html", func(p *parser.Part) (err error) {
|
||||
plain, err := html2text.FromString(string(p.Body))
|
||||
att, err := parseAttachment(p.Header)
|
||||
if err != nil {
|
||||
plain = string(p.Body)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Do we need newline here?
|
||||
plainContents += plain
|
||||
atts = append(atts, att)
|
||||
data = append(data, bytes.NewReader(p.Body))
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
if err = walker.Walk(); err != nil {
|
||||
if err = w.Walk(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Write out a mime body that doesn't include attachments.
|
||||
return
|
||||
}
|
||||
|
||||
func collectBodyParts(p *parser.Parser) (parts, plainParts []string, err error) {
|
||||
w := p.
|
||||
NewWalker().
|
||||
WithContentTypeHandler("text/plain", func(p *parser.Part) (err error) {
|
||||
parts = append(parts, string(p.Body))
|
||||
plainParts = append(plainParts, string(p.Body))
|
||||
return
|
||||
}).
|
||||
WithContentTypeHandler("text/html", func(p *parser.Part) (err error) {
|
||||
parts = append(parts, string(p.Body))
|
||||
|
||||
text, err := html2text.FromString(string(p.Body))
|
||||
if err != nil {
|
||||
text = string(p.Body)
|
||||
}
|
||||
plainParts = append(plainParts, text)
|
||||
|
||||
return
|
||||
})
|
||||
|
||||
if err = w.Walk(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func writeMimeBody(p *parser.Parser) (mimeBody string, err error) {
|
||||
writer := p.
|
||||
NewWriter().
|
||||
WithCondition(func(p *parser.Part) (keep bool) {
|
||||
if disp, _, err := p.Header.ContentDisposition(); err == nil && disp == "attachment" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
disp, _, err := p.Header.ContentDisposition()
|
||||
return err != nil || disp != "attachment"
|
||||
})
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
@ -487,72 +123,80 @@ func ParseGoMessage(r io.Reader) (m *pmapi.Message, mimeBody string, plainConten
|
||||
return
|
||||
}
|
||||
|
||||
mimeBody = buf.String()
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Parse the header to build a pmapi message.
|
||||
if m, err = parseGoMessageHeader(p.Header()); err != nil {
|
||||
return
|
||||
func parseHeader(m *pmapi.Message, h message.Header) (err error) {
|
||||
m.Header = make(mail.Header)
|
||||
|
||||
fields := h.Fields()
|
||||
|
||||
for fields.Next() {
|
||||
var text string
|
||||
|
||||
if text, err = fields.Text(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch strings.ToLower(fields.Key()) {
|
||||
case "subject":
|
||||
m.Subject = text
|
||||
|
||||
case "from":
|
||||
if m.Sender, err = mail.ParseAddress(text); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case "to":
|
||||
if m.ToList, err = mail.ParseAddressList(text); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case "reply-to":
|
||||
if m.ReplyTos, err = mail.ParseAddressList(text); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case "cc":
|
||||
if m.CCList, err = mail.ParseAddressList(text); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case "bcc":
|
||||
if m.BCCList, err = mail.ParseAddressList(text); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case "date":
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func Parse(r io.Reader, attachedPublicKey, attachedPublicKeyName string) (m *pmapi.Message, mimeBody string, plainContents string, atts []io.Reader, err error) {
|
||||
secondReader := new(bytes.Buffer)
|
||||
_, _ = secondReader.ReadFrom(r)
|
||||
func parseAttachment(h message.Header) (att *pmapi.Attachment, err error) {
|
||||
att = &pmapi.Attachment{}
|
||||
|
||||
mimeBody = secondReader.String()
|
||||
|
||||
mm, err := mail.ReadMessage(secondReader)
|
||||
if err != nil {
|
||||
if att.MIMEType, _, err = h.ContentType(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if m, err = parseHeader(mm.Header); err != nil {
|
||||
return
|
||||
}
|
||||
if _, dispParams, dispErr := h.ContentDisposition(); dispErr != nil {
|
||||
var ext []string
|
||||
|
||||
h := textproto.MIMEHeader(m.Header)
|
||||
mmBodyData, err := ioutil.ReadAll(mm.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if ext, err = mime.ExtensionsByType(att.MIMEType); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
printAccepter := pmmime.NewMIMEPrinter()
|
||||
|
||||
publicKeyAttacher := NewPublicKeyAttacher(printAccepter, attachedPublicKey, attachedPublicKeyName)
|
||||
sevenBitFilter := NewSevenBitFilter(publicKeyAttacher)
|
||||
|
||||
plainTextCollector := pmmime.NewPlainTextCollector(sevenBitFilter)
|
||||
htmlOnlyConvertor := NewHTMLOnlyConvertor(plainTextCollector)
|
||||
|
||||
visitor := pmmime.NewMimeVisitor(htmlOnlyConvertor)
|
||||
err = pmmime.VisitAll(bytes.NewReader(mmBodyData), h, visitor)
|
||||
/*
|
||||
err = visitor.VisitAll(h, bytes.NewReader(mmBodyData))
|
||||
*/
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mimeBody = printAccepter.String()
|
||||
|
||||
plainContents = plainTextCollector.GetPlainText()
|
||||
|
||||
parts, headers, err := pmmime.GetAllChildParts(bytes.NewReader(mmBodyData), h)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
convertPlainToHTML := checkHeaders(headers)
|
||||
isHTML, err := combineParts(m, parts, headers, convertPlainToHTML, &atts)
|
||||
|
||||
if isHTML {
|
||||
m.MIMEType = "text/html"
|
||||
if len(ext) > 0 {
|
||||
att.Name = "attachment" + ext[0]
|
||||
}
|
||||
} else {
|
||||
m.MIMEType = "text/plain"
|
||||
att.Name = dispParams["filename"]
|
||||
}
|
||||
|
||||
return m, mimeBody, plainContents, atts, err
|
||||
// TODO: Set att.Header
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -22,7 +22,6 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/mail"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@ -31,90 +30,6 @@ import (
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
)
|
||||
|
||||
func TestRFC822AddressFormat(t *testing.T) { //nolint[funlen]
|
||||
tests := []struct {
|
||||
address string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
" normal name <username@server.com>",
|
||||
[]string{
|
||||
"\"normal name\" <username@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" \"comma, name\" <username@server.com>",
|
||||
[]string{
|
||||
"\"comma, name\" <username@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" name <username@server.com> (ignore comment)",
|
||||
[]string{
|
||||
"\"name\" <username@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" name (ignore comment) <username@server.com>, (Comment as name) username2@server.com",
|
||||
[]string{
|
||||
"\"name\" <username@server.com>",
|
||||
"<username2@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" normal name <username@server.com>, (comment)All.(around)address@(the)server.com",
|
||||
[]string{
|
||||
"\"normal name\" <username@server.com>",
|
||||
"<All.address@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" normal name <username@server.com>, All.(\"comma, in comment\")address@(the)server.com",
|
||||
[]string{
|
||||
"\"normal name\" <username@server.com>",
|
||||
"<All.address@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" \"normal name\" <username@server.com>, \"comma, name\" <address@server.com>",
|
||||
[]string{
|
||||
"\"normal name\" <username@server.com>",
|
||||
"\"comma, name\" <address@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" \"comma, one\" <username@server.com>, \"comma, two\" <address@server.com>",
|
||||
[]string{
|
||||
"\"comma, one\" <username@server.com>",
|
||||
"\"comma, two\" <address@server.com>",
|
||||
},
|
||||
},
|
||||
{
|
||||
" \"comma, name\" <username@server.com>, another, name <address@server.com>",
|
||||
[]string{
|
||||
"\"comma, name\" <username@server.com>",
|
||||
"\"another, name\" <address@server.com>",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, data := range tests {
|
||||
uncommented := parseAddressComment(data.address)
|
||||
result, err := mail.ParseAddressList(uncommented)
|
||||
if err != nil {
|
||||
t.Errorf("Can not parse '%s' created from '%s': %v", uncommented, data.address, err)
|
||||
}
|
||||
if len(result) != len(data.expected) {
|
||||
t.Errorf("Wrong parsing of '%s' created from '%s': expected '%s' but have '%+v'", uncommented, data.address, data.expected, result)
|
||||
}
|
||||
for i, result := range result {
|
||||
if data.expected[i] != result.String() {
|
||||
t.Errorf("Wrong parsing\nof %q\ncreated from %q:\nexpected %q\nbut have %q", uncommented, data.address, data.expected, result.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func f(filename string) io.ReadCloser {
|
||||
f, err := os.Open(filepath.Join("testdata", filename))
|
||||
|
||||
@ -149,7 +64,7 @@ func TestParseMessageTextPlain(t *testing.T) {
|
||||
f := f("text_plain.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -166,7 +81,7 @@ func TestParseMessageTextPlainUTF8(t *testing.T) {
|
||||
f := f("text_plain_utf8.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -183,7 +98,7 @@ func TestParseMessageTextPlainLatin1(t *testing.T) {
|
||||
f := f("text_plain_latin1.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -200,7 +115,7 @@ func TestParseMessageTextPlainUnknownCharsetIsActuallyLatin1(t *testing.T) {
|
||||
f := f("text_plain_unknown_latin1.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -217,7 +132,7 @@ func TestParseMessageTextPlainUnknownCharsetIsActuallyLatin2(t *testing.T) {
|
||||
f := f("text_plain_unknown_latin2.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -240,7 +155,7 @@ func TestParseMessageTextPlainAlready7Bit(t *testing.T) {
|
||||
f := f("text_plain_7bit.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -257,7 +172,7 @@ func TestParseMessageTextPlainWithOctetAttachment(t *testing.T) {
|
||||
f := f("text_plain_octet_attachment.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -313,7 +228,7 @@ func TestParseMessageTextPlainWithPlainAttachment(t *testing.T) {
|
||||
f := f("text_plain_plain_attachment.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -331,7 +246,7 @@ func TestParseMessageTextPlainWithImageInline(t *testing.T) {
|
||||
f := f("text_plain_image_inline.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -353,7 +268,7 @@ func TestParseMessageWithMultipleTextParts(t *testing.T) {
|
||||
f := f("multiple_text_parts.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -372,7 +287,7 @@ func TestParseMessageTextHTML(t *testing.T) {
|
||||
f := f("text_html.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -391,7 +306,7 @@ func TestParseMessageTextHTMLAlready7Bit(t *testing.T) {
|
||||
f := f("text_html_7bit.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -410,7 +325,7 @@ func TestParseMessageTextHTMLWithOctetAttachment(t *testing.T) {
|
||||
f := f("text_html_octet_attachment.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -431,7 +346,7 @@ func _TestParseMessageTextHTMLWithPlainAttachment(t *testing.T) { // nolint[dead
|
||||
f := f("text_html_plain_attachment.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -452,7 +367,7 @@ func TestParseMessageTextHTMLWithImageInline(t *testing.T) {
|
||||
f := f("text_html_image_inline.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
@ -471,6 +386,7 @@ func TestParseMessageTextHTMLWithImageInline(t *testing.T) {
|
||||
}
|
||||
|
||||
// NOTE: Enable when bug is fixed.
|
||||
/*
|
||||
func _TestParseMessageWithAttachedPublicKey(t *testing.T) { // nolint[deadcode]
|
||||
f := f("text_plain.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
@ -489,6 +405,7 @@ func _TestParseMessageWithAttachedPublicKey(t *testing.T) { // nolint[deadcode]
|
||||
// BAD: Public key not available as an attachment!
|
||||
assert.Len(t, atts, 1)
|
||||
}
|
||||
*/
|
||||
|
||||
// NOTE: Enable when bug is fixed.
|
||||
func _TestParseMessageTextHTMLWithEmbeddedForeignEncoding(t *testing.T) { // nolint[deadcode]
|
||||
@ -497,7 +414,7 @@ func _TestParseMessageTextHTMLWithEmbeddedForeignEncoding(t *testing.T) { // nol
|
||||
f := f("text_html_embedded_foreign_encoding.eml")
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
m, mimeBody, plainContents, atts, err := Parse(f, "", "")
|
||||
m, mimeBody, plainContents, atts, err := Parse(f)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
|
||||
|
||||
Reference in New Issue
Block a user