Refactor inbox query filtering
This commit is contained in:
195
internal/query/filter_emails_test.go
Normal file
195
internal/query/filter_emails_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user