Break down monolithic beads.go and mail.go into smaller, single-purpose files: beads package: - beads_agent.go: Agent-related bead operations - beads_delegation.go: Delegation bead handling - beads_dog.go: Dog pool operations - beads_merge_slot.go: Merge slot management - beads_mr.go: Merge request operations - beads_redirect.go: Redirect bead handling - beads_rig.go: Rig bead operations - beads_role.go: Role bead management cmd package: - mail_announce.go: Announcement subcommand - mail_check.go: Mail check subcommand - mail_identity.go: Identity management - mail_inbox.go: Inbox operations - mail_queue.go: Queue subcommand - mail_search.go: Search functionality - mail_send.go: Send subcommand - mail_thread.go: Thread operations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
156 lines
3.7 KiB
Go
156 lines
3.7 KiB
Go
package cmd
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
"github.com/steveyegge/gastown/internal/events"
|
|
"github.com/steveyegge/gastown/internal/mail"
|
|
"github.com/steveyegge/gastown/internal/style"
|
|
"github.com/steveyegge/gastown/internal/workspace"
|
|
)
|
|
|
|
func runMailSend(cmd *cobra.Command, args []string) error {
|
|
var to string
|
|
|
|
if mailSendSelf {
|
|
// Auto-detect identity from cwd
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return fmt.Errorf("getting current directory: %w", err)
|
|
}
|
|
townRoot, err := workspace.FindFromCwd()
|
|
if err != nil || townRoot == "" {
|
|
return fmt.Errorf("not in a Gas Town workspace")
|
|
}
|
|
roleInfo, err := GetRoleWithContext(cwd, townRoot)
|
|
if err != nil {
|
|
return fmt.Errorf("detecting role: %w", err)
|
|
}
|
|
ctx := RoleContext{
|
|
Role: roleInfo.Role,
|
|
Rig: roleInfo.Rig,
|
|
Polecat: roleInfo.Polecat,
|
|
TownRoot: townRoot,
|
|
WorkDir: cwd,
|
|
}
|
|
to = buildAgentIdentity(ctx)
|
|
if to == "" {
|
|
return fmt.Errorf("cannot determine identity (role: %s)", ctx.Role)
|
|
}
|
|
} else if len(args) > 0 {
|
|
to = args[0]
|
|
} else {
|
|
return fmt.Errorf("address required (or use --self)")
|
|
}
|
|
|
|
// All mail uses town beads (two-level architecture)
|
|
workDir, err := findMailWorkDir()
|
|
if err != nil {
|
|
return fmt.Errorf("not in a Gas Town workspace: %w", err)
|
|
}
|
|
|
|
// Determine sender
|
|
from := detectSender()
|
|
|
|
// Create message
|
|
msg := &mail.Message{
|
|
From: from,
|
|
To: to,
|
|
Subject: mailSubject,
|
|
Body: mailBody,
|
|
}
|
|
|
|
// Set priority (--urgent overrides --priority)
|
|
if mailUrgent {
|
|
msg.Priority = mail.PriorityUrgent
|
|
} else {
|
|
msg.Priority = mail.PriorityFromInt(mailPriority)
|
|
}
|
|
if mailNotify && msg.Priority == mail.PriorityNormal {
|
|
msg.Priority = mail.PriorityHigh
|
|
}
|
|
|
|
// Set message type
|
|
msg.Type = mail.ParseMessageType(mailType)
|
|
|
|
// Set pinned flag
|
|
msg.Pinned = mailPinned
|
|
|
|
// Set wisp flag (ephemeral message) - default true, --permanent overrides
|
|
msg.Wisp = mailWisp && !mailPermanent
|
|
|
|
// Set CC recipients
|
|
msg.CC = mailCC
|
|
|
|
// Handle reply-to: auto-set type to reply and look up thread
|
|
if mailReplyTo != "" {
|
|
msg.ReplyTo = mailReplyTo
|
|
if msg.Type == mail.TypeNotification {
|
|
msg.Type = mail.TypeReply
|
|
}
|
|
|
|
// Look up original message to get thread ID
|
|
router := mail.NewRouter(workDir)
|
|
mailbox, err := router.GetMailbox(from)
|
|
if err == nil {
|
|
if original, err := mailbox.Get(mailReplyTo); err == nil {
|
|
msg.ThreadID = original.ThreadID
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate thread ID for new threads
|
|
if msg.ThreadID == "" {
|
|
msg.ThreadID = generateThreadID()
|
|
}
|
|
|
|
// Send via router
|
|
router := mail.NewRouter(workDir)
|
|
|
|
// Check if this is a list address to show fan-out details
|
|
var listRecipients []string
|
|
if strings.HasPrefix(to, "list:") {
|
|
var err error
|
|
listRecipients, err = router.ExpandListAddress(to)
|
|
if err != nil {
|
|
return fmt.Errorf("sending message: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := router.Send(msg); err != nil {
|
|
return fmt.Errorf("sending message: %w", err)
|
|
}
|
|
|
|
// Log mail event to activity feed
|
|
_ = events.LogFeed(events.TypeMail, from, events.MailPayload(to, mailSubject))
|
|
|
|
fmt.Printf("%s Message sent to %s\n", style.Bold.Render("✓"), to)
|
|
fmt.Printf(" Subject: %s\n", mailSubject)
|
|
|
|
// Show fan-out recipients for list addresses
|
|
if len(listRecipients) > 0 {
|
|
fmt.Printf(" Recipients: %s\n", strings.Join(listRecipients, ", "))
|
|
}
|
|
|
|
if len(msg.CC) > 0 {
|
|
fmt.Printf(" CC: %s\n", strings.Join(msg.CC, ", "))
|
|
}
|
|
if msg.Type != mail.TypeNotification {
|
|
fmt.Printf(" Type: %s\n", msg.Type)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateThreadID creates a random thread ID for new message threads.
|
|
func generateThreadID() string {
|
|
b := make([]byte, 6)
|
|
_, _ = rand.Read(b) // crypto/rand.Read only fails on broken system
|
|
return "thread-" + hex.EncodeToString(b)
|
|
}
|