GODT-1779: Remove go-imap

This commit is contained in:
James Houlahan
2022-08-26 17:00:21 +02:00
parent 3b0bc1ca15
commit 39433fe707
593 changed files with 12725 additions and 91626 deletions

View File

@ -1,130 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bufio"
"bytes"
"io"
)
type boundaryReader struct {
reader *bufio.Reader
closed, first bool
skipped int
nl []byte // "\r\n" or "\n" (set after seeing first boundary line)
nlDashBoundary []byte // nl + "--boundary"
dashBoundaryDash []byte // "--boundary--"
dashBoundary []byte // "--boundary"
}
func newBoundaryReader(r *bufio.Reader, boundary string) (br *boundaryReader, err error) {
b := []byte("\r\n--" + boundary + "--")
br = &boundaryReader{
reader: r,
closed: false,
first: true,
nl: b[:2],
nlDashBoundary: b[:len(b)-2],
dashBoundaryDash: b[2:],
dashBoundary: b[2 : len(b)-2],
}
err = br.writeNextPartTo(nil)
return
}
// writeNextPartTo will copy the the bytes of next part and write them to
// writer. Will return EOF if the underlying reader is empty.
func (br *boundaryReader) writeNextPartTo(part io.Writer) (err error) {
if br.closed {
return io.EOF
}
var line, slice []byte
br.skipped = 0
for {
slice, err = br.reader.ReadSlice('\n')
line = append(line, slice...)
if err == bufio.ErrBufferFull {
continue
}
br.skipped += len(line)
if err == io.EOF && br.isFinalBoundary(line) {
err = nil
br.closed = true
return
}
if err != nil {
return
}
if br.isBoundaryDelimiterLine(line) {
br.first = false
return
}
if br.isFinalBoundary(line) {
br.closed = true
return
}
if part != nil {
if _, err = part.Write(line); err != nil {
return
}
}
line = []byte{}
}
}
func (br *boundaryReader) isFinalBoundary(line []byte) bool {
if !bytes.HasPrefix(line, br.dashBoundaryDash) {
return false
}
rest := line[len(br.dashBoundaryDash):]
rest = skipLWSPChar(rest)
return len(rest) == 0 || bytes.Equal(rest, br.nl)
}
func (br *boundaryReader) isBoundaryDelimiterLine(line []byte) (ret bool) {
if !bytes.HasPrefix(line, br.dashBoundary) {
return false
}
rest := line[len(br.dashBoundary):]
rest = skipLWSPChar(rest)
if br.first && len(rest) == 1 && rest[0] == '\n' {
br.nl = br.nl[1:]
br.nlDashBoundary = br.nlDashBoundary[1:]
}
return bytes.Equal(rest, br.nl)
}
func skipLWSPChar(b []byte) []byte {
for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') {
b = b[1:]
}
return b
}

View File

