Fixing to have the proper version of go-imap from foxcpp.
This commit is contained in:
2
backend/backendutil/backendutil.go
Normal file
2
backend/backendutil/backendutil.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package backendutil provides utility functions to implement IMAP backends.
|
||||
package backendutil
|
||||
55
backend/backendutil/backendutil_test.go
Normal file
55
backend/backendutil/backendutil_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package backendutil
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var testDate, _ = time.Parse(time.RFC1123Z, "Sat, 18 Jun 2016 12:00:00 +0900")
|
||||
|
||||
const testHeaderString = "Content-Type: multipart/mixed; boundary=message-boundary\r\n" +
|
||||
"Date: Sat, 18 Jun 2016 12:00:00 +0900\r\n" +
|
||||
"From: Mitsuha Miyamizu <mitsuha.miyamizu@example.org>\r\n" +
|
||||
"Message-Id: 42@example.org\r\n" +
|
||||
"Subject: Your Name.\r\n" +
|
||||
"To: Taki Tachibana <taki.tachibana@example.org>\r\n" +
|
||||
"\r\n"
|
||||
|
||||
const testAltHeaderString = "Content-Type: multipart/alternative; boundary=b2\r\n" +
|
||||
"\r\n"
|
||||
|
||||
const testTextHeaderString = "Content-Disposition: inline\r\n" +
|
||||
"Content-Type: text/plain\r\n" +
|
||||
"\r\n"
|
||||
|
||||
const testTextBodyString = "What's your name?"
|
||||
|
||||
const testTextString = testTextHeaderString + testTextBodyString
|
||||
|
||||
const testHTMLHeaderString = "Content-Disposition: inline\r\n" +
|
||||
"Content-Type: text/html\r\n" +
|
||||
"\r\n"
|
||||
|
||||
const testHTMLBodyString = "<div>What's <i>your</i> name?</div>"
|
||||
|
||||
const testHTMLString = testHTMLHeaderString + testHTMLBodyString
|
||||
|
||||
const testAttachmentHeaderString = "Content-Disposition: attachment; filename=note.txt\r\n" +
|
||||
"Content-Type: text/plain\r\n" +
|
||||
"\r\n"
|
||||
|
||||
const testAttachmentBodyString = "My name is Mitsuha."
|
||||
|
||||
const testAttachmentString = testAttachmentHeaderString + testAttachmentBodyString
|
||||
|
||||
const testBodyString = "--message-boundary\r\n" +
|
||||
testAltHeaderString +
|
||||
"\r\n--b2\r\n" +
|
||||
testTextString +
|
||||
"\r\n--b2\r\n" +
|
||||
testHTMLString +
|
||||
"\r\n--b2--\r\n" +
|
||||
"\r\n--message-boundary\r\n" +
|
||||
testAttachmentString +
|
||||
"\r\n--message-boundary--\r\n"
|
||||
|
||||
const testMailString = testHeaderString + testBodyString
|
||||
75
backend/backendutil/body.go
Normal file
75
backend/backendutil/body.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package backendutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
var errNoSuchPart = errors.New("backendutil: no such message body part")
|
||||
|
||||
// FetchBodySection extracts a body section from a message.
|
||||
func FetchBodySection(e *message.Entity, section *imap.BodySectionName) (imap.Literal, error) {
|
||||
// First, find the requested part using the provided path
|
||||
for i := 0; i < len(section.Path); i++ {
|
||||
n := section.Path[i]
|
||||
|
||||
mr := e.MultipartReader()
|
||||
if mr == nil {
|
||||
return nil, errNoSuchPart
|
||||
}
|
||||
|
||||
for j := 1; j <= n; j++ {
|
||||
p, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
return nil, errNoSuchPart
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if j == n {
|
||||
e = p
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then, write the requested data to a buffer
|
||||
b := new(bytes.Buffer)
|
||||
|
||||
// Write the header
|
||||
mw, err := message.CreateWriter(b, e.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer mw.Close()
|
||||
|
||||
switch section.Specifier {
|
||||
case imap.TextSpecifier:
|
||||
// The header hasn't been requested. Discard it.
|
||||
b.Reset()
|
||||
case imap.EntireSpecifier:
|
||||
if len(section.Path) > 0 {
|
||||
// When selecting a specific part by index, IMAP servers
|
||||
// return only the text, not the associated MIME header.
|
||||
b.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Write the body, if requested
|
||||
switch section.Specifier {
|
||||
case imap.EntireSpecifier, imap.TextSpecifier:
|
||||
if _, err := io.Copy(mw, e.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var l imap.Literal = b
|
||||
if section.Partial != nil {
|
||||
l = bytes.NewReader(section.ExtractPartial(b.Bytes()))
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
109
backend/backendutil/body_test.go
Normal file
109
backend/backendutil/body_test.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package backendutil
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
var bodyTests = []struct {
|
||||
section string
|
||||
body string
|
||||
}{
|
||||
{
|
||||
section: "BODY[]",
|
||||
body: testMailString,
|
||||
},
|
||||
{
|
||||
section: "BODY[1.1]",
|
||||
body: testTextBodyString,
|
||||
},
|
||||
{
|
||||
section: "BODY[1.2]",
|
||||
body: testHTMLBodyString,
|
||||
},
|
||||
{
|
||||
section: "BODY[2]",
|
||||
body: testAttachmentBodyString,
|
||||
},
|
||||
{
|
||||
section: "BODY[HEADER]",
|
||||
body: testHeaderString,
|
||||
},
|
||||
{
|
||||
section: "BODY[1.1.HEADER]",
|
||||
body: testTextHeaderString,
|
||||
},
|
||||
{
|
||||
section: "BODY[2.HEADER]",
|
||||
body: testAttachmentHeaderString,
|
||||
},
|
||||
{
|
||||
section: "BODY[2.MIME]",
|
||||
body: testAttachmentHeaderString,
|
||||
},
|
||||
{
|
||||
section: "BODY[TEXT]",
|
||||
body: testBodyString,
|
||||
},
|
||||
{
|
||||
section: "BODY[1.1.TEXT]",
|
||||
body: testTextBodyString,
|
||||
},
|
||||
{
|
||||
section: "BODY[2.TEXT]",
|
||||
body: testAttachmentBodyString,
|
||||
},
|
||||
{
|
||||
section: "BODY[2.1]",
|
||||
body: "",
|
||||
},
|
||||
{
|
||||
section: "BODY[3]",
|
||||
body: "",
|
||||
},
|
||||
{
|
||||
section: "BODY[2.TEXT]<0.9>",
|
||||
body: testAttachmentBodyString[:9],
|
||||
},
|
||||
}
|
||||
|
||||
func TestFetchBodySection(t *testing.T) {
|
||||
for _, test := range bodyTests {
|
||||
test := test
|
||||
t.Run(test.section, func(t *testing.T) {
|
||||
e, err := message.Read(strings.NewReader(testMailString))
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while reading mail, got:", err)
|
||||
}
|
||||
|
||||
section, err := imap.ParseBodySectionName(imap.FetchItem(test.section))
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while parsing body section name, got:", err)
|
||||
}
|
||||
|
||||
r, err := FetchBodySection(e, section)
|
||||
if test.body == "" {
|
||||
if err == nil {
|
||||
t.Error("Expected an error while extracting non-existing body section")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while extracting body section, got:", err)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while reading body section, got:", err)
|
||||
}
|
||||
|
||||
if s := string(b); s != test.body {
|
||||
t.Errorf("Expected body section %q to be \n%s\n but got \n%s", test.section, test.body, s)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
60
backend/backendutil/bodystructure.go
Normal file
60
backend/backendutil/bodystructure.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package backendutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
// FetchBodyStructure computes a message's body structure from its content.
|
||||
func FetchBodyStructure(e *message.Entity, extended bool) (*imap.BodyStructure, error) {
|
||||
bs := new(imap.BodyStructure)
|
||||
|
||||
mediaType, mediaParams, _ := e.Header.ContentType()
|
||||
typeParts := strings.SplitN(mediaType, "/", 2)
|
||||
bs.MIMEType = typeParts[0]
|
||||
if len(typeParts) == 2 {
|
||||
bs.MIMESubType = typeParts[1]
|
||||
}
|
||||
bs.Params = mediaParams
|
||||
|
||||
bs.Id = e.Header.Get("Content-Id")
|
||||
bs.Description = e.Header.Get("Content-Description")
|
||||
bs.Encoding = e.Header.Get("Content-Encoding")
|
||||
// TODO: bs.Size
|
||||
|
||||
if mr := e.MultipartReader(); mr != nil {
|
||||
var parts []*imap.BodyStructure
|
||||
for {
|
||||
p, err := mr.NextPart()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pbs, err := FetchBodyStructure(p, extended)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parts = append(parts, pbs)
|
||||
}
|
||||
bs.Parts = parts
|
||||
}
|
||||
|
||||
// TODO: bs.Envelope, bs.BodyStructure
|
||||
// TODO: bs.Lines
|
||||
|
||||
if extended {
|
||||
bs.Extended = true
|
||||
|
||||
bs.Disposition, bs.DispositionParams, _ = e.Header.ContentDisposition()
|
||||
|
||||
// TODO: bs.Language, bs.Location
|
||||
// TODO: bs.MD5
|
||||
}
|
||||
|
||||
return bs, nil
|
||||
}
|
||||
67
backend/backendutil/bodystructure_test.go
Normal file
67
backend/backendutil/bodystructure_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package backendutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
var testBodyStructure = &imap.BodyStructure{
|
||||
MIMEType: "multipart",
|
||||
MIMESubType: "mixed",
|
||||
Params: map[string]string{"boundary": "message-boundary"},
|
||||
Parts: []*imap.BodyStructure{
|
||||
{
|
||||
MIMEType: "multipart",
|
||||
MIMESubType: "alternative",
|
||||
Params: map[string]string{"boundary": "b2"},
|
||||
Extended: true,
|
||||
Parts: []*imap.BodyStructure{
|
||||
{
|
||||
MIMEType: "text",
|
||||
MIMESubType: "plain",
|
||||
Params: map[string]string{},
|
||||
Extended: true,
|
||||
Disposition: "inline",
|
||||
DispositionParams: map[string]string{},
|
||||
},
|
||||
{
|
||||
MIMEType: "text",
|
||||
MIMESubType: "html",
|
||||
Params: map[string]string{},
|
||||
Extended: true,
|
||||
Disposition: "inline",
|
||||
DispositionParams: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
MIMEType: "text",
|
||||
MIMESubType: "plain",
|
||||
Params: map[string]string{},
|
||||
Extended: true,
|
||||
Disposition: "attachment",
|
||||
DispositionParams: map[string]string{"filename": "note.txt"},
|
||||
},
|
||||
},
|
||||
Extended: true,
|
||||
}
|
||||
|
||||
func TestFetchBodyStructure(t *testing.T) {
|
||||
e, err := message.Read(strings.NewReader(testMailString))
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while reading mail, got:", err)
|
||||
}
|
||||
|
||||
bs, err := FetchBodyStructure(e, true)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while fetching body structure, got:", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(testBodyStructure, bs) {
|
||||
t.Errorf("Expected body structure \n%+v\n but got \n%+v", testBodyStructure, bs)
|
||||
}
|
||||
}
|
||||
50
backend/backendutil/envelope.go
Normal file
50
backend/backendutil/envelope.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package backendutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-message"
|
||||
"github.com/emersion/go-message/mail"
|
||||
)
|
||||
|
||||
func headerAddressList(h mail.Header, key string) ([]*imap.Address, error) {
|
||||
addrs, err := h.AddressList(key)
|
||||
|
||||
list := make([]*imap.Address, len(addrs))
|
||||
for i, a := range addrs {
|
||||
parts := strings.SplitN(a.Address, "@", 2)
|
||||
mailbox := parts[0]
|
||||
var hostname string
|
||||
if len(parts) == 2 {
|
||||
hostname = parts[1]
|
||||
}
|
||||
|
||||
list[i] = &imap.Address{
|
||||
PersonalName: a.Name,
|
||||
MailboxName: mailbox,
|
||||
HostName: hostname,
|
||||
}
|
||||
}
|
||||
|
||||
return list, err
|
||||
}
|
||||
|
||||
// FetchEnvelope returns a message's envelope from its header.
|
||||
func FetchEnvelope(h message.Header) (*imap.Envelope, error) {
|
||||
mh := mail.Header{h}
|
||||
|
||||
env := new(imap.Envelope)
|
||||
env.Date, _ = mh.Date()
|
||||
env.Subject, _ = mh.Subject()
|
||||
env.From, _ = headerAddressList(mh, "From")
|
||||
env.Sender, _ = headerAddressList(mh, "Sender")
|
||||
env.ReplyTo, _ = headerAddressList(mh, "Reply-To")
|
||||
env.To, _ = headerAddressList(mh, "To")
|
||||
env.Cc, _ = headerAddressList(mh, "Cc")
|
||||
env.Bcc, _ = headerAddressList(mh, "Bcc")
|
||||
env.InReplyTo = mh.Get("In-Reply-To")
|
||||
env.MessageId = mh.Get("Message-Id")
|
||||
|
||||
return env, nil
|
||||
}
|
||||
39
backend/backendutil/envelope_test.go
Normal file
39
backend/backendutil/envelope_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package backendutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
var testEnvelope = &imap.Envelope{
|
||||
Date: testDate,
|
||||
Subject: "Your Name.",
|
||||
From: []*imap.Address{{PersonalName: "Mitsuha Miyamizu", MailboxName: "mitsuha.miyamizu", HostName: "example.org"}},
|
||||
Sender: []*imap.Address{},
|
||||
ReplyTo: []*imap.Address{},
|
||||
To: []*imap.Address{{PersonalName: "Taki Tachibana", MailboxName: "taki.tachibana", HostName: "example.org"}},
|
||||
Cc: []*imap.Address{},
|
||||
Bcc: []*imap.Address{},
|
||||
InReplyTo: "",
|
||||
MessageId: "42@example.org",
|
||||
}
|
||||
|
||||
func TestFetchEnvelope(t *testing.T) {
|
||||
e, err := message.Read(strings.NewReader(testMailString))
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while reading mail, got:", err)
|
||||
}
|
||||
|
||||
env, err := FetchEnvelope(e.Header)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while fetching envelope, got:", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(env, testEnvelope) {
|
||||
t.Errorf("Expected envelope \n%+v\n but got \n%+v", testEnvelope, env)
|
||||
}
|
||||
}
|
||||
40
backend/backendutil/flags.go
Normal file
40
backend/backendutil/flags.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package backendutil
|
||||
|
||||
import (
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
// UpdateFlags executes a flag operation on the flag set current.
|
||||
func UpdateFlags(current []string, op imap.FlagsOp, flags []string) []string {
|
||||
switch op {
|
||||
case imap.SetFlags:
|
||||
// TODO: keep \Recent if it is present
|
||||
return flags
|
||||
case imap.AddFlags:
|
||||
// Check for duplicates
|
||||
for _, flag := range current {
|
||||
for i, addFlag := range flags {
|
||||
if addFlag == flag {
|
||||
flags = append(flags[:i], flags[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return append(current, flags...)
|
||||
case imap.RemoveFlags:
|
||||
// Iterate through flags from the last one to the first one, to be able to
|
||||
// delete some of them.
|
||||
for i := len(current) - 1; i >= 0; i-- {
|
||||
flag := current[i]
|
||||
|
||||
for _, removeFlag := range flags {
|
||||
if removeFlag == flag {
|
||||
current = append(current[:i], current[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
return current
|
||||
}
|
||||
46
backend/backendutil/flags_test.go
Normal file
46
backend/backendutil/flags_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package backendutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
var updateFlagsTests = []struct {
|
||||
op imap.FlagsOp
|
||||
flags []string
|
||||
res []string
|
||||
}{
|
||||
{
|
||||
op: imap.AddFlags,
|
||||
flags: []string{"d", "e"},
|
||||
res: []string{"a", "b", "c", "d", "e"},
|
||||
},
|
||||
{
|
||||
op: imap.AddFlags,
|
||||
flags: []string{"a", "d", "b"},
|
||||
res: []string{"a", "b", "c", "d"},
|
||||
},
|
||||
{
|
||||
op: imap.RemoveFlags,
|
||||
flags: []string{"b", "v", "e", "a"},
|
||||
res: []string{"c"},
|
||||
},
|
||||
{
|
||||
op: imap.SetFlags,
|
||||
flags: []string{"a", "d", "e"},
|
||||
res: []string{"a", "d", "e"},
|
||||
},
|
||||
}
|
||||
|
||||
func TestUpdateFlags(t *testing.T) {
|
||||
current := []string{"a", "b", "c"}
|
||||
for _, test := range updateFlagsTests {
|
||||
got := UpdateFlags(current[:], test.op, test.flags)
|
||||
|
||||
if !reflect.DeepEqual(got, test.res) {
|
||||
t.Errorf("Expected result to be \n%v\n but got \n%v", test.res, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
225
backend/backendutil/search.go
Normal file
225
backend/backendutil/search.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package backendutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-message"
|
||||
"github.com/emersion/go-message/mail"
|
||||
)
|
||||
|
||||
func matchString(s, substr string) bool {
|
||||
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
|
||||
}
|
||||
|
||||
func bufferBody(e *message.Entity) (*bytes.Buffer, error) {
|
||||
b := new(bytes.Buffer)
|
||||
if _, err := io.Copy(b, e.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.Body = b
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func matchBody(e *message.Entity, substr string) (bool, error) {
|
||||
if s, ok := e.Body.(fmt.Stringer); ok {
|
||||
return matchString(s.String(), substr), nil
|
||||
}
|
||||
|
||||
b, err := bufferBody(e)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return matchString(b.String(), substr), nil
|
||||
}
|
||||
|
||||
type lengther interface {
|
||||
Len() int
|
||||
}
|
||||
|
||||
func bodyLen(e *message.Entity) (int, error) {
|
||||
if l, ok := e.Body.(lengther); ok {
|
||||
return l.Len(), nil
|
||||
}
|
||||
|
||||
b, err := bufferBody(e)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return b.Len(), nil
|
||||
}
|
||||
|
||||
// Match returns true if a message matches the provided criteria. Sequence
|
||||
// number, UID, flag and internal date contrainsts are not checked.
|
||||
func Match(e *message.Entity, c *imap.SearchCriteria) (bool, error) {
|
||||
// TODO: support encoded header fields for Bcc, Cc, From, To
|
||||
// TODO: add header size for Larger and Smaller
|
||||
|
||||
h := mail.Header{e.Header}
|
||||
|
||||
if !c.SentBefore.IsZero() || !c.SentSince.IsZero() {
|
||||
t, err := h.Date()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
t = t.Round(24 * time.Hour)
|
||||
|
||||
if !c.SentBefore.IsZero() && !t.Before(c.SentBefore) {
|
||||
return false, nil
|
||||
}
|
||||
if !c.SentSince.IsZero() && !t.After(c.SentSince) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
for key, wantValues := range c.Header {
|
||||
values, ok := e.Header[key]
|
||||
for _, wantValue := range wantValues {
|
||||
if wantValue == "" && !ok {
|
||||
return false, nil
|
||||
}
|
||||
if wantValue != "" {
|
||||
ok := false
|
||||
for _, v := range values {
|
||||
if matchString(v, wantValue) {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, body := range c.Body {
|
||||
if ok, err := matchBody(e, body); err != nil || !ok {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
for _, text := range c.Text {
|
||||
// TODO: also match header fields
|
||||
if ok, err := matchBody(e, text); err != nil || !ok {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if c.Larger > 0 || c.Smaller > 0 {
|
||||
n, err := bodyLen(e)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if c.Larger > 0 && uint32(n) < c.Larger {
|
||||
return false, nil
|
||||
}
|
||||
if c.Smaller > 0 && uint32(n) > c.Smaller {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, not := range c.Not {
|
||||
ok, err := Match(e, not)
|
||||
if err != nil || ok {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
for _, or := range c.Or {
|
||||
ok1, err := Match(e, or[0])
|
||||
if err != nil {
|
||||
return ok1, err
|
||||
}
|
||||
|
||||
ok2, err := Match(e, or[1])
|
||||
if err != nil || (!ok1 && !ok2) {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func matchFlags(flags map[string]bool, c *imap.SearchCriteria) bool {
|
||||
for _, f := range c.WithFlags {
|
||||
if !flags[f] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, f := range c.WithoutFlags {
|
||||
if flags[f] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, not := range c.Not {
|
||||
if matchFlags(flags, not) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, or := range c.Or {
|
||||
if !matchFlags(flags, or[0]) && !matchFlags(flags, or[1]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MatchFlags returns true if a flag list matches the provided criteria.
|
||||
func MatchFlags(flags []string, c *imap.SearchCriteria) bool {
|
||||
flagsMap := make(map[string]bool)
|
||||
for _, f := range flags {
|
||||
flagsMap[f] = true
|
||||
}
|
||||
|
||||
return matchFlags(flagsMap, c)
|
||||
}
|
||||
|
||||
// MatchSeqNumAndUid returns true if a sequence number and a UID matches the
|
||||
// provided criteria.
|
||||
func MatchSeqNumAndUid(seqNum uint32, uid uint32, c *imap.SearchCriteria) bool {
|
||||
if c.SeqNum != nil && !c.SeqNum.Contains(seqNum) {
|
||||
return false
|
||||
}
|
||||
if c.Uid != nil && !c.Uid.Contains(uid) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, not := range c.Not {
|
||||
if MatchSeqNumAndUid(seqNum, uid, not) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, or := range c.Or {
|
||||
if !MatchSeqNumAndUid(seqNum, uid, or[0]) && !MatchSeqNumAndUid(seqNum, uid, or[1]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MatchDate returns true if a date matches the provided criteria.
|
||||
func MatchDate(date time.Time, c *imap.SearchCriteria) bool {
|
||||
date = date.Round(24 * time.Hour)
|
||||
if !c.Since.IsZero() && !date.After(c.Since) {
|
||||
return false
|
||||
}
|
||||
if !c.Before.IsZero() && !date.Before(c.Before) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, not := range c.Not {
|
||||
if MatchDate(date, not) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, or := range c.Or {
|
||||
if !MatchDate(date, or[0]) && !MatchDate(date, or[1]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
264
backend/backendutil/search_test.go
Normal file
264
backend/backendutil/search_test.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package backendutil
|
||||
|
||||
import (
|
||||
"net/textproto"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-message"
|
||||
)
|
||||
|
||||
var matchTests = []struct {
|
||||
criteria *imap.SearchCriteria
|
||||
res bool
|
||||
}{
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Header: textproto.MIMEHeader{"From": {"Mitsuha"}},
|
||||
},
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Header: textproto.MIMEHeader{"To": {"Mitsuha"}},
|
||||
},
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{SentBefore: testDate.Add(48 * time.Hour)},
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Not: []*imap.SearchCriteria{{SentSince: testDate.Add(48 * time.Hour)}},
|
||||
},
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Not: []*imap.SearchCriteria{{Body: []string{"name"}}},
|
||||
},
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Text: []string{"name"},
|
||||
},
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Or: [][2]*imap.SearchCriteria{{
|
||||
{Text: []string{"i'm not in the text"}},
|
||||
{Body: []string{"i'm not in the body"}},
|
||||
}},
|
||||
},
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Header: textproto.MIMEHeader{"Message-Id": {"42@example.org"}},
|
||||
},
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Header: textproto.MIMEHeader{"Message-Id": {"43@example.org"}},
|
||||
},
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Header: textproto.MIMEHeader{"Message-Id": {""}},
|
||||
},
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Header: textproto.MIMEHeader{"Reply-To": {""}},
|
||||
},
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Larger: 10,
|
||||
},
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Smaller: 10,
|
||||
},
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Header: textproto.MIMEHeader{"Subject": {"your"}},
|
||||
},
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
criteria: &imap.SearchCriteria{
|
||||
Header: textproto.MIMEHeader{"Subject": {"Taki"}},
|
||||
},
|
||||
res: false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
for i, test := range matchTests {
|
||||
e, err := message.Read(strings.NewReader(testMailString))
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while reading entity, got:", err)
|
||||
}
|
||||
|
||||
ok, err := Match(e, test.criteria)
|
||||
if err != nil {
|
||||
t.Fatal("Expected no error while matching entity, got:", err)
|
||||
}
|
||||
|
||||
if test.res && !ok {
|
||||
t.Errorf("Expected #%v to match search criteria", i+1)
|
||||
}
|
||||
if !test.res && ok {
|
||||
t.Errorf("Expected #%v not to match search criteria", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var flagsTests = []struct {
|
||||
flags []string
|
||||
criteria *imap.SearchCriteria
|
||||
res bool
|
||||
}{
|
||||
{
|
||||
flags: []string{imap.SeenFlag},
|
||||
criteria: &imap.SearchCriteria{
|
||||
WithFlags: []string{imap.SeenFlag},
|
||||
WithoutFlags: []string{imap.FlaggedFlag},
|
||||
},
|
||||
res: true,
|
||||
},
|
||||
{
|
||||
flags: []string{imap.SeenFlag},
|
||||
criteria: &imap.SearchCriteria{
|
||||
WithFlags: []string{imap.DraftFlag},
|
||||
WithoutFlags: []string{imap.FlaggedFlag},
|
||||
},
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
flags: []string{imap.SeenFlag, imap.FlaggedFlag},
|
||||
criteria: &imap.SearchCriteria{
|
||||
WithFlags: []string{imap.SeenFlag},
|
||||
WithoutFlags: []string{imap.FlaggedFlag},
|
||||
},
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
flags: []string{imap.SeenFlag, imap.FlaggedFlag},
|
||||
criteria: &imap.SearchCriteria{
|
||||
Or: [][2]*imap.SearchCriteria{{
|
||||
{WithFlags: []string{imap.DraftFlag}},
|
||||
{WithoutFlags: []string{imap.SeenFlag}},
|
||||
}},
|
||||
},
|
||||
res: false,
|
||||
},
|
||||
{
|
||||
flags: []string{imap.SeenFlag, imap.FlaggedFlag},
|
||||
criteria: &imap.SearchCriteria{
|
||||
Not: []*imap.SearchCriteria{
|
||||
{WithFlags: []string{imap.SeenFlag}},
|
||||
},
|
||||
},
|
||||
res: false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestMatchFlags(t *testing.T) {
|
||||
for i, test := range flagsTests {
|
||||
ok := MatchFlags(test.flags, test.criteria)
|
||||
if test.res && !ok {
|
||||
t.Errorf("Expected #%v to match search criteria", i+1)
|
||||
}
|
||||
if !test.res && ok {
|
||||
t.Errorf("Expected #%v not to match search criteria", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchSeqNumAndUid(t *testing.T) {
|
||||
seqNum := uint32(42)
|
||||
uid := uint32(69)
|
||||
|
||||
c := &imap.SearchCriteria{
|
||||
Or: [][2]*imap.SearchCriteria{{
|
||||
{
|
||||
Uid: new(imap.SeqSet),
|
||||
Not: []*imap.SearchCriteria{{SeqNum: new(imap.SeqSet)}},
|
||||
},
|
||||
{
|
||||
SeqNum: new(imap.SeqSet),
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
if MatchSeqNumAndUid(seqNum, uid, c) {
|
||||
t.Error("Expected not to match criteria")
|
||||
}
|
||||
|
||||
c.Or[0][0].Uid.AddNum(uid)
|
||||
if !MatchSeqNumAndUid(seqNum, uid, c) {
|
||||
t.Error("Expected to match criteria")
|
||||
}
|
||||
|
||||
c.Or[0][0].Not[0].SeqNum.AddNum(seqNum)
|
||||
if MatchSeqNumAndUid(seqNum, uid, c) {
|
||||
t.Error("Expected not to match criteria")
|
||||
}
|
||||
|
||||
c.Or[0][1].SeqNum.AddNum(seqNum)
|
||||
if !MatchSeqNumAndUid(seqNum, uid, c) {
|
||||
t.Error("Expected to match criteria")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchDate(t *testing.T) {
|
||||
date := time.Unix(1483997966, 0)
|
||||
|
||||
c := &imap.SearchCriteria{
|
||||
Or: [][2]*imap.SearchCriteria{{
|
||||
{
|
||||
Since: date.Add(48 * time.Hour),
|
||||
Not: []*imap.SearchCriteria{{
|
||||
Since: date.Add(48 * time.Hour),
|
||||
}},
|
||||
},
|
||||
{
|
||||
Before: date.Add(-48 * time.Hour),
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
if MatchDate(date, c) {
|
||||
t.Error("Expected not to match criteria")
|
||||
}
|
||||
|
||||
c.Or[0][0].Since = date.Add(-48 * time.Hour)
|
||||
if !MatchDate(date, c) {
|
||||
t.Error("Expected to match criteria")
|
||||
}
|
||||
|
||||
c.Or[0][0].Not[0].Since = date.Add(-48 * time.Hour)
|
||||
if MatchDate(date, c) {
|
||||
t.Error("Expected not to match criteria")
|
||||
}
|
||||
|
||||
c.Or[0][1].Before = date.Add(48 * time.Hour)
|
||||
if !MatchDate(date, c) {
|
||||
t.Error("Expected to match criteria")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user