Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ func (builder *serverBuilder) build() (*Server, error) {
builder.featureFlagProvider = &unleash.NullFeatureFlagProvider{}
}

unleash.Init(builder.featureFlagProvider)

s := &Server{
dataDir: builder.dataDir,
databaseDir: builder.databaseDir,
Expand Down
36 changes: 28 additions & 8 deletions imap/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,26 @@ package imap

import (
"bytes"
"errors"
"strings"

"github.com/ProtonMail/gluon/internal/unleash"
"github.com/ProtonMail/gluon/internal/unleash/featureflags"
"github.com/ProtonMail/gluon/rfc822"
)

var errorMaximumMIMEStructureDepthExceeded = errors.New("maximum mime structure depth exceeded")

const maxMIMEStructureDepth = 64

func Structure(section *rfc822.Section) (string, string, error) {
bodyBuilder := strings.Builder{}
structureBuilder := strings.Builder{}

writer := dualParListWriter{b1: &bodyBuilder, b2: &structureBuilder}

c := newParamListWithGroup(&writer)
if err := structure(section, &c, &writer); err != nil {
if err := structure(section, &c, &writer, 0); err != nil {
return "", "", err
}

Expand All @@ -26,17 +33,30 @@ func Structure(section *rfc822.Section) (string, string, error) {
return body, structure, nil
}

func structure(section *rfc822.Section, fields *paramList, writer *dualParListWriter) error {
func structure(section *rfc822.Section, fields *paramList, writer *dualParListWriter, depth int) error {
maxMimeStructureDepthDisabled := true

featureFlagValueProvider := unleash.Get()
if featureFlagValueProvider != nil {
maxMimeStructureDepthDisabled = featureFlagValueProvider.GetFlagValue(featureflags.MaximumMIMEStructureDepthDisabled)
}

if !maxMimeStructureDepthDisabled {
if depth > maxMIMEStructureDepth {
return errorMaximumMIMEStructureDepthExceeded
}
}

children, err := section.Children()
if err != nil {
return err
}

if len(children) == 0 {
return singlePartStructure(section, fields, writer)
return singlePartStructure(section, fields, writer, depth)
}

if err := childStructures(section, fields, writer); err != nil {
if err := childStructures(section, fields, writer, depth); err != nil {
return err
}

Expand All @@ -61,7 +81,7 @@ func structure(section *rfc822.Section, fields *paramList, writer *dualParListWr
return nil
}

func singlePartStructure(section *rfc822.Section, fields *paramList, writer *dualParListWriter) error {
func singlePartStructure(section *rfc822.Section, fields *paramList, writer *dualParListWriter, depth int) error {
header, err := section.ParseHeader()
if err != nil {
return err
Expand Down Expand Up @@ -97,7 +117,7 @@ func singlePartStructure(section *rfc822.Section, fields *paramList, writer *dua

cstruct := fields.newChildList(writer)

if err := structure(child, &cstruct, writer); err != nil {
if err := structure(child, &cstruct, writer, depth+1); err != nil {
return err
}

Expand All @@ -117,7 +137,7 @@ func singlePartStructure(section *rfc822.Section, fields *paramList, writer *dua
return nil
}

func childStructures(section *rfc822.Section, c *paramList, writer *dualParListWriter) error {
func childStructures(section *rfc822.Section, c *paramList, writer *dualParListWriter, depth int) error {
children, err := section.Children()
if err != nil {
return err
Expand All @@ -126,7 +146,7 @@ func childStructures(section *rfc822.Section, c *paramList, writer *dualParListW
for _, child := range children {
cl := c.newChildList(writer)

if err := structure(child, &cl, writer); err != nil {
if err := structure(child, &cl, writer, depth+1); err != nil {
return err
}

Expand Down
88 changes: 68 additions & 20 deletions imap/structure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"path/filepath"
"testing"

"github.com/ProtonMail/gluon/internal/unleash"
"github.com/ProtonMail/gluon/internal/unleash/featureflags"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -75,15 +77,15 @@ hey there bro
}

func TestParseInvalidCharsInContenType(t *testing.T) {
const literal = `From: Nathaniel Borenstein <nsb@bellcore.com>
To: Ned Freed <ned@innosoft.com>
Subject: Sample message
MIME-Version: 1.0
Content-type: multipart/mixed; boundary="simple 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.
const literal = `From: Nathaniel Borenstein <nsb@bellcore.com>
To: Ned Freed <ned@innosoft.com>
Subject: Sample message
MIME-Version: 1.0
Content-type: multipart/mixed; boundary="simple 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.
--simple boundary
Content-type: text/plain; charset=us-ascii

Expand All @@ -95,7 +97,7 @@ X-Pm-Content-Encryption: on-import

To: someone
Subject: Fwd: embedded
Content-type: multipart/mixed; boundary="embedded-boundary"
Content-type: multipart/mixed; boundary="embedded-boundary"

--embedded-boundary
Content-Type: GIF �ɮ�;
Expand All @@ -119,15 +121,15 @@ This is the epilogue. It is also to be ignored.
}

func TestParseInvalidMimeType(t *testing.T) {
const literal = `From: Nathaniel Borenstein <nsb@bellcore.com>
To: Ned Freed <ned@innosoft.com>
Subject: Sample message
MIME-Version: 1.0
Content-type: multipart/mixed; boundary="simple 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.
const literal = `From: Nathaniel Borenstein <nsb@bellcore.com>
To: Ned Freed <ned@innosoft.com>
Subject: Sample message
MIME-Version: 1.0
Content-type: multipart/mixed; boundary="simple 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.
--simple boundary
Content-type: text/plain; charset=us-ascii

Expand All @@ -139,7 +141,7 @@ X-Pm-Content-Encryption: on-import

To: someone
Subject: Fwd: embedded
Content-type: multipart/mixed; boundary="embedded-boundary"
Content-type: multipart/mixed; boundary="embedded-boundary"

--embedded-boundary
Content-Type: application/;
Expand Down Expand Up @@ -184,3 +186,49 @@ func FuzzNewParsedMessage(f *testing.F) {
_, _ = NewParsedMessage(inputData)
})
}

func TestMaxMIMEStructureDepthExceeded_KillSwitch_Disabled(t *testing.T) {
flags := map[string]bool{
featureflags.MaximumMIMEStructureDepthDisabled: false,
}
mockProvider := unleash.NewMockFeatureFlagValueProvider(flags)
unleash.Init(mockProvider)

eml, err := os.ReadFile(filepath.Join("testdata", "mime-structure-depth.eml"))
require.NoError(t, err)
_, err = NewParsedMessage(eml)
require.Error(t, err)
require.ErrorIs(t, err, errorMaximumMIMEStructureDepthExceeded)

t.Cleanup(func() {
unleash.Init(nil)
})
}

func TestMaxMIMEStructureDepthExceeded_KillSwitch_Enabled(t *testing.T) {
flags := map[string]bool{
featureflags.MaximumMIMEStructureDepthDisabled: true,
}
mockProvider := unleash.NewMockFeatureFlagValueProvider(flags)
unleash.Init(mockProvider)

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)
})
}

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)
})
}
Loading
Loading