Refactor inbox query filtering

This commit is contained in:
2022-02-18 15:16:42 +01:00
parent f959fdb93f
commit b335a825a3
3 changed files with 324 additions and 88 deletions

View File

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

View File

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