mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 14:27:27 +00:00
Add AgentMail: paid-member mailbox reader (TUI + bot mode) on the BBS
internal/mailbox is a transport-agnostic mail client for Founding Lifetime members: read, search, compose, send, reply, flag, delete. - types/transport/client: paid-gated facade returning JSON-serializable structs (same shapes as @logicsrc/plugin-agentmail) - memory.go: in-memory transport (tests/dev/reference) + tests - imap.go/smtp.go: real backend — go-imap/v2 (Dovecot IMAP, master-user login) + net/smtp submission via the co-located relay; go-message parses bodies/attachments - reader_tui.go: Bubble Tea reader for humans (list/read/flag/delete) - bot.go: JSON in/out mode for agents (ssh mail@host list|read|send|…) - wired as the paid "Mail" hub entry and the ssh mail@ route + auth.MailNames Connects to the self-hosted Mailu stack at mail.profullstack.com / smtp.profullstack.com. Rebased onto main; build + vet + tests clean (Go 1.26). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6b4db2b934
commit
e165ecee26
13 changed files with 1660 additions and 0 deletions
173
internal/mailbox/bot.go
Normal file
173
internal/mailbox/bot.go
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
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 {
|
||||
limit, _ = strconv.Atoi(args[2])
|
||||
}
|
||||
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 {
|
||||
on = !strings.EqualFold(args[3], "off") && args[3] != "false" && args[3] != "0"
|
||||
}
|
||||
if 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue