forked from Silverfish/proton-bridge
fix: linter issues
This commit is contained in:
@ -66,10 +66,16 @@ func Parse(r io.Reader, key, keyName string) (m *pmapi.Message, mimeMessage, pla
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return m, mimeMessage, plainBody, attReaders, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectAttachments(p *parser.Parser) (atts []*pmapi.Attachment, data []io.Reader, err error) {
|
func collectAttachments(p *parser.Parser) ([]*pmapi.Attachment, []io.Reader, error) {
|
||||||
|
var (
|
||||||
|
atts []*pmapi.Attachment
|
||||||
|
data []io.Reader
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
w := p.NewWalker().
|
w := p.NewWalker().
|
||||||
RegisterContentDispositionHandler("attachment", func(p *parser.Part) error {
|
RegisterContentDispositionHandler("attachment", func(p *parser.Part) error {
|
||||||
att, err := parseAttachment(p.Header)
|
att, err := parseAttachment(p.Header)
|
||||||
@ -113,10 +119,10 @@ func collectAttachments(p *parser.Parser) (atts []*pmapi.Attachment, data []io.R
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err = w.Walk(); err != nil {
|
if err = w.Walk(); err != nil {
|
||||||
return
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return atts, data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBodies(p *parser.Parser) (richBody, plainBody string, err error) {
|
func buildBodies(p *parser.Parser) (richBody, plainBody string, err error) {
|
||||||
@ -162,7 +168,7 @@ func collectBodyParts(p *parser.Parser, preferredContentType string) (parser.Par
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return bestChoice(childParts, preferredContentType)
|
return bestChoice(childParts, preferredContentType), nil
|
||||||
}).
|
}).
|
||||||
RegisterRule("text/plain", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
|
RegisterRule("text/plain", func(p *parser.Part, visit parser.Visit) (interface{}, error) {
|
||||||
return parser.Parts{p}, nil
|
return parser.Parts{p}, nil
|
||||||
@ -204,16 +210,16 @@ func joinChildParts(childParts []parser.Parts) parser.Parts {
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func bestChoice(childParts []parser.Parts, preferredContentType string) (parser.Parts, error) {
|
func bestChoice(childParts []parser.Parts, preferredContentType string) parser.Parts {
|
||||||
// If one of the parts has preferred content type, use that.
|
// If one of the parts has preferred content type, use that.
|
||||||
for i := len(childParts) - 1; i >= 0; i-- {
|
for i := len(childParts) - 1; i >= 0; i-- {
|
||||||
if allPartsHaveContentType(childParts[i], preferredContentType) {
|
if allPartsHaveContentType(childParts[i], preferredContentType) {
|
||||||
return childParts[i], nil
|
return childParts[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, choose the last one.
|
// Otherwise, choose the last one.
|
||||||
return childParts[len(childParts)-1], nil
|
return childParts[len(childParts)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func allPartsHaveContentType(parts parser.Parts, contentType string) bool {
|
func allPartsHaveContentType(parts parser.Parts, contentType string) bool {
|
||||||
@ -368,25 +374,26 @@ func parseMessageHeader(m *pmapi.Message, h message.Header) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAttachment(h message.Header) (att *pmapi.Attachment, err error) {
|
func parseAttachment(h message.Header) (*pmapi.Attachment, error) {
|
||||||
att = &pmapi.Attachment{}
|
att := &pmapi.Attachment{}
|
||||||
|
|
||||||
mimeHeader, err := toMIMEHeader(h)
|
mimeHeader, err := toMIMEHeader(h)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
att.Header = mimeHeader
|
att.Header = mimeHeader
|
||||||
|
|
||||||
if att.MIMEType, _, err = h.ContentType(); err != nil {
|
mimeType, _, err := h.ContentType()
|
||||||
return
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
att.MIMEType = mimeType
|
||||||
|
|
||||||
if _, dispParams, dispErr := h.ContentDisposition(); dispErr != nil {
|
_, dispParams, dispErr := h.ContentDisposition()
|
||||||
var ext []string
|
if dispErr != nil {
|
||||||
|
ext, err := mime.ExtensionsByType(att.MIMEType)
|
||||||
if ext, err = mime.ExtensionsByType(att.MIMEType); err != nil {
|
if err != nil {
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ext) > 0 {
|
if len(ext) > 0 {
|
||||||
@ -398,7 +405,7 @@ func parseAttachment(h message.Header) (att *pmapi.Attachment, err error) {
|
|||||||
|
|
||||||
att.ContentID = strings.Trim(h.Get("Content-Id"), " <>")
|
att.ContentID = strings.Trim(h.Get("Content-Id"), " <>")
|
||||||
|
|
||||||
return
|
return att, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func toMailHeader(h message.Header) (mail.Header, error) {
|
func toMailHeader(h message.Header) (mail.Header, error) {
|
||||||
|
|||||||
@ -63,7 +63,7 @@ func (w *Writer) shouldFilter(p *Part) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, b := range p.Body {
|
for _, b := range p.Body {
|
||||||
if uint8(b) > 1<<7 {
|
if b > 1<<7 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,479 +0,0 @@
|
|||||||
// Copyright (c) 2020 Proton Technologies AG
|
|
||||||
//
|
|
||||||
// This file is part of ProtonMail Bridge.
|
|
||||||
//
|
|
||||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package pmmime
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"mime/multipart"
|
|
||||||
"net/http"
|
|
||||||
"net/mail"
|
|
||||||
"net/textproto"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// VisitAcceptor decides what to do with part which is processed.
|
|
||||||
// It is used by MIMEVisitor.
|
|
||||||
type VisitAcceptor interface {
|
|
||||||
Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) (err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func VisitAll(part io.Reader, h textproto.MIMEHeader, accepter VisitAcceptor) (err error) {
|
|
||||||
mediaType, _, err := getContentType(h)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return accepter.Accept(part, h, mediaType == "text/plain", true, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsLeaf(h textproto.MIMEHeader) bool {
|
|
||||||
return !strings.HasPrefix(h.Get("Content-Type"), "multipart/")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MIMEVisitor is main object to parse (visit) and process (accept) all parts of MIME message.
|
|
||||||
type MimeVisitor struct {
|
|
||||||
target VisitAcceptor
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept reads part recursively if needed.
|
|
||||||
// hasPlainSibling is there when acceptor want to check alternatives.
|
|
||||||
func (mv *MimeVisitor) Accept(part io.Reader, h textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) (err error) {
|
|
||||||
if !isFirst {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
parentMediaType, params, err := getContentType(h)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = mv.target.Accept(part, h, hasPlainSibling, true, false); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !IsLeaf(h) {
|
|
||||||
var multiparts []io.Reader
|
|
||||||
var multipartHeaders []textproto.MIMEHeader
|
|
||||||
if multiparts, multipartHeaders, err = GetMultipartParts(part, params); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hasPlainChild := false
|
|
||||||
for _, header := range multipartHeaders {
|
|
||||||
mediaType, _, _ := getContentType(header)
|
|
||||||
if mediaType == "text/plain" {
|
|
||||||
hasPlainChild = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if hasPlainSibling && parentMediaType == "multipart/related" {
|
|
||||||
hasPlainChild = true
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, p := range multiparts {
|
|
||||||
if err = mv.Accept(p, multipartHeaders[i], hasPlainChild, true, true); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = mv.target.Accept(part, h, hasPlainSibling, false, i == (len(multiparts)-1)); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMIMEVisitor returns a new mime visitor initialised with an acceptor.
|
|
||||||
func NewMimeVisitor(targetAccepter VisitAcceptor) *MimeVisitor {
|
|
||||||
return &MimeVisitor{targetAccepter}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAllChildParts(part io.Reader, h textproto.MIMEHeader) (parts []io.Reader, headers []textproto.MIMEHeader, err error) {
|
|
||||||
mediaType, params, err := getContentType(h)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(mediaType, "multipart/") {
|
|
||||||
var multiparts []io.Reader
|
|
||||||
var multipartHeaders []textproto.MIMEHeader
|
|
||||||
if multiparts, multipartHeaders, err = GetMultipartParts(part, params); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(mediaType, "alternative") {
|
|
||||||
var chosenPart io.Reader
|
|
||||||
var chosenHeader textproto.MIMEHeader
|
|
||||||
if chosenPart, chosenHeader, err = pickAlternativePart(multiparts, multipartHeaders); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var childParts []io.Reader
|
|
||||||
var childHeaders []textproto.MIMEHeader
|
|
||||||
if childParts, childHeaders, err = GetAllChildParts(chosenPart, chosenHeader); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
parts = append(parts, childParts...)
|
|
||||||
headers = append(headers, childHeaders...)
|
|
||||||
} else {
|
|
||||||
for i, p := range multiparts {
|
|
||||||
var childParts []io.Reader
|
|
||||||
var childHeaders []textproto.MIMEHeader
|
|
||||||
if childParts, childHeaders, err = GetAllChildParts(p, multipartHeaders[i]); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
parts = append(parts, childParts...)
|
|
||||||
headers = append(headers, childHeaders...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parts = append(parts, part)
|
|
||||||
headers = append(headers, h)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetMultipartParts(r io.Reader, params map[string]string) (parts []io.Reader, headers []textproto.MIMEHeader, err error) {
|
|
||||||
mr := multipart.NewReader(r, params["boundary"])
|
|
||||||
parts = []io.Reader{}
|
|
||||||
headers = []textproto.MIMEHeader{}
|
|
||||||
var p *multipart.Part
|
|
||||||
for {
|
|
||||||
p, err = mr.NextPart()
|
|
||||||
if err == io.EOF {
|
|
||||||
err = nil
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
b, _ := ioutil.ReadAll(p)
|
|
||||||
buffer := bytes.NewBuffer(b)
|
|
||||||
|
|
||||||
parts = append(parts, buffer)
|
|
||||||
headers = append(headers, p.Header)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func pickAlternativePart(parts []io.Reader, headers []textproto.MIMEHeader) (part io.Reader, h textproto.MIMEHeader, err error) {
|
|
||||||
|
|
||||||
for i, h := range headers {
|
|
||||||
mediaType, _, err := getContentType(h)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(mediaType, "multipart/") {
|
|
||||||
return parts[i], headers[i], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, h := range headers {
|
|
||||||
mediaType, _, err := getContentType(h)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if mediaType == "text/html" {
|
|
||||||
return parts[i], headers[i], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, h := range headers {
|
|
||||||
mediaType, _, err := getContentType(h)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if mediaType == "text/plain" {
|
|
||||||
return parts[i], headers[i], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we get all the way here, part will be nil.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// "Parse address comment" as defined in http://tools.wordtothewise.com/rfc/822
|
|
||||||
// FIXME: Does not work for address groups.
|
|
||||||
// NOTE: This should be removed for go>1.10 (please check).
|
|
||||||
func parseAddressComment(raw string) string {
|
|
||||||
parsed := []string{}
|
|
||||||
for _, item := range regexp.MustCompile("[,;]").Split(raw, -1) {
|
|
||||||
re := regexp.MustCompile("[(][^)]*[)]")
|
|
||||||
comments := strings.Join(re.FindAllString(item, -1), " ")
|
|
||||||
comments = strings.Replace(comments, "(", "", -1)
|
|
||||||
comments = strings.Replace(comments, ")", "", -1)
|
|
||||||
withoutComments := re.ReplaceAllString(item, "")
|
|
||||||
addr, err := mail.ParseAddress(withoutComments)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if addr.Name == "" {
|
|
||||||
addr.Name = comments
|
|
||||||
}
|
|
||||||
parsed = append(parsed, addr.String())
|
|
||||||
}
|
|
||||||
return strings.Join(parsed, ", ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodePart(partReader io.Reader, header textproto.MIMEHeader) (decodedPart io.Reader) {
|
|
||||||
decodedPart = 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assume 'text/plain' if missing.
|
|
||||||
func getContentType(header textproto.MIMEHeader) (mediatype string, params map[string]string, err error) {
|
|
||||||
contentType := header.Get("Content-Type")
|
|
||||||
if contentType == "" {
|
|
||||||
contentType = "text/plain"
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseMediaType(contentType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== MIME Printer ===================================
|
|
||||||
// Simply print resulting MIME tree into text form.
|
|
||||||
// TODO move this to file mime_printer.go.
|
|
||||||
|
|
||||||
type stack []string
|
|
||||||
|
|
||||||
func (s stack) Push(v string) stack {
|
|
||||||
return append(s, v)
|
|
||||||
}
|
|
||||||
func (s stack) Pop() (stack, string) {
|
|
||||||
l := len(s)
|
|
||||||
return s[:l-1], s[l-1]
|
|
||||||
}
|
|
||||||
func (s stack) Peek() string {
|
|
||||||
return s[len(s)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
type MIMEPrinter struct {
|
|
||||||
result *bytes.Buffer
|
|
||||||
boundaryStack stack
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMIMEPrinter() (pd *MIMEPrinter) {
|
|
||||||
return &MIMEPrinter{
|
|
||||||
result: bytes.NewBuffer([]byte("")),
|
|
||||||
boundaryStack: stack{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pd *MIMEPrinter) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) (err error) {
|
|
||||||
if isFirst {
|
|
||||||
http.Header(header).Write(pd.result)
|
|
||||||
pd.result.Write([]byte("\n"))
|
|
||||||
if IsLeaf(header) {
|
|
||||||
pd.result.ReadFrom(partReader)
|
|
||||||
} else {
|
|
||||||
_, params, _ := getContentType(header)
|
|
||||||
boundary := params["boundary"]
|
|
||||||
pd.boundaryStack = pd.boundaryStack.Push(boundary)
|
|
||||||
pd.result.Write([]byte("\nThis is a multi-part message in MIME format.\n--" + boundary + "\n"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !isLast {
|
|
||||||
pd.result.Write([]byte("\n--" + pd.boundaryStack.Peek() + "\n"))
|
|
||||||
} else {
|
|
||||||
var boundary string
|
|
||||||
pd.boundaryStack, boundary = pd.boundaryStack.Pop()
|
|
||||||
pd.result.Write([]byte("\n--" + boundary + "--\n.\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pd *MIMEPrinter) String() string {
|
|
||||||
return pd.result.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== PlainText Collector =========================
|
|
||||||
// Collect contents of all non-attachment text/plain parts and return it as a string.
|
|
||||||
// TODO move this to file collector_plaintext.go.
|
|
||||||
|
|
||||||
type PlainTextCollector struct {
|
|
||||||
target VisitAcceptor
|
|
||||||
plainTextContents *bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPlainTextCollector(targetAccepter VisitAcceptor) *PlainTextCollector {
|
|
||||||
return &PlainTextCollector{
|
|
||||||
target: targetAccepter,
|
|
||||||
plainTextContents: bytes.NewBuffer([]byte("")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ptc *PlainTextCollector) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) (err error) {
|
|
||||||
if isFirst {
|
|
||||||
if IsLeaf(header) {
|
|
||||||
mediaType, _, _ := getContentType(header)
|
|
||||||
disp, _, _ := ParseMediaType(header.Get("Content-Disposition"))
|
|
||||||
if mediaType == "text/plain" && disp != "attachment" {
|
|
||||||
partData, _ := ioutil.ReadAll(partReader)
|
|
||||||
decodedPart := decodePart(bytes.NewReader(partData), header)
|
|
||||||
|
|
||||||
if buffer, err := ioutil.ReadAll(decodedPart); err == nil {
|
|
||||||
buffer, err = DecodeCharset(buffer, header.Get("Content-Type"))
|
|
||||||
if err != nil {
|
|
||||||
log.Warnln("Decode charset error:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ptc.plainTextContents.Write(buffer)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ptc.target.Accept(bytes.NewReader(partData), header, hasPlainSibling, isFirst, isLast)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = ptc.target.Accept(partReader, header, hasPlainSibling, isFirst, isLast)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ptc PlainTextCollector) GetPlainText() string {
|
|
||||||
return ptc.plainTextContents.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== Body Collector ==============
|
|
||||||
// Collect contents of all non-attachment parts and return it as a string.
|
|
||||||
// TODO move this to file collector_body.go.
|
|
||||||
|
|
||||||
type BodyCollector struct {
|
|
||||||
target VisitAcceptor
|
|
||||||
htmlBodyBuffer *bytes.Buffer
|
|
||||||
plainBodyBuffer *bytes.Buffer
|
|
||||||
htmlHeaderBuffer *bytes.Buffer
|
|
||||||
plainHeaderBuffer *bytes.Buffer
|
|
||||||
hasHtml bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBodyCollector(targetAccepter VisitAcceptor) *BodyCollector {
|
|
||||||
return &BodyCollector{
|
|
||||||
target: targetAccepter,
|
|
||||||
htmlBodyBuffer: bytes.NewBuffer([]byte("")),
|
|
||||||
plainBodyBuffer: bytes.NewBuffer([]byte("")),
|
|
||||||
htmlHeaderBuffer: bytes.NewBuffer([]byte("")),
|
|
||||||
plainHeaderBuffer: bytes.NewBuffer([]byte("")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bc *BodyCollector) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) (err error) {
|
|
||||||
// TODO: Collect html and plaintext - if there's html with plain sibling don't include plain/text.
|
|
||||||
if isFirst {
|
|
||||||
if IsLeaf(header) {
|
|
||||||
mediaType, _, _ := getContentType(header)
|
|
||||||
disp, _, _ := ParseMediaType(header.Get("Content-Disposition"))
|
|
||||||
if disp != "attachment" {
|
|
||||||
partData, _ := ioutil.ReadAll(partReader)
|
|
||||||
decodedPart := decodePart(bytes.NewReader(partData), header)
|
|
||||||
if buffer, err := ioutil.ReadAll(decodedPart); err == nil {
|
|
||||||
buffer, err = DecodeCharset(buffer, header.Get("Content-Type"))
|
|
||||||
if err != nil {
|
|
||||||
log.Warnln("Decode charset error:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if mediaType == "text/html" {
|
|
||||||
bc.hasHtml = true
|
|
||||||
http.Header(header).Write(bc.htmlHeaderBuffer)
|
|
||||||
bc.htmlBodyBuffer.Write(buffer)
|
|
||||||
} else if mediaType == "text/plain" {
|
|
||||||
http.Header(header).Write(bc.plainHeaderBuffer)
|
|
||||||
bc.plainBodyBuffer.Write(buffer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = bc.target.Accept(bytes.NewReader(partData), header, hasPlainSibling, isFirst, isLast)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = bc.target.Accept(partReader, header, hasPlainSibling, isFirst, isLast)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bc *BodyCollector) GetBody() (string, string) {
|
|
||||||
if bc.hasHtml {
|
|
||||||
return bc.htmlBodyBuffer.String(), "text/html"
|
|
||||||
} else {
|
|
||||||
return bc.plainBodyBuffer.String(), "text/plain"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bc *BodyCollector) GetHeaders() string {
|
|
||||||
if bc.hasHtml {
|
|
||||||
return bc.htmlHeaderBuffer.String()
|
|
||||||
} else {
|
|
||||||
return bc.plainHeaderBuffer.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================== Attachments Collector ==============
|
|
||||||
// Collect contents of all attachment parts and return them as a string.
|
|
||||||
// TODO move this to file collector_attachment.go.
|
|
||||||
|
|
||||||
type AttachmentsCollector struct {
|
|
||||||
target VisitAcceptor
|
|
||||||
attBuffers []string
|
|
||||||
attHeaders []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAttachmentsCollector(targetAccepter VisitAcceptor) *AttachmentsCollector {
|
|
||||||
return &AttachmentsCollector{
|
|
||||||
target: targetAccepter,
|
|
||||||
attBuffers: []string{},
|
|
||||||
attHeaders: []string{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ac *AttachmentsCollector) Accept(partReader io.Reader, header textproto.MIMEHeader, hasPlainSibling bool, isFirst, isLast bool) (err error) {
|
|
||||||
if isFirst {
|
|
||||||
if IsLeaf(header) {
|
|
||||||
mediaType, _, _ := getContentType(header)
|
|
||||||
disp, _, _ := ParseMediaType(header.Get("Content-Disposition"))
|
|
||||||
if (mediaType != "text/html" && mediaType != "text/plain") || disp == "attachment" {
|
|
||||||
partData, _ := ioutil.ReadAll(partReader)
|
|
||||||
decodedPart := decodePart(bytes.NewReader(partData), header)
|
|
||||||
|
|
||||||
if buffer, err := ioutil.ReadAll(decodedPart); err == nil {
|
|
||||||
buffer, err = DecodeCharset(buffer, header.Get("Content-Type"))
|
|
||||||
if err != nil {
|
|
||||||
log.Warnln("Decode charset error:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
headerBuf := new(bytes.Buffer)
|
|
||||||
http.Header(header).Write(headerBuf)
|
|
||||||
ac.attHeaders = append(ac.attHeaders, headerBuf.String())
|
|
||||||
ac.attBuffers = append(ac.attBuffers, string(buffer))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ac.target.Accept(bytes.NewReader(partData), header, hasPlainSibling, isFirst, isLast)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = ac.target.Accept(partReader, header, hasPlainSibling, isFirst, isLast)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ac AttachmentsCollector) GetAttachments() []string {
|
|
||||||
return ac.attBuffers
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ac AttachmentsCollector) GetAttHeaders() []string {
|
|
||||||
return ac.attHeaders
|
|
||||||
}
|
|
||||||
@ -1,231 +0,0 @@
|
|||||||
// Copyright (c) 2020 Proton Technologies AG
|
|
||||||
//
|
|
||||||
// This file is part of ProtonMail Bridge.
|
|
||||||
//
|
|
||||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU General Public License
|
|
||||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package pmmime
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"io/ioutil"
|
|
||||||
"net/mail"
|
|
||||||
|
|
||||||
"net/textproto"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func minimalParse(mimeBody string) (readBody string, plainContents string, err error) {
|
|
||||||
mm, err := mail.ReadMessage(strings.NewReader(mimeBody))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h := textproto.MIMEHeader(mm.Header)
|
|
||||||
mmBodyData, err := ioutil.ReadAll(mm.Body)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
printAccepter := NewMIMEPrinter()
|
|
||||||
plainTextCollector := NewPlainTextCollector(printAccepter)
|
|
||||||
visitor := NewMimeVisitor(plainTextCollector)
|
|
||||||
err = VisitAll(bytes.NewReader(mmBodyData), h, visitor)
|
|
||||||
|
|
||||||
readBody = printAccepter.String()
|
|
||||||
plainContents = plainTextCollector.GetPlainText()
|
|
||||||
|
|
||||||
return readBody, plainContents, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func androidParse(mimeBody string) (body, headers string, atts, attHeaders []string, err error) {
|
|
||||||
mm, err := mail.ReadMessage(strings.NewReader(mimeBody))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h := textproto.MIMEHeader(mm.Header)
|
|
||||||
mmBodyData, err := ioutil.ReadAll(mm.Body)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
printAccepter := NewMIMEPrinter()
|
|
||||||
bodyCollector := NewBodyCollector(printAccepter)
|
|
||||||
attachmentsCollector := NewAttachmentsCollector(bodyCollector)
|
|
||||||
mimeVisitor := NewMimeVisitor(attachmentsCollector)
|
|
||||||
err = VisitAll(bytes.NewReader(mmBodyData), h, mimeVisitor)
|
|
||||||
|
|
||||||
body, _ = bodyCollector.GetBody()
|
|
||||||
headers = bodyCollector.GetHeaders()
|
|
||||||
atts = attachmentsCollector.GetAttachments()
|
|
||||||
attHeaders = attachmentsCollector.GetAttHeaders()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseBoundaryIsEmpty(t *testing.T) {
|
|
||||||
testMessage :=
|
|
||||||
`Date: Sun, 10 Mar 2019 11:10:06 -0600
|
|
||||||
In-Reply-To: <abcbase64@protonmail.com>
|
|
||||||
X-Original-To: enterprise@protonmail.com
|
|
||||||
References: <abc64@unicoderns.com> <abc63@protonmail.com> <abc64@protonmail.com> <abc65@mail.gmail.com> <abc66@protonmail.com>
|
|
||||||
To: "ProtonMail" <enterprise@protonmail.com>
|
|
||||||
X-Pm-Origin: external
|
|
||||||
Delivered-To: enterprise@protonmail.com
|
|
||||||
Content-Type: multipart/mixed; boundary=ac7e36bd45425e70b4dab2128f34172e4dc3f9ff2eeb47e909267d4252794ec7
|
|
||||||
Reply-To: XYZ <xyz@xyz.com>
|
|
||||||
Mime-Version: 1.0
|
|
||||||
Subject: Encrypted Message
|
|
||||||
Return-Path: <xyz@xyz.com>
|
|
||||||
From: XYZ <xyz@xyz.com>
|
|
||||||
X-Pm-ConversationID-Id: gNX9bDPLmBgFZ-C3Tdlb628cas1Xl0m4dql5nsWzQAEI-WQv0ytfwPR4-PWELEK0_87XuFOgetc239Y0pjPYHQ==
|
|
||||||
X-Pm-Date: Sun, 10 Mar 2019 18:10:06 +0100
|
|
||||||
Message-Id: <68c11e46-e611-d9e4-edc1-5ec96bac77cc@unicoderns.com>
|
|
||||||
X-Pm-Transfer-Encryption: TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)
|
|
||||||
X-Pm-External-Id: <68c11e46-e611-d9e4-edc1-5ec96bac77cc@unicoderns.com>
|
|
||||||
X-Pm-Internal-Id: _iJ8ETxcqXTSK8IzCn0qFpMUTwvRf-xJUtldRA1f6yHdmXjXzKleG3F_NLjZL3FvIWVHoItTxOuuVXcukwwW3g==
|
|
||||||
Openpgp: preference=signencrypt
|
|
||||||
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Thunderbird/60.4.0
|
|
||||||
X-Pm-Content-Encryption: end-to-end
|
|
||||||
|
|
||||||
--ac7e36bd45425e70b4dab2128f34172e4dc3f9ff2eeb47e909267d4252794ec7
|
|
||||||
Content-Disposition: inline
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
Content-Type: multipart/mixed; charset=utf-8
|
|
||||||
|
|
||||||
Content-Type: multipart/mixed; boundary="xnAIW3Turb9YQZ2rXc2ZGZH45WepHIZyy";
|
|
||||||
protected-headers="v1"
|
|
||||||
From: XYZ <xyz@xyz.com>
|
|
||||||
To: "ProtonMail" <enterprise@protonmail.com>
|
|
||||||
Subject: Encrypted Message
|
|
||||||
Message-ID: <68c11e46-e611-d9e4-edc1-5ec96bac77cc@unicoderns.com>
|
|
||||||
References: <abc64@unicoderns.com> <abc63@protonmail.com> <abc64@protonmail.com> <abc65@mail.gmail.com> <abc66@protonmail.com>
|
|
||||||
In-Reply-To: <abcbase64@protonmail.com>
|
|
||||||
|
|
||||||
--xnAIW3Turb9YQZ2rXc2ZGZH45WepHIZyy
|
|
||||||
Content-Type: text/rfc822-headers; protected-headers="v1"
|
|
||||||
Content-Disposition: inline
|
|
||||||
|
|
||||||
From: XYZ <xyz@xyz.com>
|
|
||||||
To: ProtonMail <enterprise@protonmail.com>
|
|
||||||
Subject: Re: Encrypted Message
|
|
||||||
|
|
||||||
--xnAIW3Turb9YQZ2rXc2ZGZH45WepHIZyy
|
|
||||||
Content-Type: multipart/alternative;
|
|
||||||
boundary="------------F9E5AA6D49692F51484075E3"
|
|
||||||
Content-Language: en-US
|
|
||||||
|
|
||||||
This is a multi-part message in MIME format.
|
|
||||||
--------------F9E5AA6D49692F51484075E3
|
|
||||||
Content-Type: text/plain; charset=utf-8
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
Hi ...
|
|
||||||
|
|
||||||
--------------F9E5AA6D49692F51484075E3
|
|
||||||
Content-Type: text/html; charset=utf-8
|
|
||||||
Content-Transfer-Encoding: quoted-printable
|
|
||||||
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
</head>
|
|
||||||
<body text=3D"#000000" bgcolor=3D"#FFFFFF">
|
|
||||||
<p>Hi .. </p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
--------------F9E5AA6D49692F51484075E3--
|
|
||||||
|
|
||||||
--xnAIW3Turb9YQZ2rXc2ZGZH45WepHIZyy--
|
|
||||||
|
|
||||||
--ac7e36bd45425e70b4dab2128f34172e4dc3f9ff2eeb47e909267d4252794ec7--
|
|
||||||
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
body, content, err := minimalParse(testMessage)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("should have error but is", err)
|
|
||||||
}
|
|
||||||
t.Log("==BODY==")
|
|
||||||
t.Log(body)
|
|
||||||
t.Log("==CONTENT==")
|
|
||||||
t.Log(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
|
||||||
testMessage :=
|
|
||||||
`From: John Doe <example@example.com>
|
|
||||||
MIME-Version: 1.0
|
|
||||||
Content-Type: multipart/mixed;
|
|
||||||
boundary="XXXXboundary text"
|
|
||||||
|
|
||||||
This is a multipart message in MIME format.
|
|
||||||
|
|
||||||
--XXXXboundary text
|
|
||||||
Content-Type: text/plain; charset=utf-8
|
|
||||||
|
|
||||||
this is the body text
|
|
||||||
|
|
||||||
--XXXXboundary text
|
|
||||||
Content-Type: text/html; charset=utf-8
|
|
||||||
|
|
||||||
<html><body>this is the html body text</body></html>
|
|
||||||
|
|
||||||
--XXXXboundary text
|
|
||||||
Content-Type: text/plain; charset=utf-8
|
|
||||||
Content-Disposition: attachment;
|
|
||||||
filename="test.txt"
|
|
||||||
|
|
||||||
this is the attachment text
|
|
||||||
|
|
||||||
--XXXXboundary text--
|
|
||||||
|
|
||||||
|
|
||||||
`
|
|
||||||
body, heads, att, attHeads, err := androidParse(testMessage)
|
|
||||||
if err != nil {
|
|
||||||
t.Error("parse error", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("==BODY:")
|
|
||||||
fmt.Println(body)
|
|
||||||
fmt.Println("==BODY HEADERS:")
|
|
||||||
fmt.Println(heads)
|
|
||||||
|
|
||||||
fmt.Println("==ATTACHMENTS:")
|
|
||||||
fmt.Println(att)
|
|
||||||
fmt.Println("==ATTACHMENT HEADERS:")
|
|
||||||
fmt.Println(attHeads)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseAddressComment(t *testing.T) {
|
|
||||||
parsingExamples := map[string]string{
|
|
||||||
"": "",
|
|
||||||
"(Only Comment) here@pm.me": "\"Only Comment\" <here@pm.me>",
|
|
||||||
"Normal Name (With Comment) <here@pm.me>": "\"Normal Name\" <here@pm.me>",
|
|
||||||
"<Muhammed.(I am the greatest)Ali@(the)Vegas.WBA>": "\"I am the greatest the\" <Muhammed.Ali@Vegas.WBA>",
|
|
||||||
}
|
|
||||||
|
|
||||||
for raw, expected := range parsingExamples {
|
|
||||||
parsed := parseAddressComment(raw)
|
|
||||||
if expected != parsed {
|
|
||||||
t.Errorf("When parsing %q expected %q but have %q", raw, expected, parsed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user