From d448264a27734e08ce9e666c8a20392911b4ee61 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 01:07:05 +0000 Subject: [PATCH] feat(gmail): add single-message modify command (#281) (thanks @zerone0x) --- CHANGELOG.md | 1 + internal/cmd/gmail_batch.go | 5 +---- internal/cmd/gmail_labels_utils.go | 9 +++++++++ internal/cmd/gmail_messages.go | 5 +---- internal/cmd/gmail_messages_modify_test.go | 15 +++++++++++---- internal/cmd/gmail_thread.go | 6 +----- 6 files changed, 24 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c2acac..2ffcba2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Sheets: add `sheets create --parent` to place new spreadsheets in a Drive folder. (#424) — thanks @ManManavadaria. - Calendar: add `calendar subscribe` (aliases `sub`, `add-calendar`) to add a shared calendar to the current account’s calendar list. (#327) — thanks @cdthompson. - Gmail: add `watch serve --history-types` filtering (`messageAdded|messageDeleted|labelAdded|labelRemoved`) and include `deletedMessageIds` in webhook payloads. (#168) — thanks @salmonumbrella. +- Gmail: add `gmail messages modify` for single-message label changes, complementing thread- and batch-level modify flows. (#281) — thanks @zerone0x. - Contacts: support `--org`, `--title`, `--url`, `--note`, and `--custom` on create/update; include custom fields in get output with deterministic ordering. (#199) — thanks @phuctm97. - Drive: add `drive ls --all` (alias `--global`) to list across all accessible files; make `--all` and `--parent` mutually exclusive. (#107) — thanks @struong. - Docs: update install docs to use the official Homebrew core formula (`brew install gogcli`). (#361) — thanks @zeldrisho. diff --git a/internal/cmd/gmail_batch.go b/internal/cmd/gmail_batch.go index 2d46c60..7cf9200 100644 --- a/internal/cmd/gmail_batch.go +++ b/internal/cmd/gmail_batch.go @@ -109,14 +109,11 @@ func (c *GmailBatchModifyCmd) Run(ctx context.Context, flags *RootFlags) error { return err } - idMap, err := fetchLabelNameToID(svc) + addIDs, removeIDs, err := resolveModifyLabelIDs(svc, addLabels, removeLabels) if err != nil { return err } - addIDs := resolveLabelIDs(addLabels, idMap) - removeIDs := resolveLabelIDs(removeLabels, idMap) - err = svc.Users.Messages.BatchModify("me", &gmail.BatchModifyMessagesRequest{ Ids: ids, AddLabelIds: addIDs, diff --git a/internal/cmd/gmail_labels_utils.go b/internal/cmd/gmail_labels_utils.go index 4cf8a72..b2c98f3 100644 --- a/internal/cmd/gmail_labels_utils.go +++ b/internal/cmd/gmail_labels_utils.go @@ -30,6 +30,15 @@ func resolveLabelIDs(labels []string, nameToID map[string]string) []string { return out } +func resolveModifyLabelIDs(svc *gmail.Service, addLabels, removeLabels []string) ([]string, []string, error) { + idMap, err := fetchLabelNameToID(svc) + if err != nil { + return nil, nil, err + } + + return resolveLabelIDs(addLabels, idMap), resolveLabelIDs(removeLabels, idMap), nil +} + func ensureLabelNameAvailable(svc *gmail.Service, name string) error { idMap, err := fetchLabelNameToID(svc) if err != nil { diff --git a/internal/cmd/gmail_messages.go b/internal/cmd/gmail_messages.go index fb86a64..9e3ba1d 100644 --- a/internal/cmd/gmail_messages.go +++ b/internal/cmd/gmail_messages.go @@ -186,14 +186,11 @@ func (c *GmailMessagesModifyCmd) Run(ctx context.Context, flags *RootFlags) erro return err } - idMap, err := fetchLabelNameToID(svc) + addIDs, removeIDs, err := resolveModifyLabelIDs(svc, addLabels, removeLabels) if err != nil { return err } - addIDs := resolveLabelIDs(addLabels, idMap) - removeIDs := resolveLabelIDs(removeLabels, idMap) - _, err = svc.Users.Messages.Modify("me", messageID, &gmail.ModifyMessageRequest{ AddLabelIds: addIDs, RemoveLabelIds: removeIDs, diff --git a/internal/cmd/gmail_messages_modify_test.go b/internal/cmd/gmail_messages_modify_test.go index da9c55a..cce153c 100644 --- a/internal/cmd/gmail_messages_modify_test.go +++ b/internal/cmd/gmail_messages_modify_test.go @@ -39,7 +39,7 @@ func TestGmailMessagesModifyCmd_JSON(t *testing.T) { RemoveLabelIds []string `json:"removeLabelIds"` } _ = json.NewDecoder(r.Body).Decode(&body) - if len(body.AddLabelIds) != 1 || body.AddLabelIds[0] != "TRASH" { + if len(body.AddLabelIds) != 1 || body.AddLabelIds[0] != "Label_1" { http.Error(w, "bad addLabelIds", http.StatusBadRequest) return } @@ -79,7 +79,7 @@ func TestGmailMessagesModifyCmd_JSON(t *testing.T) { if err := runKong(t, &GmailMessagesModifyCmd{}, []string{ "msg1", - "--add", "TRASH", + "--add", "Custom", "--remove", "INBOX", }, ctx, flags); err != nil { t.Fatalf("execute: %v", err) @@ -97,7 +97,7 @@ func TestGmailMessagesModifyCmd_JSON(t *testing.T) { if parsed.Modified != "msg1" { t.Fatalf("unexpected modified: %q", parsed.Modified) } - if len(parsed.AddedLabels) != 1 || parsed.AddedLabels[0] != "TRASH" { + if len(parsed.AddedLabels) != 1 || parsed.AddedLabels[0] != "Label_1" { t.Fatalf("unexpected added labels: %#v", parsed.AddedLabels) } if len(parsed.RemovedLabels) != 1 || parsed.RemovedLabels[0] != "INBOX" { @@ -113,7 +113,7 @@ func TestGmailMessagesModifyCmd_JSON(t *testing.T) { if err := runKong(t, &GmailMessagesModifyCmd{}, []string{ "msg1", - "--add", "TRASH", + "--add", "Custom", "--remove", "INBOX", }, ctx, flags); err != nil { t.Fatalf("execute plain: %v", err) @@ -148,4 +148,11 @@ func TestGmailMessagesModifyCmd_ValidationErrors(t *testing.T) { t.Fatalf("expected validation error, got %v", err) } }) + + t.Run("empty message id", func(t *testing.T) { + err := runKong(t, &GmailMessagesModifyCmd{}, []string{"", "--add", "INBOX"}, ctx, flags) + if err == nil || !strings.Contains(err.Error(), "empty messageId") { + t.Fatalf("expected empty messageId error, got %v", err) + } + }) } diff --git a/internal/cmd/gmail_thread.go b/internal/cmd/gmail_thread.go index 9c23853..e46d994 100644 --- a/internal/cmd/gmail_thread.go +++ b/internal/cmd/gmail_thread.go @@ -212,15 +212,11 @@ func (c *GmailThreadModifyCmd) Run(ctx context.Context, flags *RootFlags) error return err } - // Resolve label names to IDs - idMap, err := fetchLabelNameToID(svc) + addIDs, removeIDs, err := resolveModifyLabelIDs(svc, addLabels, removeLabels) if err != nil { return err } - addIDs := resolveLabelIDs(addLabels, idMap) - removeIDs := resolveLabelIDs(removeLabels, idMap) - // Use Gmail's Threads.Modify API _, err = svc.Users.Threads.Modify("me", threadID, &gmail.ModifyThreadRequest{ AddLabelIds: addIDs,