agentbbs/internal/mailbox/bot.go
RissRIce ee06082ce1
fix(mailbox): reject invalid bot toggle values (#80)
Co-authored-by: rissrice2105-agent <rissrice2105-agent@users.noreply.github.com>
2026-07-07 05:40:35 -07:00

192 lines
4.7 KiB
Go

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 <mailbox> <uid> full message (marks seen; "peek" 4th arg to keep unseen)
search <query> [mailbox] search summaries
send read a JSON Draft on stdin, send it
reply <mailbox> <uid> read {"text":...,"replyAll":bool} on stdin
flag <mailbox> <uid> [on|off] set/clear the flagged flag
seen <mailbox> <uid> [on|off] set/clear the seen flag
delete <mailbox> <uid> 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 <query> [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 <mailbox> <uid>", 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
}
}