1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
|
package lib
import (
"io"
"strings"
"unicode"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/log"
"git.sr.ht/~rjarry/aerc/lib/rfc822"
"git.sr.ht/~rjarry/aerc/models"
"git.sr.ht/~rjarry/aerc/worker/types"
"git.sr.ht/~rjarry/go-opt/v2"
)
func Search(messages []rfc822.RawMessage, criteria *types.SearchCriteria) ([]models.UID, error) {
criteria.PrepareHeader()
requiredParts := GetRequiredParts(criteria)
var matchedUids []models.UID
for _, m := range messages {
success, err := SearchMessage(m, criteria, requiredParts)
if err != nil {
return nil, err
} else if success {
matchedUids = append(matchedUids, m.UID())
}
}
return matchedUids, nil
}
// searchMessage executes the search criteria for the given RawMessage,
// returns true if search succeeded
func SearchMessage(message rfc822.RawMessage, criteria *types.SearchCriteria,
parts MsgParts,
) (bool, error) {
if criteria == nil {
return true, nil
}
// setup parts of the message to use in the search
// this is so that we try to minimise reading unnecessary parts
var (
flags models.Flags
info *models.MessageInfo
text string
err error
)
if parts&FLAGS > 0 {
flags, err = message.ModelFlags()
if err != nil {
return false, err
}
}
info, err = rfc822.MessageInfo(message)
if err != nil {
return false, err
}
switch {
case parts&BODY > 0:
path := lib.FindFirstNonMultipart(info.BodyStructure, nil)
reader, err := message.NewReader()
if err != nil {
return false, err
}
defer reader.Close()
msg, err := rfc822.ReadMessage(reader)
if err != nil {
return false, err
}
part, err := rfc822.FetchEntityPartReader(msg, path)
if err != nil {
return false, err
}
bytes, err := io.ReadAll(part)
if err != nil {
return false, err
}
text = string(bytes)
case parts&ALL > 0:
reader, err := message.NewReader()
if err != nil {
return false, err
}
defer reader.Close()
bytes, err := io.ReadAll(reader)
if err != nil {
return false, err
}
text = string(bytes)
default:
text = info.Envelope.Subject
}
// now search through the criteria
// implicit AND at the moment so fail fast
if criteria.Headers != nil {
for k, v := range criteria.Headers {
headerValue := info.RFC822Headers.Get(k)
for _, text := range v {
if !containsSmartCase(headerValue, text) {
return false, nil
}
}
}
}
args := opt.LexArgs(strings.Join(criteria.Terms, " "))
for _, searchTerm := range args.Args() {
if !containsSmartCase(text, searchTerm) {
return false, nil
}
}
if criteria.WithFlags != 0 {
if !flags.Has(criteria.WithFlags) {
return false, nil
}
}
if criteria.WithoutFlags != 0 {
if flags.Has(criteria.WithoutFlags) {
return false, nil
}
}
if parts&DATE > 0 {
if date, err := info.RFC822Headers.Date(); err != nil {
log.Errorf("Failed to get date from header: %v", err)
} else {
if !criteria.StartDate.IsZero() {
if date.Before(criteria.StartDate) {
return false, nil
}
}
if !criteria.EndDate.IsZero() {
if date.After(criteria.EndDate) {
return false, nil
}
}
}
}
return true, nil
}
// containsSmartCase is a smarter version of strings.Contains for searching.
// Is case-insensitive unless substr contains an upper case character
func containsSmartCase(s string, substr string) bool {
if hasUpper(substr) {
return strings.Contains(s, substr)
}
return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
}
func hasUpper(s string) bool {
for _, r := range s {
if unicode.IsUpper(r) {
return true
}
}
return false
}
// The parts of a message, kind of
type MsgParts int
const NONE MsgParts = 0
const (
FLAGS MsgParts = 1 << iota
HEADER
DATE
BODY
ALL
)
// Returns a bitmask of the parts of the message required to be loaded for the
// given criteria
func GetRequiredParts(criteria *types.SearchCriteria) MsgParts {
required := NONE
if criteria == nil {
return required
}
if len(criteria.Headers) > 0 {
required |= HEADER
}
if !criteria.StartDate.IsZero() || !criteria.EndDate.IsZero() {
required |= DATE
}
if criteria.SearchBody {
required |= BODY
}
if criteria.SearchAll {
required |= ALL
}
if criteria.WithFlags != 0 {
required |= FLAGS
}
if criteria.WithoutFlags != 0 {
required |= FLAGS
}
return required
}
|