fix(gmail): constrain system-label searches
This commit is contained in:
parent
c4df6c34ea
commit
7f1ef97e15
@ -11,6 +11,7 @@
|
||||
|
||||
### Fixed
|
||||
- Calendar: display `calendar events` times and JSON local fields in the calendar timezone instead of preserving arbitrary event offsets. (#493)
|
||||
- Gmail: apply Gmail system-label filters for searches like `in:spam is:unread` so thread, message, and batch message searches do not return read spam. (#449)
|
||||
- Gmail: build outbound `Date` headers with the configured timezone so replies do not inherit a wrong host-local offset. (#514, #472) — thanks @dinakars777.
|
||||
- Gmail: preserve renewed watch expiration fields when a long-running `gmail watch serve` process records push delivery state after `gmail watch renew` runs separately. (#526)
|
||||
- Gmail: auto-fill draft reply subjects from the original message when `gmail drafts create --reply-to-message-id` omits `--subject`. (#488) — thanks @jbowerbir.
|
||||
|
||||
@ -175,3 +175,49 @@ func TestExecute_GmailMessagesSearch_JSON_IncludeBody(t *testing.T) {
|
||||
t.Fatalf("expected decoded body, got: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecute_GmailMessagesSearch_AppliesSystemLabelFilters(t *testing.T) {
|
||||
origNew := newGmailService
|
||||
t.Cleanup(func() { newGmailService = origNew })
|
||||
|
||||
var gotQuery string
|
||||
var gotLabels []string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
switch {
|
||||
case strings.Contains(path, "/users/me/messages") && !strings.Contains(path, "/users/me/messages/"):
|
||||
gotQuery = r.URL.Query().Get("q")
|
||||
gotLabels = r.URL.Query()["labelIds"]
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"messages": []map[string]any{},
|
||||
})
|
||||
return
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
svc, err := gmail.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
|
||||
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--account", "a@b.com", "gmail", "messages", "search", "in:spam is:unread", "--max", "1000"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
if gotQuery != "in:spam is:unread" {
|
||||
t.Fatalf("unexpected query: %q", gotQuery)
|
||||
}
|
||||
assertSameStrings(t, gotLabels, []string{"SPAM", "UNREAD"})
|
||||
}
|
||||
|
||||
@ -127,3 +127,49 @@ func TestExecute_GmailSearch_Text_NoResults(t *testing.T) {
|
||||
t.Fatalf("unexpected stderr: %q", errOut)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecute_GmailSearch_AppliesSystemLabelFilters(t *testing.T) {
|
||||
origNew := newGmailService
|
||||
t.Cleanup(func() { newGmailService = origNew })
|
||||
|
||||
var gotQuery string
|
||||
var gotLabels []string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
switch {
|
||||
case strings.Contains(path, "/users/me/threads") && !strings.Contains(path, "/users/me/threads/"):
|
||||
gotQuery = r.URL.Query().Get("q")
|
||||
gotLabels = r.URL.Query()["labelIds"]
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"threads": []map[string]any{},
|
||||
})
|
||||
return
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
svc, err := gmail.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
|
||||
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--account", "a@b.com", "gmail", "search", "in:spam is:unread", "--max", "1000"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
if gotQuery != "in:spam is:unread" {
|
||||
t.Fatalf("unexpected query: %q", gotQuery)
|
||||
}
|
||||
assertSameStrings(t, gotLabels, []string{"SPAM", "UNREAD"})
|
||||
}
|
||||
|
||||
@ -177,6 +177,9 @@ func searchMessageIDs(ctx context.Context, svc *gmail.Service, query string, lim
|
||||
MaxResults(batchSize).
|
||||
Fields("messages(id),nextPageToken").
|
||||
Context(ctx)
|
||||
if labelIDs := gmailQuerySystemLabelIDs(query); len(labelIDs) > 0 {
|
||||
call = call.LabelIds(labelIDs...)
|
||||
}
|
||||
if pageToken != "" {
|
||||
call = call.PageToken(pageToken)
|
||||
}
|
||||
|
||||
@ -56,6 +56,9 @@ func (c *GmailMessagesSearchCmd) Run(ctx context.Context, flags *RootFlags) erro
|
||||
MaxResults(c.Max).
|
||||
Fields("messages(id,threadId),nextPageToken").
|
||||
Context(ctx)
|
||||
if labelIDs := gmailQuerySystemLabelIDs(query); len(labelIDs) > 0 {
|
||||
call = call.LabelIds(labelIDs...)
|
||||
}
|
||||
if strings.TrimSpace(pageToken) != "" {
|
||||
call = call.PageToken(pageToken)
|
||||
}
|
||||
|
||||
@ -44,6 +44,9 @@ func (c *GmailSearchCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
Q(query).
|
||||
MaxResults(c.Max).
|
||||
Context(ctx)
|
||||
if labelIDs := gmailQuerySystemLabelIDs(query); len(labelIDs) > 0 {
|
||||
call = call.LabelIds(labelIDs...)
|
||||
}
|
||||
if strings.TrimSpace(pageToken) != "" {
|
||||
call = call.PageToken(pageToken)
|
||||
}
|
||||
|
||||
73
internal/cmd/gmail_search_labels.go
Normal file
73
internal/cmd/gmail_search_labels.go
Normal file
@ -0,0 +1,73 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
gmailSystemLabelUnread = "UNREAD"
|
||||
gmailSystemLabelStarred = "STARRED"
|
||||
gmailSystemLabelImportant = "IMPORTANT"
|
||||
gmailSystemLabelInbox = "INBOX"
|
||||
gmailSystemLabelSent = "SENT"
|
||||
gmailSystemLabelDraft = "DRAFT"
|
||||
gmailSystemLabelSpam = "SPAM"
|
||||
gmailSystemLabelTrash = "TRASH"
|
||||
)
|
||||
|
||||
func gmailQuerySystemLabelIDs(query string) []string {
|
||||
fields := strings.Fields(query)
|
||||
for _, field := range fields {
|
||||
token := strings.ToLower(strings.TrimSpace(field))
|
||||
if token == "or" || strings.ContainsAny(token, "{}") {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var labels []string
|
||||
seen := map[string]bool{}
|
||||
for _, field := range fields {
|
||||
token := strings.ToLower(strings.Trim(field, `"'(),[]`))
|
||||
if token == "" || strings.HasPrefix(token, "-") {
|
||||
continue
|
||||
}
|
||||
if labelID := gmailQuerySystemLabelID(token); labelID != "" && !seen[labelID] {
|
||||
seen[labelID] = true
|
||||
labels = append(labels, labelID)
|
||||
}
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
func gmailQuerySystemLabelID(token string) string {
|
||||
switch token {
|
||||
case "is:unread", "label:unread":
|
||||
return gmailSystemLabelUnread
|
||||
case "is:starred", "label:starred":
|
||||
return gmailSystemLabelStarred
|
||||
case "is:important", "label:important":
|
||||
return gmailSystemLabelImportant
|
||||
case "in:inbox", "label:inbox":
|
||||
return gmailSystemLabelInbox
|
||||
case "in:sent", "label:sent":
|
||||
return gmailSystemLabelSent
|
||||
case "in:draft", "in:drafts", "label:draft", "label:drafts":
|
||||
return gmailSystemLabelDraft
|
||||
case "in:spam", "is:spam", "label:spam":
|
||||
return gmailSystemLabelSpam
|
||||
case "in:trash", "label:trash":
|
||||
return gmailSystemLabelTrash
|
||||
case "category:primary":
|
||||
return "CATEGORY_PERSONAL"
|
||||
case "category:social":
|
||||
return "CATEGORY_SOCIAL"
|
||||
case "category:promotions":
|
||||
return "CATEGORY_PROMOTIONS"
|
||||
case "category:updates":
|
||||
return "CATEGORY_UPDATES"
|
||||
case "category:forums":
|
||||
return "CATEGORY_FORUMS"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@ -123,6 +123,24 @@ func captureStderr(t *testing.T, fn func()) string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func assertSameStrings(t *testing.T, got, want []string) {
|
||||
t.Helper()
|
||||
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
seen := make(map[string]int, len(got))
|
||||
for _, v := range got {
|
||||
seen[v]++
|
||||
}
|
||||
for _, v := range want {
|
||||
seen[v]--
|
||||
if seen[v] < 0 {
|
||||
t.Fatalf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func withStdin(t *testing.T, input string, fn func()) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user