forked from Silverfish/proton-bridge
GODT-1779: Remove go-imap
This commit is contained in:
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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 {
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
@ -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)
|
||||
}
|
||||
@ -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>
|
||||
`))
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -23,8 +23,4 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
rfc822Message = "message/rfc822"
|
||||
)
|
||||
|
||||
var log = logrus.WithField("pkg", "pkg/message") //nolint:gochecknoglobals
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user