Refactor inbox query filtering
This commit is contained in:
parent
f959fdb93f
commit
b335a825a3
7
go.sum
7
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=
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
filtered := filterEmails(emails, req.Search)
|
||||
|
||||
for _, eml := range emails {
|
||||
match := true
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
if strings.Contains(addr.Name, search.To) || strings.Contains(addr.Address, search.To) {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
match = false
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
func matchFrom(eml *model.Email, search *InboxSearch) bool {
|
||||
if search.From == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
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) {
|
||||
if strings.Contains(addr.Name, search.From) || strings.Contains(addr.Address, search.From) {
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
match = false
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
if !req.Search.After.IsZero() && !eml.SentAt.After(req.Search.After) {
|
||||
match = false
|
||||
func matchAfter(eml *model.Email, search *InboxSearch) bool {
|
||||
if search.After.IsZero() {
|
||||
return true
|
||||
}
|
||||
|
||||
if !req.Search.Before.IsZero() && !eml.SentAt.Before(req.Search.Before) {
|
||||
match = false
|
||||
return eml.SentAt.After(search.After)
|
||||
}
|
||||
|
||||
if req.Search.Headers != nil {
|
||||
found := false
|
||||
func matchBefore(eml *model.Email, search *InboxSearch) bool {
|
||||
if search.Before.IsZero() {
|
||||
return true
|
||||
}
|
||||
|
||||
for searchKey, searchValue := range req.Search.Headers {
|
||||
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) {
|
||||
found = true
|
||||
if !strings.Contains(hv, searchValue) {
|
||||
matchesHeader = false
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
if !matchesHeader {
|
||||
matches = false
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
if !matches {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
match = false
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
if match {
|
||||
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 &InboxData{filtered}, nil
|
||||
return filtered
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue