From bf89d548d3a614099892bf1a1f7fa6b7719bf8da Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Thu, 16 Nov 2023 15:48:04 +0100 Subject: [PATCH] fix(GODT-2576): Correctly handle Forwarded messages from Thunderbird Thunderbird uses `In-Reply-To` with `X-Forwarded-Message-Id` to signal to the SMTP server that it is forwarding a message. --- go.mod | 2 +- go.sum | 4 +- internal/services/smtp/smtp.go | 10 +- pkg/message/parser.go | 4 + pkg/message/parser_test.go | 10 ++ ...text_plain_utf8_reply_to_and_x_forward.eml | 7 ++ tests/features/smtp/send/send_reply.feature | 111 +++++++++++++++++- 7 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 pkg/message/testdata/text_plain_utf8_reply_to_and_x_forward.eml diff --git a/go.mod b/go.mod index e71607e3..9b3a7615 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.0 github.com/ProtonMail/gluon v0.17.1-0.20231114153341-2ecbdd2739f7 github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a - github.com/ProtonMail/go-proton-api v0.4.1-0.20231116074655-c9bc6f71eef0 + github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc github.com/ProtonMail/gopenpgp/v2 v2.7.4-proton github.com/PuerkitoBio/goquery v1.8.1 github.com/abiosoft/ishell v2.0.0+incompatible diff --git a/go.sum b/go.sum index 65817bb4..cca14c81 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,8 @@ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7 h1:+j+Kd/ github.com/ProtonMail/go-message v0.13.1-0.20230526094639-b62c999c85b7/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= -github.com/ProtonMail/go-proton-api v0.4.1-0.20231116074655-c9bc6f71eef0 h1:tUK7x2Vm2bnCdij/gKyXxgV9v1A680BdDOEQfBm1Rz0= -github.com/ProtonMail/go-proton-api v0.4.1-0.20231116074655-c9bc6f71eef0/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8= +github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc h1:GBRKoFAldApEMkMrsFN1ZxG0eG797w6LTv/dFMDcsqQ= +github.com/ProtonMail/go-proton-api v0.4.1-0.20231116144214-8a47c8d92fbc/go.mod h1:WEXJqj5DSc2YI77SgXdpMY0nk33Qy92Vu2r4tOEazA8= github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8= github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/ProtonMail/go-srp v0.0.7 h1:Sos3Qk+th4tQR64vsxGIxYpN3rdnG9Wf9K4ZloC1JrI= diff --git a/internal/services/smtp/smtp.go b/internal/services/smtp/smtp.go index 522e5ce0..ae62c648 100644 --- a/internal/services/smtp/smtp.go +++ b/internal/services/smtp/smtp.go @@ -217,7 +217,7 @@ func (s *Service) sendWithKey( return proton.Message{}, fmt.Errorf("unsupported MIME type: %v", message.MIMEType) } - draft, err := s.createDraft(ctx, addrKR, emails, from, to, parentID, message.InReplyTo, proton.DraftTemplate{ + draft, err := s.createDraft(ctx, addrKR, emails, from, to, parentID, message.InReplyTo, message.XForward, proton.DraftTemplate{ Subject: message.Subject, Body: decBody, MIMEType: message.MIMEType, @@ -353,6 +353,7 @@ func (s *Service) createDraft( to []string, parentID string, replyToID string, + xForwardID string, template proton.DraftTemplate, ) (proton.Message, error) { // Check sender: set the sender if it's missing. @@ -388,7 +389,12 @@ func (s *Service) createDraft( var action proton.CreateDraftAction if len(replyToID) > 0 { - action = proton.ReplyAction + // Thunderbird fills both ReplyTo and adds an X-Forwarded-Message-Id header when forwarding. + if replyToID == xForwardID { + action = proton.ForwardAction + } else { + action = proton.ReplyAction + } } else { action = proton.ForwardAction } diff --git a/pkg/message/parser.go b/pkg/message/parser.go index 76557e62..7f15f442 100644 --- a/pkg/message/parser.go +++ b/pkg/message/parser.go @@ -60,6 +60,7 @@ type Message struct { References []string ExternalID string InReplyTo string + XForward string } type Attachment struct { @@ -520,6 +521,9 @@ func parseMessageHeader(h message.Header, allowInvalidAddressLists bool) (Messag case "in-reply-to": m.InReplyTo = regexp.MustCompile("<(.*)>").ReplaceAllString(fields.Value(), "$1") + case "x-forwarded-message-id": + m.XForward = regexp.MustCompile("<(.*)>").ReplaceAllString(fields.Value(), "$1") + case "references": for _, ref := range strings.Fields(fields.Value()) { for _, ref := range strings.Split(ref, ",") { diff --git a/pkg/message/parser_test.go b/pkg/message/parser_test.go index 24b2101d..3c67621b 100644 --- a/pkg/message/parser_test.go +++ b/pkg/message/parser_test.go @@ -786,6 +786,16 @@ func TestParseTextPlainWithDocxAttachmentCyrillic(t *testing.T) { assert.Equal(t, "АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЧЏЗШ.docx", m.Attachments[0].Name) } +func TestParseInReplyToAndXForward(t *testing.T) { + f := getFileReader("text_plain_utf8_reply_to_and_x_forward.eml") + + m, err := Parse(f) + require.NoError(t, err) + + require.Equal(t, "00000@protonmail.com", m.XForward) + require.Equal(t, "00000@protonmail.com", m.InReplyTo) +} + func TestPatchNewLineWithHtmlBreaks(t *testing.T) { { input := []byte("\nfoo\nbar\n\n\nzz\nddd") diff --git a/pkg/message/testdata/text_plain_utf8_reply_to_and_x_forward.eml b/pkg/message/testdata/text_plain_utf8_reply_to_and_x_forward.eml new file mode 100644 index 00000000..c5f99043 --- /dev/null +++ b/pkg/message/testdata/text_plain_utf8_reply_to_and_x_forward.eml @@ -0,0 +1,7 @@ +From: Sender +To: Receiver +Content-Type: text/plain; charset=utf-8 +X-Forwarded-Message-Id: <00000@protonmail.com> +In-Reply-To: <00000@protonmail.com> + +body \ No newline at end of file diff --git a/tests/features/smtp/send/send_reply.feature b/tests/features/smtp/send/send_reply.feature index 4727e1bc..57b90e4c 100644 --- a/tests/features/smtp/send/send_reply.feature +++ b/tests/features/smtp/send/send_reply.feature @@ -277,4 +277,113 @@ Feature: SMTP send reply And IMAP client "1" eventually sees the following messages in "INBOX": | from | subject | in-reply-to | references | | [user:user2]@[domain] | FW - Please Reply | | | - | [user:user2]@[domain] | FW - Please Reply Again | | | \ No newline at end of file + | [user:user2]@[domain] | FW - Please Reply Again | | | + + @long-black + Scenario: Reply with In-Reply-To and X-Forwarded-Message-Id sets forwarded flag + # User1 send the initial message. + When SMTP client "1" sends the following message from "[user:user1]@[domain]" to "[user:user2]@[domain]": + """ + From: Bridge Test <[user:user1]@[domain]> + To: Internal Bridge <[user:user2]@[domain]> + Subject: Please Reply + Message-ID: + + hello + + """ + Then it succeeds + Then IMAP client "1" eventually sees the following messages in "Sent": + | from | to | subject | message-id | + | [user:user1]@[domain] | [user:user2]@[domain] | Please Reply | | + # login user2. + And the user logs in with username "[user:user2]" and password "password" + And user "[user:user2]" connects and authenticates IMAP client "2" + And user "[user:user2]" connects and authenticates SMTP client "2" + And user "[user:user2]" finishes syncing + # User2 receive the message. + Then IMAP client "2" eventually sees the following messages in "INBOX": + | from | subject | message-id | reply-to | + | [user:user1]@[domain] | Please Reply | | [user:user1]@[domain] | + # User2 reply to it. + When SMTP client "2" sends the following message from "[user:user2]@[domain]" to "[user:user1]@[domain]": + """ + From: Internal Bridge <[user:user2]@[domain]> + To: Bridge Test <[user:user1]@[domain]> + Content-Type: text/plain + Subject: FW - Please Reply + In-Reply-To: + Message-ID: + X-Forwarded-Message-Id: + + Heya + + """ + Then it succeeds + Then IMAP client "2" eventually sees the following messages in "Sent": + | from | to | subject | in-reply-to | references | + | [user:user2]@[domain] | [user:user1]@[domain] | FW - Please Reply | | | + When IMAP client "2" selects "INBOX" + And it succeeds + Then IMAP client "2" eventually sees that message at row 1 has the flag "forwarded" + And it succeeds + Then IMAP client "2" eventually sees that message at row 1 does not have the flag "\Answered" + And it succeeds + # User1 receive the reply.| + And IMAP client "1" eventually sees the following messages in "INBOX": + | from | subject | in-reply-to | references | + | [user:user2]@[domain] | FW - Please Reply | | | + + @long-black + Scenario: Reply with In-Reply-To sets answered flag + # User1 send the initial message. + When SMTP client "1" sends the following message from "[user:user1]@[domain]" to "[user:user2]@[domain]": + """ + From: Bridge Test <[user:user1]@[domain]> + To: Internal Bridge <[user:user2]@[domain]> + Subject: Please Reply + Message-ID: + + hello + + """ + Then it succeeds + Then IMAP client "1" eventually sees the following messages in "Sent": + | from | to | subject | message-id | + | [user:user1]@[domain] | [user:user2]@[domain] | Please Reply | | + # login user2. + And the user logs in with username "[user:user2]" and password "password" + And user "[user:user2]" connects and authenticates IMAP client "2" + And user "[user:user2]" connects and authenticates SMTP client "2" + And user "[user:user2]" finishes syncing + # User2 receive the message. + Then IMAP client "2" eventually sees the following messages in "INBOX": + | from | subject | message-id | reply-to | + | [user:user1]@[domain] | Please Reply | | [user:user1]@[domain] | + # User2 reply to it. + When SMTP client "2" sends the following message from "[user:user2]@[domain]" to "[user:user1]@[domain]": + """ + From: Internal Bridge <[user:user2]@[domain]> + To: Bridge Test <[user:user1]@[domain]> + Content-Type: text/plain + Subject: FW - Please Reply + In-Reply-To: + Message-ID: + + Heya + + """ + Then it succeeds + Then IMAP client "2" eventually sees the following messages in "Sent": + | from | to | subject | in-reply-to | references | + | [user:user2]@[domain] | [user:user1]@[domain] | FW - Please Reply | | | + When IMAP client "2" selects "INBOX" + And it succeeds + Then IMAP client "2" eventually sees that message at row 1 has the flag "\Answered" + And it succeeds + Then IMAP client "2" eventually sees that message at row 1 does not have the flag "forwarded" + And it succeeds + # User1 receive the reply.| + And IMAP client "1" eventually sees the following messages in "INBOX": + | from | subject | in-reply-to | references | + | [user:user2]@[domain] | FW - Please Reply | | | \ No newline at end of file