init: pristine aerc 0.20.0 source
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/go-ini/ini"
|
||||
)
|
||||
|
||||
func ParseFolderMap(r io.Reader) (map[string]string, []string, error) {
|
||||
cfg, err := ini.Load(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sec, err := cfg.GetSection("")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
order := sec.KeyStrings()
|
||||
|
||||
for _, k := range order {
|
||||
v, err := sec.GetKey(k)
|
||||
switch {
|
||||
case v.String() == "":
|
||||
return nil, nil, fmt.Errorf("no value for key '%s'", k)
|
||||
case err != nil:
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return sec.KeysHash(), order, nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package lib_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/worker/lib"
|
||||
)
|
||||
|
||||
func TestFolderMap(t *testing.T) {
|
||||
text := `#this is comment
|
||||
|
||||
Sent = [Gmail]/Sent
|
||||
|
||||
# a comment between entries
|
||||
Spam=[Gmail]/Spam # this is comment after the values
|
||||
`
|
||||
fmap, order, err := lib.ParseFolderMap(strings.NewReader(text))
|
||||
if err != nil {
|
||||
t.Errorf("parsing failed: %v", err)
|
||||
}
|
||||
|
||||
want_map := map[string]string{
|
||||
"Sent": "[Gmail]/Sent",
|
||||
"Spam": "[Gmail]/Spam",
|
||||
}
|
||||
want_order := []string{"Sent", "Spam"}
|
||||
|
||||
if !reflect.DeepEqual(order, want_order) {
|
||||
t.Errorf("order is not correct; want: %v, got: %v",
|
||||
want_order, order)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(fmap, want_map) {
|
||||
t.Errorf("map is not correct; want: %v, got: %v",
|
||||
want_map, fmap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderMap_ExpectFails(t *testing.T) {
|
||||
tests := []string{
|
||||
`key = `,
|
||||
` = value`,
|
||||
` = `,
|
||||
`key = #value`,
|
||||
}
|
||||
for _, text := range tests {
|
||||
_, _, err := lib.ParseFolderMap(strings.NewReader(text))
|
||||
if err == nil {
|
||||
t.Errorf("expected to fail, but it did not: %v", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/emersion/go-message/mail"
|
||||
)
|
||||
|
||||
// LimitHeaders returns a new Header with the specified headers included or
|
||||
// excluded
|
||||
func LimitHeaders(hdr *mail.Header, fields []string, exclude bool) *mail.Header {
|
||||
fieldMap := make(map[string]struct{}, len(fields))
|
||||
for _, f := range fields {
|
||||
fieldMap[strings.ToLower(f)] = struct{}{}
|
||||
}
|
||||
nh := &mail.Header{}
|
||||
curFields := hdr.Fields()
|
||||
for curFields.Next() {
|
||||
key := strings.ToLower(curFields.Key())
|
||||
_, present := fieldMap[key]
|
||||
// XOR exclude and present. When they are equal, it means we
|
||||
// should not add the header to the new header struct
|
||||
if exclude == present {
|
||||
continue
|
||||
}
|
||||
nh.Add(key, curFields.Value())
|
||||
}
|
||||
return nh
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
"github.com/emersion/go-maildir"
|
||||
)
|
||||
|
||||
type MaildirStore struct {
|
||||
root string
|
||||
maildirpp bool // whether to use Maildir++ directory layout
|
||||
}
|
||||
|
||||
func NewMaildirStore(root string, maildirpp bool) (*MaildirStore, error) {
|
||||
f, err := os.Open(root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
s, err := f.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !s.IsDir() {
|
||||
return nil, fmt.Errorf("Given maildir '%s' not a directory", root)
|
||||
}
|
||||
return &MaildirStore{
|
||||
root: root, maildirpp: maildirpp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MaildirStore) FolderMap() (map[string]maildir.Dir, error) {
|
||||
folders := make(map[string]maildir.Dir)
|
||||
if s.maildirpp {
|
||||
// In Maildir++ layout, INBOX is the root folder
|
||||
folders["INBOX"] = maildir.Dir(s.root)
|
||||
}
|
||||
err := filepath.Walk(s.root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid path '%s': error: %w", path, err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip maildir's default directories
|
||||
n := info.Name()
|
||||
if n == "new" || n == "tmp" || n == "cur" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
// Get the relative path from the parent directory
|
||||
dirPath, err := filepath.Rel(s.root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip the parent directory
|
||||
if dirPath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Drop dirs that lack {new,tmp,cur} subdirs
|
||||
for _, sub := range []string{"new", "tmp", "cur"} {
|
||||
if _, err := os.Stat(filepath.Join(path, sub)); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if s.maildirpp {
|
||||
// In Maildir++ layout, mailboxes are stored in a single directory
|
||||
// and prefixed with a dot, and subfolders are separated by dots.
|
||||
if !strings.HasPrefix(dirPath, ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
dirPath = strings.TrimPrefix(dirPath, ".")
|
||||
dirPath = strings.ReplaceAll(dirPath, ".", "/")
|
||||
folders[dirPath] = maildir.Dir(path)
|
||||
|
||||
// Since all mailboxes are stored in a single directory, don't
|
||||
// recurse into subdirectories
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
folders[dirPath] = maildir.Dir(path)
|
||||
return nil
|
||||
})
|
||||
return folders, err
|
||||
}
|
||||
|
||||
// Folder returns a maildir.Dir with the specified name inside the Store
|
||||
func (s *MaildirStore) Dir(name string) maildir.Dir {
|
||||
if s.maildirpp {
|
||||
// Use Maildir++ layout
|
||||
if name == "INBOX" {
|
||||
return maildir.Dir(s.root)
|
||||
}
|
||||
return maildir.Dir(filepath.Join(s.root, "."+strings.ReplaceAll(name, "/", ".")))
|
||||
}
|
||||
return maildir.Dir(filepath.Join(s.root, name))
|
||||
}
|
||||
|
||||
// uidReg matches filename encoded UIDs in maildirs synched with mbsync or
|
||||
// OfflineIMAP
|
||||
var uidReg = regexp.MustCompile(`,U=\d+`)
|
||||
|
||||
func StripUIDFromMessageFilename(basename string) string {
|
||||
return uidReg.ReplaceAllString(basename, "")
|
||||
}
|
||||
|
||||
var MaildirToFlag = map[maildir.Flag]models.Flags{
|
||||
maildir.FlagReplied: models.AnsweredFlag,
|
||||
maildir.FlagSeen: models.SeenFlag,
|
||||
maildir.FlagTrashed: models.DeletedFlag,
|
||||
maildir.FlagFlagged: models.FlaggedFlag,
|
||||
maildir.FlagDraft: models.DraftFlag,
|
||||
maildir.FlagPassed: models.ForwardedFlag,
|
||||
}
|
||||
|
||||
var FlagToMaildir = map[models.Flags]maildir.Flag{
|
||||
models.AnsweredFlag: maildir.FlagReplied,
|
||||
models.SeenFlag: maildir.FlagSeen,
|
||||
models.DeletedFlag: maildir.FlagTrashed,
|
||||
models.FlaggedFlag: maildir.FlagFlagged,
|
||||
models.DraftFlag: maildir.FlagDraft,
|
||||
models.ForwardedFlag: maildir.FlagPassed,
|
||||
}
|
||||
|
||||
func FromMaildirFlags(maildirFlags []maildir.Flag) models.Flags {
|
||||
var flags models.Flags
|
||||
for _, maildirFlag := range maildirFlags {
|
||||
if flag, ok := MaildirToFlag[maildirFlag]; ok {
|
||||
flags |= flag
|
||||
}
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
func ToMaildirFlags(flags models.Flags) []maildir.Flag {
|
||||
var maildirFlags []maildir.Flag
|
||||
for flag, maildirFlag := range FlagToMaildir {
|
||||
if flags.Has(flag) {
|
||||
maildirFlags = append(maildirFlags, maildirFlag)
|
||||
}
|
||||
}
|
||||
return maildirFlags
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// FileSize returns the size of the file specified by name
|
||||
func FileSize(name string) (uint32, error) {
|
||||
fileInfo, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to obtain fileinfo: %w", err)
|
||||
}
|
||||
return uint32(fileInfo.Size()), nil
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||
"github.com/emersion/go-message/mail"
|
||||
)
|
||||
|
||||
func Sort(messageInfos []*models.MessageInfo,
|
||||
criteria []*types.SortCriterion,
|
||||
) ([]models.UID, error) {
|
||||
// loop through in reverse to ensure we sort by non-primary fields first
|
||||
for i := len(criteria) - 1; i >= 0; i-- {
|
||||
criterion := criteria[i]
|
||||
switch criterion.Field {
|
||||
case types.SortArrival:
|
||||
sortSlice(criterion, messageInfos, func(i, j int) bool {
|
||||
return messageInfos[i].InternalDate.Before(messageInfos[j].InternalDate)
|
||||
})
|
||||
case types.SortCc:
|
||||
sortAddresses(messageInfos, criterion,
|
||||
func(msgInfo *models.MessageInfo) []*mail.Address {
|
||||
return msgInfo.Envelope.Cc
|
||||
})
|
||||
case types.SortDate:
|
||||
sortSlice(criterion, messageInfos, func(i, j int) bool {
|
||||
return messageInfos[i].Envelope.Date.Before(messageInfos[j].Envelope.Date)
|
||||
})
|
||||
case types.SortFrom:
|
||||
sortAddresses(messageInfos, criterion,
|
||||
func(msgInfo *models.MessageInfo) []*mail.Address {
|
||||
return msgInfo.Envelope.From
|
||||
})
|
||||
case types.SortRead:
|
||||
sortFlags(messageInfos, criterion, models.SeenFlag)
|
||||
case types.SortFlagged:
|
||||
sortFlags(messageInfos, criterion, models.FlaggedFlag)
|
||||
case types.SortSize:
|
||||
sortSlice(criterion, messageInfos, func(i, j int) bool {
|
||||
return messageInfos[i].Size < messageInfos[j].Size
|
||||
})
|
||||
case types.SortSubject:
|
||||
sortStrings(messageInfos, criterion,
|
||||
func(msgInfo *models.MessageInfo) string {
|
||||
subject := strings.ToLower(msgInfo.Envelope.Subject)
|
||||
subject = strings.TrimPrefix(subject, "re: ")
|
||||
return strings.TrimPrefix(subject, "fwd: ")
|
||||
})
|
||||
case types.SortTo:
|
||||
sortAddresses(messageInfos, criterion,
|
||||
func(msgInfo *models.MessageInfo) []*mail.Address {
|
||||
return msgInfo.Envelope.To
|
||||
})
|
||||
}
|
||||
}
|
||||
var uids []models.UID
|
||||
// copy in reverse as msgList displays backwards
|
||||
for i := len(messageInfos) - 1; i >= 0; i-- {
|
||||
uids = append(uids, messageInfos[i].Uid)
|
||||
}
|
||||
return uids, nil
|
||||
}
|
||||
|
||||
func sortAddresses(messageInfos []*models.MessageInfo, criterion *types.SortCriterion,
|
||||
getValue func(*models.MessageInfo) []*mail.Address,
|
||||
) {
|
||||
sortSlice(criterion, messageInfos, func(i, j int) bool {
|
||||
addressI, addressJ := getValue(messageInfos[i]), getValue(messageInfos[j])
|
||||
var firstI, firstJ *mail.Address
|
||||
if len(addressI) > 0 {
|
||||
firstI = addressI[0]
|
||||
}
|
||||
if len(addressJ) > 0 {
|
||||
firstJ = addressJ[0]
|
||||
}
|
||||
if firstI != nil && firstJ != nil {
|
||||
getName := func(addr *mail.Address) string {
|
||||
if addr.Name != "" {
|
||||
return addr.Name
|
||||
} else {
|
||||
return addr.Address
|
||||
}
|
||||
}
|
||||
return getName(firstI) < getName(firstJ)
|
||||
} else {
|
||||
return firstI != nil && firstJ == nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func sortFlags(messageInfos []*models.MessageInfo, criterion *types.SortCriterion,
|
||||
testFlag models.Flags,
|
||||
) {
|
||||
var slice []*boolStore
|
||||
for _, msgInfo := range messageInfos {
|
||||
slice = append(slice, &boolStore{
|
||||
Value: msgInfo.Flags.Has(testFlag),
|
||||
MsgInfo: msgInfo,
|
||||
})
|
||||
}
|
||||
sortSlice(criterion, slice, func(i, j int) bool {
|
||||
valI, valJ := slice[i].Value, slice[j].Value
|
||||
return valI && !valJ
|
||||
})
|
||||
for i := 0; i < len(messageInfos); i++ {
|
||||
messageInfos[i] = slice[i].MsgInfo
|
||||
}
|
||||
}
|
||||
|
||||
func sortStrings(messageInfos []*models.MessageInfo, criterion *types.SortCriterion,
|
||||
getValue func(*models.MessageInfo) string,
|
||||
) {
|
||||
var slice []*lexiStore
|
||||
for _, msgInfo := range messageInfos {
|
||||
slice = append(slice, &lexiStore{
|
||||
Value: getValue(msgInfo),
|
||||
MsgInfo: msgInfo,
|
||||
})
|
||||
}
|
||||
sortSlice(criterion, slice, func(i, j int) bool {
|
||||
return slice[i].Value < slice[j].Value
|
||||
})
|
||||
for i := 0; i < len(messageInfos); i++ {
|
||||
messageInfos[i] = slice[i].MsgInfo
|
||||
}
|
||||
}
|
||||
|
||||
type lexiStore struct {
|
||||
Value string
|
||||
MsgInfo *models.MessageInfo
|
||||
}
|
||||
|
||||
type boolStore struct {
|
||||
Value bool
|
||||
MsgInfo *models.MessageInfo
|
||||
}
|
||||
|
||||
func sortSlice(criterion *types.SortCriterion, slice interface{}, less func(i, j int) bool) {
|
||||
if criterion.Reverse {
|
||||
sort.SliceStable(slice, func(i, j int) bool {
|
||||
return less(j, i)
|
||||
})
|
||||
} else {
|
||||
sort.SliceStable(slice, less)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user