diff --git a/go.sum b/go.sum index 8e1111c..7aa0a7f 100644 --- a/go.sum +++ b/go.sum @@ -51,7 +51,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e h1:ba7YsgX5OV8FjGi5ZWml8Jng6oBrJAb3ahqWMJ5Ce8Q= github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= @@ -104,9 +103,7 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ= github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -159,7 +156,6 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02n github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -211,7 +207,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -235,7 +230,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -293,7 +287,6 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/query/filter_emails_test.go b/internal/query/filter_emails_test.go new file mode 100644 index 0000000..79f01c7 --- /dev/null +++ b/internal/query/filter_emails_test.go @@ -0,0 +1,195 @@ +package query + +import ( + "net/mail" + "testing" + "time" + + "forge.cadoles.com/wpetit/fake-smtp/internal/model" + "github.com/pkg/errors" +) + +type emailMatcherTestCase struct { + Name string + Search *InboxSearch + Email *model.Email + Matcher emailMatcherFunc + Expect bool +} + +func TestEmailMatcher(t *testing.T) { + t.Parallel() + + johnDoeAddr, err := mail.ParseAddress("john.doe@test.local") + if err != nil { + t.Fatal(errors.WithStack(err)) + } + + adaLovelaceAddr, err := mail.ParseAddress("ada.lovelace@test.local") + if err != nil { + t.Fatal(errors.WithStack(err)) + } + + now := time.Now() + + testCases := []emailMatcherTestCase{ + { + Name: "Simple matching header", + Email: &model.Email{ + Headers: map[string][]string{ + "X-Swift-To": {johnDoeAddr.Address}, + }, + }, + Search: &InboxSearch{ + Headers: map[string]string{ + "X-Swift-To": johnDoeAddr.Address, + }, + }, + Matcher: matchHeaders, + Expect: true, + }, + { + Name: "Multiple matching header", + Email: &model.Email{ + Headers: map[string][]string{ + "X-Swift-To": {johnDoeAddr.Address}, + "Content-Type": {"multipart/alternative; boundary=\"_=_swift_1645181013_7b80ab8ab386ba4fcaff4f6f79593adb_=_\""}, + }, + }, + Search: &InboxSearch{ + Headers: map[string]string{ + "X-Swift-To": johnDoeAddr.Address, + "Content-Type": "multipart/alternative", + }, + }, + Matcher: matchHeaders, + Expect: true, + }, + { + Name: "Simple non matching header", + Email: &model.Email{ + Headers: map[string][]string{ + "X-Swift-To": {johnDoeAddr.Address}, + }, + }, + Search: &InboxSearch{ + Headers: map[string]string{ + "X-Swift-To": adaLovelaceAddr.Address, + }, + }, + Matcher: matchHeaders, + Expect: false, + }, + { + Name: "Multiple non matching headers", + Email: &model.Email{ + Headers: map[string][]string{ + "X-Swift-To": {johnDoeAddr.Address}, + "Content-Type": {"foo/bar"}, + }, + }, + Search: &InboxSearch{ + Headers: map[string]string{ + "X-Swift-To": johnDoeAddr.Address, + "Content-Type": "multipart/alternative", + }, + }, + Matcher: matchHeaders, + Expect: false, + }, + { + Name: "Simple to", + Email: &model.Email{ + To: []*mail.Address{johnDoeAddr}, + }, + Search: &InboxSearch{ + To: johnDoeAddr.Address, + }, + Matcher: matchTo, + Expect: true, + }, + { + Name: "Simple from", + Email: &model.Email{ + From: []*mail.Address{johnDoeAddr}, + }, + Search: &InboxSearch{ + From: johnDoeAddr.Address, + }, + Matcher: matchFrom, + Expect: true, + }, + { + Name: "Simple after", + Email: &model.Email{ + SentAt: now, + }, + Search: &InboxSearch{ + After: now.Add(-5 * time.Second), + }, + Matcher: matchAfter, + Expect: true, + }, + { + Name: "Simple before", + Email: &model.Email{ + SentAt: now, + }, + Search: &InboxSearch{ + Before: now.Add(5 * time.Second), + }, + Matcher: matchBefore, + Expect: true, + }, + { + Name: "Matching composite", + Email: &model.Email{ + SentAt: now, + Headers: map[string][]string{ + "X-Swift-To": {johnDoeAddr.Address}, + "Content-Type": {"multipart/alternative; boundary=\"_=_swift_1645181013_7b80ab8ab386ba4fcaff4f6f79593adb_=_\""}, + }, + }, + Search: &InboxSearch{ + Before: now.Add(5 * time.Second), + Headers: map[string]string{ + "X-Swift-To": johnDoeAddr.Address, + "Content-Type": "multipart/alternative", + }, + }, + Matcher: and(matchBefore, matchHeaders), + Expect: true, + }, + { + Name: "Non matching composite", + Email: &model.Email{ + SentAt: now, + Headers: map[string][]string{ + "X-Swift-To": {johnDoeAddr.Address}, + "Content-Type": {"multipart/alternative; boundary=\"_=_swift_1645181013_7b80ab8ab386ba4fcaff4f6f79593adb_=_\""}, + }, + }, + Search: &InboxSearch{ + Before: now.Add(5 * time.Second), + Headers: map[string]string{ + "X-Swift-To": adaLovelaceAddr.Address, + "Content-Type": "multipart/alternative", + }, + }, + Matcher: and(matchBefore, matchHeaders), + Expect: false, + }, + } + + for _, tc := range testCases { + func(tc emailMatcherTestCase) { + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + + if e, g := tc.Expect, tc.Matcher(tc.Email, tc.Search); e != g { + t.Errorf("'%s': expected '%v', got '%v'", tc.Name, e, g) + } + }) + }(tc) + } +} diff --git a/internal/query/get_inbox.go b/internal/query/get_inbox.go index 9bb508c..ef35fa5 100644 --- a/internal/query/get_inbox.go +++ b/internal/query/get_inbox.go @@ -105,87 +105,135 @@ func HandleGetInbox(ctx context.Context, qry cqrs.Query) (interface{}, error) { return &InboxData{emails}, nil } - filtered := make([]*model.Email, 0, len(emails)) - - for _, eml := range emails { - match := true - - if req.Search.To != "" { - found := false - - for _, addr := range eml.To { - if strings.Contains(addr.Name, req.Search.To) || strings.Contains(addr.Address, req.Search.To) { - found = true - - break - } - } - - if !found { - match = false - } - } - - if req.Search.From != "" { - found := false - - for _, addr := range eml.From { - if strings.Contains(addr.Name, req.Search.From) || strings.Contains(addr.Address, req.Search.From) { - found = true - - break - } - } - - if !found { - match = false - } - } - - if !req.Search.After.IsZero() && !eml.SentAt.After(req.Search.After) { - match = false - } - - if !req.Search.Before.IsZero() && !eml.SentAt.Before(req.Search.Before) { - match = false - } - - if req.Search.Headers != nil { - found := false - - for searchKey, searchValue := range req.Search.Headers { - for headerKey, headerValues := range eml.Headers { - if searchKey != headerKey { - continue - } - - for _, hv := range headerValues { - if strings.Contains(hv, searchValue) { - found = true - - break - } - } - - if found { - break - } - } - - if found { - break - } - } - - if !found { - match = false - } - } - - if match { - filtered = append(filtered, eml) - } - } + filtered := filterEmails(emails, req.Search) return &InboxData{filtered}, nil } + +var matchers = []emailMatcherFunc{ + matchTo, + matchFrom, + matchBefore, + matchAfter, + matchHeaders, +} + +type emailMatcherFunc func(*model.Email, *InboxSearch) bool + +func matchTo(eml *model.Email, search *InboxSearch) bool { + if search.To == "" { + return true + } + + found := false + + for _, addr := range eml.To { + if strings.Contains(addr.Name, search.To) || strings.Contains(addr.Address, search.To) { + found = true + + break + } + } + + return found +} + +func matchFrom(eml *model.Email, search *InboxSearch) bool { + if search.From == "" { + return true + } + + found := false + + for _, addr := range eml.From { + if strings.Contains(addr.Name, search.From) || strings.Contains(addr.Address, search.From) { + found = true + + break + } + } + + return found +} + +func matchAfter(eml *model.Email, search *InboxSearch) bool { + if search.After.IsZero() { + return true + } + + return eml.SentAt.After(search.After) +} + +func matchBefore(eml *model.Email, search *InboxSearch) bool { + if search.Before.IsZero() { + return true + } + + return eml.SentAt.Before(search.Before) +} + +func matchHeaders(eml *model.Email, search *InboxSearch) bool { + if eml.Headers == nil { + return true + } + + matches := true + + for searchKey, searchValue := range search.Headers { + for headerKey, headerValues := range eml.Headers { + if searchKey != headerKey { + continue + } + + matchesHeader := true + + for _, hv := range headerValues { + if !strings.Contains(hv, searchValue) { + matchesHeader = false + + break + } + } + + if !matchesHeader { + matches = false + + break + } + } + + if !matches { + break + } + } + + return matches +} + +func and(matchers ...emailMatcherFunc) emailMatcherFunc { + return func(eml *model.Email, search *InboxSearch) bool { + for _, match := range matchers { + if !match(eml, search) { + return false + } + } + + return true + } +} + +func filterEmails(emails []*model.Email, search *InboxSearch) []*model.Email { + filtered := make([]*model.Email, 0) + + match := and(matchers...) + + for _, eml := range emails { + if !match(eml, search) { + continue + } + + filtered = append(filtered, eml) + } + + return filtered +}