init: pristine aerc 0.20.0 source

This commit is contained in:
Mortdecai
2026-04-07 19:54:54 -04:00
commit 083402a548
502 changed files with 68722 additions and 0 deletions
+101
View File
@@ -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()
}
+74
View File
@@ -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)
}
}
}
+46
View File
@@ -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",
}