From a1d2ffac33fba7281a11bb4579447ace66d10cb9 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Wed, 19 Aug 2026 01:07:19 -0400 Subject: [PATCH 1/2] Add trash and spam actions --- .surface | 2 + API-COVERAGE.md | 2 + README.md | 6 +- internal/cmd/help.go | 2 +- internal/cmd/help_test.go | 4 +- internal/cmd/root.go | 2 + internal/cmd/trash.go | 101 ++++++++++++++++++++++++ internal/cmd/trash_test.go | 152 +++++++++++++++++++++++++++++++++++++ internal/tui/mail.go | 5 ++ internal/tui/mail_test.go | 3 +- skills/hey/SKILL.md | 23 +++++- tests/smoke/trash_test.go | 67 ++++++++++++++++ 12 files changed, 362 insertions(+), 7 deletions(-) create mode 100644 internal/cmd/trash.go create mode 100644 internal/cmd/trash_test.go create mode 100644 tests/smoke/trash_test.go diff --git a/.surface b/.surface index 7aa484f8..8d65f3d6 100644 --- a/.surface +++ b/.surface @@ -73,6 +73,7 @@ hey seen hey setup hey skill hey skill install +hey spam hey threads hey timetrack hey timetrack current @@ -91,5 +92,6 @@ hey todo list hey todo list --all hey todo list --limit hey todo uncomplete +hey trash hey tui hey unseen diff --git a/API-COVERAGE.md b/API-COVERAGE.md index 3f62dbca..721b0bde 100644 --- a/API-COVERAGE.md +++ b/API-COVERAGE.md @@ -22,6 +22,8 @@ The legacy `internal/client/` is used only for HTML-scraping gap operations mark | `/topics/{id}.json` | GET | SDK `Topics().Get` | `hey forward ` | covered | | `/entries/{id}/forwards/new.json` | GET | SDK `Entries().NewForward` | `hey forward ` | covered | | `/postings/moves.json` | POST | SDK `Postings().Move` | `hey move --to `, TUI `m` | covered | +| `/postings/trash.json` | POST | SDK `Postings().MoveToTrash` | `hey trash `, TUI `t` | covered | +| `/postings/spam.json` | POST | SDK `Postings().MarkSpam` | `hey spam `, TUI `s` | covered | | `/calendar/days/{date}/habits/{id}/completions.json` | POST | SDK `Habits().Complete` | `hey habit complete ` | covered | | `/calendar/days/{date}/habits/{id}/completions.json` | DELETE | SDK `Habits().Uncomplete` | `hey habit uncomplete ` | covered | | `/calendar/days/{date}/journal_entry.json` | GET | SDK `Journal().Get` | `hey journal read [date]` | partial: falls back to legacy | diff --git a/README.md b/README.md index 503750f9..a0b25ebf 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ hey auth logout # clear credentials Run `hey` to launch the interactive terminal UI. -Navigate between mailboxes and email threads. Use Enter to open a thread, `r` to reply, `f` to forward, `m` to move a thread, and Escape or `q` to go back. +Navigate between mailboxes and email threads. Use Enter to open a thread, `r` to reply, `f` to forward, `m` to move, `t` to trash, `s` to mark as spam, and Escape or `q` to go back. ## CLI Commands @@ -77,9 +77,11 @@ hey compose --to user@example.com --cc bob@example.com --bcc carol@example.org - hey drafts # list drafts hey move 12345 --to feed # move a thread to another box hey move 12345 67890 --to "paper trail" # move multiple threads +hey trash 12345 # move a thread to Trash +hey spam 12345 # mark a thread as spam ``` -`hey move` takes the `id` values returned by `hey box --json`. Destinations are Imbox, The Feed, Set Aside, Reply Later, or Paper Trail. Bubble Up requires a scheduled date and is not available through `hey move`. +`hey move`, `hey trash`, and `hey spam` take the `id` values returned by `hey box --json`. Move destinations are Imbox, The Feed, Set Aside, Reply Later, or Paper Trail. Bubble Up requires a scheduled date and is not available through `hey move`. Trashing a shared thread removes your access instead of deleting it for everyone. ### Calendars diff --git a/internal/cmd/help.go b/internal/cmd/help.go index c9b4fa9e..033f26db 100644 --- a/internal/cmd/help.go +++ b/internal/cmd/help.go @@ -17,7 +17,7 @@ var curatedCategories = []struct { }{ { heading: "EMAIL", - names: []string{"boxes", "box", "threads", "compose", "reply", "forward", "drafts", "seen", "unseen", "move"}, + names: []string{"boxes", "box", "threads", "compose", "reply", "forward", "drafts", "seen", "unseen", "move", "trash", "spam"}, }, { heading: "CALENDAR & TASKS", diff --git a/internal/cmd/help_test.go b/internal/cmd/help_test.go index 783dc5b0..9421a75f 100644 --- a/internal/cmd/help_test.go +++ b/internal/cmd/help_test.go @@ -27,7 +27,7 @@ func TestCuratedCommandHelpUsesUserFacingLanguage(t *testing.T) { func TestEmailCommandHelpKeepsPostingAsAnInternalTerm(t *testing.T) { root := newRootCmd() - for _, name := range []string{"boxes", "box", "seen", "unseen", "move"} { + for _, name := range []string{"boxes", "box", "seen", "unseen", "move", "trash", "spam"} { t.Run(name, func(t *testing.T) { command, _, err := root.Find([]string{name}) if err != nil { @@ -73,6 +73,8 @@ EMAIL seen Mark email threads as seen unseen Mark email threads as unseen move Move email threads to another box + trash Move email threads to Trash + spam Mark email threads as spam CALENDAR & TASKS calendars List calendars diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 3d657a9b..5740e827 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -133,6 +133,8 @@ func newRootCmd() *cobra.Command { root.AddCommand(newSeenCommand().cmd) root.AddCommand(newUnseenCommand().cmd) root.AddCommand(newMoveCommand().cmd) + root.AddCommand(newTrashCommand().cmd) + root.AddCommand(newSpamCommand().cmd) root.AddCommand(newSetupCommand()) root.AddCommand(newTuiCommand().cmd) root.AddCommand(newSkillCommand().cmd) diff --git a/internal/cmd/trash.go b/internal/cmd/trash.go new file mode 100644 index 00000000..2eb3f134 --- /dev/null +++ b/internal/cmd/trash.go @@ -0,0 +1,101 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/basecamp/hey-cli/internal/output" +) + +type trashCommand struct { + cmd *cobra.Command +} + +func newTrashCommand() *trashCommand { + trashCommand := &trashCommand{} + trashCommand.cmd = &cobra.Command{ + Use: "trash ...", + Short: "Move email threads to Trash", + Long: "Move one or more email threads to Trash. For a shared thread, HEY removes your access instead of deleting it for everyone.", + Example: ` hey trash 12345 + hey trash 12345 67890`, + Annotations: map[string]string{ + "agent_notes": "Accepts one or more box item IDs from hey box output. Shared threads lose your access rather than being deleted for everyone.", + }, + RunE: trashCommand.run, + Args: usageMinOneArg(), + } + + return trashCommand +} + +func (c *trashCommand) run(cmd *cobra.Command, args []string) error { + if err := requireAuth(); err != nil { + return err + } + + ids, err := parseIntArgs(args) + if err != nil { + return err + } + + if err := sdk.Postings().MoveToTrash(cmd.Context(), ids...); err != nil { + return convertSDKError(err) + } + + summary := fmt.Sprintf("%d %s moved to Trash", len(ids), threadNoun(len(ids))) + if writer.IsStyled() { + fmt.Fprintln(cmd.OutOrStdout(), summary+".") + return nil + } + + return writeOK(nil, output.WithSummary(summary)) +} + +// spam + +type spamCommand struct { + cmd *cobra.Command +} + +func newSpamCommand() *spamCommand { + spamCommand := &spamCommand{} + spamCommand.cmd = &cobra.Command{ + Use: "spam ...", + Short: "Mark email threads as spam", + Long: "Mark one or more email threads as spam. HEY moves the threads to Spam and trains its filters.", + Example: ` hey spam 12345 + hey spam 12345 67890`, + Annotations: map[string]string{ + "agent_notes": "Accepts one or more box item IDs from hey box output. Marks each thread as spam and removes it from the current box.", + }, + RunE: spamCommand.run, + Args: usageMinOneArg(), + } + + return spamCommand +} + +func (c *spamCommand) run(cmd *cobra.Command, args []string) error { + if err := requireAuth(); err != nil { + return err + } + + ids, err := parseIntArgs(args) + if err != nil { + return err + } + + if err := sdk.Postings().MarkSpam(cmd.Context(), ids...); err != nil { + return convertSDKError(err) + } + + summary := fmt.Sprintf("%d %s marked as spam", len(ids), threadNoun(len(ids))) + if writer.IsStyled() { + fmt.Fprintln(cmd.OutOrStdout(), summary+".") + return nil + } + + return writeOK(nil, output.WithSummary(summary)) +} diff --git a/internal/cmd/trash_test.go b/internal/cmd/trash_test.go new file mode 100644 index 00000000..abc46c68 --- /dev/null +++ b/internal/cmd/trash_test.go @@ -0,0 +1,152 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/basecamp/hey-cli/internal/apierr" + "github.com/basecamp/hey-cli/internal/output" +) + +type recordedRemoval struct { + method string + path string + postingIDs []int64 + status int + requests int +} + +func removalServer(t *testing.T) (*httptest.Server, *recordedRemoval) { + t.Helper() + recorded := &recordedRemoval{status: http.StatusNoContent} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + recorded.requests++ + recorded.method = r.Method + recorded.path = r.URL.Path + var body struct { + PostingIDs []int64 `json:"posting_ids"` + } + _ = json.NewDecoder(r.Body).Decode(&body) + recorded.postingIDs = body.PostingIDs + + switch r.URL.Path { + case "/postings/trash.json", "/postings/spam.json": + w.WriteHeader(recorded.status) + default: + http.NotFound(w, r) + } + })) + t.Cleanup(server.Close) + return server, recorded +} + +func runRemoval(t *testing.T, server *httptest.Server, command string, args ...string) (output.Response, error) { + t.Helper() + t.Setenv("HEY_TOKEN", "test-token") + t.Setenv("HEY_NO_KEYRING", "1") + t.Setenv("HEY_BASE_URL", "") + tmpDir := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tmpDir) + t.Setenv("XDG_STATE_HOME", tmpDir) + t.Setenv("XDG_CACHE_HOME", tmpDir) + + root := newRootCmd() + var buf bytes.Buffer + root.SetOut(&buf) + root.SetErr(&buf) + root.SetArgs(append([]string{command, "--json", "--base-url", server.URL}, args...)) + + err := root.Execute() + var resp output.Response + if buf.Len() > 0 { + _ = json.Unmarshal(buf.Bytes(), &resp) + } + return resp, err +} + +func TestTrashAndSpam(t *testing.T) { + tests := []struct { + name string + command string + args []string + path string + summary string + }{ + {"trash one", "trash", []string{"12345"}, "/postings/trash.json", "1 thread moved to Trash"}, + {"trash multiple", "trash", []string{"12345", "67890"}, "/postings/trash.json", "2 threads moved to Trash"}, + {"spam one", "spam", []string{"12345"}, "/postings/spam.json", "1 thread marked as spam"}, + {"spam multiple", "spam", []string{"12345", "67890"}, "/postings/spam.json", "2 threads marked as spam"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server, recorded := removalServer(t) + resp, err := runRemoval(t, server, tt.command, tt.args...) + if err != nil { + t.Fatalf("%s failed: %v", tt.command, err) + } + if recorded.method != http.MethodPost || recorded.path != tt.path { + t.Errorf("request = %s %s, want POST %s", recorded.method, recorded.path, tt.path) + } + if len(recorded.postingIDs) != len(tt.args) { + t.Fatalf("posting_ids = %v, want %d IDs", recorded.postingIDs, len(tt.args)) + } + for i, want := range []int64{12345, 67890}[:len(tt.args)] { + if recorded.postingIDs[i] != want { + t.Errorf("posting_ids[%d] = %d, want %d", i, recorded.postingIDs[i], want) + } + } + if resp.Summary != tt.summary { + t.Errorf("summary = %q, want %q", resp.Summary, tt.summary) + } + }) + } +} + +func TestTrashAndSpamRequireIDs(t *testing.T) { + for _, command := range []string{"trash", "spam"} { + t.Run(command, func(t *testing.T) { + server, recorded := removalServer(t) + _, err := runRemoval(t, server, command) + if err == nil || !strings.Contains(err.Error(), "Usage:") { + t.Fatalf("missing ID should produce a usage error, got %v", err) + } + if recorded.requests != 0 { + t.Errorf("missing ID made %d requests", recorded.requests) + } + }) + } +} + +func TestTrashAndSpamRejectInvalidIDsBeforeRequest(t *testing.T) { + for _, command := range []string{"trash", "spam"} { + t.Run(command, func(t *testing.T) { + server, recorded := removalServer(t) + _, err := runRemoval(t, server, command, "not-an-id") + var cliErr *apierr.Error + if !errors.As(err, &cliErr) || cliErr.Code != "usage" { + t.Fatalf("invalid ID should produce a usage error, got %v", err) + } + if recorded.requests != 0 { + t.Errorf("invalid ID made %d requests", recorded.requests) + } + }) + } +} + +func TestTrashAndSpamReportServerFailures(t *testing.T) { + for _, command := range []string{"trash", "spam"} { + t.Run(command, func(t *testing.T) { + server, recorded := removalServer(t) + recorded.status = http.StatusUnprocessableEntity + if _, err := runRemoval(t, server, command, "12345"); err == nil { + t.Fatal("server failure should be reported") + } + }) + } +} diff --git a/internal/tui/mail.go b/internal/tui/mail.go index 4c358eec..2647ca48 100644 --- a/internal/tui/mail.go +++ b/internal/tui/mail.go @@ -271,6 +271,7 @@ func (v *mailView) HelpBindings() []helpBinding { {"d", "feed"}, {"p", "paper trail"}, {"t", "trash"}, + {"s", "spam"}, {"-", "mute"}, } } @@ -526,6 +527,10 @@ func (v *mailView) handlePostingAction(key string) tea.Cmd { return v.doPostingAction("Thread moved to Trash", true, boxID, p.ID, func() error { return v.vc.sdk.Postings().MoveToTrash(v.vc.ctx, p.ID) }) + case "s": + return v.doPostingAction("Thread marked as spam", true, boxID, p.ID, func() error { + return v.vc.sdk.Postings().MarkSpam(v.vc.ctx, p.ID) + }) case "-": return v.doPostingAction("Thread muted", true, boxID, p.ID, func() error { return v.vc.sdk.Postings().Mute(v.vc.ctx, p.ID) diff --git a/internal/tui/mail_test.go b/internal/tui/mail_test.go index 20cba9c9..8bbf9de2 100644 --- a/internal/tui/mail_test.go +++ b/internal/tui/mail_test.go @@ -251,6 +251,7 @@ func TestMailViewPostingKeysCallExpectedEndpoints(t *testing.T) { {"feed", "d", "/postings/moves.json", 2, true, "Thread moved to The Feed", false}, {"paper trail", "p", "/postings/moves.json", 5, true, "Thread moved to Paper Trail", false}, {"trash", "t", "/postings/trash.json", 0, true, "Thread moved to Trash", false}, + {"spam", "s", "/postings/spam.json", 0, true, "Thread marked as spam", false}, {"mute", "-", "/postings/mutings.json", 0, true, "Thread muted", false}, } @@ -1072,7 +1073,7 @@ func TestMailViewHelpBindings(t *testing.T) { for _, b := range bindings { keys[b.key] = true } - for _, expected := range []string{"r", "f", "m", "e", "l", "a", "t"} { + for _, expected := range []string{"r", "f", "m", "e", "l", "a", "t", "s"} { if !keys[expected] { t.Errorf("missing help binding for key %q", expected) } diff --git a/skills/hey/SKILL.md b/skills/hey/SKILL.md index 7b22f69b..d35d5362 100644 --- a/skills/hey/SKILL.md +++ b/skills/hey/SKILL.md @@ -25,7 +25,11 @@ triggers: - hey seen - hey unseen - hey move + - hey trash + - hey spam - move email + - trash email + - mark as spam - mark as read - mark as seen - mark as unseen @@ -104,6 +108,8 @@ CLI for HEY email: mailboxes, email threads, replies, compose, calendars, todos, | Mark as seen | `hey seen 12345` | | Mark as unseen | `hey unseen 12345` | | Move email threads | `hey move 12345 --to feed` | +| Move email threads to Trash | `hey trash 12345` | +| Mark email threads as spam | `hey spam 12345` | | Complete habit | `hey habit complete 123` | | Uncomplete habit | `hey habit uncomplete 123` | | Start time tracking | `hey timetrack start` | @@ -129,6 +135,8 @@ Want to read email? ├── Mark as seen? → hey seen ├── Mark as unseen? → hey unseen ├── Move to another box? → hey move --to +├── Move to Trash? → hey trash +├── Mark as spam? → hey spam └── Launch interactive UI? → hey (no args, launches TUI) ``` @@ -170,7 +178,7 @@ hey box 123 --json # List emails in box (by ID) Box names: `imbox`, `feedbox`, `trailbox`, `asidebox`, `laterbox`, `bubblebox` -**Response format:** `hey box` returns `{"box": {...}, "postings": [...]}`. The `postings` array is the API representation of the email threads in that box. Each item has: `id` (box item ID), `topic_id` (thread ID), `name` (subject), `seen` (read status), `created_at`, `contacts`, `summary`, `app_url`. Use `id` for `hey seen`, `hey unseen`, and `hey move`. Use `topic_id` for `hey threads`, `hey reply`, and `hey forward`. +**Response format:** `hey box` returns `{"box": {...}, "postings": [...]}`. The `postings` array is the API representation of the email threads in that box. Each item has: `id` (box item ID), `topic_id` (thread ID), `name` (subject), `seen` (read status), `created_at`, `contacts`, `summary`, `app_url`. Use `id` for `hey seen`, `hey unseen`, `hey move`, `hey trash`, and `hey spam`. Use `topic_id` for `hey threads`, `hey reply`, and `hey forward`. ### Email - Threads @@ -179,7 +187,7 @@ hey threads --json # Read full email thread hey threads --html # Read with raw HTML content ``` -**ID note:** Every email thread returned by `hey box` has an `id` (its box item ID) and a `topic_id` (its thread ID). `hey seen`, `hey unseen`, and `hey move` expect `id`. `hey threads`, `hey reply`, and `hey forward` expect `topic_id`. The `app_url` field also contains the thread ID as a fallback (e.g. `https://app.hey.com/topics/123` → `123`). +**ID note:** Every email thread returned by `hey box` has an `id` (its box item ID) and a `topic_id` (its thread ID). `hey seen`, `hey unseen`, `hey move`, `hey trash`, and `hey spam` expect `id`. `hey threads`, `hey reply`, and `hey forward` expect `topic_id`. The `app_url` field also contains the thread ID as a fallback (e.g. `https://app.hey.com/topics/123` → `123`). ### Email - Reply, Forward & Compose @@ -214,6 +222,17 @@ hey move 12345 67890 --to "paper trail" # Move multiple threads Takes box item IDs (the `id` field from `hey box --json`). `--to` accepts a box name, kind, or ID. Supported destinations are Imbox, The Feed, Set Aside, Reply Later, and Paper Trail. Bubble Up requires a scheduled date and is not supported by this command. +### Email - Trash and Spam + +```bash +hey trash 12345 # Move one thread to Trash +hey trash 12345 67890 # Move multiple threads to Trash +hey spam 12345 # Mark one thread as spam +hey spam 12345 67890 # Mark multiple threads as spam +``` + +Takes box item IDs (the `id` field from `hey box --json`). Trashing a shared thread removes your access instead of deleting it for everyone. Marking a thread as spam moves it to Spam and trains HEY's filters. + ### Drafts ```bash diff --git a/tests/smoke/trash_test.go b/tests/smoke/trash_test.go new file mode 100644 index 00000000..afcdab72 --- /dev/null +++ b/tests/smoke/trash_test.go @@ -0,0 +1,67 @@ +package smoke_test + +import ( + "encoding/json" + "fmt" + "testing" +) + +func TestTrash(t *testing.T) { + uid := uniqueID() + subject := fmt.Sprintf("Disposable trash test %s", uid) + _, stderr, code := hey(t, "compose", + "--to", "david@basecamp.com", + "--subject", subject, + "-m", "This disposable thread verifies the Trash command.", + "--json", + ) + if code != 0 { + t.Skipf("could not create a disposable thread (exit %d): %s", code, stderr) + } + + boxResp := heyJSON(t, "box", "imbox") + type Posting struct { + ID int `json:"id"` + Name string `json:"name"` + } + type BoxResp struct { + Postings []Posting `json:"postings"` + } + box := dataAs[BoxResp](t, boxResp) + + var postingID int + for _, posting := range box.Postings { + if posting.Name == subject { + postingID = posting.ID + break + } + } + if postingID == 0 { + t.Skip("disposable thread did not appear in Imbox") + } + + stdout := heyOK(t, "trash", intStr(postingID), "--json") + var trashResp Response + if err := json.Unmarshal([]byte(stdout), &trashResp); err != nil { + t.Fatalf("failed to parse trash response: %v", err) + } + assertContains(t, trashResp.Summary, "1 thread moved to Trash") + + refreshed := dataAs[BoxResp](t, heyJSON(t, "box", "imbox")) + for _, posting := range refreshed.Postings { + if posting.ID == postingID { + t.Errorf("trashed thread %d is still in Imbox", postingID) + } + } +} + +func TestTrashAndSpamValidation(t *testing.T) { + for _, command := range []string{"trash", "spam"} { + t.Run(command+" requires an ID", func(t *testing.T) { + heyFail(t, command, "--json") + }) + t.Run(command+" rejects invalid IDs", func(t *testing.T) { + heyFail(t, command, "not-a-number", "--json") + }) + } +} From 236d4530ceaa99d821b59e3388b111717c110ae5 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Wed, 19 Aug 2026 01:11:00 -0400 Subject: [PATCH 2/2] Give spam its own command file --- internal/cmd/spam.go | 54 +++++++++++++++++++++++++++++++++++++++++++ internal/cmd/trash.go | 47 ------------------------------------- 2 files changed, 54 insertions(+), 47 deletions(-) create mode 100644 internal/cmd/spam.go diff --git a/internal/cmd/spam.go b/internal/cmd/spam.go new file mode 100644 index 00000000..8793806f --- /dev/null +++ b/internal/cmd/spam.go @@ -0,0 +1,54 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/basecamp/hey-cli/internal/output" +) + +type spamCommand struct { + cmd *cobra.Command +} + +func newSpamCommand() *spamCommand { + spamCommand := &spamCommand{} + spamCommand.cmd = &cobra.Command{ + Use: "spam ...", + Short: "Mark email threads as spam", + Long: "Mark one or more email threads as spam. HEY moves the threads to Spam and trains its filters.", + Example: ` hey spam 12345 + hey spam 12345 67890`, + Annotations: map[string]string{ + "agent_notes": "Accepts one or more box item IDs from hey box output. Marks each thread as spam and removes it from the current box.", + }, + RunE: spamCommand.run, + Args: usageMinOneArg(), + } + + return spamCommand +} + +func (c *spamCommand) run(cmd *cobra.Command, args []string) error { + if err := requireAuth(); err != nil { + return err + } + + ids, err := parseIntArgs(args) + if err != nil { + return err + } + + if err := sdk.Postings().MarkSpam(cmd.Context(), ids...); err != nil { + return convertSDKError(err) + } + + summary := fmt.Sprintf("%d %s marked as spam", len(ids), threadNoun(len(ids))) + if writer.IsStyled() { + fmt.Fprintln(cmd.OutOrStdout(), summary+".") + return nil + } + + return writeOK(nil, output.WithSummary(summary)) +} diff --git a/internal/cmd/trash.go b/internal/cmd/trash.go index 2eb3f134..52bb2271 100644 --- a/internal/cmd/trash.go +++ b/internal/cmd/trash.go @@ -52,50 +52,3 @@ func (c *trashCommand) run(cmd *cobra.Command, args []string) error { return writeOK(nil, output.WithSummary(summary)) } - -// spam - -type spamCommand struct { - cmd *cobra.Command -} - -func newSpamCommand() *spamCommand { - spamCommand := &spamCommand{} - spamCommand.cmd = &cobra.Command{ - Use: "spam ...", - Short: "Mark email threads as spam", - Long: "Mark one or more email threads as spam. HEY moves the threads to Spam and trains its filters.", - Example: ` hey spam 12345 - hey spam 12345 67890`, - Annotations: map[string]string{ - "agent_notes": "Accepts one or more box item IDs from hey box output. Marks each thread as spam and removes it from the current box.", - }, - RunE: spamCommand.run, - Args: usageMinOneArg(), - } - - return spamCommand -} - -func (c *spamCommand) run(cmd *cobra.Command, args []string) error { - if err := requireAuth(); err != nil { - return err - } - - ids, err := parseIntArgs(args) - if err != nil { - return err - } - - if err := sdk.Postings().MarkSpam(cmd.Context(), ids...); err != nil { - return convertSDKError(err) - } - - summary := fmt.Sprintf("%d %s marked as spam", len(ids), threadNoun(len(ids))) - if writer.IsStyled() { - fmt.Fprintln(cmd.OutOrStdout(), summary+".") - return nil - } - - return writeOK(nil, output.WithSummary(summary)) -}