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
+149
View File
@@ -0,0 +1,149 @@
package extensions
import (
"fmt"
"strings"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-imap/responses"
"github.com/emersion/go-imap/utf7"
)
// A LIST-STATUS client
type ListStatusClient struct {
c *client.Client
}
func NewListStatusClient(c *client.Client) *ListStatusClient {
return &ListStatusClient{c}
}
// SupportListStatus checks if the server supports the LIST-STATUS extension.
func (c *ListStatusClient) SupportListStatus() (bool, error) {
return c.c.Support("LIST-STATUS")
}
// ListStatus performs a LIST-STATUS command, listing mailboxes and also
// retrieving the requested status items. A nil channel can be passed in order
// to only retrieve the STATUS responses
func (c *ListStatusClient) ListStatus(
ref string,
name string,
items []imap.StatusItem,
ch chan *imap.MailboxInfo,
) ([]*imap.MailboxStatus, error) {
if ch != nil {
defer close(ch)
}
if c.c.State() != imap.AuthenticatedState && c.c.State() != imap.SelectedState {
return nil, client.ErrNotLoggedIn
}
cmd := &ListStatusCommand{
Reference: ref,
Mailbox: name,
Items: items,
}
res := &ListStatusResponse{Mailboxes: ch}
status, err := c.c.Execute(cmd, res)
if err != nil {
return nil, err
}
return res.Statuses, status.Err()
}
// ListStatusCommand is a LIST command, as defined in RFC 3501 section 6.3.8. If
// Subscribed is set to true, LSUB will be used instead. Mailbox statuses will
// be returned if Items is not nil
type ListStatusCommand struct {
Reference string
Mailbox string
Subscribed bool
Items []imap.StatusItem
}
func (cmd *ListStatusCommand) Command() *imap.Command {
name := "LIST"
if cmd.Subscribed {
name = "LSUB"
}
enc := utf7.Encoding.NewEncoder()
ref, _ := enc.String(cmd.Reference)
mailbox, _ := enc.String(cmd.Mailbox)
items := make([]string, len(cmd.Items))
if cmd.Items != nil {
for i, item := range cmd.Items {
items[i] = string(item)
}
}
args := fmt.Sprintf("RETURN (STATUS (%s))", strings.Join(items, " "))
return &imap.Command{
Name: name,
Arguments: []interface{}{ref, mailbox, imap.RawString(args)},
}
}
// A LIST-STATUS response
type ListStatusResponse struct {
Mailboxes chan *imap.MailboxInfo
Subscribed bool
Statuses []*imap.MailboxStatus
}
func (r *ListStatusResponse) Name() string {
if r.Subscribed {
return "LSUB"
} else {
return "LIST"
}
}
func (r *ListStatusResponse) Handle(resp imap.Resp) error {
name, _, ok := imap.ParseNamedResp(resp)
if !ok {
return responses.ErrUnhandled
}
switch name {
case "LIST":
if r.Mailboxes == nil {
return nil
}
res := responses.List{Mailboxes: r.Mailboxes}
return res.Handle(resp)
case "STATUS":
res := responses.Status{
Mailbox: new(imap.MailboxStatus),
}
err := res.Handle(resp)
if err != nil {
return err
}
r.Statuses = append(r.Statuses, res.Mailbox)
default:
return responses.ErrUnhandled
}
return nil
}
func (r *ListStatusResponse) WriteTo(w *imap.Writer) error {
respName := r.Name()
for mbox := range r.Mailboxes {
fields := []interface{}{imap.RawString(respName)}
fields = append(fields, mbox.Format()...)
resp := imap.NewUntaggedResp(fields)
if err := resp.WriteTo(w); err != nil {
return err
}
}
return nil
}
+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",
}