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) }