Skip to content

Commit 41979a7

Browse files
authored
feat(mail): add move and archive commands (#10)
Resolves #9
1 parent b84f500 commit 41979a7

2 files changed

Lines changed: 116 additions & 0 deletions

File tree

cmd/msx/main.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ commands:
4343
whoami Show the current Graph account
4444
mail List mail with optional filters
4545
mail-get Fetch one mail message by id
46+
mail-move Move one or more mail messages to another folder
47+
mail-archive Move one or more mail messages to Archive
4648
agenda List calendar events in a time range
4749
event-get Fetch one calendar event by id
4850
files List or search OneDrive files
@@ -108,6 +110,10 @@ func run(args []string) error {
108110
return cmdMail(s, g, rest)
109111
case "mail-get":
110112
return cmdMailGet(s, g, rest)
113+
case "mail-move":
114+
return cmdMailMove(s, g, rest)
115+
case "mail-archive":
116+
return cmdMailArchive(s, g, rest)
111117
case "agenda":
112118
return cmdAgenda(s, g, rest)
113119
case "event-get":
@@ -433,6 +439,59 @@ func cmdMailGet(s *store.Store, g globalFlags, args []string) error {
433439
return emit(g, "mail-get", data)
434440
}
435441

442+
func cmdMailMove(s *store.Store, g globalFlags, args []string) error {
443+
fs := flag.NewFlagSet("mail-move", flag.ContinueOnError)
444+
destination := fs.String("destination", "", "destination folder id or well-known folder name")
445+
destinationAlias := fs.String("dest-folder", "", "alias for --destination")
446+
if err := fs.Parse(args); err != nil {
447+
return err
448+
}
449+
if *destination == "" {
450+
*destination = *destinationAlias
451+
}
452+
if *destination == "" || fs.NArg() == 0 {
453+
return fmt.Errorf("usage: msx mail-move --destination <folder-id|well-known-name> <message-id> [message-id ...]")
454+
}
455+
return runMailMove(s, g, *destination, fs.Args(), "mail-move")
456+
}
457+
458+
func cmdMailArchive(s *store.Store, g globalFlags, args []string) error {
459+
fs := flag.NewFlagSet("mail-archive", flag.ContinueOnError)
460+
if err := fs.Parse(args); err != nil {
461+
return err
462+
}
463+
if fs.NArg() == 0 {
464+
return fmt.Errorf("usage: msx mail-archive <message-id> [message-id ...]")
465+
}
466+
return runMailMove(s, g, "archive", fs.Args(), "mail-archive")
467+
}
468+
469+
func runMailMove(s *store.Store, g globalFlags, destination string, ids []string, command string) error {
470+
client := newGraphClient(s, g.profile)
471+
results := make([]map[string]any, 0, len(ids))
472+
for _, id := range ids {
473+
id = strings.TrimSpace(id)
474+
if id == "" {
475+
continue
476+
}
477+
body, err := json.Marshal(map[string]any{"destinationId": destination})
478+
if err != nil {
479+
return err
480+
}
481+
data, err := client.RequestWithBody("POST", "/me/messages/"+url.PathEscape(id)+"/move", nil, body)
482+
if err != nil {
483+
return fmt.Errorf("move %s: %w", id, err)
484+
}
485+
results = append(results, data)
486+
}
487+
return emit(g, command, map[string]any{
488+
"ok": true,
489+
"destination": destination,
490+
"count": len(results),
491+
"value": results,
492+
})
493+
}
494+
436495
func cmdAgenda(s *store.Store, g globalFlags, args []string) error {
437496
fs := flag.NewFlagSet("agenda", flag.ContinueOnError)
438497
top := fs.Int("top", 20, "maximum number of events")

cmd/msx/main_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,63 @@ func TestRunDetailCommandsAndNextAgainstTestServer(t *testing.T) {
196196
}
197197
}
198198

199+
func TestRunMailMoveAndArchiveCommands(t *testing.T) {
200+
t.Setenv("MSX_HOME", t.TempDir())
201+
seedProfile(t, os.Getenv("MSX_HOME"), "personal")
202+
203+
var moved []struct {
204+
Path string
205+
Body string
206+
}
207+
withGraphHTTPClient(t, func(r *http.Request) (*http.Response, error) {
208+
if r.Method != http.MethodPost {
209+
t.Fatalf("expected POST, got %s", r.Method)
210+
}
211+
body, err := io.ReadAll(r.Body)
212+
if err != nil {
213+
t.Fatal(err)
214+
}
215+
moved = append(moved, struct {
216+
Path string
217+
Body string
218+
}{Path: r.URL.Path, Body: string(body)})
219+
if strings.HasSuffix(r.URL.Path, "/msg-1/move") {
220+
return jsonHTTPResponse(http.StatusOK, `{"id":"msg-1-moved","subject":"hello"}`), nil
221+
}
222+
if strings.HasSuffix(r.URL.Path, "/msg-2/move") {
223+
return jsonHTTPResponse(http.StatusOK, `{"id":"msg-2-moved","subject":"bye"}`), nil
224+
}
225+
t.Fatalf("unexpected request path: %s", r.URL.Path)
226+
return nil, nil
227+
})
228+
t.Setenv("MSX_GRAPH_BASE_URL", "https://graph.example.test/v1.0")
229+
230+
stdout, stderr, err := captureRun([]string{"--profile", "personal", "mail-move", "--destination", "Archive", "msg-1"})
231+
if err != nil {
232+
t.Fatalf("mail-move failed: %v stderr=%s", err, stderr)
233+
}
234+
if !strings.Contains(stdout, `"destination": "Archive"`) || !strings.Contains(stdout, `"count": 1`) {
235+
t.Fatalf("unexpected mail-move stdout: %s", stdout)
236+
}
237+
238+
stdout, stderr, err = captureRun([]string{"--profile", "personal", "mail-archive", "msg-2"})
239+
if err != nil {
240+
t.Fatalf("mail-archive failed: %v stderr=%s", err, stderr)
241+
}
242+
if !strings.Contains(stdout, `"destination": "archive"`) || !strings.Contains(stdout, `"count": 1`) {
243+
t.Fatalf("unexpected mail-archive stdout: %s", stdout)
244+
}
245+
if len(moved) != 2 {
246+
t.Fatalf("expected 2 move calls, got %d", len(moved))
247+
}
248+
if moved[0].Path != "/v1.0/me/messages/msg-1/move" || !strings.Contains(moved[0].Body, `"destinationId":"Archive"`) {
249+
t.Fatalf("unexpected first move call: %+v", moved[0])
250+
}
251+
if moved[1].Path != "/v1.0/me/messages/msg-2/move" || !strings.Contains(moved[1].Body, `"destinationId":"archive"`) {
252+
t.Fatalf("unexpected second move call: %+v", moved[1])
253+
}
254+
}
255+
199256
func TestRunMailSubjectFilterPreservesTopLevelShape(t *testing.T) {
200257
t.Setenv("MSX_HOME", t.TempDir())
201258
seedProfile(t, os.Getenv("MSX_HOME"), "personal")

0 commit comments

Comments
 (0)