mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-11 05:06:51 +00:00
fix(BRIDGE-231): fix reversed header order in messages.
This commit is contained in:
@ -369,30 +369,30 @@ func getMessageHeader(msg proton.Message, opts JobOptions) message.Header {
|
|||||||
|
|
||||||
// SetText will RFC2047-encode.
|
// SetText will RFC2047-encode.
|
||||||
if msg.Subject != "" {
|
if msg.Subject != "" {
|
||||||
hdr.SetText("Subject", msg.Subject)
|
setUTF8EncodedHeaderIfNeeded(&hdr, "Subject", msg.Subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
// mail.Address.String() will RFC2047-encode if necessary.
|
// mail.Address.String() will RFC2047-encode if necessary.
|
||||||
if !addressEmpty(msg.Sender) {
|
if !addressEmpty(msg.Sender) {
|
||||||
hdr.Set("From", msg.Sender.String())
|
setHeaderIfNeeded(&hdr, "From", msg.Sender.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(msg.ReplyTos) > 0 && !msg.IsDraft() {
|
if len(msg.ReplyTos) > 0 && !msg.IsDraft() {
|
||||||
if !(len(msg.ReplyTos) == 1 && addressEmpty(msg.ReplyTos[0])) {
|
if !(len(msg.ReplyTos) == 1 && addressEmpty(msg.ReplyTos[0])) {
|
||||||
hdr.Set("Reply-To", toAddressList(msg.ReplyTos))
|
setHeaderIfNeeded(&hdr, "Reply-To", toAddressList(msg.ReplyTos))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(msg.ToList) > 0 {
|
if len(msg.ToList) > 0 {
|
||||||
hdr.Set("To", toAddressList(msg.ToList))
|
setHeaderIfNeeded(&hdr, "To", toAddressList(msg.ToList))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(msg.CCList) > 0 {
|
if len(msg.CCList) > 0 {
|
||||||
hdr.Set("Cc", toAddressList(msg.CCList))
|
setHeaderIfNeeded(&hdr, "Cc", toAddressList(msg.CCList))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(msg.BCCList) > 0 {
|
if len(msg.BCCList) > 0 {
|
||||||
hdr.Set("Bcc", toAddressList(msg.BCCList))
|
setHeaderIfNeeded(&hdr, "Bcc", toAddressList(msg.BCCList))
|
||||||
}
|
}
|
||||||
|
|
||||||
setMessageIDIfNeeded(msg, &hdr)
|
setMessageIDIfNeeded(msg, &hdr)
|
||||||
@ -401,7 +401,7 @@ func getMessageHeader(msg proton.Message, opts JobOptions) message.Header {
|
|||||||
if opts.SanitizeDate {
|
if opts.SanitizeDate {
|
||||||
if date, err := rfc5322.ParseDateTime(hdr.Get("Date")); err != nil || date.Before(time.Unix(0, 0)) {
|
if date, err := rfc5322.ParseDateTime(hdr.Get("Date")); err != nil || date.Before(time.Unix(0, 0)) {
|
||||||
msgDate := SanitizeMessageDate(msg.Time)
|
msgDate := SanitizeMessageDate(msg.Time)
|
||||||
hdr.Set("Date", msgDate.In(time.UTC).Format(time.RFC1123Z))
|
setHeaderIfNeeded(&hdr, "Date", msgDate.In(time.UTC).Format(time.RFC1123Z))
|
||||||
// We clobbered the date so we save it under X-Original-Date only if no such value exists.
|
// We clobbered the date so we save it under X-Original-Date only if no such value exists.
|
||||||
if !hdr.Has("X-Original-Date") {
|
if !hdr.Has("X-Original-Date") {
|
||||||
hdr.Set("X-Original-Date", date.In(time.UTC).Format(time.RFC1123Z))
|
hdr.Set("X-Original-Date", date.In(time.UTC).Format(time.RFC1123Z))
|
||||||
@ -412,7 +412,7 @@ func getMessageHeader(msg proton.Message, opts JobOptions) message.Header {
|
|||||||
// Set our internal ID if requested.
|
// Set our internal ID if requested.
|
||||||
// This is important for us to detect whether APPENDed things are actually "move like outlook".
|
// This is important for us to detect whether APPENDed things are actually "move like outlook".
|
||||||
if opts.AddInternalID {
|
if opts.AddInternalID {
|
||||||
hdr.Set("X-Pm-Internal-Id", msg.ID)
|
setHeaderIfNeeded(&hdr, "X-Pm-Internal-Id", msg.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set our external ID if requested.
|
// Set our external ID if requested.
|
||||||
@ -426,7 +426,7 @@ func getMessageHeader(msg proton.Message, opts JobOptions) message.Header {
|
|||||||
// Set our server date if requested.
|
// Set our server date if requested.
|
||||||
// Can be useful to see how long it took for a message to arrive.
|
// Can be useful to see how long it took for a message to arrive.
|
||||||
if opts.AddMessageDate {
|
if opts.AddMessageDate {
|
||||||
hdr.Set("X-Pm-Date", time.Unix(msg.Time, 0).In(time.UTC).Format(time.RFC1123Z))
|
setHeaderIfNeeded(&hdr, "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...).
|
// Include the message ID in the references (supposedly this somehow improves outlook support...).
|
||||||
@ -463,6 +463,25 @@ func setMessageIDIfNeeded(msg proton.Message, hdr *message.Header) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setTextHeaderIfNeeded sets a text (UTF-encoded) header entry if its does not exists or if value is changed.
|
||||||
|
// Not systematically overwriting the value prevents it from being moved to the top (Del + Add) if not changed.
|
||||||
|
func setUTF8EncodedHeaderIfNeeded(header *message.Header, k, v string) {
|
||||||
|
encoded := mime.QEncoding.Encode("utf-8", v)
|
||||||
|
if header.Has(k) && (header.Get(k) == encoded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
header.Set(k, encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setHeaderIfNeeded sets a header entry if its does not exists or if value is changed.
|
||||||
|
// Not systematically overwriting the value prevents it from being moved to the top (Del + Add) if not changed.
|
||||||
|
func setHeaderIfNeeded(header *message.Header, key, value string) {
|
||||||
|
if header.Has(key) && (header.Get(key) == value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
header.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
func getTextPartHeader(hdr message.Header, body []byte, mimeType rfc822.MIMEType) message.Header {
|
func getTextPartHeader(hdr message.Header, body []byte, mimeType rfc822.MIMEType) message.Header {
|
||||||
params := make(map[string]string)
|
params := make(map[string]string)
|
||||||
|
|
||||||
@ -509,8 +528,9 @@ func getAttachmentPartHeader(att proton.Attachment) message.Header {
|
|||||||
|
|
||||||
func toMessageHeader(hdr proton.Headers) message.Header {
|
func toMessageHeader(hdr proton.Headers) message.Header {
|
||||||
var res message.Header
|
var res message.Header
|
||||||
|
// go-message's message.Header are in reversed order (you should only add fields at the top, so storing in reverse order offer faster performances).
|
||||||
for _, key := range hdr.Order {
|
for i := len(hdr.Order) - 1; i >= 0; i-- {
|
||||||
|
key := hdr.Order[i]
|
||||||
for _, val := range hdr.Values[key] {
|
for _, val := range hdr.Values[key] {
|
||||||
// Using AddRaw instead of Add to save key-value pair as byte buffer within Header.
|
// 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
|
// This buffer is used latter on in message writer to construct message and avoid crash
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -92,6 +93,67 @@ func newRawTestMessageWithHeaders(messageID, addressID, mimeType, body string, d
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newTestMessageFromRFC822(t *testing.T, literal []byte) proton.Message {
|
||||||
|
// Note attachment are not supported.
|
||||||
|
p := rfc822.Parse(literal)
|
||||||
|
h, err := p.ParseHeader()
|
||||||
|
require.NoError(t, err)
|
||||||
|
var parsedHeaders proton.Headers
|
||||||
|
parsedHeaders.Values = make(map[string][]string)
|
||||||
|
h.Entries(func(key, val string) {
|
||||||
|
parsedHeaders.Values[key] = []string{val}
|
||||||
|
parsedHeaders.Order = append(parsedHeaders.Order, key)
|
||||||
|
})
|
||||||
|
var mailHeaders = mail.Header(parsedHeaders.Values)
|
||||||
|
require.True(t, h.Has("Content-Type"))
|
||||||
|
mime, _, err := rfc822.ParseMIMEType(h.Get("Content-Type"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
date, err := mailHeaders.Date()
|
||||||
|
require.NoError(t, err)
|
||||||
|
sender, err := mail.ParseAddress(parsedHeaders.Values["From"][0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return proton.Message{
|
||||||
|
MessageMetadata: proton.MessageMetadata{
|
||||||
|
ID: "messageID",
|
||||||
|
AddressID: "addressID",
|
||||||
|
LabelIDs: []string{},
|
||||||
|
ExternalID: "",
|
||||||
|
Subject: parsedHeaders.Values["Subject"][0],
|
||||||
|
Sender: sender,
|
||||||
|
ToList: parseAddressList(t, mailHeaders, "To"),
|
||||||
|
CCList: parseAddressList(t, mailHeaders, "Cc"),
|
||||||
|
BCCList: parseAddressList(t, mailHeaders, "Bcc"),
|
||||||
|
ReplyTos: parseAddressList(t, mailHeaders, "Reply-To"),
|
||||||
|
Flags: 0,
|
||||||
|
Time: date.Unix(),
|
||||||
|
Size: 0,
|
||||||
|
Unread: false,
|
||||||
|
IsReplied: false,
|
||||||
|
IsRepliedAll: false,
|
||||||
|
IsForwarded: false,
|
||||||
|
NumAttachments: 0,
|
||||||
|
},
|
||||||
|
Header: string(h.Raw()),
|
||||||
|
ParsedHeaders: parsedHeaders,
|
||||||
|
Body: string(p.Body()),
|
||||||
|
MIMEType: mime,
|
||||||
|
Attachments: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAddressList(t *testing.T, header mail.Header, key string) []*mail.Address {
|
||||||
|
var result []*mail.Address
|
||||||
|
if len(header.Get(key)) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := header.AddressList(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func addTestAttachment(
|
func addTestAttachment(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
kr *crypto.KeyRing,
|
kr *crypto.KeyRing,
|
||||||
|
|||||||
@ -18,9 +18,14 @@
|
|||||||
package message
|
package message
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
gomessage "github.com/emersion/go-message"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestHeaderLines(t *testing.T) {
|
func TestHeaderLines(t *testing.T) {
|
||||||
@ -130,3 +135,69 @@ func FuzzReadHeaderBody(f *testing.F) {
|
|||||||
_, _, _ = readHeaderBody(b)
|
_, _, _ = readHeaderBody(b)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHeaderOrder(t *testing.T) {
|
||||||
|
literal := []byte(`X-Pm-Content-Encryption: end-to-end
|
||||||
|
X-Pm-Origin: internal
|
||||||
|
Subject: header test
|
||||||
|
To: Test Proton <test@proton.me>
|
||||||
|
From: Dummy Recipient <dummy@proton.me>
|
||||||
|
Date: Tue, 15 Oct 2024 07:54:39 +0000
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: multipart/mixed;boundary=---------------------a136fc3851075ca3f022f5c3ec6bf8f5
|
||||||
|
Message-Id: <1rYR51zNVZdyCXVvAZ8C9N8OaBg4wO_wg6VlSoLK_Mv-2AaiF5UL-vE_tIZ6FdYP8ylsuV3fpaKUpVwuUcnQ6ql_83aEgZvfC5QcZbind1k=@proton.me>
|
||||||
|
X-Pm-Spamscore: 0
|
||||||
|
Received: from mail.protonmail.ch by mail.protonmail.ch; Tue, 15 Oct 2024 07:54:43 +0000
|
||||||
|
X-Original-To: test@proton.me
|
||||||
|
Return-Path: <dummy@proton.me>
|
||||||
|
Delivered-To: test@proton.me
|
||||||
|
|
||||||
|
lorem`)
|
||||||
|
|
||||||
|
// build a proton message
|
||||||
|
message := newTestMessageFromRFC822(t, literal)
|
||||||
|
options := JobOptions{
|
||||||
|
IgnoreDecryptionErrors: true,
|
||||||
|
SanitizeDate: true,
|
||||||
|
AddInternalID: true,
|
||||||
|
AddExternalID: true,
|
||||||
|
AddMessageDate: true,
|
||||||
|
AddMessageIDReference: true,
|
||||||
|
SanitizeMBOXHeaderLine: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the headers using bridge's algorithm, sanitizing fields.
|
||||||
|
hdr := getTextPartHeader(getMessageHeader(message, options), []byte(message.Body), message.MIMEType)
|
||||||
|
var b bytes.Buffer
|
||||||
|
w, err := gomessage.CreateWriter(&b, hdr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_ = w.Close()
|
||||||
|
|
||||||
|
// split the header
|
||||||
|
str := string(regexp.MustCompile(`\r\n(\s+)`).ReplaceAll(b.Bytes(), nil)) // join multi
|
||||||
|
lines := strings.Split(str, "\r\n")
|
||||||
|
|
||||||
|
// Check we have the expected order
|
||||||
|
require.Equal(t, len(lines), 17)
|
||||||
|
|
||||||
|
// The fields added or modified are at the top
|
||||||
|
require.True(t, strings.HasPrefix(lines[0], "Content-Type: multipart/mixed;boundary=")) // we changed the boundary
|
||||||
|
require.True(t, strings.HasPrefix(lines[1], "References: ")) // Reference was added
|
||||||
|
require.True(t, strings.HasPrefix(lines[2], "X-Pm-Date: ")) // X-Pm-Date was added
|
||||||
|
require.True(t, strings.HasPrefix(lines[3], "X-Pm-Internal-Id: ")) // X-Pm-Internal-Id was added
|
||||||
|
require.Equal(t, `To: "Test Proton" <test@proton.me>`, lines[4]) // Name was double quoted
|
||||||
|
require.Equal(t, `From: "Dummy Recipient" <dummy@proton.me>`, lines[5]) // Name was double quoted
|
||||||
|
|
||||||
|
// all other fields appear in their original order
|
||||||
|
require.Equal(t, `X-Pm-Content-Encryption: end-to-end`, lines[6])
|
||||||
|
require.Equal(t, `X-Pm-Origin: internal`, lines[7])
|
||||||
|
require.Equal(t, `Subject: header test`, lines[8])
|
||||||
|
require.Equal(t, `Date: Tue, 15 Oct 2024 07:54:39 +0000`, lines[9])
|
||||||
|
require.Equal(t, `Mime-Version: 1.0`, lines[10])
|
||||||
|
require.Equal(t, `Message-Id: <1rYR51zNVZdyCXVvAZ8C9N8OaBg4wO_wg6VlSoLK_Mv-2AaiF5UL-vE_tIZ6FdYP8ylsuV3fpaKUpVwuUcnQ6ql_83aEgZvfC5QcZbind1k=@proton.me>`, lines[11])
|
||||||
|
require.Equal(t, `X-Pm-Spamscore: 0`, lines[12])
|
||||||
|
require.Equal(t, `Received: from mail.protonmail.ch by mail.protonmail.ch; Tue, 15 Oct 2024 07:54:43 +0000`, lines[13])
|
||||||
|
require.Equal(t, `X-Original-To: test@proton.me`, lines[14])
|
||||||
|
require.Equal(t, `Return-Path: <dummy@proton.me>`, lines[15])
|
||||||
|
require.Equal(t, `Delivered-To: test@proton.me`, lines[16])
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user