mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +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
|
|
@ -61,6 +61,7 @@ import (
|
|||
"github.com/profullstack/agentbbs/internal/hub"
|
||||
"github.com/profullstack/agentbbs/internal/irc"
|
||||
"github.com/profullstack/agentbbs/internal/mail"
|
||||
"github.com/profullstack/agentbbs/internal/mailbox"
|
||||
"github.com/profullstack/agentbbs/internal/news"
|
||||
"github.com/profullstack/agentbbs/internal/payments"
|
||||
"github.com/profullstack/agentbbs/internal/plugin"
|
||||
|
|
@ -317,6 +318,8 @@ func (a *app) router() wish.Middleware {
|
|||
a.handleIRC(s)
|
||||
case auth.IsNewsName(user):
|
||||
a.handleNews(s)
|
||||
case auth.IsMailName(user):
|
||||
a.handleMail(s)
|
||||
case isVideo:
|
||||
a.handleVideo(s, code)
|
||||
case user == "agent":
|
||||
|
|
@ -472,6 +475,28 @@ func (a *app) sessionApps(s ssh.Session, su store.User, guest bool) []hub.Sessio
|
|||
Cmd: sessionExec{run: func() error { return a.runNews(s, su.Name) }},
|
||||
})
|
||||
|
||||
// Mail — a Founding Lifetime Member perk: the AgentMail TUI.
|
||||
mailLock := ""
|
||||
switch {
|
||||
case guest:
|
||||
mailLock = membersOnly
|
||||
case !su.Premium:
|
||||
mailLock = "Founding Lifetime Member feature ($99 one-time) — upgrade: ssh join@" + a.host
|
||||
}
|
||||
apps = append(apps, hub.SessionApp{
|
||||
Title: "Mail",
|
||||
Description: "your " + a.fe.Domain + " mailbox",
|
||||
Locked: mailLock,
|
||||
Cmd: sessionExec{run: func() error {
|
||||
c, err := a.mailClientFor(su)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
return mailbox.RunReader(s, c)
|
||||
}},
|
||||
})
|
||||
|
||||
// Tor — a Founding Lifetime Member perk: a torsocks shell in the pod.
|
||||
torLock := ""
|
||||
switch {
|
||||
|
|
@ -1273,6 +1298,83 @@ func (a *app) runNews(s ssh.Session, name string) error {
|
|||
return news.RunReader(s, addr, name)
|
||||
}
|
||||
|
||||
// mailClientFor builds a paid-gated AgentMail client for a member, connecting to
|
||||
// the self-hosted Mailu backend. IMAP uses Dovecot master-user auth (login
|
||||
// "<name>*<master>") so the BBS gateway can open any member's mailbox with one
|
||||
// secret; SMTP defaults to the co-located relay (no auth). Returns an error if
|
||||
// the IMAP connection/login fails.
|
||||
func (a *app) mailClientFor(su store.User) (*mailbox.Client, error) {
|
||||
domain := env("AGENTBBS_MAIL_DOMAIN", "mail.profullstack.com")
|
||||
login := su.Name
|
||||
if master := os.Getenv("AGENTBBS_MAIL_MASTER_USER"); master != "" {
|
||||
login = su.Name + "*" + master
|
||||
}
|
||||
cfg := mailbox.IMAPConfig{
|
||||
IMAPAddr: env("AGENTBBS_MAIL_IMAP_ADDR", domain+":993"),
|
||||
SMTPAddr: env("AGENTBBS_MAIL_SMTP_ADDR", "127.0.0.1:25"),
|
||||
Username: login,
|
||||
Password: os.Getenv("AGENTBBS_MAIL_MASTER_PASS"),
|
||||
// SMTPUser/SMTPPass left empty: submit via the trusted local relay.
|
||||
}
|
||||
tr, err := mailbox.NewIMAPTransport(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mailbox.NewClient(tr, mailbox.Identity{Name: su.Name, Paid: su.Premium}, domain, 50), nil
|
||||
}
|
||||
|
||||
// handleMail routes a Founding Lifetime member into AgentMail: an interactive
|
||||
// TUI when a PTY is present and no command is given, or the JSON bot mode
|
||||
// (ssh mail@host <cmd>, or no PTY) for agents.
|
||||
func (a *app) handleMail(s ssh.Session) {
|
||||
fp := auth.Fingerprint(s.PublicKey())
|
||||
if fp == "" {
|
||||
wish.Println(s, "mail@ needs your registered SSH key. New here? ssh join@"+a.host)
|
||||
_ = s.Exit(1)
|
||||
return
|
||||
}
|
||||
u, found, err := a.st.UserByFingerprint(fp)
|
||||
if err != nil || !found {
|
||||
wish.Println(s, "key not registered — run: ssh join@"+a.host)
|
||||
_ = s.Exit(1)
|
||||
return
|
||||
}
|
||||
if u.Banned {
|
||||
wish.Println(s, "this account is suspended.")
|
||||
_ = s.Exit(1)
|
||||
return
|
||||
}
|
||||
if !a.ensurePremium(&u) {
|
||||
wish.Println(s, " mail is a Founding Lifetime Member feature ($99 one-time). Upgrade: ssh join@"+a.host)
|
||||
_ = s.Exit(1)
|
||||
return
|
||||
}
|
||||
sessID, _ := a.st.RecordSession(u.ID, s.User(), remoteIP(s), "mail")
|
||||
defer func() { _ = a.st.EndSession(sessID) }()
|
||||
|
||||
c, err := a.mailClientFor(u)
|
||||
if err != nil {
|
||||
wish.Println(s, "mail: "+err.Error())
|
||||
_ = s.Exit(1)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
args := s.Command()
|
||||
_, _, hasPty := s.Pty()
|
||||
if len(args) > 0 || !hasPty {
|
||||
// Agent/bot mode: JSON in, JSON out.
|
||||
if err := mailbox.RunBot(s.Context(), c, args, s, s); err != nil {
|
||||
_ = s.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := mailbox.RunReader(s, c); err != nil {
|
||||
wish.Println(s, "mail: "+err.Error())
|
||||
_ = s.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// handleTorCmd runs an arbitrary command through Tor (torsocks) inside the
|
||||
// member's pod, never on the host. Premium; requires a PTY.
|
||||
func (a *app) handleTorCmd(s ssh.Session) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue