diff --git a/imap/structure.go b/imap/structure.go index 3953074a..d7a3996e 100644 --- a/imap/structure.go +++ b/imap/structure.go @@ -73,7 +73,11 @@ func structure(section *rfc822.Section, fields *paramList, writer *dualParListWr fields.addString(writer, mimeSubType) extWriter := writer.toSingleWriterFrom2nd() - fields.addMap(extWriter, mimeParams) + if len(mimeParams) == 0 { + fields.addString(extWriter, "") + } else { + fields.addMap(extWriter, mimeParams) + } addDispInfo(fields, extWriter, header) fields.addString(extWriter, header.Get("Content-Language")). addString(extWriter, header.Get("Content-Location")) @@ -92,13 +96,38 @@ func singlePartStructure(section *rfc822.Section, fields *paramList, writer *dua return err } + featureFlagProvider := unleash.Get() fields. addString(writer, mimeType). - addString(writer, mimeSubType). - addMap(writer, mimeParams). + addString(writer, mimeSubType) + + // Per RFC3501 if `body-field-params` should be NIL if empty + if len(mimeParams) == 0 { + fields.addString(writer, "") + } else { + fields.addMap(writer, mimeParams) + } + + // Per RFC2045, if Content-Transfer-Encoding is missing, then we should assuume + // that the content is `7BIT`. + contentTransferEncoding := func() string { + value := header.Get("Content-Transfer-Encoding") + // if default 7bit behavior is disabled, just return whatever is in the header. + if featureFlagProvider.GetFlagValue(featureflags.ContentTransferEncodingDefault7BitDisabled) { + return value + } + + if value == "" { + value = "7BIT" + } + + return value + } + + fields. addString(writer, header.Get("Content-Id")). addString(writer, header.Get("Content-Description")). - addString(writer, header.Get("Content-Transfer-Encoding")). + addString(writer, contentTransferEncoding()). addNumber(writer, len(section.Body())) if mimeType == "message" && mimeSubType == "rfc822" { diff --git a/imap/structure_test.go b/imap/structure_test.go index c2fe878a..aed5add5 100644 --- a/imap/structure_test.go +++ b/imap/structure_test.go @@ -72,7 +72,18 @@ hey there bro require.NoError(t, err) require.NotNil(t, parsed) - expected := "((\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"quoted-printable\" 6 2)(\"message\" \"rfc822\" (\"name\" \"ISO-8859-1.eml\") NIL NIL NIL 127 (NIL \"ISO-8859-1\" ((NIL NIL \"random-mail\" \"pm.me\")) ((NIL NIL \"random-mail\" \"pm.me\")) ((NIL NIL \"random-mail\" \"pm.me\")) ((NIL NIL \"random-mail2\" \"pm.me\")) NIL NIL NIL NIL)(\"text\" \"plain\" (\"charset\" \"iso-8859-1\") NIL NIL NIL 14 1) 6) \"mixed\")" + expected := "((\"text\" \"plain\" (\"charset\" \"utf-8\") NIL NIL \"quoted-printable\" 6 2)(\"message\" \"rfc822\" (\"name\" \"ISO-8859-1.eml\") NIL NIL \"7BIT\" 127 (NIL \"ISO-8859-1\" ((NIL NIL \"random-mail\" \"pm.me\")) ((NIL NIL \"random-mail\" \"pm.me\")) ((NIL NIL \"random-mail\" \"pm.me\")) ((NIL NIL \"random-mail2\" \"pm.me\")) NIL NIL NIL NIL)(\"text\" \"plain\" (\"charset\" \"iso-8859-1\") NIL NIL \"7BIT\" 14 1) 6) \"mixed\")" + require.Equal(t, expected, parsed.Body) +} + +func TestStructureNoContentTypeParams(t *testing.T) { + const message = "Content-Type: text/plain\r\nContent-Transfer-Encoding: 7bit\r\n\r\nbody\r\n" + + parsed, err := NewParsedMessage([]byte(message)) + require.NoError(t, err) + require.NotNil(t, parsed) + + expected := "(\"text\" \"plain\" NIL NIL NIL \"7bit\" 6 1)" require.Equal(t, expected, parsed.Body) } @@ -182,7 +193,6 @@ func FuzzNewParsedMessage(f *testing.F) { f.Add(inSeed2) f.Fuzz(func(t *testing.T, inputData []byte) { - _, _ = NewParsedMessage(inputData) }) } @@ -201,7 +211,7 @@ func TestMaxMIMEStructureDepthExceeded_KillSwitch_Disabled(t *testing.T) { require.ErrorIs(t, err, errorMaximumMIMEStructureDepthExceeded) t.Cleanup(func() { - unleash.Init(nil) + unleash.Init(&unleash.NullFeatureFlagProvider{}) }) } @@ -218,17 +228,6 @@ func TestMaxMIMEStructureDepthExceeded_KillSwitch_Enabled(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { - unleash.Init(nil) - }) -} - -func TestMaxMIMEStructureDepthExceeded_NoFFProvider(t *testing.T) { - eml, err := os.ReadFile(filepath.Join("testdata", "mime-structure-depth.eml")) - require.NoError(t, err) - _, err = NewParsedMessage(eml) - require.NoError(t, err) - - t.Cleanup(func() { - unleash.Init(nil) + unleash.Init(&unleash.NullFeatureFlagProvider{}) }) } diff --git a/internal/unleash/featureflags/flags.go b/internal/unleash/featureflags/flags.go index 5a0a4e58..e5cfa971 100644 --- a/internal/unleash/featureflags/flags.go +++ b/internal/unleash/featureflags/flags.go @@ -1,11 +1,12 @@ package featureflags const ( - CommandWatcherGlobalDisabled = "InboxBridgeGenericImapOkHeartbeatDisabled" - CommandWatcherNonThunderbirdDisabled = "InboxBridgeGenericImapOkHeartbeatNonThunderbirdDisabled" - ConnectionLimiterDisabled = "InboxBridgeGluonConnectionLimiterDisabled" - ConnectionLimiterDefaultLimitsDisabled = "InboxBridgeGluonConnectionLimiterDefaultLimitsDisabled" - ConnectionCounterConnectionsLimitDisabled = "InboxBridgeGluonRollingCounterConnectionLimitDisabled" - MaximumMIMEStructureDepthDisabled = "InboxBridgeGluonMaximumMimeStructureDepthLimitDisabled" - ApplySentryEventsDisabled = "InboxDesktopGluonApplySentryEventsDisabled" + CommandWatcherGlobalDisabled = "InboxBridgeGenericImapOkHeartbeatDisabled" + CommandWatcherNonThunderbirdDisabled = "InboxBridgeGenericImapOkHeartbeatNonThunderbirdDisabled" + ConnectionLimiterDisabled = "InboxBridgeGluonConnectionLimiterDisabled" + ConnectionLimiterDefaultLimitsDisabled = "InboxBridgeGluonConnectionLimiterDefaultLimitsDisabled" + ConnectionCounterConnectionsLimitDisabled = "InboxBridgeGluonRollingCounterConnectionLimitDisabled" + MaximumMIMEStructureDepthDisabled = "InboxBridgeGluonMaximumMimeStructureDepthLimitDisabled" + ApplySentryEventsDisabled = "InboxDesktopGluonApplySentryEventsDisabled" + ContentTransferEncodingDefault7BitDisabled = "InboxDesktopGluonNoContentTransferEncoding7BitDefaultDisabled" ) diff --git a/internal/unleash/singleton.go b/internal/unleash/singleton.go index ffec7999..80efd8f1 100644 --- a/internal/unleash/singleton.go +++ b/internal/unleash/singleton.go @@ -22,5 +22,9 @@ func Init(provider FeatureFlagValueProvider) { } func Get() FeatureFlagValueProvider { + if testing.Testing() && instance == nil { + instance = &NullFeatureFlagProvider{} + return instance + } return instance } diff --git a/tests/helper_test.go b/tests/helper_test.go index 776c5dc0..359f4901 100644 --- a/tests/helper_test.go +++ b/tests/helper_test.go @@ -880,6 +880,17 @@ func newEmptyBodyStructureValidator() *bodyStructureValidator { require.Empty(tb, s) } + validEncoding := func(tb testing.TB, s string) { + validValues := []string{"7BIT", "8BIT", "BINARY", "BASE64", "QUOTED-PRINTABLE", ""} + tb.Helper() + for _, value := range validValues { + if strings.EqualFold(value, s) { + return + } + } + require.Failf(tb, "string not in allowed values", "got %q, want one of %v", s, validValues) + } + return &bodyStructureValidator{ validateMIMEType: emptyString, validateMimeSubType: emptyString, @@ -888,7 +899,7 @@ func newEmptyBodyStructureValidator() *bodyStructureValidator { }, validateID: emptyString, validateDescription: emptyString, - validateEncoding: emptyString, + validateEncoding: validEncoding, validateSize: func(tb testing.TB, u uint32) { require.Zero(tb, u) },