@ -18,14 +18,22 @@
package message
import (
"context"
"io"
"sync"
"bytes"
"encoding/base64"
"mime"
"net/mail"
"strings"
"time"
"unicode/utf8"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-rfc5322"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/ProtonMail/proton-bridge/v2/pkg/pool"
"github.com/ProtonMail/proton-bridge/v2/pkg/algo"
"github.com/emersion/go-message"
"github.com/emersion/go-message/textproto"
"github.com/pkg/errors"
"gitlab.protontech.ch/go/liteapi"
)
var (
@ -33,199 +41,526 @@ var (
ErrNoSuchKeyRing = errors.New("the keyring to decrypt this message could not be found")
)
const (
BackgroundPriority = 1 << iota
ForegroundPriority
)
// InternalIDDomain is used as a placeholder for reference/message ID headers to improve compatibility with various clients.
const InternalIDDomain = `protonmail.internalid`
type Builder struct {
pool *pool.Pool
jobs map[string]*Job
lock sync.Mutex
}
func BuildRFC822(kr *crypto.KeyRing, msg liteapi.Message, attData map[string][]byte, opts JobOptions) ([]byte, error) {
switch {
case len(msg.Attachments) > 0:
return buildMultipartRFC822(kr, msg, attData, opts)
type Fetcher interface {
GetMessage(context.Context, string) (*pmapi.Message, error)
GetAttachment(context.Context, string) (io.ReadCloser, error)
KeyRingForAddressID(string) (*crypto.KeyRing, error)
}
case msg.MIMEType == "multipart/mixed":
return buildPGPRFC822(kr, msg, opts)
// NewBuilder creates a new builder which manages the given number of fetch/attach/build workers.
// - fetchWorkers: the number of workers which fetch messages from API
// - attachWorkers: the number of workers which fetch attachments from API.
//
// The returned builder is ready to handle jobs -- see (*Builder).NewJob for more information.
//
// Call (*Builder).Done to shut down the builder and stop all workers.
func NewBuilder(fetchWorkers, attachmentWorkers int) *Builder {
attachmentPool := pool.New(attachmentWorkers, newAttacherWorkFunc())
fetcherPool := pool.New(fetchWorkers, newFetcherWorkFunc(attachmentPool))
return &Builder{
pool: fetcherPool,
jobs: make(map[string]*Job),
default:
return buildSimpleRFC822(kr, msg, opts)
}
}
func (builder *Builder) NewJob(ctx context.Context, fetcher Fetcher, messageID string, prio int) (*Job, pool.DoneFunc) {
return builder.NewJobWithOptions(ctx, fetcher, messageID, JobOptions{}, prio)
}
func (builder *Builder) NewJobWithOptions(ctx context.Context, fetcher Fetcher, messageID string, opts JobOptions, prio int) (*Job, pool.DoneFunc) {
builder.lock.Lock()
defer builder.lock.Unlock()
if job, ok := builder.jobs[messageID]; ok {
if job.GetPriority() < prio {
job.SetPriority(prio)
func buildSimpleRFC822(kr *crypto.KeyRing, msg liteapi.Message, opts JobOptions) ([]byte, error) {
dec, err := msg.Decrypt(kr)
if err != nil {
if !opts.IgnoreDecryptionErrors {
return nil, errors.Wrap(ErrDecryptionFailed, err.Error())
}
return job, job.done
return buildMultipartRFC822(kr, msg, nil, opts)
}
job, done := builder.pool.NewJob(
&fetchReq{
ctx: ctx,
fetcher: fetcher,
messageID: messageID,
options: opts,
},
prio,
)
hdr := getTextPartHeader(getMessageHeader(msg, opts), dec, msg.MIMEType)
buildDone := func() {
builder.lock.Lock()
defer builder.lock.Unlock()
buf := new(bytes.Buffer)
// Remove the job from the builder.
delete(builder.jobs, messageID)
// And mark it as done.
done()
}
buildJob := &Job{
Job: job,
done: buildDone,
}
builder.jobs[messageID] = buildJob
return buildJob, buildDone
}
func (builder *Builder) Done() {
// NOTE(GODT-1158): Stop worker pool.
}
type fetchReq struct {
ctx context.Context
fetcher Fetcher
messageID string
options JobOptions
}
type attachReq struct {
ctx context.Context
fetcher Fetcher
message *pmapi.Message
}
type Job struct {
*pool.Job
done pool.DoneFunc
}
func (job *Job) GetResult() ([]byte, error) {
res, err := job.Job.GetResult()
w, err := message.CreateWriter(buf, hdr)
if err != nil {
return nil, err
}
return res.([]byte), nil //nolint:forcetypeassert
}
// NOTE: This is not used because it is actually not doing what was expected: It
// downloads all the attachments which belongs to one message sequentially
// within one goroutine. We should have one job per one attachment. This doesn't look
// like a bottle neck right now.
func newAttacherWorkFunc() pool.WorkFunc {
return func(payload interface{}, prio int) (interface{}, error) {
req, ok := payload.(*attachReq)
if !ok {
panic("bad payload type")
}
res := make(map[string][]byte)
for _, att := range req.message.Attachments {
rc, err := req.fetcher.GetAttachment(req.ctx, att.ID)
if err != nil {
return nil, err
}
b, err := io.ReadAll(rc)
if err != nil {
return nil, err
}
if err := rc.Close(); err != nil {
return nil, err
}
res[att.ID] = b
}
return res, nil
if _, err := w.Write(dec); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func newFetcherWorkFunc(attachmentPool *pool.Pool) pool.WorkFunc {
return func(payload interface{}, prio int) (interface{}, error) {
req, ok := payload.(*fetchReq)
if !ok {
panic("bad payload type")
}
func buildMultipartRFC822(
kr *crypto.KeyRing,
msg liteapi.Message,
attData map[string][]byte,
opts JobOptions,
) ([]byte, error) {
boundary := newBoundary(msg.ID)
msg, err := req.fetcher.GetMessage(req.ctx, req.messageID)
if err != nil {
hdr := getMessageHeader(msg, opts)
hdr.SetContentType("multipart/mixed", map[string]string{"boundary": boundary.gen()})
buf := new(bytes.Buffer)
w, err := message.CreateWriter(buf, hdr)
if err != nil {
return nil, err
}
var (
inlineAtts []liteapi.Attachment
inlineData [][]byte
attachAtts []liteapi.Attachment
attachData [][]byte
)
for _, att := range msg.Attachments {
if att.Disposition == liteapi.InlineDisposition {
inlineAtts = append(inlineAtts, att)
inlineData = append(inlineData, attData[att.ID])
} else {
attachAtts = append(attachAtts, att)
attachData = append(attachData, attData[att.ID])
}
}
if len(inlineAtts) > 0 {
if err := writeRelatedParts(w, kr, boundary, msg, inlineAtts, inlineData, opts); err != nil {
return nil, err
}
} else if err := writeTextPart(w, kr, msg, opts); err != nil {
return nil, err
}
attData := make(map[string][]byte)
for i, att := range attachAtts {
if err := writeAttachmentPart(w, kr, att, attachData[i], opts); err != nil {
return nil, err
}
}
for _, att := range msg.Attachments {
// NOTE: Potential place for optimization:
// Use attachmentPool to download each attachment in
// separate parallel job. It is not straightforward
// because we need to make sure we call attachment-job-done
// function in case of any error or after we collect all
// attachment bytes asynchronously.
rc, err := req.fetcher.GetAttachment(req.ctx, att.ID)
if err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
b, err := io.ReadAll(rc)
if err != nil {
_ = rc.Close()
return nil, err
}
return buf.Bytes(), nil
}
if err := rc.Close(); err != nil {
return nil, err
}
attData[att.ID] = b
func writeTextPart(
w *message.Writer,
kr *crypto.KeyRing,
msg liteapi.Message,
opts JobOptions,
) error {
dec, err := msg.Decrypt(kr)
if err != nil {
if !opts.IgnoreDecryptionErrors {
return errors.Wrap(ErrDecryptionFailed, err.Error())
}
kr, err := req.fetcher.KeyRingForAddressID(msg.AddressID)
if err != nil {
return nil, ErrNoSuchKeyRing
return writeCustomTextPart(w, msg, err)
}
return writePart(w, getTextPartHeader(message.Header{}, dec, msg.MIMEType), dec)
}
func writeAttachmentPart(
w *message.Writer,
kr *crypto.KeyRing,
att liteapi.Attachment,
attData []byte,
opts JobOptions,
) error {
kps, err := base64.StdEncoding.DecodeString(att.KeyPackets)
if err != nil {
return err
}
msg := crypto.NewPGPSplitMessage(kps, attData).GetPGPMessage()
dec, err := kr.Decrypt(msg, nil, crypto.GetUnixTime())
if err != nil {
if !opts.IgnoreDecryptionErrors {
return errors.Wrap(ErrDecryptionFailed, err.Error())
}
return buildRFC822(kr, msg, attData, req.options)
log.
WithField("attID", att.ID).
WithError(err).
Warn("Attachment decryption failed")
return writeCustomAttachmentPart(w, att, msg, err)
}
return writePart(w, getAttachmentPartHeader(att), dec.GetBinary())
}
func writeRelatedParts(
w *message.Writer,
kr *crypto.KeyRing,
boundary *boundary,
msg liteapi.Message,
atts []liteapi.Attachment,
attData [][]byte,
opts JobOptions,
) error {
hdr := message.Header{}
hdr.SetContentType("multipart/related", map[string]string{"boundary": boundary.gen()})
return createPart(w, hdr, func(rel *message.Writer) error {
if err := writeTextPart(rel, kr, msg, opts); err != nil {
return err
}
for i, att := range atts {
if err := writeAttachmentPart(rel, kr, att, attData[i], opts); err != nil {
return err
}
}
return nil
})
}
func buildPGPRFC822(kr *crypto.KeyRing, msg liteapi.Message, opts JobOptions) ([]byte, error) {
dec, err := msg.Decrypt(kr)
if err != nil {
if !opts.IgnoreDecryptionErrors {
return nil, errors.Wrap(ErrDecryptionFailed, err.Error())
}
return buildPGPMIMEFallbackRFC822(msg, opts)
}
hdr := getMessageHeader(msg, opts)
sigs, err := msg.ExtractSignatures(kr)
if err != nil {
log.WithError(err).WithField("id", msg.ID).Warn("Extract signature failed")
}
if len(sigs) > 0 {
return writeMultipartSignedRFC822(hdr, dec, sigs[0])
}
return writeMultipartEncryptedRFC822(hdr, dec)
}
func buildPGPMIMEFallbackRFC822(msg liteapi.Message, opts JobOptions) ([]byte, error) {
hdr := getMessageHeader(msg, opts)
hdr.SetContentType("multipart/encrypted", map[string]string{
"boundary": newBoundary(msg.ID).gen(),
"protocol": "application/pgp-encrypted",
})
buf := new(bytes.Buffer)
w, err := message.CreateWriter(buf, hdr)
if err != nil {
return nil, err
}
var encHdr message.Header
encHdr.SetContentType("application/pgp-encrypted", nil)
encHdr.Set("Content-Description", "PGP/MIME version identification")
if err := writePart(w, encHdr, []byte("Version: 1")); err != nil {
return nil, err
}
var dataHdr message.Header
dataHdr.SetContentType("application/octet-stream", map[string]string{"name": "encrypted.asc"})
dataHdr.SetContentDisposition("inline", map[string]string{"filename": "encrypted.asc"})
dataHdr.Set("Content-Description", "OpenPGP encrypted message")
if err := writePart(w, dataHdr, []byte(msg.Body)); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func writeMultipartSignedRFC822(header message.Header, body []byte, sig liteapi.Signature) ([]byte, error) { //nolint:funlen
buf := new(bytes.Buffer)
boundary := newBoundary("").gen()
header.SetContentType("multipart/signed", map[string]string{
"micalg": sig.Hash,
"protocol": "application/pgp-signature",
"boundary": boundary,
})
if err := textproto.WriteHeader(buf, header.Header); err != nil {
return nil, err
}
mw := textproto.NewMultipartWriter(buf)
if err := mw.SetBoundary(boundary); err != nil {
return nil, err
}
bodyHeader, bodyData, err := readHeaderBody(body)
if err != nil {
return nil, err
}
bodyPart, err := mw.CreatePart(*bodyHeader)
if err != nil {
return nil, err
}
if _, err := bodyPart.Write(bodyData); err != nil {
return nil, err
}
var sigHeader message.Header
sigHeader.SetContentType("application/pgp-signature", map[string]string{"name": "OpenPGP_signature.asc"})
sigHeader.SetContentDisposition("attachment", map[string]string{"filename": "OpenPGP_signature"})
sigHeader.Set("Content-Description", "OpenPGP digital signature")
sigPart, err := mw.CreatePart(sigHeader.Header)
if err != nil {
return nil, err
}
sigData, err := sig.Data.GetArmored()
if err != nil {
return nil, err
}
if _, err := sigPart.Write([]byte(sigData)); err != nil {
return nil, err
}
if err := mw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func writeMultipartEncryptedRFC822(header message.Header, body []byte) ([]byte, error) {
buf := new(bytes.Buffer)
bodyHeader, bodyData, err := readHeaderBody(body)
if err != nil {
return nil, err
}
// If parsed header is empty then either it is malformed or it is missing.
// Anyway message could not be considered multipart/mixed anymore since there will be no boundary.
if bodyHeader.Len() == 0 {
header.Del("Content-Type")
}
entFields := bodyHeader.Fields()
for entFields.Next() {
header.Set(entFields.Key(), entFields.Value())
}
if err := textproto.WriteHeader(buf, header.Header); err != nil {
return nil, err
}
if _, err := buf.Write(bodyData); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func getMessageHeader(msg liteapi.Message, opts JobOptions) message.Header { //nolint:funlen
hdr := toMessageHeader(msg.ParsedHeaders)
// SetText will RFC2047-encode.
if msg.Subject != "" {
hdr.SetText("Subject", msg.Subject)
}
// mail.Address.String() will RFC2047-encode if necessary.
if msg.Sender != nil {
hdr.Set("From", msg.Sender.String())
}
if len(msg.ReplyTos) > 0 {
hdr.Set("Reply-To", toAddressList(msg.ReplyTos))
}
if len(msg.ToList) > 0 {
hdr.Set("To", toAddressList(msg.ToList))
}
if len(msg.CCList) > 0 {
hdr.Set("Cc", toAddressList(msg.CCList))
}
if len(msg.BCCList) > 0 {
hdr.Set("Bcc", toAddressList(msg.BCCList))
}
setMessageIDIfNeeded(msg, &hdr)
// Sanitize the date; it needs to have a valid unix timestamp.
if opts.SanitizeDate {
if date, err := rfc5322.ParseDateTime(hdr.Get("Date")); err != nil || date.Before(time.Unix(0, 0)) {
msgDate := SanitizeMessageDate(msg.Time)
hdr.Set("Date", msgDate.In(time.UTC).Format(time.RFC1123Z))
// We clobbered the date so we save it under X-Original-Date.
hdr.Set("X-Original-Date", date.In(time.UTC).Format(time.RFC1123Z))
}
}
// Set our internal ID if requested.
// This is important for us to detect whether APPENDed things are actually "move like outlook".
if opts.AddInternalID {
hdr.Set("X-Pm-Internal-Id", msg.ID)
}
// Set our external ID if requested.
// This was useful during debugging of applemail recovered messages; doesn't help with any behaviour.
if opts.AddExternalID {
hdr.Set("X-Pm-External-Id", "<"+msg.ExternalID+">")
}
// Set our server date if requested.
// Can be useful to see how long it took for a message to arrive.
if opts.AddMessageDate {
hdr.Set("X-Pm-Date", time.Unix(msg.Time, 0).In(time.UTC).Format(time.RFC1123Z))
}
// Include the message ID in the references (supposedly this somehow improves outlook support...).
if opts.AddMessageIDReference {
if references := hdr.Get("References"); !strings.Contains(references, msg.ID) {
hdr.Set("References", references+" <"+msg.ID+"@"+InternalIDDomain+">")
}
}
return hdr
}
// SanitizeMessageDate will return time from msgTime timestamp. If timestamp is
// not after epoch the RFC822 publish day will be used. No message should
// realistically be older than RFC822 itself.
func SanitizeMessageDate(msgTime int64) time.Time {
if msgTime := time.Unix(msgTime, 0); msgTime.After(time.Unix(0, 0)) {
return msgTime
}
return time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC)
}
// setMessageIDIfNeeded sets Message-Id from ExternalID or ID if it's not
// already set.
func setMessageIDIfNeeded(msg liteapi.Message, hdr *message.Header) {
if hdr.Get("Message-Id") == "" {
if msg.ExternalID != "" {
hdr.Set("Message-Id", "<"+msg.ExternalID+">")
} else {
hdr.Set("Message-Id", "<"+msg.ID+"@"+InternalIDDomain+">")
}
}
}
func getTextPartHeader(hdr message.Header, body []byte, mimeType rfc822.MIMEType) message.Header {
params := make(map[string]string)
if utf8.Valid(body) {
params["charset"] = "utf-8"
}
hdr.SetContentType(string(mimeType), params)
// Use quoted-printable for all text/... parts
hdr.Set("Content-Transfer-Encoding", "quoted-printable")
return hdr
}
func getAttachmentPartHeader(att liteapi.Attachment) message.Header {
hdr := toMessageHeader(liteapi.Headers(att.Headers))
// All attachments have a content type.
hdr.SetContentType(string(att.MIMEType), map[string]string{"name": mime.QEncoding.Encode("utf-8", att.Name)})
// All attachments have a content disposition.
hdr.SetContentDisposition(string(att.Disposition), map[string]string{"filename": mime.QEncoding.Encode("utf-8", att.Name)})
// Use base64 for all attachments except embedded RFC822 messages.
if att.MIMEType != rfc822.MessageRFC822 {
hdr.Set("Content-Transfer-Encoding", "base64")
} else {
hdr.Del("Content-Transfer-Encoding")
}
return hdr
}
func toMessageHeader(hdr liteapi.Headers) message.Header {
var res message.Header
for key, val := range hdr {
for _, val := range val {
// Using AddRaw instead of Add to save key-value pair as byte buffer within Header.
// This buffer is used latter on in message writer to construct message and avoid crash
// when key length is more than 76 characters long.
res.AddRaw([]byte(key + ": " + val + "\r\n"))
}
}
return res
}
func toAddressList(addrs []*mail.Address) string {
res := make([]string, len(addrs))
for i, addr := range addrs {
res[i] = addr.String()
}
return strings.Join(res, ", ")
}
func createPart(w *message.Writer, hdr message.Header, fn func(*message.Writer) error) error {
part, err := w.CreatePart(hdr)
if err != nil {
return err
}
if err := fn(part); err != nil {
return err
}
return part.Close()
}
func writePart(w *message.Writer, hdr message.Header, body []byte) error {
return createPart(w, hdr, func(part *message.Writer) error {
if _, err := part.Write(body); err != nil {
return errors.Wrap(err, "failed to write part body")
}
return nil
})
}
type boundary struct {
val string
}
func newBoundary(seed string) *boundary {
return &boundary{val: seed}
}
func (bw *boundary) gen() string {
bw.val = algo.HashHexSHA256(bw.val)
return bw.val
}

View File

@ -1,35 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"github.com/ProtonMail/proton-bridge/v2/pkg/algo"
)
type boundary struct {
val string
}
func newBoundary(seed string) *boundary {
return &boundary{val: seed}
}
func (bw *boundary) gen() string {
bw.val = algo.HashHexSHA256(bw.val)
return bw.val
}

View File

@ -23,14 +23,14 @@ import (
"github.com/ProtonMail/gopenpgp/v2/constants"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/emersion/go-message"
"gitlab.protontech.ch/go/liteapi"
)
// writeCustomTextPart writes an armored-PGP text part for a message body that couldn't be decrypted.
func writeCustomTextPart(
w *message.Writer,
msg *pmapi.Message,
msg liteapi.Message,
decError error,
) error {
enc, err := crypto.NewPGPMessageFromArmored(msg.Body)
@ -48,7 +48,7 @@ func writeCustomTextPart(
var hdr message.Header
hdr.SetContentType(msg.MIMEType, nil)
hdr.SetContentType(string(msg.MIMEType), nil)
part, err := w.CreatePart(hdr)
if err != nil {
@ -65,7 +65,7 @@ func writeCustomTextPart(
// writeCustomAttachmentPart writes an armored-PGP data part for an attachment that couldn't be decrypted.
func writeCustomAttachmentPart(
w *message.Writer,
att *pmapi.Attachment,
att liteapi.Attachment,
msg *crypto.PGPMessage,
decError error,
) error {
@ -82,7 +82,7 @@ func writeCustomAttachmentPart(
var hdr message.Header
hdr.SetContentType("application/octet-stream", map[string]string{"name": filename})
hdr.SetContentDisposition(att.Disposition, map[string]string{"filename": filename})
hdr.SetContentDisposition(string(att.Disposition), map[string]string{"filename": filename})
part, err := w.CreatePart(hdr)
if err != nil {

View File

@ -1,168 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bytes"
"encoding/base64"
"io"
"mime"
"mime/multipart"
"net/http"
"net/textproto"
"strings"
"github.com/ProtonMail/gopenpgp/v2/crypto"
pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/emersion/go-message"
"github.com/emersion/go-textwrapper"
)
// BuildEncrypted is used for importing encrypted message.
func BuildEncrypted(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) ([]byte, error) { //nolint:funlen
b := &bytes.Buffer{}
boundary := newBoundary(m.ID).gen()
// Overwrite content for main header for import.
// Even if message has just simple body we should upload as multipart/mixed.
// Each part has encrypted body and header reflects the original header.
mainHeader := convertGoMessageToTextprotoHeader(getMessageHeader(m, JobOptions{}))
mainHeader.Set("Content-Type", "multipart/mixed; boundary="+boundary)
mainHeader.Del("Content-Disposition")
mainHeader.Del("Content-Transfer-Encoding")
if err := WriteHeader(b, mainHeader); err != nil {
return nil, err
}
mw := multipart.NewWriter(b)
if err := mw.SetBoundary(boundary); err != nil {
return nil, err
}
// Write the body part.
bodyHeader := make(textproto.MIMEHeader)
bodyHeader.Set("Content-Type", m.MIMEType+"; charset=utf-8")
bodyHeader.Set("Content-Disposition", pmapi.DispositionInline)
bodyHeader.Set("Content-Transfer-Encoding", "7bit")
p, err := mw.CreatePart(bodyHeader)
if err != nil {
return nil, err
}
// First, encrypt the message body.
if err := m.Encrypt(kr, kr); err != nil {
return nil, err
}
if _, err := io.WriteString(p, m.Body); err != nil {
return nil, err
}
// Write the attachments parts.
for i := 0; i < len(m.Attachments); i++ {
att := m.Attachments[i]
r := readers[i]
h := getAttachmentHeader(att, false)
p, err := mw.CreatePart(h)
if err != nil {
return nil, err
}
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
// Create encrypted writer.
pgpMessage, err := kr.Encrypt(crypto.NewPlainMessage(data), nil)
if err != nil {
return nil, err
}
ww := textwrapper.NewRFC822(p)
bw := base64.NewEncoder(base64.StdEncoding, ww)
if _, err := bw.Write(pgpMessage.GetBinary()); err != nil {
return nil, err
}
if err := bw.Close(); err != nil {
return nil, err
}
}
if err := mw.Close(); err != nil {
return nil, err
}
return b.Bytes(), nil
}
func convertGoMessageToTextprotoHeader(h message.Header) textproto.MIMEHeader {
out := make(textproto.MIMEHeader)
hf := h.Fields()
for hf.Next() {
// go-message fields are in the reverse order.
// textproto.MIMEHeader is not ordered except for the values of
// the same key which are ordered
key := textproto.CanonicalMIMEHeaderKey(hf.Key())
out[key] = append([]string{hf.Value()}, out[key]...)
}
return out
}
func getAttachmentHeader(att *pmapi.Attachment, buildForIMAP bool) textproto.MIMEHeader {
mediaType := att.MIMEType
if mediaType == "application/pgp-encrypted" {
mediaType = "application/octet-stream"
}
transferEncoding := "base64"
if mediaType == rfc822Message && buildForIMAP {
transferEncoding = "8bit"
}
encodedName := pmmime.EncodeHeader(att.Name)
disposition := "attachment" //nolint:goconst
if strings.Contains(att.Header.Get("Content-Disposition"), pmapi.DispositionInline) {
disposition = pmapi.DispositionInline
}
h := make(textproto.MIMEHeader)
h.Set("Content-Type", mime.FormatMediaType(mediaType, map[string]string{"name": encodedName}))
if transferEncoding != "" {
h.Set("Content-Transfer-Encoding", transferEncoding)
}
h.Set("Content-Disposition", mime.FormatMediaType(disposition, map[string]string{"filename": encodedName}))
// Forward some original header lines.
forward := []string{"Content-Id", "Content-Description", "Content-Location"}
for _, k := range forward {
v := att.Header.Get(k)
if v != "" {
h.Set(k, v)
}
}
return h
}
func WriteHeader(w io.Writer, h textproto.MIMEHeader) (err error) {
if err = http.Header(h).Write(w); err != nil {
return
}
_, err = io.WriteString(w, "\r\n")
return
}

View File

@ -21,46 +21,24 @@ import (
"bufio"
"bytes"
"encoding/base64"
"io"
"strings"
"testing"
"time"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v2/pkg/message/mocks"
"github.com/ProtonMail/proton-bridge/v2/pkg/message/parser"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gitlab.protontech.ch/go/liteapi"
"golang.org/x/text/encoding/htmlindex"
)
func newTestFetcher(
m *gomock.Controller,
kr *crypto.KeyRing,
msg *pmapi.Message,
attData ...[]byte,
) Fetcher {
f := mocks.NewMockFetcher(m)
f.EXPECT().GetMessage(gomock.Any(), msg.ID).Return(msg, nil)
for i, att := range msg.Attachments {
f.EXPECT().GetAttachment(gomock.Any(), att.ID).Return(newTestReadCloser(attData[i]), nil)
}
f.EXPECT().KeyRingForAddressID(msg.AddressID).Return(kr, nil)
return f
}
func newTestMessage(
t *testing.T,
kr *crypto.KeyRing,
messageID, addressID, mimeType, body string, //nolint:unparam
date time.Time,
) *pmapi.Message {
) liteapi.Message {
enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), kr)
require.NoError(t, err)
@ -70,57 +48,47 @@ func newTestMessage(
return newRawTestMessage(messageID, addressID, mimeType, arm, date)
}
func newRawTestMessage(messageID, addressID, mimeType, body string, date time.Time) *pmapi.Message {
return &pmapi.Message{
ID: messageID,
AddressID: addressID,
MIMEType: mimeType,
Header: map[string][]string{
func newRawTestMessage(messageID, addressID, mimeType, body string, date time.Time) liteapi.Message {
return liteapi.Message{
MessageMetadata: liteapi.MessageMetadata{
ID: messageID,
AddressID: addressID,
Time: date.Unix(),
},
ParsedHeaders: liteapi.Headers{
"Content-Type": {mimeType},
"Date": {date.In(time.UTC).Format(time.RFC1123Z)},
},
Body: body,
Time: date.Unix(),
MIMEType: rfc822.MIMEType(mimeType),
Body: body,
}
}
func addTestAttachment(
t *testing.T,
kr *crypto.KeyRing,
msg *pmapi.Message,
msg *liteapi.Message,
attachmentID, name, mimeType, disposition, data string,
) []byte {
enc, err := kr.EncryptAttachment(crypto.NewPlainMessageFromString(data), attachmentID+".bin")
require.NoError(t, err)
msg.Attachments = append(msg.Attachments, &pmapi.Attachment{
msg.Attachments = append(msg.Attachments, liteapi.Attachment{
ID: attachmentID,
Name: name,
MIMEType: mimeType,
Header: map[string][]string{
MIMEType: rfc822.MIMEType(mimeType),
Headers: liteapi.Headers{
"Content-Type": {mimeType},
"Content-Disposition": {disposition},
"Content-Transfer-Encoding": {"base64"},
},
Disposition: disposition,
Disposition: liteapi.Disposition(disposition),
KeyPackets: base64.StdEncoding.EncodeToString(enc.GetBinaryKeyPacket()),
})
return enc.GetBinaryDataPacket()
}
type testReadCloser struct {
io.Reader
}
func newTestReadCloser(b []byte) *testReadCloser {
return &testReadCloser{Reader: bytes.NewReader(b)}
}
func (testReadCloser) Close() error {
return nil
}
type testSection struct {
t *testing.T
part *parser.Part
@ -130,21 +98,18 @@ type testSection struct {
// NOTE: Each section is parsed individually --> cleaner test code but slower... improve this one day?
func section(t *testing.T, b []byte, section ...int) *testSection {
p, err := parser.New(bytes.NewReader(b))
assert.NoError(t, err)
require.NoError(t, err)
part, err := p.Section(section)
require.NoError(t, err)
bs, err := NewBodyStructure(bytes.NewReader(b))
require.NoError(t, err)
raw, err := bs.GetSection(bytes.NewReader(b), section)
s, err := rfc822.Parse(b).Part(section...)
require.NoError(t, err)
return &testSection{
t: t,
part: part,
raw: raw,
raw: s.Literal(),
}
}
@ -249,7 +214,7 @@ type isMatcher struct {
}
func (matcher isMatcher) match(t *testing.T, have string) {
assert.Equal(t, matcher.want, have)
require.Equal(t, matcher.want, have)
}
func is(want string) isMatcher {
@ -265,7 +230,7 @@ type isNotMatcher struct {
}
func (matcher isNotMatcher) match(t *testing.T, have string) {
assert.NotEqual(t, matcher.notWant, have)
require.NotEqual(t, matcher.notWant, have)
}
func isNot(notWant string) isNotMatcher {
@ -277,7 +242,7 @@ type containsMatcher struct {
}
func (matcher containsMatcher) match(t *testing.T, have string) {
assert.Contains(t, have, matcher.contains)
require.Contains(t, have, matcher.contains)
}
func contains(contains string) containsMatcher {
@ -296,7 +261,7 @@ func (matcher decryptsToMatcher) match(t *testing.T, have string) {
dec, err := matcher.kr.Decrypt(haveMsg, nil, crypto.GetUnixTime())
require.NoError(t, err)
assert.Equal(t, matcher.want, string(dec.GetBinary()))
require.Equal(t, matcher.want, string(dec.GetBinary()))
}
func decryptsTo(kr *crypto.KeyRing, want string) decryptsToMatcher {
@ -315,7 +280,7 @@ func (matcher decodesToMatcher) match(t *testing.T, have string) {
dec, err := enc.NewDecoder().String(have)
require.NoError(t, err)
assert.Equal(t, matcher.want, dec)
require.Equal(t, matcher.want, dec)
}
func decodesTo(charset string, want string) decodesToMatcher {
@ -328,8 +293,8 @@ type verifiesAgainstMatcher struct {
}
func (matcher verifiesAgainstMatcher) match(t *testing.T, have string) {
assert.NoError(t, matcher.kr.VerifyDetached(
crypto.NewPlainMessage(bytes.TrimSuffix([]byte(have), []byte("\r\n"))),
require.NoError(t, matcher.kr.VerifyDetached(
crypto.NewPlainMessage([]byte(have)),
matcher.sig,
crypto.GetUnixTime()),
)
@ -347,7 +312,7 @@ func (matcher maxLineLengthMatcher) match(t *testing.T, have string) {
scanner := bufio.NewScanner(strings.NewReader(have))
for scanner.Scan() {
assert.Less(t, len(scanner.Text()), matcher.wantMax)
require.Less(t, len(scanner.Text()), matcher.wantMax)
}
}

View File

@ -1,544 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bytes"
"encoding/base64"
"mime"
"net/mail"
"strings"
"time"
"unicode/utf8"
"github.com/ProtonMail/go-rfc5322"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/emersion/go-message"
"github.com/emersion/go-message/textproto"
"github.com/pkg/errors"
)
func buildRFC822(kr *crypto.KeyRing, msg *pmapi.Message, attData map[string][]byte, opts JobOptions) ([]byte, error) {
switch {
case len(msg.Attachments) > 0:
return buildMultipartRFC822(kr, msg, attData, opts)
case msg.MIMEType == "multipart/mixed":
return buildPGPRFC822(kr, msg, opts)
default:
return buildSimpleRFC822(kr, msg, opts)
}
}
func buildSimpleRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions) ([]byte, error) {
dec, err := msg.Decrypt(kr)
if err != nil {
if !opts.IgnoreDecryptionErrors {
return nil, errors.Wrap(ErrDecryptionFailed, err.Error())
}
return buildMultipartRFC822(kr, msg, nil, opts)
}
hdr := getTextPartHeader(getMessageHeader(msg, opts), dec, msg.MIMEType)
buf := new(bytes.Buffer)
w, err := message.CreateWriter(buf, hdr)
if err != nil {
return nil, err
}
if _, err := w.Write(dec); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func buildMultipartRFC822(
kr *crypto.KeyRing,
msg *pmapi.Message,
attData map[string][]byte,
opts JobOptions,
) ([]byte, error) {
boundary := newBoundary(msg.ID)
hdr := getMessageHeader(msg, opts)
hdr.SetContentType("multipart/mixed", map[string]string{"boundary": boundary.gen()})
buf := new(bytes.Buffer)
w, err := message.CreateWriter(buf, hdr)
if err != nil {
return nil, err
}
var (
inlineAtts []*pmapi.Attachment
inlineData [][]byte
attachAtts []*pmapi.Attachment
attachData [][]byte
)
for _, att := range msg.Attachments {
if att.Disposition == pmapi.DispositionInline {
inlineAtts = append(inlineAtts, att)
inlineData = append(inlineData, attData[att.ID])
} else {
attachAtts = append(attachAtts, att)
attachData = append(attachData, attData[att.ID])
}
}
if len(inlineAtts) > 0 {
if err := writeRelatedParts(w, kr, boundary, msg, inlineAtts, inlineData, opts); err != nil {
return nil, err
}
} else if err := writeTextPart(w, kr, msg, opts); err != nil {
return nil, err
}
for i, att := range attachAtts {
if err := writeAttachmentPart(w, kr, att, attachData[i], opts); err != nil {
return nil, err
}
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func writeTextPart(
w *message.Writer,
kr *crypto.KeyRing,
msg *pmapi.Message,
opts JobOptions,
) error {
dec, err := msg.Decrypt(kr)
if err != nil {
if !opts.IgnoreDecryptionErrors {
return errors.Wrap(ErrDecryptionFailed, err.Error())
}
return writeCustomTextPart(w, msg, err)
}
return writePart(w, getTextPartHeader(message.Header{}, dec, msg.MIMEType), dec)
}
func writeAttachmentPart(
w *message.Writer,
kr *crypto.KeyRing,
att *pmapi.Attachment,
attData []byte,
opts JobOptions,
) error {
kps, err := base64.StdEncoding.DecodeString(att.KeyPackets)
if err != nil {
return err
}
msg := crypto.NewPGPSplitMessage(kps, attData).GetPGPMessage()
dec, err := kr.Decrypt(msg, nil, crypto.GetUnixTime())
if err != nil {
if !opts.IgnoreDecryptionErrors {
return errors.Wrap(ErrDecryptionFailed, err.Error())
}
log.
WithField("attID", att.ID).
WithField("msgID", att.MessageID).
WithError(err).
Warn("Attachment decryption failed")
return writeCustomAttachmentPart(w, att, msg, err)
}
return writePart(w, getAttachmentPartHeader(att), dec.GetBinary())
}
func writeRelatedParts(
w *message.Writer,
kr *crypto.KeyRing,
boundary *boundary,
msg *pmapi.Message,
atts []*pmapi.Attachment,
attData [][]byte,
opts JobOptions,
) error {
hdr := message.Header{}
hdr.SetContentType("multipart/related", map[string]string{"boundary": boundary.gen()})
return createPart(w, hdr, func(rel *message.Writer) error {
if err := writeTextPart(rel, kr, msg, opts); err != nil {
return err
}
for i, att := range atts {
if err := writeAttachmentPart(rel, kr, att, attData[i], opts); err != nil {
return err
}
}
return nil
})
}
func buildPGPRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions) ([]byte, error) {
dec, err := msg.Decrypt(kr)
if err != nil {
if !opts.IgnoreDecryptionErrors {
return nil, errors.Wrap(ErrDecryptionFailed, err.Error())
}
return buildPGPMIMEFallbackRFC822(msg, opts)
}
hdr := getMessageHeader(msg, opts)
sigs, err := msg.ExtractSignatures(kr)
if err != nil {
log.WithError(err).WithField("id", msg.ID).Warn("Extract signature failed")
}
if len(sigs) > 0 {
return writeMultipartSignedRFC822(hdr, dec, sigs[0])
}
return writeMultipartEncryptedRFC822(hdr, dec)
}
func buildPGPMIMEFallbackRFC822(msg *pmapi.Message, opts JobOptions) ([]byte, error) {
hdr := getMessageHeader(msg, opts)
hdr.SetContentType("multipart/encrypted", map[string]string{
"boundary": newBoundary(msg.ID).gen(),
"protocol": "application/pgp-encrypted",
})
buf := new(bytes.Buffer)
w, err := message.CreateWriter(buf, hdr)
if err != nil {
return nil, err
}
var encHdr message.Header
encHdr.SetContentType("application/pgp-encrypted", nil)
encHdr.Set("Content-Description", "PGP/MIME version identification")
if err := writePart(w, encHdr, []byte("Version: 1")); err != nil {
return nil, err
}
var dataHdr message.Header
dataHdr.SetContentType("application/octet-stream", map[string]string{"name": "encrypted.asc"})
dataHdr.SetContentDisposition("inline", map[string]string{"filename": "encrypted.asc"})
dataHdr.Set("Content-Description", "OpenPGP encrypted message")
if err := writePart(w, dataHdr, []byte(msg.Body)); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func writeMultipartSignedRFC822(header message.Header, body []byte, sig pmapi.Signature) ([]byte, error) { //nolint:funlen
buf := new(bytes.Buffer)
boundary := newBoundary("").gen()
header.SetContentType("multipart/signed", map[string]string{
"micalg": sig.Hash,
"protocol": "application/pgp-signature",
"boundary": boundary,
})
if err := textproto.WriteHeader(buf, header.Header); err != nil {
return nil, err
}
mw := textproto.NewMultipartWriter(buf)
if err := mw.SetBoundary(boundary); err != nil {
return nil, err
}
bodyHeader, bodyData, err := readHeaderBody(body)
if err != nil {
return nil, err
}
bodyPart, err := mw.CreatePart(*bodyHeader)
if err != nil {
return nil, err
}
if _, err := bodyPart.Write(bodyData); err != nil {
return nil, err
}
var sigHeader message.Header
sigHeader.SetContentType("application/pgp-signature", map[string]string{"name": "OpenPGP_signature.asc"})
sigHeader.SetContentDisposition("attachment", map[string]string{"filename": "OpenPGP_signature"})
sigHeader.Set("Content-Description", "OpenPGP digital signature")
sigPart, err := mw.CreatePart(sigHeader.Header)
if err != nil {
return nil, err
}
sigData, err := crypto.NewPGPSignature(sig.Data).GetArmored()
if err != nil {
return nil, err
}
if _, err := sigPart.Write([]byte(sigData)); err != nil {
return nil, err
}
if err := mw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func writeMultipartEncryptedRFC822(header message.Header, body []byte) ([]byte, error) {
buf := new(bytes.Buffer)
bodyHeader, bodyData, err := readHeaderBody(body)
if err != nil {
return nil, err
}
// If parsed header is empty then either it is malformed or it is missing.
// Anyway message could not be considered multipart/mixed anymore since there will be no boundary.
if bodyHeader.Len() == 0 {
header.Del("Content-Type")
}
entFields := bodyHeader.Fields()
for entFields.Next() {
header.Set(entFields.Key(), entFields.Value())
}
if err := textproto.WriteHeader(buf, header.Header); err != nil {
return nil, err
}
if _, err := buf.Write(bodyData); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func getMessageHeader(msg *pmapi.Message, opts JobOptions) message.Header { //nolint:funlen
hdr := toMessageHeader(msg.Header)
// SetText will RFC2047-encode.
if msg.Subject != "" {
hdr.SetText("Subject", msg.Subject)
}
// mail.Address.String() will RFC2047-encode if necessary.
if msg.Sender != nil {
hdr.Set("From", msg.Sender.String())
}
if len(msg.ReplyTos) > 0 {
hdr.Set("Reply-To", toAddressList(msg.ReplyTos))
}
if len(msg.ToList) > 0 {
hdr.Set("To", toAddressList(msg.ToList))
}
if len(msg.CCList) > 0 {
hdr.Set("Cc", toAddressList(msg.CCList))
}
if len(msg.BCCList) > 0 {
hdr.Set("Bcc", toAddressList(msg.BCCList))
}
setMessageIDIfNeeded(msg, &hdr)
// Sanitize the date; it needs to have a valid unix timestamp.
if opts.SanitizeDate {
if date, err := rfc5322.ParseDateTime(hdr.Get("Date")); err != nil || date.Before(time.Unix(0, 0)) {
msgDate := SanitizeMessageDate(msg.Time)
hdr.Set("Date", msgDate.In(time.UTC).Format(time.RFC1123Z))
// We clobbered the date so we save it under X-Original-Date.
hdr.Set("X-Original-Date", date.In(time.UTC).Format(time.RFC1123Z))
}
}
// Set our internal ID if requested.
// This is important for us to detect whether APPENDed things are actually "move like outlook".
if opts.AddInternalID {
hdr.Set("X-Pm-Internal-Id", msg.ID)
}
// Set our external ID if requested.
// This was useful during debugging of applemail recovered messages; doesn't help with any behaviour.
if opts.AddExternalID {
hdr.Set("X-Pm-External-Id", "<"+msg.ExternalID+">")
}
// Set our server date if requested.
// Can be useful to see how long it took for a message to arrive.
if opts.AddMessageDate {
hdr.Set("X-Pm-Date", time.Unix(msg.Time, 0).In(time.UTC).Format(time.RFC1123Z))
}
// Include the message ID in the references (supposedly this somehow improves outlook support...).
if opts.AddMessageIDReference {
if references := hdr.Get("References"); !strings.Contains(references, msg.ID) {
hdr.Set("References", references+" <"+msg.ID+"@"+pmapi.InternalIDDomain+">")
}
}
return hdr
}
// SanitizeMessageDate will return time from msgTime timestamp. If timestamp is
// not after epoch the RFC822 publish day will be used. No message should
// realistically be older than RFC822 itself.
func SanitizeMessageDate(msgTime int64) time.Time {
if msgTime := time.Unix(msgTime, 0); msgTime.After(time.Unix(0, 0)) {
return msgTime
}
return time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC)
}
// setMessageIDIfNeeded sets Message-Id from ExternalID or ID if it's not
// already set.
func setMessageIDIfNeeded(msg *pmapi.Message, hdr *message.Header) {
if hdr.Get("Message-Id") == "" {
if msg.ExternalID != "" {
hdr.Set("Message-Id", "<"+msg.ExternalID+">")
} else {
hdr.Set("Message-Id", "<"+msg.ID+"@"+pmapi.InternalIDDomain+">")
}
}
}
func getTextPartHeader(hdr message.Header, body []byte, mimeType string) message.Header {
params := make(map[string]string)
if utf8.Valid(body) {
params["charset"] = "utf-8"
}
hdr.SetContentType(mimeType, params)
// Use quoted-printable for all text/... parts
hdr.Set("Content-Transfer-Encoding", "quoted-printable")
return hdr
}
func getAttachmentPartHeader(att *pmapi.Attachment) message.Header {
hdr := toMessageHeader(mail.Header(att.Header))
// All attachments have a content type.
hdr.SetContentType(att.MIMEType, map[string]string{"name": mime.QEncoding.Encode("utf-8", att.Name)})
// All attachments have a content disposition.
hdr.SetContentDisposition(att.Disposition, map[string]string{"filename": mime.QEncoding.Encode("utf-8", att.Name)})
// Use base64 for all attachments except embedded RFC822 messages.
if att.MIMEType != rfc822Message {
hdr.Set("Content-Transfer-Encoding", "base64")
} else {
hdr.Del("Content-Transfer-Encoding")
}
return hdr
}
func toMessageHeader(hdr mail.Header) message.Header {
var res message.Header
for key, val := range hdr {
for _, val := range val {
// Using AddRaw instead of Add to save key-value pair as byte buffer within Header.
// This buffer is used latter on in message writer to construct message and avoid crash
// when key length is more than 76 characters long.
res.AddRaw([]byte(key + ": " + val + "\r\n"))
}
}
return res
}
func toAddressList(addrs []*mail.Address) string {
res := make([]string, len(addrs))
for i, addr := range addrs {
res[i] = addr.String()
}
return strings.Join(res, ", ")
}
func createPart(w *message.Writer, hdr message.Header, fn func(*message.Writer) error) error {
part, err := w.CreatePart(hdr)
if err != nil {
return err
}
if err := fn(part); err != nil {
return err
}
return part.Close()
}
func writePart(w *message.Writer, hdr message.Header, body []byte) error {
return createPart(w, hdr, func(part *message.Writer) error {
if _, err := part.Write(body); err != nil {
return errors.Wrap(err, "failed to write part body")
}
return nil
})
}

File diff suppressed because it is too large Load Diff

View File

@ -1,245 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bytes"
"encoding/base64"
"io"
"mime"
"mime/quotedprintable"
"strings"
"github.com/ProtonMail/gopenpgp/v2/crypto"
pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime"
"github.com/emersion/go-message/textproto"
"github.com/pkg/errors"
)
func EncryptRFC822(kr *crypto.KeyRing, r io.Reader) ([]byte, error) {
b, err := io.ReadAll(r)
if err != nil {
return nil, err
}
header, body, err := readHeaderBody(b)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
result, err := writeEncryptedPart(kr, header, bytes.NewReader(body))
if err != nil {
return nil, err
}
if err := textproto.WriteHeader(buf, *header); err != nil {
return nil, err
}
if _, err := result.WriteTo(buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func writeEncryptedPart(kr *crypto.KeyRing, header *textproto.Header, r io.Reader) (io.WriterTo, error) {
decoder := getTransferDecoder(r, header.Get("Content-Transfer-Encoding"))
encoded := new(bytes.Buffer)
contentType, contentParams, err := parseContentType(header.Get("Content-Type"))
// Ignoring invalid media parameter makes it work for invalid tutanota RFC2047-encoded attachment filenames since we often only really need the content type and not the optional media parameters.
if err != nil && !errors.Is(err, mime.ErrInvalidMediaParameter) {
return nil, err
}
switch {
case contentType == "", strings.HasPrefix(contentType, "text/"), strings.HasPrefix(contentType, "message/"):
header.Del("Content-Transfer-Encoding")
if charset, ok := contentParams["charset"]; ok {
if reader, err := pmmime.CharsetReader(charset, decoder); err == nil {
decoder = reader
// We can decode the charset to utf-8 so let's set that as the content type charset parameter.
contentParams["charset"] = "utf-8"
header.Set("Content-Type", mime.FormatMediaType(contentType, contentParams))
}
}
if err := encode(&writeCloser{encoded}, func(w io.Writer) error {
return writeEncryptedTextPart(w, decoder, kr)
}); err != nil {
return nil, err
}
case contentType == "multipart/encrypted":
if _, err := encoded.ReadFrom(decoder); err != nil {
return nil, err
}
case strings.HasPrefix(contentType, "multipart/"):
if err := encode(&writeCloser{encoded}, func(w io.Writer) error {
return writeEncryptedMultiPart(kr, w, header, decoder)
}); err != nil {
return nil, err
}
default:
header.Set("Content-Transfer-Encoding", "base64")
if err := encode(base64.NewEncoder(base64.StdEncoding, encoded), func(w io.Writer) error {
return writeEncryptedAttachmentPart(w, decoder, kr)
}); err != nil {
return nil, err
}
}
return encoded, nil
}
func writeEncryptedTextPart(w io.Writer, r io.Reader, kr *crypto.KeyRing) error {
dec, err := io.ReadAll(r)
if err != nil {
return err
}
var arm string
if msg, err := crypto.NewPGPMessageFromArmored(string(dec)); err != nil {
enc, err := kr.Encrypt(crypto.NewPlainMessage(dec), kr)
if err != nil {
return err
}
if arm, err = enc.GetArmored(); err != nil {
return err
}
} else if arm, err = msg.GetArmored(); err != nil {
return err
}
if _, err := io.WriteString(w, arm); err != nil {
return err
}
return nil
}
func writeEncryptedAttachmentPart(w io.Writer, r io.Reader, kr *crypto.KeyRing) error {
dec, err := io.ReadAll(r)
if err != nil {
return err
}
enc, err := kr.Encrypt(crypto.NewPlainMessage(dec), kr)
if err != nil {
return err
}
if _, err := w.Write(enc.GetBinary()); err != nil {
return err
}
return nil
}
func writeEncryptedMultiPart(kr *crypto.KeyRing, w io.Writer, header *textproto.Header, r io.Reader) error {
_, contentParams, err := parseContentType(header.Get("Content-Type"))
if err != nil {
return err
}
scanner, err := newPartScanner(r, contentParams["boundary"])
if err != nil {
return err
}
parts, err := scanner.scanAll()
if err != nil {
return err
}
writer := newPartWriter(w, contentParams["boundary"])
for _, part := range parts {
header, body, err := readHeaderBody(part.b)
if err != nil {
return err
}
result, err := writeEncryptedPart(kr, header, bytes.NewReader(body))
if err != nil {
return err
}
if err := writer.createPart(func(w io.Writer) error {
if err := textproto.WriteHeader(w, *header); err != nil {
return err
}
if _, err := result.WriteTo(w); err != nil {
return err
}
return nil
}); err != nil {
return err
}
}
return writer.done()
}
func getTransferDecoder(r io.Reader, encoding string) io.Reader {
switch strings.ToLower(encoding) {
case "base64":
return base64.NewDecoder(base64.StdEncoding, r)
case "quoted-printable":
return quotedprintable.NewReader(r)
default:
return r
}
}
func encode(wc io.WriteCloser, fn func(io.Writer) error) error {
if err := fn(wc); err != nil {
return err
}
return wc.Close()
}
type writeCloser struct {
io.Writer
}
func (writeCloser) Close() error { return nil }
func parseContentType(val string) (string, map[string]string, error) {
if val == "" {
val = "text/plain"
}
return pmmime.ParseMediaType(val)
}

View File

@ -1,101 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bytes"
"os"
"testing"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/stretchr/testify/require"
)
func TestEncryptRFC822(t *testing.T) {
literal, err := os.ReadFile("testdata/text_plain_latin1.eml")
require.NoError(t, err)
key, err := crypto.GenerateKey("name", "email", "rsa", 2048)
require.NoError(t, err)
kr, err := crypto.NewKeyRing(key)
require.NoError(t, err)
enc, err := EncryptRFC822(kr, bytes.NewReader(literal))
require.NoError(t, err)
section(t, enc).
expectContentType(is(`text/plain`)).
expectContentTypeParam(`charset`, is(`utf-8`)).
expectBody(decryptsTo(kr, `ééééééé`))
}
func TestEncryptRFC822Multipart(t *testing.T) {
literal, err := os.ReadFile("testdata/multipart_alternative_nested.eml")
require.NoError(t, err)
key, err := crypto.GenerateKey("name", "email", "rsa", 2048)
require.NoError(t, err)
kr, err := crypto.NewKeyRing(key)
require.NoError(t, err)
enc, err := EncryptRFC822(kr, bytes.NewReader(literal))
require.NoError(t, err)
section(t, enc).
expectContentType(is(`multipart/alternative`))
section(t, enc, 1).
expectContentType(is(`multipart/alternative`))
section(t, enc, 1, 1).
expectContentType(is(`text/plain`)).
expectBody(decryptsTo(kr, "*multipart 1.1*\n\n"))
section(t, enc, 1, 2).
expectContentType(is(`text/html`)).
expectBody(decryptsTo(kr, `<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<b>multipart 1.2</b>
</body>
</html>
`))
section(t, enc, 2).
expectContentType(is(`multipart/alternative`))
section(t, enc, 2, 1).
expectContentType(is(`text/plain`)).
expectBody(decryptsTo(kr, "*multipart 2.1*\n\n"))
section(t, enc, 2, 2).
expectContentType(is(`text/html`)).
expectBody(decryptsTo(kr, `<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<b>multipart 2.2</b>
</body>
</html>
`))
}

View File

@ -1,67 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"net/mail"
"net/textproto"
"strings"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/emersion/go-imap"
)
// GetEnvelope will prepare envelope from pmapi message and cached header.
func GetEnvelope(msg *pmapi.Message, header textproto.MIMEHeader) *imap.Envelope {
hdr := toMessageHeader(mail.Header(header))
setMessageIDIfNeeded(msg, &hdr)
return &imap.Envelope{
Date: SanitizeMessageDate(msg.Time),
Subject: msg.Subject,
From: getAddresses([]*mail.Address{msg.Sender}),
Sender: getAddresses([]*mail.Address{msg.Sender}),
ReplyTo: getAddresses(msg.ReplyTos),
To: getAddresses(msg.ToList),
Cc: getAddresses(msg.CCList),
Bcc: getAddresses(msg.BCCList),
InReplyTo: hdr.Get("In-Reply-To"),
MessageId: hdr.Get("Message-Id"),
}
}
func getAddresses(addrs []*mail.Address) (imapAddrs []*imap.Address) {
for _, a := range addrs {
if a == nil {
continue
}
parts := strings.SplitN(a.Address, "@", 2)
if len(parts) != 2 {
continue
}
imapAddrs = append(imapAddrs, &imap.Address{
PersonalName: a.Name,
MailboxName: parts[0],
HostName: parts[1],
})
}
return
}

View File

@ -1,61 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/emersion/go-imap"
)
// Various client specific flags.
const (
AppleMailJunkFlag = "$Junk"
ThunderbirdJunkFlag = "Junk"
ThunderbirdNonJunkFlag = "NonJunk"
)
// GetFlags returns imap flags from pmapi message attributes.
func GetFlags(m *pmapi.Message) (flags []string) {
if !m.Unread {
flags = append(flags, imap.SeenFlag)
}
if !m.Has(pmapi.FlagSent) && !m.Has(pmapi.FlagReceived) {
flags = append(flags, imap.DraftFlag)
}
if m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll) {
flags = append(flags, imap.AnsweredFlag)
}
hasSpam := false
for _, l := range m.LabelIDs {
if l == pmapi.StarredLabel {
flags = append(flags, imap.FlaggedFlag)
}
if l == pmapi.SpamLabel {
flags = append(flags, AppleMailJunkFlag, ThunderbirdJunkFlag)
hasSpam = true
}
}
if !hasSpam {
flags = append(flags, ThunderbirdNonJunkFlag)
}
return
}

View File

@ -1,27 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"github.com/ProtonMail/go-rfc5322"
pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime"
)
func init() { //nolint:gochecknoinits
rfc5322.CharsetReader = pmmime.CharsetReader
}

View File

@ -23,8 +23,4 @@ import (
"github.com/sirupsen/logrus"
)
const (
rfc822Message = "message/rfc822"
)
var log = logrus.WithField("pkg", "pkg/message") //nolint:gochecknoglobals

View File

@ -1,83 +0,0 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/v2/pkg/message (interfaces: Fetcher)
// Package mocks is a generated GoMock package.
package mocks
import (
context "context"
io "io"
reflect "reflect"
crypto "github.com/ProtonMail/gopenpgp/v2/crypto"
pmapi "github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
)
// MockFetcher is a mock of Fetcher interface.
type MockFetcher struct {
ctrl *gomock.Controller
recorder *MockFetcherMockRecorder
}
// MockFetcherMockRecorder is the mock recorder for MockFetcher.
type MockFetcherMockRecorder struct {
mock *MockFetcher
}
// NewMockFetcher creates a new mock instance.
func NewMockFetcher(ctrl *gomock.Controller) *MockFetcher {
mock := &MockFetcher{ctrl: ctrl}
mock.recorder = &MockFetcherMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFetcher) EXPECT() *MockFetcherMockRecorder {
return m.recorder
}
// GetAttachment mocks base method.
func (m *MockFetcher) GetAttachment(arg0 context.Context, arg1 string) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAttachment", arg0, arg1)
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAttachment indicates an expected call of GetAttachment.
func (mr *MockFetcherMockRecorder) GetAttachment(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachment", reflect.TypeOf((*MockFetcher)(nil).GetAttachment), arg0, arg1)
}
// GetMessage mocks base method.
func (m *MockFetcher) GetMessage(arg0 context.Context, arg1 string) (*pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMessage", arg0, arg1)
ret0, _ := ret[0].(*pmapi.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMessage indicates an expected call of GetMessage.
func (mr *MockFetcherMockRecorder) GetMessage(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockFetcher)(nil).GetMessage), arg0, arg1)
}
// KeyRingForAddressID mocks base method.
func (m *MockFetcher) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyRingForAddressID", arg0)
ret0, _ := ret[0].(*crypto.KeyRing)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// KeyRingForAddressID indicates an expected call of KeyRingForAddressID.
func (mr *MockFetcherMockRecorder) KeyRingForAddressID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyRingForAddressID", reflect.TypeOf((*MockFetcher)(nil).KeyRingForAddressID), arg0)
}

View File

@ -23,98 +23,146 @@ import (
"io"
"mime"
"net/mail"
"net/textproto"
"regexp"
"strings"
"github.com/ProtonMail/gluon/rfc822"
"github.com/ProtonMail/go-rfc5322"
"github.com/ProtonMail/proton-bridge/v2/pkg/message/parser"
pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime"
"github.com/ProtonMail/proton-bridge/v2/pkg/pmapi"
"github.com/bradenaw/juniper/xslices"
"github.com/emersion/go-message"
"github.com/jaytaylor/html2text"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"gitlab.protontech.ch/go/liteapi"
)
// Parse parses RAW message.
func Parse(r io.Reader) (m *pmapi.Message, mimeBody, plainBody string, attReaders []io.Reader, err error) {
defer func() {
r := recover()
if r == nil {
return
}
type MIMEBody string
err = fmt.Errorf("panic while parsing message: %v", r)
type Body string
type Message struct {
Header mail.Header
MIMEBody MIMEBody
RichBody Body
PlainBody Body
Time int64
ExternalID string
Subject string
Sender *mail.Address
ToList []*mail.Address
CCList []*mail.Address
BCCList []*mail.Address
ReplyTos []*mail.Address
MIMEType rfc822.MIMEType
Attachments []Attachment
}
func (m *Message) Recipients() []string {
var recipients []string
for _, addresses := range [][]*mail.Address{m.ToList, m.CCList, m.BCCList} {
recipients = append(recipients, xslices.Map(addresses, func(address *mail.Address) string {
return address.Address
})...)
}
return recipients
}
type Attachment struct {
Header mail.Header
Name string
ContentID string
MIMEType string
Disposition string
Data []byte
}
// Parse parses an RFC822 message.
func Parse(r io.Reader) (m Message, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic while parsing message: %v", r)
}
}()
p, err := parser.New(r)
if err != nil {
return nil, "", "", nil, errors.Wrap(err, "failed to create new parser")
return Message{}, errors.Wrap(err, "failed to create new parser")
}
m, plainBody, attReaders, err = ParserWithParser(p)
if err != nil {
return nil, "", "", nil, errors.Wrap(err, "failed to parse the message")
}
mimeBody, err = BuildMIMEBody(p)
if err != nil {
return nil, "", "", nil, errors.Wrap(err, "failed to build mime body")
}
return m, mimeBody, plainBody, attReaders, nil
return parse(p)
}
// ParserWithParser parses message from Parser without building MIME body.
func ParserWithParser(p *parser.Parser) (m *pmapi.Message, plainBody string, attReaders []io.Reader, err error) {
logrus.Trace("Parsing message")
// Parse parses an RFC822 message using an existing parser.
func ParseWithParser(p *parser.Parser) (m Message, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic while parsing message: %v", r)
}
}()
if err = convertEncodedTransferEncoding(p); err != nil {
err = errors.Wrap(err, "failed to convert encoded transfer encodings")
return
}
if err = convertForeignEncodings(p); err != nil {
err = errors.Wrap(err, "failed to convert foreign encodings")
return
}
m = pmapi.NewMessage()
if err = parseMessageHeader(m, p.Root().Header); err != nil {
err = errors.Wrap(err, "failed to parse message header")
return
}
if m.Attachments, attReaders, err = collectAttachments(p); err != nil {
err = errors.Wrap(err, "failed to collect attachments")
return
}
if m.Body, plainBody, err = buildBodies(p); err != nil {
err = errors.Wrap(err, "failed to build bodies")
return
}
if m.MIMEType, err = determineMIMEType(p); err != nil {
err = errors.Wrap(err, "failed to determine mime type")
return
}
return m, plainBody, attReaders, nil
return parse(p)
}
// BuildMIMEBody builds mime body from the parser returned by NewParser.
func BuildMIMEBody(p *parser.Parser) (mimeBody string, err error) {
mimeBodyBuffer := new(bytes.Buffer)
if err = p.NewWriter().Write(mimeBodyBuffer); err != nil {
err = errors.Wrap(err, "failed to write out mime message")
return
func parse(p *parser.Parser) (Message, error) {
if err := convertEncodedTransferEncoding(p); err != nil {
return Message{}, errors.Wrap(err, "failed to convert encoded transfer encoding")
}
return mimeBodyBuffer.String(), nil
if err := convertForeignEncodings(p); err != nil {
return Message{}, errors.Wrap(err, "failed to convert foreign encodings")
}
m, err := parseMessageHeader(p.Root().Header)
if err != nil {
return Message{}, errors.Wrap(err, "failed to parse message header")
}
atts, err := collectAttachments(p)
if err != nil {
return Message{}, errors.Wrap(err, "failed to collect attachments")
}
m.Attachments = atts
richBody, plainBody, err := buildBodies(p)
if err != nil {
return Message{}, errors.Wrap(err, "failed to build bodies")
}
mimeBody, err := buildMIMEBody(p)
if err != nil {
return Message{}, errors.Wrap(err, "failed to build mime body")
}
m.RichBody = Body(richBody)
m.PlainBody = Body(plainBody)
m.MIMEBody = MIMEBody(mimeBody)
mimeType, err := determineMIMEType(p)
if err != nil {
return Message{}, errors.Wrap(err, "failed to get mime type")
}
m.MIMEType = rfc822.MIMEType(mimeType)
return m, nil
}
// buildMIMEBody builds mime body from the parser returned by NewParser.
func buildMIMEBody(p *parser.Parser) (mimeBody string, err error) {
buf := new(bytes.Buffer)
if err := p.NewWriter().Write(buf); err != nil {
return "", fmt.Errorf("failed to write message: %w", err)
}
return buf.String(), nil
}
// convertEncodedTransferEncoding decodes any RFC2047-encoded content transfer encodings.
@ -158,33 +206,30 @@ func convertForeignEncodings(p *parser.Parser) error {
Walk()
}
func collectAttachments(p *parser.Parser) ([]*pmapi.Attachment, []io.Reader, error) {
func collectAttachments(p *parser.Parser) ([]Attachment, error) {
var (
atts []*pmapi.Attachment
data []io.Reader
atts []Attachment
err error
)
w := p.NewWalker().
RegisterContentDispositionHandler("attachment", func(p *parser.Part) error {
att, err := parseAttachment(p.Header)
att, err := parseAttachment(p.Header, p.Body)
if err != nil {
return err
}
atts = append(atts, att)
data = append(data, bytes.NewReader(p.Body))
return nil
}).
RegisterContentTypeHandler("text/calendar", func(p *parser.Part) error {
att, err := parseAttachment(p.Header)
att, err := parseAttachment(p.Header, p.Body)
if err != nil {
return err
}
atts = append(atts, att)
data = append(data, bytes.NewReader(p.Body))
return nil
}).
@ -196,22 +241,21 @@ func collectAttachments(p *parser.Parser) ([]*pmapi.Attachment, []io.Reader, err
return nil
}
att, err := parseAttachment(p.Header)
att, err := parseAttachment(p.Header, p.Body)
if err != nil {
return err
}
atts = append(atts, att)
data = append(data, bytes.NewReader(p.Body))
return nil
})
if err = w.Walk(); err != nil {
return nil, nil, err
return nil, err
}
return atts, data, nil
return atts, nil
}
// buildBodies collects all text/html and text/plain parts and returns two bodies,
@ -400,24 +444,14 @@ func getPlainBody(part *parser.Part) []byte {
}
}
func AttachPublicKey(p *parser.Parser, key, keyName string) {
h := message.Header{}
func parseMessageHeader(h message.Header) (Message, error) { //nolint:funlen
var m Message
h.Set("Content-Type", fmt.Sprintf(`application/pgp-keys; name="%v.asc"; filename="%v.asc"`, keyName, keyName))
h.Set("Content-Disposition", fmt.Sprintf(`attachment; name="%v.asc"; filename="%v.asc"`, keyName, keyName))
h.Set("Content-Transfer-Encoding", "base64")
p.Root().AddChild(&parser.Part{
Header: h,
Body: []byte(key),
})
}
func parseMessageHeader(m *pmapi.Message, h message.Header) error { //nolint:funlen
mimeHeader, err := toMailHeader(h)
if err != nil {
return err
return Message{}, err
}
m.Header = mimeHeader
fields := h.Fields()
@ -428,7 +462,7 @@ func parseMessageHeader(m *pmapi.Message, h message.Header) error { //nolint:fun
s, err := fields.Text()
if err != nil {
if s, err = pmmime.DecodeHeader(fields.Value()); err != nil {
return errors.Wrap(err, "failed to parse subject")
return Message{}, errors.Wrap(err, "failed to parse subject")
}
}
@ -437,7 +471,7 @@ func parseMessageHeader(m *pmapi.Message, h message.Header) error { //nolint:fun
case "from":
sender, err := rfc5322.ParseAddressList(fields.Value())
if err != nil {
return errors.Wrap(err, "failed to parse from")
return Message{}, errors.Wrap(err, "failed to parse from")
}
if len(sender) > 0 {
m.Sender = sender[0]
@ -446,35 +480,35 @@ func parseMessageHeader(m *pmapi.Message, h message.Header) error { //nolint:fun
case "to":
toList, err := rfc5322.ParseAddressList(fields.Value())
if err != nil {
return errors.Wrap(err, "failed to parse to")
return Message{}, errors.Wrap(err, "failed to parse to")
}
m.ToList = toList
case "reply-to":
replyTos, err := rfc5322.ParseAddressList(fields.Value())
if err != nil {
return errors.Wrap(err, "failed to parse reply-to")
return Message{}, errors.Wrap(err, "failed to parse reply-to")
}
m.ReplyTos = replyTos
case "cc":
ccList, err := rfc5322.ParseAddressList(fields.Value())
if err != nil {
return errors.Wrap(err, "failed to parse cc")
return Message{}, errors.Wrap(err, "failed to parse cc")
}
m.CCList = ccList
case "bcc":
bccList, err := rfc5322.ParseAddressList(fields.Value())
if err != nil {
return errors.Wrap(err, "failed to parse bcc")
return Message{}, errors.Wrap(err, "failed to parse bcc")
}
m.BCCList = bccList
case "date":
date, err := rfc5322.ParseDateTime(fields.Value())
if err != nil {
return errors.Wrap(err, "failed to parse date")
return Message{}, errors.Wrap(err, "failed to parse date")
}
m.Time = date.Unix()
@ -483,48 +517,47 @@ func parseMessageHeader(m *pmapi.Message, h message.Header) error { //nolint:fun
}
}
return nil
return m, nil
}
func parseAttachment(h message.Header) (*pmapi.Attachment, error) {
att := &pmapi.Attachment{}
func parseAttachment(h message.Header, body []byte) (Attachment, error) {
att := Attachment{
Data: body,
}
mimeHeader, err := toMIMEHeader(h)
mimeHeader, err := toMailHeader(h)
if err != nil {
return nil, err
return Attachment{}, err
}
att.Header = mimeHeader
mimeType, mimeTypeParams, err := h.ContentType()
if err != nil {
return nil, err
return Attachment{}, err
}
att.MIMEType = mimeType
// Prefer attachment name from filename param in content disposition.
// If not available, try to get it from name param in content type.
// Otherwise fallback to attachment.bin.
_, dispParams, dispErr := h.ContentDisposition()
if dispErr != nil {
ext, err := mime.ExtensionsByType(att.MIMEType)
if err != nil {
return nil, err
}
if disp, dispParams, err := h.ContentDisposition(); err == nil {
att.Disposition = disp
if len(ext) > 0 {
att.Name = "attachment" + ext[0]
if filename, ok := dispParams["filename"]; ok {
att.Name = filename
}
} else {
att.Name = dispParams["filename"]
}
if att.Name == "" {
att.Name = mimeTypeParams["name"]
}
if att.Name == "" && mimeType == rfc822Message {
att.Name = "message.eml"
}
if att.Name == "" {
att.Name = "attachment.bin"
if filename, ok := mimeTypeParams["name"]; ok {
att.Name = filename
} else if mimeType == string(rfc822.MessageRFC822) {
att.Name = "message.eml"
} else if ext, err := mime.ExtensionsByType(att.MIMEType); err == nil && len(ext) > 0 {
att.Name = "attachment" + ext[0]
} else {
att.Name = "attachment.bin"
}
}
// Only set ContentID if it should be inline;
@ -534,9 +567,12 @@ func parseAttachment(h message.Header) (*pmapi.Attachment, error) {
// (This is necessary because some clients don't set Content-Disposition at all,
// so we need to rely on other information to deduce if it's inline or attachment.)
if h.Has("Content-Disposition") {
if disp, _, err := h.ContentDisposition(); err != nil {
return nil, err
} else if disp == pmapi.DispositionInline {
disp, _, err := h.ContentDisposition()
if err != nil {
return Attachment{}, err
}
if disp == string(liteapi.InlineDisposition) {
att.ContentID = strings.Trim(h.Get("Content-Id"), " <>")
}
} else if h.Has("Content-Id") {
@ -559,19 +595,6 @@ func toMailHeader(h message.Header) (mail.Header, error) {
return mimeHeader, nil
}
func toMIMEHeader(h message.Header) (textproto.MIMEHeader, error) {
mimeHeader := make(textproto.MIMEHeader)
if err := forEachDecodedHeaderField(h, func(key, val string) error {
mimeHeader[key] = []string{val}
return nil
}); err != nil {
return nil, err
}
return mimeHeader, nil
}
func forEachDecodedHeaderField(h message.Header, fn func(string, string) error) error {
fields := h.Fields()

View File

@ -18,6 +18,7 @@
package parser
import (
"fmt"
"io"
"github.com/emersion/go-message"
@ -67,6 +68,19 @@ func (p *Parser) Root() *Part {
return p.root
}
func (p *Parser) AttachPublicKey(key, keyName string) {
h := message.Header{}
h.Set("Content-Type", fmt.Sprintf(`application/pgp-keys; name="%v.asc"; filename="%v.asc"`, keyName, keyName))
h.Set("Content-Disposition", fmt.Sprintf(`attachment; name="%v.asc"; filename="%v.asc"`, keyName, keyName))
h.Set("Content-Transfer-Encoding", "base64")
p.Root().AddChild(&Part{
Header: h,
Body: []byte(key),
})
}
// Section returns the message part referred to by the given section. A section
// is zero or more integers. For example, section 1.2.3 will return the third
// part of the second part of the first part of the message.

View File

@ -18,6 +18,7 @@
package message
import (
"bytes"
"image/png"
"io"
"os"
@ -33,129 +34,129 @@ import (
func TestParseLongHeaderLine(t *testing.T) {
f := getFileReader("long_header_line.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseLongHeaderLineMultiline(t *testing.T) {
f := getFileReader("long_header_line_multiline.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextPlain(t *testing.T) {
f := getFileReader("text_plain.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextPlainUTF8(t *testing.T) {
f := getFileReader("text_plain_utf8.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextPlainLatin1(t *testing.T) {
f := getFileReader("text_plain_latin1.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "ééééééé", m.Body)
assert.Equal(t, "ééééééé", plainBody)
assert.Equal(t, "ééééééé", string(m.RichBody))
assert.Equal(t, "ééééééé", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextPlainUTF8Subject(t *testing.T) {
f := getFileReader("text_plain_utf8_subject.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, `汉字汉字汉`, m.Subject)
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextPlainLatin2Subject(t *testing.T) {
f := getFileReader("text_plain_latin2_subject.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, `If you can read this you understand the example.`, m.Subject)
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextPlainUnknownCharsetIsActuallyLatin1(t *testing.T) {
f := getFileReader("text_plain_unknown_latin1.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "ééééééé", m.Body)
assert.Equal(t, "ééééééé", plainBody)
assert.Equal(t, "ééééééé", string(m.RichBody))
assert.Equal(t, "ééééééé", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextPlainUnknownCharsetIsActuallyLatin2(t *testing.T) {
f := getFileReader("text_plain_unknown_latin2.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
@ -167,97 +168,97 @@ func TestParseTextPlainUnknownCharsetIsActuallyLatin2(t *testing.T) {
expect, _ := charmap.ISO8859_1.NewDecoder().Bytes(latin2)
assert.NotEqual(t, []byte("řšřšřš"), expect)
assert.Equal(t, string(expect), m.Body)
assert.Equal(t, string(expect), plainBody)
assert.Equal(t, string(expect), string(m.RichBody))
assert.Equal(t, string(expect), string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextPlainAlready7Bit(t *testing.T) {
f := getFileReader("text_plain_7bit.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextPlainWithOctetAttachment(t *testing.T) {
f := getFileReader("text_plain_octet_attachment.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
require.Len(t, attReaders, 1)
assert.Equal(t, readerToString(attReaders[0]), "if you are reading this, hi!")
require.Len(t, m.Attachments, 1)
assert.Equal(t, string(m.Attachments[0].Data), "if you are reading this, hi!")
}
func TestParseTextPlainWithOctetAttachmentGoodFilename(t *testing.T) {
f := getFileReader("text_plain_octet_attachment_good_2231_filename.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
assert.Len(t, attReaders, 1)
assert.Equal(t, readerToString(attReaders[0]), "if you are reading this, hi!")
assert.Len(t, m.Attachments, 1)
assert.Equal(t, string(m.Attachments[0].Data), "if you are reading this, hi!")
assert.Equal(t, "😁😂.txt", m.Attachments[0].Name)
}
func TestParseTextPlainWithRFC822Attachment(t *testing.T) {
f := getFileReader("text_plain_rfc822_attachment.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
assert.Len(t, attReaders, 1)
assert.Len(t, m.Attachments, 1)
assert.Equal(t, "message.eml", m.Attachments[0].Name)
}
func TestParseTextPlainWithOctetAttachmentBadFilename(t *testing.T) {
f := getFileReader("text_plain_octet_attachment_bad_2231_filename.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
assert.Len(t, attReaders, 1)
assert.Equal(t, readerToString(attReaders[0]), "if you are reading this, hi!")
assert.Len(t, m.Attachments, 1)
assert.Equal(t, string(m.Attachments[0].Data), "if you are reading this, hi!")
assert.Equal(t, "attachment.bin", m.Attachments[0].Name)
}
func TestParseTextPlainWithOctetAttachmentNameInContentType(t *testing.T) {
f := getFileReader("text_plain_octet_attachment_name_in_contenttype.eml")
m, _, _, _, err := Parse(f) //nolint:dogsled
m, err := Parse(f) //nolint:dogsled
require.NoError(t, err)
assert.Equal(t, "attachment-contenttype.txt", m.Attachments[0].Name)
@ -266,7 +267,7 @@ func TestParseTextPlainWithOctetAttachmentNameInContentType(t *testing.T) {
func TestParseTextPlainWithOctetAttachmentNameConflict(t *testing.T) {
f := getFileReader("text_plain_octet_attachment_name_conflict.eml")
m, _, _, _, err := Parse(f) //nolint:dogsled
m, err := Parse(f) //nolint:dogsled
require.NoError(t, err)
assert.Equal(t, "attachment-disposition.txt", m.Attachments[0].Name)
@ -275,49 +276,49 @@ func TestParseTextPlainWithOctetAttachmentNameConflict(t *testing.T) {
func TestParseTextPlainWithPlainAttachment(t *testing.T) {
f := getFileReader("text_plain_plain_attachment.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
require.Len(t, attReaders, 1)
assert.Equal(t, readerToString(attReaders[0]), "attachment")
require.Len(t, m.Attachments, 1)
assert.Equal(t, string(m.Attachments[0].Data), "attachment")
}
func TestParseTextPlainEmptyAddresses(t *testing.T) {
f := getFileReader("text_plain_empty_addresses.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextPlainWithImageInline(t *testing.T) {
f := getFileReader("text_plain_image_inline.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
// The inline image is an 8x8 mic-dropping gopher.
require.Len(t, attReaders, 1)
img, err := png.DecodeConfig(attReaders[0])
require.Len(t, m.Attachments, 1)
img, err := png.DecodeConfig(bytes.NewReader(m.Attachments[0].Data))
require.NoError(t, err)
assert.Equal(t, 8, img.Width)
assert.Equal(t, 8, img.Height)
@ -326,111 +327,111 @@ func TestParseTextPlainWithImageInline(t *testing.T) {
func TestParseTextPlainWithDuplicateCharset(t *testing.T) {
f := getFileReader("text_plain_duplicate_charset.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseWithMultipleTextParts(t *testing.T) {
f := getFileReader("multiple_text_parts.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body\nsome other part of the message", m.Body)
assert.Equal(t, "body\nsome other part of the message", plainBody)
assert.Equal(t, "body\nsome other part of the message", string(m.RichBody))
assert.Equal(t, "body\nsome other part of the message", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextHTML(t *testing.T) {
f := getFileReader("text_html.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> without attachment</body></html>", m.Body)
assert.Equal(t, "This is body of *HTML mail* without attachment", plainBody)
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> without attachment</body></html>", string(m.RichBody))
assert.Equal(t, "This is body of *HTML mail* without attachment", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextHTMLAlready7Bit(t *testing.T) {
f := getFileReader("text_html_7bit.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> without attachment</body></html>", m.Body)
assert.Equal(t, "This is body of *HTML mail* without attachment", plainBody)
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> without attachment</body></html>", string(m.RichBody))
assert.Equal(t, "This is body of *HTML mail* without attachment", string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseTextHTMLWithOctetAttachment(t *testing.T) {
f := getFileReader("text_html_octet_attachment.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", m.Body)
assert.Equal(t, "This is body of *HTML mail* with attachment", plainBody)
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", string(m.RichBody))
assert.Equal(t, "This is body of *HTML mail* with attachment", string(m.PlainBody))
require.Len(t, attReaders, 1)
assert.Equal(t, readerToString(attReaders[0]), "if you are reading this, hi!")
require.Len(t, m.Attachments, 1)
assert.Equal(t, string(m.Attachments[0].Data), "if you are reading this, hi!")
}
func TestParseTextHTMLWithPlainAttachment(t *testing.T) {
f := getFileReader("text_html_plain_attachment.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
// BAD: plainBody should not be empty!
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", m.Body)
assert.Equal(t, "This is body of *HTML mail* with attachment", plainBody)
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", string(m.RichBody))
assert.Equal(t, "This is body of *HTML mail* with attachment", string(m.PlainBody))
require.Len(t, attReaders, 1)
assert.Equal(t, readerToString(attReaders[0]), "attachment")
require.Len(t, m.Attachments, 1)
assert.Equal(t, string(m.Attachments[0].Data), "attachment")
}
func TestParseTextHTMLWithImageInline(t *testing.T) {
f := getFileReader("text_html_image_inline.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
assert.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", m.Body)
assert.Equal(t, "This is body of *HTML mail* with attachment", plainBody)
assert.Equal(t, "<html><head></head><body>This is body of <b>HTML mail</b> with attachment</body></html>", string(m.RichBody))
assert.Equal(t, "This is body of *HTML mail* with attachment", string(m.PlainBody))
// The inline image is an 8x8 mic-dropping gopher.
require.Len(t, attReaders, 1)
img, err := png.DecodeConfig(attReaders[0])
require.Len(t, m.Attachments, 1)
img, err := png.DecodeConfig(bytes.NewReader(m.Attachments[0].Data))
require.NoError(t, err)
assert.Equal(t, 8, img.Width)
assert.Equal(t, 8, img.Height)
@ -441,40 +442,42 @@ func TestParseWithAttachedPublicKey(t *testing.T) {
p, err := parser.New(f)
require.NoError(t, err)
m, plainBody, attReaders, err := ParserWithParser(p)
AttachPublicKey(p, "publickey", "publickeyname")
m, err := ParseWithParser(p)
require.NoError(t, err)
p.AttachPublicKey("publickey", "publickeyname")
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, "body", m.Body)
assert.Equal(t, "body", plainBody)
assert.Equal(t, "body", string(m.RichBody))
assert.Equal(t, "body", string(m.PlainBody))
// The pubkey should not be collected as an attachment.
// We upload the pubkey when creating the draft.
require.Len(t, attReaders, 0)
require.Len(t, m.Attachments, 0)
}
func TestParseTextHTMLWithEmbeddedForeignEncoding(t *testing.T) {
f := getFileReader("text_html_embedded_foreign_encoding.eml")
m, _, plainBody, attReaders, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@pm.me>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@pm.me>`, m.ToList[0].String())
assert.Equal(t, `<html><head><meta charset="UTF-8"/></head><body>latin2 řšřš</body></html>`, m.Body)
assert.Equal(t, `latin2 řšřš`, plainBody)
assert.Equal(t, `<html><head><meta charset="UTF-8"/></head><body>latin2 řšřš</body></html>`, string(m.RichBody))
assert.Equal(t, `latin2 řšřš`, string(m.PlainBody))
assert.Len(t, attReaders, 0)
assert.Len(t, m.Attachments, 0)
}
func TestParseMultipartAlternative(t *testing.T) {
f := getFileReader("multipart_alternative.eml")
m, _, plainBody, _, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"schizofrenic" <schizofrenic@pm.me>`, m.Sender.String())
@ -487,15 +490,15 @@ func TestParseMultipartAlternative(t *testing.T) {
<b>aoeuaoeu</b>
</body></html>`, m.Body)
</body></html>`, string(m.RichBody))
assert.Equal(t, "*aoeuaoeu*\n\n", plainBody)
assert.Equal(t, "*aoeuaoeu*\n\n", string(m.PlainBody))
}
func TestParseMultipartAlternativeNested(t *testing.T) {
f := getFileReader("multipart_alternative_nested.eml")
m, _, plainBody, _, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"schizofrenic" <schizofrenic@pm.me>`, m.Sender.String())
@ -508,15 +511,15 @@ func TestParseMultipartAlternativeNested(t *testing.T) {
<b>multipart 2.2</b>
</body></html>`, m.Body)
</body></html>`, string(m.RichBody))
assert.Equal(t, "*multipart 2.1*\n\n", plainBody)
assert.Equal(t, "*multipart 2.1*\n\n", string(m.PlainBody))
}
func TestParseMultipartAlternativeLatin1(t *testing.T) {
f := getFileReader("multipart_alternative_latin1.eml")
m, _, plainBody, _, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"schizofrenic" <schizofrenic@pm.me>`, m.Sender.String())
@ -529,52 +532,52 @@ func TestParseMultipartAlternativeLatin1(t *testing.T) {
<b>aoeuaoeu</b>
</body></html>`, m.Body)
</body></html>`, string(m.RichBody))
assert.Equal(t, "*aoeuaoeu*\n\n", plainBody)
assert.Equal(t, "*aoeuaoeu*\n\n", string(m.PlainBody))
}
func TestParseWithTrailingEndOfMailIndicator(t *testing.T) {
f := getFileReader("text_html_trailing_end_of_mail.eml")
m, _, plainBody, _, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@sender.com>`, m.Sender.String())
assert.Equal(t, `"Receiver" <receiver@receiver.com>`, m.ToList[0].String())
assert.Equal(t, "<!DOCTYPE html><html><head></head><body>boo!</body></html>", m.Body)
assert.Equal(t, "boo!", plainBody)
assert.Equal(t, "<!DOCTYPE html><html><head></head><body>boo!</body></html>", string(m.RichBody))
assert.Equal(t, "boo!", string(m.PlainBody))
}
func TestParseEncodedContentType(t *testing.T) {
f := getFileReader("rfc2047-content-transfer-encoding.eml")
m, _, plainBody, _, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@sender.com>`, m.Sender.String())
assert.Equal(t, `<user@somewhere.org>`, m.ToList[0].String())
assert.Equal(t, "bodybodybody\n", plainBody)
assert.Equal(t, "bodybodybody\n", string(m.PlainBody))
}
func TestParseNonEncodedContentType(t *testing.T) {
f := getFileReader("non-encoded-content-transfer-encoding.eml")
m, _, plainBody, _, err := Parse(f)
m, err := Parse(f)
require.NoError(t, err)
assert.Equal(t, `"Sender" <sender@sender.com>`, m.Sender.String())
assert.Equal(t, `<user@somewhere.org>`, m.ToList[0].String())
assert.Equal(t, "bodybodybody\n", plainBody)
assert.Equal(t, "bodybodybody\n", string(m.PlainBody))
}
func TestParseEncodedContentTypeBad(t *testing.T) {
f := getFileReader("rfc2047-content-transfer-encoding-bad.eml")
_, _, _, _, err := Parse(f) //nolint:dogsled
_, err := Parse(f) //nolint:dogsled
require.Error(t, err)
}
@ -587,7 +590,7 @@ func (panicReader) Read(p []byte) (int, error) {
func TestParsePanic(t *testing.T) {
var err error
require.NotPanics(t, func() {
_, _, _, _, err = Parse(&panicReader{})
_, err = Parse(&panicReader{})
})
require.Error(t, err)
}
@ -600,12 +603,3 @@ func getFileReader(filename string) io.Reader {
return f
}
func readerToString(r io.Reader) string {
b, err := io.ReadAll(r)
if err != nil {
panic(err)
}
return string(b)
}

View File

@ -1,96 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bufio"
"bytes"
"errors"
"io"
)
type partScanner struct {
r *bufio.Reader
boundary string
progress int
}
type part struct {
b []byte
offset int
}
func newPartScanner(r io.Reader, boundary string) (*partScanner, error) {
scanner := &partScanner{r: bufio.NewReader(r), boundary: boundary}
if _, _, err := scanner.readToBoundary(); err != nil {
return nil, err
}
return scanner, nil
}
func (s *partScanner) scanAll() ([]part, error) {
var parts []part
for {
offset := s.progress
b, more, err := s.readToBoundary()
if err != nil {
return nil, err
}
if !more {
return parts, nil
}
parts = append(parts, part{b: b, offset: offset})
}
}
func (s *partScanner) readToBoundary() ([]byte, bool, error) {
var res []byte
for {
line, err := s.r.ReadBytes('\n')
if err != nil {
if !errors.Is(err, io.EOF) {
return nil, false, err
}
if len(line) == 0 {
return nil, false, nil
}
}
s.progress += len(line)
switch {
case bytes.HasPrefix(bytes.TrimSpace(line), []byte("--"+s.boundary)):
return bytes.TrimSuffix(bytes.TrimSuffix(res, []byte("\n")), []byte("\r")), true, nil
case bytes.HasSuffix(bytes.TrimSpace(line), []byte(s.boundary+"--")):
return bytes.TrimSuffix(bytes.TrimSuffix(res, []byte("\n")), []byte("\r")), false, nil
default:
res = append(res, line...)
}
}
}

View File

@ -1,136 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestScanner(t *testing.T) {
const literal = `this part of the text should be ignored
--longrandomstring
body1
--longrandomstring
body2
--longrandomstring--
`
scanner, err := newPartScanner(strings.NewReader(literal), "longrandomstring")
require.NoError(t, err)
parts, err := scanner.scanAll()
require.NoError(t, err)
assert.Equal(t, "\nbody1\n", string(parts[0].b))
assert.Equal(t, "\nbody2\n", string(parts[1].b))
assert.Equal(t, "\nbody1\n", literal[parts[0].offset:parts[0].offset+len(parts[0].b)])
assert.Equal(t, "\nbody2\n", literal[parts[1].offset:parts[1].offset+len(parts[1].b)])
}
func TestScannerNested(t *testing.T) {
const literal = `This is the preamble. It is to be ignored, though it
is a handy place for mail composers to include an
explanatory note to non-MIME compliant readers.
--simple boundary
Content-type: multipart/mixed; boundary="nested boundary"
This is the preamble. It is to be ignored, though it
is a handy place for mail composers to include an
explanatory note to non-MIME compliant readers.
--nested boundary
Content-type: text/plain; charset=us-ascii
This part does not end with a linebreak.
--nested boundary
Content-type: text/plain; charset=us-ascii
This part does end with a linebreak.
--nested boundary--
--simple boundary
Content-type: text/plain; charset=us-ascii
This part does end with a linebreak.
--simple boundary--
This is the epilogue. It is also to be ignored.
`
scanner, err := newPartScanner(strings.NewReader(literal), "simple boundary")
require.NoError(t, err)
parts, err := scanner.scanAll()
require.NoError(t, err)
assert.Equal(t, `Content-type: multipart/mixed; boundary="nested boundary"
This is the preamble. It is to be ignored, though it
is a handy place for mail composers to include an
explanatory note to non-MIME compliant readers.
--nested boundary
Content-type: text/plain; charset=us-ascii
This part does not end with a linebreak.
--nested boundary
Content-type: text/plain; charset=us-ascii
This part does end with a linebreak.
--nested boundary--`, string(parts[0].b))
assert.Equal(t, `Content-type: text/plain; charset=us-ascii
This part does end with a linebreak.
`, string(parts[1].b))
}
func TestScannerNoFinalLinebreak(t *testing.T) {
const literal = `--nested boundary
Content-type: text/plain; charset=us-ascii
This part does not end with a linebreak.
--nested boundary
Content-type: text/plain; charset=us-ascii
This part does end with a linebreak.
--nested boundary--`
scanner, err := newPartScanner(strings.NewReader(literal), "nested boundary")
require.NoError(t, err)
parts, err := scanner.scanAll()
require.NoError(t, err)
assert.Equal(t, `Content-type: text/plain; charset=us-ascii
This part does not end with a linebreak.`, string(parts[0].b))
assert.Equal(t, `Content-type: text/plain; charset=us-ascii
This part does end with a linebreak.
`, string(parts[1].b))
}

View File

@ -1,395 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bufio"
"bytes"
"io"
"net/textproto"
"strconv"
"strings"
pmmime "github.com/ProtonMail/proton-bridge/v2/pkg/mime"
"github.com/emersion/go-imap"
"github.com/pkg/errors"
"github.com/vmihailenco/msgpack/v5"
)
// BodyStructure is used to parse an email into MIME sections and then generate
// body structure for IMAP server.
type BodyStructure map[string]*SectionInfo
// SectionInfo is used to hold data about parts of each section.
type SectionInfo struct {
Header []byte
Start, BSize, Size, Lines int
reader io.Reader
isHeaderReadFinished bool
}
// Read will also count the final size of section.
func (si *SectionInfo) Read(p []byte) (n int, err error) {
n, err = si.reader.Read(p)
si.Size += n
si.Lines += bytes.Count(p, []byte("\n"))
si.readHeader(p)
return
}
// readHeader appends read data to Header until empty line is found.
func (si *SectionInfo) readHeader(p []byte) {
if si.isHeaderReadFinished {
return
}
si.Header = append(si.Header, p...)
if i := bytes.Index(si.Header, []byte("\n\r\n")); i > 0 {
si.Header = si.Header[:i+3]
si.isHeaderReadFinished = true
return
}
// textproto works also with simple line ending so we should be liberal
// as well.
if i := bytes.Index(si.Header, []byte("\n\n")); i > 0 {
si.Header = si.Header[:i+2]
si.isHeaderReadFinished = true
}
}
// GetMIMEHeader parses bytes and return MIME header.
func (si *SectionInfo) GetMIMEHeader() (textproto.MIMEHeader, error) {
return textproto.NewReader(bufio.NewReader(bytes.NewReader(si.Header))).ReadMIMEHeader()
}
func NewBodyStructure(reader io.Reader) (structure *BodyStructure, err error) {
structure = &BodyStructure{}
err = structure.Parse(reader)
return
}
// DeserializeBodyStructure will create new structure from msgpack bytes.
func DeserializeBodyStructure(raw []byte) (*BodyStructure, error) {
bs := &BodyStructure{}
err := msgpack.Unmarshal(raw, bs)
if err != nil {
return nil, errors.Wrap(err, "cannot deserialize bodystructure")
}
return bs, err
}
// Serialize will write msgpack bytes.
func (bs *BodyStructure) Serialize() ([]byte, error) {
data, err := msgpack.Marshal(bs)
if err != nil {
return nil, errors.Wrap(err, "cannot serialize bodystructure")
}
return data, nil
}
// Parse will read the mail and create all body structures.
func (bs *BodyStructure) Parse(r io.Reader) error {
return bs.parseAllChildSections(r, []int{}, 0)
}
func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, start int) (err error) { //nolint:funlen
info := &SectionInfo{
Start: start,
Size: 0,
BSize: 0,
Lines: 0,
reader: r,
}
bufInfo := bufio.NewReader(info)
tp := textproto.NewReader(bufInfo)
tpHeader, err := tp.ReadMIMEHeader()
if err != nil {
return
}
bodyInfo := &SectionInfo{reader: tp.R}
bodyReader := bufio.NewReader(bodyInfo)
mediaType, params, _ := pmmime.ParseMediaType(tpHeader.Get("Content-Type"))
// If multipart, call getAllParts, else read to count lines.
if (strings.HasPrefix(mediaType, "multipart/") || mediaType == rfc822Message) && params["boundary"] != "" {
nextPath := getChildPath(currentPath)
var br *boundaryReader
br, err = newBoundaryReader(bodyReader, params["boundary"])
// New reader seeks first boundary.
if err != nil {
// Return also EOF.
return
}
for err == nil {
start += br.skipped
part := &bytes.Buffer{}
err = br.writeNextPartTo(part)
if err != nil {
break
}
err = bs.parseAllChildSections(part, nextPath, start)
part.Reset()
nextPath[len(nextPath)-1]++
}
br.reader = nil
if err == io.EOF {
err = nil
}
if err != nil {
return
}
} else {
// Count length.
_, _ = bodyReader.WriteTo(io.Discard)
}
// Clear all buffers.
bodyReader = nil //nolint:wastedassign // just to be sure we clear garbage collector
bodyInfo.reader = nil
tp.R = nil
tp = nil //nolint:wastedassign // just to be sure we clear garbage collector
bufInfo = nil //nolint:ineffassign,wastedassign // just to be sure we clear garbage collector
info.reader = nil
// Store boundaries.
info.BSize = bodyInfo.Size
path := stringPathFromInts(currentPath)
(*bs)[path] = info
// Fix start of subsections.
newPath := getChildPath(currentPath)
shift := info.Size - info.BSize
subInfo, err := bs.getInfo(newPath)
// If it has subparts.
for err == nil {
subInfo.Start += shift
// Level down.
subInfo, err = bs.getInfo(append(newPath, 1))
if err == nil {
newPath = append(newPath, 1)
continue
}
// Next.
newPath[len(newPath)-1]++
subInfo, err = bs.getInfo(newPath)
if err == nil {
continue
}
// Level up.
for {
newPath = newPath[:len(newPath)-1]
if len(newPath) > 0 {
newPath[len(newPath)-1]++
subInfo, err = bs.getInfo(newPath)
if err != nil {
err = nil
continue
}
}
break
}
// The end.
if len(newPath) == 0 {
break
}
}
return nil
}
// getChildPath will return the first child path of parent path.
// NOTE: Return value can be used to iterate over parts so it is necessary to
// copy parrent values in order to not rewrite values in parent.
func getChildPath(parent []int) []int {
// append alloc inline is the fasted way to copy
return append(append(make([]int, 0, len(parent)+1), parent...), 1)
}
func stringPathFromInts(ints []int) (ret string) {
for i, n := range ints {
if i != 0 {
ret += "."
}
ret += strconv.Itoa(n)
}
return
}
func (bs *BodyStructure) hasInfo(sectionPath []int) bool {
_, err := bs.getInfo(sectionPath)
return err == nil
}
func (bs *BodyStructure) getInfoCheckSection(sectionPath []int) (sectionInfo *SectionInfo, err error) {
if len(*bs) == 1 && len(sectionPath) == 1 && sectionPath[0] == 1 {
sectionPath = []int{}
}
return bs.getInfo(sectionPath)
}
func (bs *BodyStructure) getInfo(sectionPath []int) (sectionInfo *SectionInfo, err error) {
path := stringPathFromInts(sectionPath)
sectionInfo, ok := (*bs)[path]
if !ok {
err = errors.New("wrong section " + path)
}
return
}
// GetSection returns bytes of section including MIME header.
func (bs *BodyStructure) GetSection(wholeMail io.ReadSeeker, sectionPath []int) (section []byte, err error) {
info, err := bs.getInfoCheckSection(sectionPath)
if err != nil {
return
}
return goToOffsetAndReadNBytes(wholeMail, info.Start, info.Size)
}
// GetSectionContent returns bytes of section content (excluding MIME header).
func (bs *BodyStructure) GetSectionContent(wholeMail io.ReadSeeker, sectionPath []int) (section []byte, err error) {
info, err := bs.getInfoCheckSection(sectionPath)
if err != nil {
return
}
return goToOffsetAndReadNBytes(wholeMail, info.Start+info.Size-info.BSize, info.BSize)
}
// GetMailHeader returns the main header of mail.
func (bs *BodyStructure) GetMailHeader() (header textproto.MIMEHeader, err error) {
return bs.GetSectionHeader([]int{})
}
// GetMailHeaderBytes returns the bytes with main mail header.
// Warning: It can contain extra lines.
func (bs *BodyStructure) GetMailHeaderBytes() (header []byte, err error) {
return bs.GetSectionHeaderBytes([]int{})
}
func goToOffsetAndReadNBytes(wholeMail io.ReadSeeker, offset, length int) ([]byte, error) {
if length == 0 {
return []byte{}, nil
}
if length < 0 {
return nil, errors.New("requested negative length")
}
if offset > 0 {
if _, err := wholeMail.Seek(int64(offset), io.SeekStart); err != nil {
return nil, err
}
}
out := make([]byte, length)
_, err := wholeMail.Read(out)
return out, err
}
// GetSectionHeader returns the mime header of specified section.
func (bs *BodyStructure) GetSectionHeader(sectionPath []int) (textproto.MIMEHeader, error) {
info, err := bs.getInfoCheckSection(sectionPath)
if err != nil {
return nil, err
}
return info.GetMIMEHeader()
}
// GetSectionHeaderBytes returns raw header bytes of specified section.
func (bs *BodyStructure) GetSectionHeaderBytes(sectionPath []int) ([]byte, error) {
info, err := bs.getInfoCheckSection(sectionPath)
if err != nil {
return nil, err
}
return info.Header, nil
}
// IMAPBodyStructure will prepare imap bodystructure recurently for given part.
// Use empty path to create whole email structure.
func (bs *BodyStructure) IMAPBodyStructure(currentPart []int) (imapBS *imap.BodyStructure, err error) {
var info *SectionInfo
if info, err = bs.getInfo(currentPart); err != nil {
return
}
tpHeader, err := info.GetMIMEHeader()
if err != nil {
return
}
mediaType, params, _ := pmmime.ParseMediaType(tpHeader.Get("Content-Type"))
mediaTypeSep := strings.Split(mediaType, "/")
// If it is empty or missing it will not crash.
mediaTypeSep = append(mediaTypeSep, "")
imapBS = &imap.BodyStructure{
MIMEType: mediaTypeSep[0],
MIMESubType: mediaTypeSep[1],
Params: params,
Size: uint32(info.BSize),
Lines: uint32(info.Lines),
}
if val := tpHeader.Get("Content-ID"); val != "" {
imapBS.Id = val
}
if val := tpHeader.Get("Content-Transfer-Encoding"); val != "" {
imapBS.Encoding = val
}
if val := tpHeader.Get("Content-Description"); val != "" {
imapBS.Description = val
}
if val := tpHeader.Get("Content-Disposition"); val != "" {
imapBS.Disposition = val
}
nextPart := append(currentPart, 1) //nolint:gocritic
for {
if !bs.hasInfo(nextPart) {
break
}
var subStruct *imap.BodyStructure
subStruct, err = bs.IMAPBodyStructure(nextPart)
if err != nil {
return
}
if imapBS.Parts == nil {
imapBS.Parts = []*imap.BodyStructure{}
}
imapBS.Parts = append(imapBS.Parts, subStruct)
nextPart[len(nextPart)-1]++
}
return imapBS, nil
}

View File

@ -1,599 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bytes"
"fmt"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
var enableDebug = false //nolint:global
func debug(msg string, v ...interface{}) {
if !enableDebug {
return
}
_, file, line, _ := runtime.Caller(1)
fmt.Printf("%s:%d: \033[2;33m"+msg+"\033[0;39m\n", append([]interface{}{filepath.Base(file), line}, v...)...)
}
func TestParseBodyStructure(t *testing.T) {
expectedStructure := map[string]string{
"": "multipart/mixed; boundary=\"0000MAIN\"",
"1": "text/plain",
"2": "application/octet-stream",
"3": "message/rfc822; boundary=\"0003MSG\"",
"3.1": "text/plain",
"3.2": "application/octet-stream",
"4": "multipart/mixed; boundary=\"0004ATTACH\"",
"4.1": "image/gif",
"4.2": "message/rfc822; boundary=\"0042MSG\"",
"4.2.1": "text/plain",
"4.2.2": "multipart/alternative; boundary=\"0422ALTER\"",
"4.2.2.1": "text/plain",
"4.2.2.2": "text/html",
}
mailReader := strings.NewReader(sampleMail)
bs, err := NewBodyStructure(mailReader)
require.NoError(t, err)
paths := []string{}
for path := range *bs {
paths = append(paths, path)
}
sort.Strings(paths)
debug("%10s: %-50s %5s %5s %5s %5s", "section", "type", "start", "size", "bsize", "lines")
for _, path := range paths {
sec := (*bs)[path]
header, err := sec.GetMIMEHeader()
require.NoError(t, err)
contentType := header.Get("Content-Type")
debug("%10s: %-50s %5d %5d %5d %5d", path, contentType, sec.Start, sec.Size, sec.BSize, sec.Lines)
require.Equal(t, expectedStructure[path], contentType)
}
require.True(t, len(*bs) == len(expectedStructure), "Wrong number of sections expected %d but have %d", len(expectedStructure), len(*bs))
}
func TestParseBodyStructurePGP(t *testing.T) {
expectedStructure := map[string]string{
"": "multipart/signed; micalg=pgp-sha256; protocol=\"application/pgp-signature\"; boundary=\"MHEDFShwcX18dyE3X7RXujo5fjpgdjHNM\"",
"1": "multipart/mixed; boundary=\"FBBl2LNv76z8UkvHhSkT9vLwVwxqV8378\"; protected-headers=\"v1\"",
"1.1": "multipart/mixed; boundary=\"------------F97C8ED4878E94675762AE43\"",
"1.1.1": "multipart/alternative; boundary=\"------------041318B15DD3FA540FED32C6\"",
"1.1.1.1": "text/plain; charset=utf-8; format=flowed",
"1.1.1.2": "text/html; charset=utf-8",
"1.1.2": "application/pdf; name=\"minimal.pdf\"",
"1.1.3": "application/pgp-keys; name=\"OpenPGP_0x161C0875822359F7.asc\"",
"2": "application/pgp-signature; name=\"OpenPGP_signature.asc\"",
}
b, err := os.ReadFile("testdata/enc-body-structure.eml")
require.NoError(t, err)
bs, err := NewBodyStructure(bytes.NewReader(b))
require.NoError(t, err)
haveStructure := map[string]string{}
for path := range *bs {
header, err := (*bs)[path].GetMIMEHeader()
require.NoError(t, err)
haveStructure[path] = header.Get("Content-Type")
}
require.Equal(t, expectedStructure, haveStructure)
}
func TestGetSection(t *testing.T) {
structReader := strings.NewReader(sampleMail)
bs, err := NewBodyStructure(structReader)
require.NoError(t, err)
// Bad paths
wantPaths := [][]int{{0}, {-1}, {3, 2, 3}}
for _, wantPath := range wantPaths {
_, err = bs.getInfo(wantPath)
require.Error(t, err, "path %v", wantPath)
}
// Whole section.
for _, try := range testPaths {
mailReader := strings.NewReader(sampleMail)
info, err := bs.getInfo(try.path)
require.NoError(t, err)
section, err := bs.GetSection(mailReader, try.path)
require.NoError(t, err)
debug("section %v: %d %d\n___\n%s\n‾‾‾\n", try.path, info.Start, info.Size, string(section))
require.True(t, string(section) == try.expectedSection, "not same as expected:\n___\n%s\n‾‾‾", try.expectedSection)
}
// Body content.
for _, try := range testPaths {
mailReader := strings.NewReader(sampleMail)
info, err := bs.getInfo(try.path)
require.NoError(t, err)
section, err := bs.GetSectionContent(mailReader, try.path)
require.NoError(t, err)
debug("content %v: %d %d\n___\n%s\n‾‾‾\n", try.path, info.Start+info.Size-info.BSize, info.BSize, string(section))
require.True(t, string(section) == try.expectedBody, "not same as expected:\n___\n%s\n‾‾‾", try.expectedBody)
}
}
func TestGetSecionNoMIMEParts(t *testing.T) {
wantBody := "This is just a simple mail with no multipart structure.\n"
wantHeader := `Subject: Sample mail
From: John Doe <jdoe@machine.example>
To: Mary Smith <mary@example.net>
Date: Fri, 21 Nov 1997 09:55:06 -0600
Content-Type: plain/text
`
wantMail := wantHeader + wantBody
r := require.New(t)
bs, err := NewBodyStructure(strings.NewReader(wantMail))
r.NoError(err)
// Bad parts
wantPaths := [][]int{{0}, {2}, {1, 2, 3}}
for _, wantPath := range wantPaths {
_, err = bs.getInfoCheckSection(wantPath)
r.Error(err, "path %v: %d %d\n__\n%s\n", wantPath)
}
debug := func(wantPath []int, info *SectionInfo, section []byte) string {
if info == nil {
info = &SectionInfo{}
}
return fmt.Sprintf("path %v %q: %d %d\n___\n%s\n‾‾‾\n",
wantPath, stringPathFromInts(wantPath), info.Start, info.Size,
string(section),
)
}
// Ok Parts
wantPaths = [][]int{{}, {1}}
for _, p := range wantPaths {
wantPath := append([]int{}, p...)
info, err := bs.getInfoCheckSection(wantPath)
r.NoError(err, debug(wantPath, info, []byte{}))
section, err := bs.GetSection(strings.NewReader(wantMail), wantPath)
r.NoError(err, debug(wantPath, info, section))
r.Equal(wantMail, string(section), debug(wantPath, info, section))
haveBody, err := bs.GetSectionContent(strings.NewReader(wantMail), wantPath)
r.NoError(err, debug(wantPath, info, haveBody))
r.Equal(wantBody, string(haveBody), debug(wantPath, info, haveBody))
haveHeader, err := bs.GetSectionHeaderBytes(wantPath)
r.NoError(err, debug(wantPath, info, haveHeader))
r.Equal(wantHeader, string(haveHeader), debug(wantPath, info, haveHeader))
}
}
func TestGetMainHeaderBytes(t *testing.T) {
wantHeader := []byte(`Subject: Sample mail
From: John Doe <jdoe@machine.example>
To: Mary Smith <mary@example.net>
Date: Fri, 21 Nov 1997 09:55:06 -0600
Content-Type: multipart/mixed; boundary="0000MAIN"
`)
structReader := strings.NewReader(sampleMail)
bs, err := NewBodyStructure(structReader)
require.NoError(t, err)
haveHeader, err := bs.GetMailHeaderBytes()
require.NoError(t, err)
require.Equal(t, wantHeader, haveHeader)
}
/* Structure example:
HEADER ([RFC-2822] header of the message)
TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED
1 TEXT/PLAIN
2 APPLICATION/OCTET-STREAM
3 MESSAGE/RFC822
3.HEADER ([RFC-2822] header of the message)
3.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED
3.1 TEXT/PLAIN
3.2 APPLICATION/OCTET-STREAM
4 MULTIPART/MIXED
4.1 IMAGE/GIF
4.1.MIME ([MIME-IMB] header for the IMAGE/GIF)
4.2 MESSAGE/RFC822
4.2.HEADER ([RFC-2822] header of the message)
4.2.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED
4.2.1 TEXT/PLAIN
4.2.2 MULTIPART/ALTERNATIVE
4.2.2.1 TEXT/PLAIN
4.2.2.2 TEXT/RICHTEXT
*/
var sampleMail = `Subject: Sample mail
From: John Doe <jdoe@machine.example>
To: Mary Smith <mary@example.net>
Date: Fri, 21 Nov 1997 09:55:06 -0600
Content-Type: multipart/mixed; boundary="0000MAIN"
main summary
--0000MAIN
Content-Type: text/plain
1. main message
--0000MAIN
Content-Type: application/octet-stream
Content-Disposition: inline; filename="main_signature.sig"
Content-Transfer-Encoding: base64
2/MainOctetStream
--0000MAIN
Subject: Inside mail 3
From: Mary Smith <mary@example.net>
To: John Doe <jdoe@machine.example>
Date: Fri, 20 Nov 1997 09:55:06 -0600
Content-Type: message/rfc822; boundary="0003MSG"
3. message summary
--0003MSG
Content-Type: text/plain
3.1 message text
--0003MSG
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="msg_3_signature.sig"
Content-Transfer-Encoding: base64
3/2/MessageOctestStream/==
--0003MSG--
--0000MAIN
Content-Type: multipart/mixed; boundary="0004ATTACH"
4 attach summary
--0004ATTACH
Content-Type: image/gif
Content-Disposition: attachment; filename="att4.1_gif.sig"
Content-Transfer-Encoding: base64
4/1/Gif=
--0004ATTACH
Subject: Inside mail 4.2
From: Mary Smith <mary@example.net>
To: John Doe <jdoe@machine.example>
Date: Fri, 10 Nov 1997 09:55:06 -0600
Content-Type: message/rfc822; boundary="0042MSG"
4.2 message summary
--0042MSG
Content-Type: text/plain
4.2.1 message text
--0042MSG
Content-Type: multipart/alternative; boundary="0422ALTER"
4.2.2 alternative summary
--0422ALTER
Content-Type: text/plain
4.2.2.1 plain text
--0422ALTER
Content-Type: text/html
<h1>4.2.2.2 html text</h1>
--0422ALTER--
--0042MSG--
--0004ATTACH--
--0000MAIN--
`
var testPaths = []struct {
path []int
expectedSection, expectedBody string
}{
{
[]int{},
sampleMail,
`main summary
--0000MAIN
Content-Type: text/plain
1. main message
--0000MAIN
Content-Type: application/octet-stream
Content-Disposition: inline; filename="main_signature.sig"
Content-Transfer-Encoding: base64
2/MainOctetStream
--0000MAIN
Subject: Inside mail 3
From: Mary Smith <mary@example.net>
To: John Doe <jdoe@machine.example>
Date: Fri, 20 Nov 1997 09:55:06 -0600
Content-Type: message/rfc822; boundary="0003MSG"
3. message summary
--0003MSG
Content-Type: text/plain
3.1 message text
--0003MSG
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="msg_3_signature.sig"
Content-Transfer-Encoding: base64
3/2/MessageOctestStream/==
--0003MSG--
--0000MAIN
Content-Type: multipart/mixed; boundary="0004ATTACH"
4 attach summary
--0004ATTACH
Content-Type: image/gif
Content-Disposition: attachment; filename="att4.1_gif.sig"
Content-Transfer-Encoding: base64
4/1/Gif=
--0004ATTACH
Subject: Inside mail 4.2
From: Mary Smith <mary@example.net>
To: John Doe <jdoe@machine.example>
Date: Fri, 10 Nov 1997 09:55:06 -0600
Content-Type: message/rfc822; boundary="0042MSG"
4.2 message summary
--0042MSG
Content-Type: text/plain
4.2.1 message text
--0042MSG
Content-Type: multipart/alternative; boundary="0422ALTER"
4.2.2 alternative summary
--0422ALTER
Content-Type: text/plain
4.2.2.1 plain text
--0422ALTER
Content-Type: text/html
<h1>4.2.2.2 html text</h1>
--0422ALTER--
--0042MSG--
--0004ATTACH--
--0000MAIN--
`,
},
{
[]int{1},
`Content-Type: text/plain
1. main message
`,
`1. main message
`,
},
{
[]int{3},
`Subject: Inside mail 3
From: Mary Smith <mary@example.net>
To: John Doe <jdoe@machine.example>
Date: Fri, 20 Nov 1997 09:55:06 -0600
Content-Type: message/rfc822; boundary="0003MSG"
3. message summary
--0003MSG
Content-Type: text/plain
3.1 message text
--0003MSG
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="msg_3_signature.sig"
Content-Transfer-Encoding: base64
3/2/MessageOctestStream/==
--0003MSG--
`,
`3. message summary
--0003MSG
Content-Type: text/plain
3.1 message text
--0003MSG
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="msg_3_signature.sig"
Content-Transfer-Encoding: base64
3/2/MessageOctestStream/==
--0003MSG--
`,
},
{
[]int{3, 1},
`Content-Type: text/plain
3.1 message text
`,
`3.1 message text
`,
},
{
[]int{3, 2},
`Content-Type: application/octet-stream
Content-Disposition: attachment; filename="msg_3_signature.sig"
Content-Transfer-Encoding: base64
3/2/MessageOctestStream/==
`,
`3/2/MessageOctestStream/==
`,
},
{
[]int{4, 2, 2, 1},
`Content-Type: text/plain
4.2.2.1 plain text
`,
`4.2.2.1 plain text
`,
},
{
[]int{4, 2, 2, 2},
`Content-Type: text/html
<h1>4.2.2.2 html text</h1>
`,
`<h1>4.2.2.2 html text</h1>
`,
},
}
func TestBodyStructureSerialize(t *testing.T) {
r := require.New(t)
want := &BodyStructure{
"1": {
Header: []byte("Content: type"),
Start: 1,
Size: 2,
BSize: 3,
Lines: 4,
},
"1.1.1": {
Header: []byte("X-Pm-Key: id"),
Start: 11,
Size: 12,
BSize: 13,
Lines: 14,
reader: bytes.NewBuffer([]byte("this should not be serialized")),
},
}
raw, err := want.Serialize()
r.NoError(err)
have, err := DeserializeBodyStructure(raw)
r.NoError(err)
// Before compare remove reader (should not be serialized)
(*want)["1.1.1"].reader = nil
r.Equal(want, have)
}
func TestSectionInfoReadHeader(t *testing.T) {
r := require.New(t)
testData := []struct {
wantHeader, mail string
}{
{
"key1: val1\nkey2: val2\n\n",
"key1: val1\nkey2: val2\n\nbody is here\n\nand it is not confused",
},
{
"key1:\n val1\n\n",
"key1:\n val1\n\nbody is here",
},
{
"key1: val1\r\nkey2: val2\r\n\r\n",
"key1: val1\r\nkey2: val2\r\n\r\nbody is here\r\n\r\nand it is not confused",
},
}
for _, td := range testData {
bs, err := NewBodyStructure(strings.NewReader(td.mail))
r.NoError(err, "case %q", td.mail)
haveHeader, err := bs.GetMailHeaderBytes()
r.NoError(err, "case %q", td.mail)
r.Equal(td.wantHeader, string(haveHeader), "case %q", td.mail)
}
}

View File

@ -1,48 +0,0 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"fmt"
"io"
)
type partWriter struct {
w io.Writer
boundary string
}
func newPartWriter(w io.Writer, boundary string) *partWriter {
return &partWriter{w: w, boundary: boundary}
}
func (w *partWriter) createPart(fn func(io.Writer) error) error {
if _, err := fmt.Fprintf(w.w, "\r\n--%v\r\n", w.boundary); err != nil {
return err
}
return fn(w.w)
}
func (w *partWriter) done() error {
if _, err := fmt.Fprintf(w.w, "\r\n--%v--\r\n", w.boundary); err != nil {
return err
}
return nil
}