init: pristine aerc 0.20.0 source
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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