Repository: emersion/go-imap
Pull request #378 (2020-08-02 08:53:34)

backendutil: Improve Match function


Patch:
diff --git a/backend/backendutil/search.go b/backend/backendutil/search.go
index 31347ce..50327ac 100644
--- a/backend/backendutil/search.go
+++ b/backend/backendutil/search.go
@@ -10,6 +10,7 @@ import (
 	"github.com/emersion/go-imap"
 	"github.com/emersion/go-message"
 	"github.com/emersion/go-message/mail"
+	"github.com/emersion/go-message/textproto"
 )
 
 func matchString(s, substr string) bool {
@@ -41,16 +42,28 @@ type lengther interface {
 	Len() int
 }
 
+type countWriter struct {
+	N int
+}
+
+func (w *countWriter) Write(b []byte) (int, error) {
+	w.N += len(b)
+	return len(b), nil
+}
+
 func bodyLen(e *message.Entity) (int, error) {
+	headerSize := countWriter{}
+	textproto.WriteHeader(&headerSize, e.Header.Header)
+
 	if l, ok := e.Body.(lengther); ok {
-		return l.Len(), nil
+		return l.Len() + headerSize.N, nil
 	}
 
 	b, err := bufferBody(e)
 	if err != nil {
 		return 0, err
 	}
-	return b.Len(), nil
+	return b.Len() + headerSize.N, nil
 }
 
 // Match returns true if a message and its metadata matches the provided
@@ -66,12 +79,12 @@ func Match(e *message.Entity, seqNum, uid uint32, date time.Time, flags []string
 		if err != nil {
 			return false, err
 		}
-		t = t.Round(24 * time.Hour)
+		t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
 
 		if !c.SentBefore.IsZero() && !t.Before(c.SentBefore) {
 			return false, nil
 		}
-		if !c.SentSince.IsZero() && !t.After(c.SentSince) {
+		if !c.SentSince.IsZero() && t.Before(c.SentSince) {
 			return false, nil
 		}
 	}
@@ -104,8 +117,17 @@ func Match(e *message.Entity, seqNum, uid uint32, date time.Time, flags []string
 		}
 	}
 	for _, text := range c.Text {
-		// TODO: also match header fields
-		if ok, err := matchBody(e, text); err != nil || !ok {
+		headerMatch := false
+		for f := e.Header.Fields(); f.Next(); {
+			decoded, err := f.Text()
+			if err != nil {
+				continue
+			}
+			if strings.Contains(f.Key()+": "+decoded, text) {
+				headerMatch = true
+			}
+		}
+		if ok, err := matchBody(e, text); err != nil || !ok && !headerMatch {
 			return false, err
 		}
 	}
@@ -194,7 +216,10 @@ func matchSeqNumAndUid(seqNum uint32, uid uint32, c *imap.SearchCriteria) bool {
 }
 
 func matchDate(date time.Time, c *imap.SearchCriteria) bool {
-	date = date.Round(24 * time.Hour)
+	// We discard time zone information by setting it to UTC.
+	// RFC 3501 explicitly requires zone unaware date comparison.
+	date = time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, time.UTC)
+
 	if !c.Since.IsZero() && !date.After(c.Since) {
 		return false
 	}


Test patch:
diff --git a/backend/backendutil/search_test.go b/backend/backendutil/search_test.go
index f05ab9e..94001c5 100644
--- a/backend/backendutil/search_test.go
+++ b/backend/backendutil/search_test.go
@@ -378,9 +378,9 @@ func TestMatchIssue298Regression(t *testing.T) {
 		t.Fatal("Expected no error while reading entity, got:", err)
 	}
 
-	// Search for body size > 1 ("LARGER 1"), which should match messages #2 and #3
+	// Search for body size > 15 ("LARGER 15"), which should match messages #2 and #3
 	criteria := &imap.SearchCriteria{
-		Larger: 1,
+		Larger: 15,
 	}
 	ok1, err := Match(e1, 1, 101, time.Now(), nil, criteria)
 	if err != nil {
@@ -404,9 +404,9 @@ func TestMatchIssue298Regression(t *testing.T) {
 		t.Errorf("Expected message #3 to match search criteria")
 	}
 
-	// Search for body size < 3 ("SMALLER 3"), which should match messages #1 and #2
+	// Search for body size < 17 ("SMALLER 17"), which should match messages #1 and #2
 	criteria = &imap.SearchCriteria{
-		Smaller: 3,
+		Smaller: 17,
 	}
 	ok1, err = Match(e1, 1, 101, time.Now(), nil, criteria)
 	if err != nil {