diff --git a/internal/imap/mailbox_message.go b/internal/imap/mailbox_message.go index 5ae84d9c..e127fa12 100644 --- a/internal/imap/mailbox_message.go +++ b/internal/imap/mailbox_message.go @@ -40,6 +40,10 @@ import ( openpgperrors "golang.org/x/crypto/openpgp/errors" ) +var ( + rfc822Birthday = time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC) //nolint[gochecknoglobals] +) + type doNotCacheError struct{ e error } func (dnc *doNotCacheError) Error() string { return dnc.e.Error() } @@ -605,7 +609,7 @@ func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) ( } tmpBuf := &bytes.Buffer{} - mainHeader := message.GetHeader(m) + mainHeader := buildHeader(m) if err = writeHeader(tmpBuf, mainHeader); err != nil { return } @@ -703,3 +707,23 @@ func (im *imapMailbox) buildMessageInner(m *pmapi.Message, kr *crypto.KeyRing) ( } return structure, msgBody, err } + +func buildHeader(msg *pmapi.Message) textproto.MIMEHeader { + header := message.GetHeader(msg) + + msgTime := time.Unix(msg.Time, 0) + + // Apple Mail crashes fetching messages with date older than 1970. + // There is no point having message older than RFC itself, it's not possible. + d, err := msg.Header.Date() + if err != nil || d.Before(rfc822Birthday) || msgTime.Before(rfc822Birthday) { + if err != nil || d.IsZero() { + header.Set("X-Original-Date", msgTime.Format(time.RFC1123Z)) + } else { + header.Set("X-Original-Date", d.Format(time.RFC1123Z)) + } + header.Set("Date", rfc822Birthday.Format(time.RFC1123Z)) + } + + return header +} diff --git a/test/fakeapi/controller_control.go b/test/fakeapi/controller_control.go index 21ee61f7..c677554d 100644 --- a/test/fakeapi/controller_control.go +++ b/test/fakeapi/controller_control.go @@ -20,8 +20,10 @@ package fakeapi import ( "errors" "fmt" + "net/mail" "strings" + messageUtils "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/pmapi" ) @@ -139,6 +141,7 @@ func (ctl *Controller) AddUserMessage(username string, message *pmapi.Message) ( } message.ID = ctl.messageIDGenerator.next("") message.LabelIDs = append(message.LabelIDs, pmapi.AllMailLabel) + message.Header = mail.Header(messageUtils.GetHeader(message)) ctl.messagesByUsername[username] = append(ctl.messagesByUsername[username], message) ctl.resetUsers() return message.ID, nil diff --git a/test/features/bridge/imap/message/fetch.feature b/test/features/bridge/imap/message/fetch.feature index 577bd0b8..e7ad8946 100644 --- a/test/features/bridge/imap/message/fetch.feature +++ b/test/features/bridge/imap/message/fetch.feature @@ -60,3 +60,17 @@ Feature: IMAP fetch messages When IMAP client fetches by UID "1:*" Then IMAP response is "OK" And IMAP response has 2 message + + Scenario: Fetch of very old message sent from the moon succeeds with modified date + Given there are messages in mailbox "Folders/mbox" for "user" + | from | to | subject | time | + | john.doe@mail.com | user@pm.me | foo | 1969-07-20T00:00:00 | + And there is IMAP client logged in as "user" + And there is IMAP client selected in "Folders/mbox" + When IMAP client sends command "FETCH 1:* rfc822" + Then IMAP response is "OK" + And IMAP response contains "Date: Fri, 13 Aug 1982" + And IMAP response contains "X-Original-Date: Sun, 20 Jul 1969" + # We had bug to incorectly set empty date, so let's make sure + # there is no reference anywhere in the response. + And IMAP response does not contain "Date: Thu, 01 Jan 1970" diff --git a/test/imap_checks_test.go b/test/imap_checks_test.go index 5dab6f28..28dd777f 100644 --- a/test/imap_checks_test.go +++ b/test/imap_checks_test.go @@ -32,6 +32,8 @@ func IMAPChecksFeatureContext(s *godog.Suite) { s.Step(`^IMAP response to "([^"]*)" contains "([^"]*)"$`, imapResponseNamedContains) s.Step(`^IMAP response has (\d+) message(?:s)?$`, imapResponseHasNumberOfMessages) s.Step(`^IMAP response to "([^"]*)" has (\d+) message(?:s)?$`, imapResponseNamedHasNumberOfMessages) + s.Step(`^IMAP response does not contain "([^"]*)"$`, imapResponseDoesNotContain) + s.Step(`^IMAP response to "([^"]*)" does not contain "([^"]*)"$`, imapResponseNamedDoesNotContain) s.Step(`^IMAP client receives update marking message seq "([^"]*)" as read within (\d+) seconds$`, imapClientReceivesUpdateMarkingMessageSeqAsReadWithin) s.Step(`^IMAP client "([^"]*)" receives update marking message seq "([^"]*)" as read within (\d+) seconds$`, imapClientNamedReceivesUpdateMarkingMessageSeqAsReadWithin) s.Step(`^IMAP client receives update marking message seq "([^"]*)" as unread within (\d+) seconds$`, imapClientReceivesUpdateMarkingMessageSeqAsUnreadWithin) @@ -73,6 +75,16 @@ func imapResponseNamedHasNumberOfMessages(clientID string, expectedCount int) er return ctx.GetTestingError() } +func imapResponseDoesNotContain(notExpectedResponse string) error { + return imapResponseNamedDoesNotContain("imap", notExpectedResponse) +} + +func imapResponseNamedDoesNotContain(clientID, notExpectedResponse string) error { + res := ctx.GetIMAPLastResponse(clientID) + res.AssertNotSections(notExpectedResponse) + return ctx.GetTestingError() +} + func imapClientReceivesUpdateMarkingMessageSeqAsReadWithin(messageSeq string, seconds int) error { return imapClientNamedReceivesUpdateMarkingMessageSeqAsReadWithin("imap", messageSeq, seconds) } diff --git a/test/mocks/imap_response.go b/test/mocks/imap_response.go index 79b66bd4..113ab55c 100644 --- a/test/mocks/imap_response.go +++ b/test/mocks/imap_response.go @@ -160,6 +160,16 @@ func (ir *IMAPResponse) AssertSections(wantRegexps ...string) *IMAPResponse { return ir } +// AssertNotSections is similar to AssertSections but is the opposite. +// It means it just tries to find all "regexps" in the response. +func (ir *IMAPResponse) AssertNotSections(unwantedRegexps ...string) *IMAPResponse { + ir.wait() + for _, unwantedRegexp := range unwantedRegexps { + a.Error(ir.t, ir.hasSectionRegexp(unwantedRegexp), "regexp %v found\nSections: %v", unwantedRegexp, ir.sections) + } + return ir +} + // WaitForSections is the same as AssertSections but waits for `timeout` before giving up. func (ir *IMAPResponse) WaitForSections(timeout time.Duration, wantRegexps ...string) { a.Eventually(ir.t, func() bool { diff --git a/test/store_setup_test.go b/test/store_setup_test.go index e0e8c5ee..9f428e24 100644 --- a/test/store_setup_test.go +++ b/test/store_setup_test.go @@ -20,6 +20,7 @@ package tests import ( "fmt" "net/mail" + "net/textproto" "strconv" "strings" "time" @@ -97,6 +98,7 @@ func thereAreMessagesInMailboxesForAddressOfUser(mailboxNames, bddAddressID, bdd LabelIDs: labelIDs, AddressID: account.AddressID(), } + header := make(textproto.MIMEHeader) if message.HasLabelID(pmapi.SentLabel) { message.Flags |= pmapi.FlagSent @@ -143,12 +145,14 @@ func thereAreMessagesInMailboxesForAddressOfUser(mailboxNames, bddAddressID, bdd return internalError(err, "parsing time") } message.Time = date.Unix() + header.Set("Date", date.Format(time.RFC1123Z)) case "deleted": hasDeletedFlag = cell.Value == "true" default: return fmt.Errorf("unexpected column name: %s", head[n].Value) } } + message.Header = mail.Header(header) lastMessageID, err := ctx.GetPMAPIController().AddUserMessage(account.Username(), message) if err != nil { return internalError(err, "adding message") diff --git a/unreleased.md b/unreleased.md index 4d79f360..acc2c56d 100644 --- a/unreleased.md +++ b/unreleased.md @@ -20,3 +20,4 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) ### Fixed * GODT-135 Support parameters in SMTP `FROM MAIL` command, such as `BODY=7BIT`, or empty value `FROM MAIL:<>` used by some clients. * GODT-338 GODT-781 GODT-857 GODT-866 Flaky tests. +* GODT-773 Replace old dates with birthday of RFC822 to not crash Apple Mail. Original is available under `X-Original-Date` header.