feat: parse most header values

This commit is contained in:
James Houlahan
2020-07-02 12:50:09 +02:00
parent 953150cfdb
commit 45b863f931
5 changed files with 147 additions and 658 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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())