init: pristine aerc 0.20.0 source
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package xgmext
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib/log"
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
"github.com/emersion/go-imap"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/emersion/go-imap/commands"
|
||||
"github.com/emersion/go-imap/responses"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
func NewHandler(c *client.Client) *handler {
|
||||
return &handler{client: c}
|
||||
}
|
||||
|
||||
func (h handler) FetchEntireThreads(requested []models.UID) ([]models.UID, error) {
|
||||
threadIds, err := h.fetchThreadIds(requested)
|
||||
if err != nil {
|
||||
return nil,
|
||||
fmt.Errorf("failed to fetch thread IDs: %w", err)
|
||||
}
|
||||
uids, err := h.searchUids(threadIds)
|
||||
if err != nil {
|
||||
return nil,
|
||||
fmt.Errorf("failed to search for thread IDs: %w", err)
|
||||
}
|
||||
return uids, nil
|
||||
}
|
||||
|
||||
func (h handler) fetchThreadIds(uids []models.UID) ([]string, error) {
|
||||
messages := make(chan *imap.Message)
|
||||
done := make(chan error)
|
||||
|
||||
thriditem := imap.FetchItem("X-GM-THRID")
|
||||
items := []imap.FetchItem{
|
||||
thriditem,
|
||||
}
|
||||
|
||||
m := make(map[string]struct{}, len(uids))
|
||||
go func() {
|
||||
defer log.PanicHandler()
|
||||
for msg := range messages {
|
||||
if msg == nil {
|
||||
continue
|
||||
}
|
||||
item, ok := msg.Items[thriditem].(string)
|
||||
if ok {
|
||||
m[item] = struct{}{}
|
||||
}
|
||||
}
|
||||
done <- nil
|
||||
}()
|
||||
|
||||
var set imap.SeqSet
|
||||
for _, uid := range uids {
|
||||
set.AddNum(models.UidToUint32(uid))
|
||||
}
|
||||
err := h.client.UidFetch(&set, items, messages)
|
||||
<-done
|
||||
|
||||
thrid := make([]string, 0, len(m))
|
||||
for id := range m {
|
||||
thrid = append(thrid, id)
|
||||
}
|
||||
return thrid, err
|
||||
}
|
||||
|
||||
func (h handler) searchUids(thrid []string) ([]models.UID, error) {
|
||||
if len(thrid) == 0 {
|
||||
return nil, errors.New("no thread IDs provided")
|
||||
}
|
||||
return h.runSearch(NewThreadIDSearch(thrid))
|
||||
}
|
||||
|
||||
func (h handler) RawSearch(rawSearch string) ([]models.UID, error) {
|
||||
return h.runSearch(NewRawSearch(rawSearch))
|
||||
}
|
||||
|
||||
func (h handler) runSearch(cmd imap.Commander) ([]models.UID, error) {
|
||||
if h.client.State() != imap.SelectedState {
|
||||
return nil, errors.New("no mailbox selected")
|
||||
}
|
||||
cmd = &commands.Uid{Cmd: cmd}
|
||||
res := new(responses.Search)
|
||||
status, err := h.client.Execute(cmd, res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("imap execute failed: %w", err)
|
||||
}
|
||||
var uids []models.UID
|
||||
for _, i := range res.Ids {
|
||||
uids = append(uids, models.Uint32ToUid(i))
|
||||
}
|
||||
return uids, status.Err()
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package xgmext
|
||||
|
||||
import "github.com/emersion/go-imap"
|
||||
|
||||
type threadIDSearch struct {
|
||||
Charset string
|
||||
ThreadIDs []string
|
||||
}
|
||||
|
||||
// NewThreadIDSearch return an imap.Command to search UIDs for the provided
|
||||
// thread IDs using the X-GM-EXT-1 (Gmail extension)
|
||||
func NewThreadIDSearch(threadIDs []string) *threadIDSearch {
|
||||
return &threadIDSearch{
|
||||
Charset: "UTF-8",
|
||||
ThreadIDs: threadIDs,
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *threadIDSearch) Command() *imap.Command {
|
||||
const threadSearchKey = "X-GM-THRID"
|
||||
|
||||
var args []interface{}
|
||||
if cmd.Charset != "" {
|
||||
args = append(args, imap.RawString("CHARSET"))
|
||||
args = append(args, imap.RawString(cmd.Charset))
|
||||
}
|
||||
|
||||
// we want to produce a search query that looks like this:
|
||||
// SEARCH CHARSET UTF-8 OR OR X-GM-THRID 1771431779961568536 \
|
||||
// X-GM-THRID 1765355745646219617 X-GM-THRID 1771500774375286796
|
||||
for i := 0; i < len(cmd.ThreadIDs)-1; i++ {
|
||||
args = append(args, imap.RawString("OR"))
|
||||
}
|
||||
|
||||
for _, thrid := range cmd.ThreadIDs {
|
||||
args = append(args, imap.RawString(threadSearchKey))
|
||||
args = append(args, imap.RawString(thrid))
|
||||
}
|
||||
|
||||
return &imap.Command{
|
||||
Name: "SEARCH",
|
||||
Arguments: args,
|
||||
}
|
||||
}
|
||||
|
||||
type rawSearch struct {
|
||||
Charset string
|
||||
Search string
|
||||
}
|
||||
|
||||
func NewRawSearch(search string) *rawSearch {
|
||||
return &rawSearch{
|
||||
Charset: "UTF-8",
|
||||
Search: search,
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *rawSearch) Command() *imap.Command {
|
||||
const key = "X-GM-RAW"
|
||||
|
||||
var args []interface{}
|
||||
if cmd.Charset != "" {
|
||||
args = append(args, imap.RawString("CHARSET"))
|
||||
args = append(args, imap.RawString(cmd.Charset))
|
||||
}
|
||||
|
||||
args = append(args, imap.RawString(key))
|
||||
args = append(args, imap.RawString(cmd.Search))
|
||||
|
||||
return &imap.Command{
|
||||
Name: "SEARCH",
|
||||
Arguments: args,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package xgmext_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/worker/imap/extensions/xgmext"
|
||||
"github.com/emersion/go-imap"
|
||||
)
|
||||
|
||||
func TestXGMEXT_ThreadIDSearch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ids []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "search for single id",
|
||||
ids: []string{"1234"},
|
||||
want: "* SEARCH CHARSET UTF-8 X-GM-THRID 1234\r\n",
|
||||
},
|
||||
{
|
||||
name: "search for multiple id",
|
||||
ids: []string{"1234", "5678", "2345"},
|
||||
want: "* SEARCH CHARSET UTF-8 OR OR X-GM-THRID 1234 X-GM-THRID 5678 X-GM-THRID 2345\r\n",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
cmd := xgmext.NewThreadIDSearch(test.ids).Command()
|
||||
var buf bytes.Buffer
|
||||
err := cmd.WriteTo(imap.NewWriter(&buf))
|
||||
if err != nil {
|
||||
t.Errorf("failed to write command: %v", err)
|
||||
}
|
||||
if got := buf.String(); got != test.want {
|
||||
t.Errorf("test '%s' failed: got: '%s', but wanted: '%s'",
|
||||
test.name, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestXGMEXT_RawSearch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
search string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "search messages from mailing list",
|
||||
search: "list:info@example.com",
|
||||
want: "* SEARCH CHARSET UTF-8 X-GM-RAW list:info@example.com\r\n",
|
||||
},
|
||||
{
|
||||
name: "search for an exact phrase",
|
||||
search: "\"good morning\"",
|
||||
want: "* SEARCH CHARSET UTF-8 X-GM-RAW \"good morning\"\r\n",
|
||||
},
|
||||
{
|
||||
name: "group multiple search terms together",
|
||||
search: "subject:(dinner movie)",
|
||||
want: "* SEARCH CHARSET UTF-8 X-GM-RAW subject:(dinner movie)\r\n",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
cmd := xgmext.NewRawSearch(test.search).Command()
|
||||
var buf bytes.Buffer
|
||||
err := cmd.WriteTo(imap.NewWriter(&buf))
|
||||
if err != nil {
|
||||
t.Errorf("failed to write command: %v", err)
|
||||
}
|
||||
if got := buf.String(); got != test.want {
|
||||
t.Errorf("test '%s' failed: got: '%s', but wanted: '%s'",
|
||||
test.name, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package xgmext
|
||||
|
||||
var Terms = []string{
|
||||
"from:",
|
||||
"to:",
|
||||
"cc:",
|
||||
"bcc:",
|
||||
"subject:",
|
||||
"label:",
|
||||
"deliveredto:",
|
||||
"category:primary",
|
||||
"category:social",
|
||||
"category:promotions",
|
||||
"category:updates",
|
||||
"category:forums",
|
||||
"category:reservations",
|
||||
"category:purchases",
|
||||
"has:",
|
||||
"has:attachment",
|
||||
"has:drive",
|
||||
"has:document",
|
||||
"has:spreadsheet",
|
||||
"has:presentation",
|
||||
"has:youtube",
|
||||
"list:",
|
||||
"filename:",
|
||||
"in:",
|
||||
"is:",
|
||||
"is:important",
|
||||
"is:read",
|
||||
"is:unread",
|
||||
"is:starred",
|
||||
"after:",
|
||||
"before:",
|
||||
"older:",
|
||||
"newer:",
|
||||
"older_than:",
|
||||
"newer_than:",
|
||||
"size:",
|
||||
"larger:",
|
||||
"smaller:",
|
||||
"rfc822msgid:",
|
||||
"OR",
|
||||
"AND",
|
||||
"AROUND",
|
||||
}
|
||||
Reference in New Issue
Block a user