package mailbox import ( "context" "encoding/json" "fmt" "io" "strconv" "strings" ) // BotUsage is printed when a bot/agent invokes mail with no/unknown command. const BotUsage = `agentmail (non-interactive mode) — JSON in, JSON out: mailboxes list folders list [mailbox] [limit] message summaries (default INBOX) read full message (marks seen; "peek" 4th arg to keep unseen) search [mailbox] search summaries send read a JSON Draft on stdin, send it reply read {"text":...,"replyAll":bool} on stdin flag [on|off] set/clear the flagged flag seen [on|off] set/clear the seen flag delete delete a message Example: ssh mail@bbs.profullstack.com list INBOX 20` // RunBot executes one non-interactive command for agents/bots, writing JSON to // out. It returns an error (also emitted as {"error":...}) on failure. func RunBot(ctx context.Context, c *Client, args []string, in io.Reader, out io.Writer) error { if len(args) == 0 { fmt.Fprintln(out, BotUsage) return nil } enc := json.NewEncoder(out) enc.SetIndent("", " ") fail := func(err error) error { _ = enc.Encode(map[string]string{"error": err.Error()}) return err } switch strings.ToLower(args[0]) { case "mailboxes": v, err := c.Mailboxes(ctx) if err != nil { return fail(err) } return enc.Encode(v) case "list", "ls": mailbox := Inbox if len(args) > 1 { mailbox = args[1] } limit := 0 if len(args) > 2 { n, err := strconv.Atoi(args[2]) if err != nil || n <= 0 { return fail(fmt.Errorf("invalid list limit %q", args[2])) } limit = n } v, err := c.List(ctx, mailbox, limit) if err != nil { return fail(err) } return enc.Encode(v) case "read", "show": mailbox, uid, err := mailboxUID(args) if err != nil { return fail(err) } peek := len(args) > 3 && strings.EqualFold(args[3], "peek") msg, ok, err := c.Read(ctx, mailbox, uid, peek) if err != nil { return fail(err) } if !ok { return fail(notFound(mailbox, uid)) } return enc.Encode(msg) case "search": if len(args) < 2 { return fail(fmt.Errorf("usage: search [mailbox]")) } mailbox := "" if len(args) > 2 { mailbox = args[2] } v, err := c.Search(ctx, args[1], mailbox, 0) if err != nil { return fail(err) } return enc.Encode(v) case "send": var d Draft if err := json.NewDecoder(in).Decode(&d); err != nil { return fail(fmt.Errorf("send expects a JSON Draft on stdin: %w", err)) } res, err := c.Send(ctx, d) if err != nil { return fail(err) } return enc.Encode(res) case "reply": mailbox, uid, err := mailboxUID(args) if err != nil { return fail(err) } var body struct { Text string `json:"text"` ReplyAll bool `json:"replyAll"` } if err := json.NewDecoder(in).Decode(&body); err != nil { return fail(fmt.Errorf("reply expects {\"text\":...} on stdin: %w", err)) } orig, ok, err := c.Read(ctx, mailbox, uid, true) if err != nil { return fail(err) } if !ok { return fail(notFound(mailbox, uid)) } res, err := c.Reply(ctx, orig, body.Text, body.ReplyAll) if err != nil { return fail(err) } return enc.Encode(res) case "flag", "seen": mailbox, uid, err := mailboxUID(args) if err != nil { return fail(err) } on := true if len(args) > 3 { var ok bool on, ok = parseBotBool(args[3]) if !ok { return fail(fmt.Errorf("invalid %s value %q; use on/off", args[0], args[3])) } } if strings.ToLower(args[0]) == "flag" { err = c.Flag(ctx, mailbox, uid, on) } else { err = c.MarkSeen(ctx, mailbox, uid, on) } if err != nil { return fail(err) } return enc.Encode(map[string]any{"ok": true, "mailbox": mailbox, "uid": uid, args[0]: on}) case "delete", "rm": mailbox, uid, err := mailboxUID(args) if err != nil { return fail(err) } if err := c.Delete(ctx, mailbox, uid); err != nil { return fail(err) } return enc.Encode(map[string]any{"ok": true, "deleted": map[string]any{"mailbox": mailbox, "uid": uid}}) default: fmt.Fprintln(out, BotUsage) return fmt.Errorf("unknown command: %s", args[0]) } } func mailboxUID(args []string) (string, uint32, error) { if len(args) < 3 { return "", 0, fmt.Errorf("usage: %s ", args[0]) } uid, err := strconv.ParseUint(args[2], 10, 32) if err != nil { return "", 0, fmt.Errorf("invalid uid %q", args[2]) } return args[1], uint32(uid), nil } func parseBotBool(value string) (bool, bool) { switch strings.ToLower(strings.TrimSpace(value)) { case "on", "true", "1": return true, true case "off", "false", "0": return false, true default: return false, false } }