feat: one-turn sending and SKILL (#459)

Drew Smirnoff created

Change summary

README.md                       |   8 +
SKILLS/send-email/SKILL.md      |  82 ++++++++++++++++
docs/docs/Features/AI_AGENTS.md |  91 ++++++++++++++++++
docs/docs/Features/CLI.md       | 121 ++++++++++++++++++++++++
docs/docs/index.md              |   3 
docs/docs/usage.md              |  26 +++++
main.go                         | 171 +++++++++++++++++++++++++++++++++++
7 files changed, 500 insertions(+), 2 deletions(-)

Detailed changes

README.md 🔗

@@ -22,11 +22,17 @@
 
 **A powerful, feature-rich email client for your terminal.** Built with Go and the Bubble Tea TUI framework, Matcha brings a beautiful, modern email experience to the command line with support for rich content, multiple accounts, and advanced terminal features.
 
+![Demo GIF](public/assets/demo.gif)
 
+### AI Agent Support
 
+Matcha can be used by autonomous AI agents to send emails on your behalf. The `matcha send` CLI command provides a non-interactive interface for composing and sending emails.
 
+```bash
+matcha send --to alice@example.com --subject "Hello" --body "Sent by my AI agent"
+```
 
-![Demo GIF](public/assets/demo.gif)
+[Learn more](https://docs.matcha.floatpane.com/Features/AI_AGENTS)
 
 ## Documentation
 

SKILLS/send-email/SKILL.md 🔗

@@ -0,0 +1,82 @@
+---
+name: send-email
+description: >
+  Use when the user wants to send an email, compose a message, or email someone.
+  Triggers: send email, email someone, compose email, write email, mail to, message to,
+  send a message, draft and send. Works with multiple accounts and recipients.
+---
+
+# Send Email Skill
+
+Send emails from the terminal using matcha's configured accounts.
+
+## How It Works
+
+Matcha has a `send` CLI subcommand that sends emails non-interactively:
+
+```
+matcha send --to <recipients> --subject <subject> --body <body> [flags]
+```
+
+## Available Flags
+
+| Flag | Description |
+|------|-------------|
+| `--to` | Recipient(s), comma-separated **(required)** |
+| `--subject` | Email subject **(required)** |
+| `--body` | Email body (Markdown supported). Use `"-"` to read from stdin |
+| `--from` | Sender account email (defaults to first configured account) |
+| `--cc` | CC recipient(s), comma-separated |
+| `--bcc` | BCC recipient(s), comma-separated |
+| `--attach` | Attachment file path (can be repeated for multiple files) |
+| `--signature` | Append default signature (default: true). Use `--signature=false` to disable |
+| `--sign-smime` | Sign with S/MIME (uses account default if not set) |
+| `--encrypt-smime` | Encrypt with S/MIME |
+| `--sign-pgp` | Sign with PGP (uses account default if not set) |
+
+## Instructions
+
+1. **Always ask the user** for the recipient (`--to`) and subject (`--subject`) if not provided.
+2. **Ask for the body** or compose it based on the user's intent. The body supports Markdown.
+3. **Account selection**: If the user specifies which account to send from, use `--from <email>`. Otherwise omit it to use the default account.
+4. **Attachments**: If the user mentions files to attach, use `--attach <path>` for each one. This flag can be repeated.
+5. **Stdin body**: For long or multi-line bodies, pipe content into the command with `--body -`.
+6. **Show the command** to the user before running it so they can confirm.
+
+## Examples
+
+Simple email:
+```bash
+matcha send --to alice@example.com --subject "Meeting tomorrow" --body "Hi Alice, can we meet at 2pm?"
+```
+
+From a specific account:
+```bash
+matcha send --from work@company.com --to client@example.com --subject "Invoice" --body "Please find the invoice attached." --attach ~/Documents/invoice.pdf
+```
+
+Multiple recipients with CC:
+```bash
+matcha send --to alice@example.com,bob@example.com --cc manager@example.com --subject "Project update" --body "The project is on track."
+```
+
+Long body from stdin:
+```bash
+cat ~/notes/report.md | matcha send --to team@example.com --subject "Weekly Report" --body -
+```
+
+Multiple attachments:
+```bash
+matcha send --to alice@example.com --subject "Files" --body "Here are the files." --attach report.pdf --attach data.csv
+```
+
+No signature:
+```bash
+matcha send --to alice@example.com --subject "Quick note" --body "Thanks!" --signature=false
+```
+
+## Account Configuration
+
+Accounts are configured in `~/.config/matcha/config.json`. The `--from` flag matches against both the login email and fetch email fields. If omitted, the first configured account is used.
+
+To list configured accounts, the user can check their matcha settings in the TUI.

docs/docs/Features/AI_AGENTS.md 🔗

@@ -0,0 +1,91 @@
+---
+title: AI Agents
+sidebar_position: 11
+---
+
+# AI Agent Integration
+
+Matcha can be used by AI coding agents (like Claude Code) to send emails on your behalf. This is powered by the [`matcha send`](./CLI.md) CLI command and an optional Claude Code skill.
+
+## How It Works
+
+The `matcha send` command provides a non-interactive interface for sending emails. AI agents can construct and execute this command to send emails without launching the TUI.
+
+```bash
+matcha send --to alice@example.com --subject "Meeting notes" --body "Here are the notes from today."
+```
+
+The agent can:
+
+- Send to one or multiple recipients (`--to`, `--cc`, `--bcc`)
+- Choose which account to send from (`--from`)
+- Attach files (`--attach`)
+- Pipe in long message bodies from stdin (`--body -`)
+- Control signing and encryption (`--sign-smime`, `--sign-pgp`, `--encrypt-smime`)
+
+See the full [CLI reference](./CLI.md) for all available flags.
+
+Matcha ships with a built-in skill at `SKILLS/send-email/`.
+
+### What the Skill Does
+
+When you ask an AI agent to send an email, the skill provides it with:
+
+- The correct `matcha send` command syntax
+- All available flags and their usage
+- Instructions to confirm with you before sending
+- Examples for common scenarios
+
+### Usage
+
+Just ask the agent naturally:
+
+> "Send an email to alice@example.com about the project deadline"
+
+> "Email the team the weekly report from my work account"
+
+> "Send bob the meeting notes with the slides attached"
+
+The agent will compose the appropriate `matcha send` command, show it to you for confirmation, and execute it.
+
+### Installing the Skill Globally for Claude Code
+
+The skill is included in the matcha repository at `./SKILLS/send-email/SKILL.md`. To make it available in all your projects, symlink it to your global Claude Code skills directory:
+
+```bash
+mkdir -p ~/.claude/skills
+ln -s /path/to/matcha/.claude/skills/send-email ~/.claude/skills/send-email
+```
+
+After linking, the `send-email` skill will be available in any project where you use Claude Code.
+
+## Scripting and Automation
+
+Beyond AI agents, `matcha send` works in any automation context:
+
+**Cron job for daily reports:**
+
+```bash
+# Send a daily digest at 9am
+0 9 * * * cat /tmp/daily-report.md | matcha send --to team@company.com --subject "Daily Report" --body -
+```
+
+**Git hook for notifications:**
+
+```bash
+#!/bin/sh
+matcha send --to reviewer@company.com --subject "New commit pushed" \
+  --body "A new commit was pushed to the repository."
+```
+
+**CI/CD pipeline notifications:**
+
+```bash
+matcha send --to devops@company.com --subject "Deploy complete" \
+  --body "Version $VERSION deployed to production." --signature=false
+```
+
+## Prerequisites
+
+- At least one email account must be configured in matcha. Run `matcha` (the TUI) to set up an account if you haven't already.
+- The account's password or OAuth2 tokens must be stored in the system keyring.

docs/docs/Features/CLI.md 🔗

@@ -0,0 +1,121 @@
+---
+title: CLI
+sidebar_position: 10
+---
+
+# CLI Commands
+
+Matcha provides several subcommands for non-interactive use. These work without launching the TUI and are ideal for scripts, cron jobs, and AI agent integration.
+
+## matcha send
+
+Send an email directly from the command line.
+
+```bash
+matcha send --to <recipients> --subject <subject> [flags]
+```
+
+### Flags
+
+| Flag | Description |
+|------|-------------|
+| `--to` | Recipient(s), comma-separated **(required)** |
+| `--subject` | Email subject **(required)** |
+| `--body` | Email body (Markdown supported). Use `"-"` to read from stdin |
+| `--from` | Sender account email. Defaults to first configured account |
+| `--cc` | CC recipient(s), comma-separated |
+| `--bcc` | BCC recipient(s), comma-separated |
+| `--attach` | Attachment file path. Can be repeated for multiple files |
+| `--signature` | Append default signature (default: `true`). Use `--signature=false` to disable |
+| `--sign-smime` | Sign with S/MIME. Uses account default if not set |
+| `--encrypt-smime` | Encrypt with S/MIME |
+| `--sign-pgp` | Sign with PGP. Uses account default if not set |
+
+### Examples
+
+**Simple email:**
+
+```bash
+matcha send --to alice@example.com --subject "Meeting tomorrow" --body "Can we meet at 2pm?"
+```
+
+**Send from a specific account:**
+
+```bash
+matcha send --from work@company.com --to client@example.com --subject "Invoice" \
+  --body "Please find the invoice attached." --attach ~/Documents/invoice.pdf
+```
+
+**Multiple recipients with CC:**
+
+```bash
+matcha send --to alice@example.com,bob@example.com --cc manager@example.com \
+  --subject "Project update" --body "The project is on track."
+```
+
+**Read body from stdin (useful for piping):**
+
+```bash
+cat ~/notes/report.md | matcha send --to team@example.com --subject "Weekly Report" --body -
+```
+
+**Multiple attachments:**
+
+```bash
+matcha send --to alice@example.com --subject "Files" --body "Here are the files." \
+  --attach report.pdf --attach data.csv
+```
+
+**Without signature:**
+
+```bash
+matcha send --to alice@example.com --subject "Quick note" --body "Thanks!" --signature=false
+```
+
+### Account Selection
+
+The `--from` flag matches against both the login email and fetch email of your configured accounts. If omitted, the first configured account is used.
+
+```bash
+# Use your work account
+matcha send --from work@company.com --to someone@example.com --subject "Hi" --body "Hello"
+```
+
+### Exit Codes
+
+| Code | Meaning |
+|------|---------|
+| `0` | Email sent successfully |
+| `1` | Error (missing flags, bad config, send failure) |
+
+## matcha update
+
+Check for and install the latest version of Matcha.
+
+```bash
+matcha update
+```
+
+Automatically detects your installation method (Homebrew, Snap, Flatpak, WinGet, or binary) and updates accordingly.
+
+## matcha gmail
+
+Manage Gmail OAuth2 authorization.
+
+```bash
+matcha gmail auth <email>     # Authorize a Gmail account (opens browser)
+matcha gmail token <email>    # Print a fresh access token
+matcha gmail revoke <email>   # Revoke and delete stored tokens
+```
+
+Requires OAuth2 client credentials in `~/.config/matcha/oauth_client.json`. See the [Gmail setup guide](../setup-guides/gmail.md) for details.
+
+## matcha version
+
+Print the current version.
+
+```bash
+matcha --version
+matcha -v
+matcha version
+```

docs/docs/index.md 🔗

@@ -31,6 +31,9 @@ Matcha is packed with features to make email management in the terminal a breeze
 - [**Drafts**](./Features/DRAFTS.md) - Save unfinished emails and pick them up later.
 - [**UI**](./Features/UI.md) - A responsive, tabbed interface with color-coding and clear focus management.
 - [**Advanced**](./Features/ADVANCED.md) - Automatic updates, smart image rendering, and performance optimization.
+- [**CLI**](./Features/CLI.md) - Send emails and manage accounts from the command line.
+- [**AI Agents**](./Features/AI_AGENTS.md) - Let AI coding agents send emails through Matcha.
+- [**Plugins**](./Features/Plugins.md) - Extend Matcha with Lua plugins.
 
 ### Image & Hyperlink Support
 

docs/docs/usage.md 🔗

@@ -59,7 +59,21 @@ On first launch, Matcha will prompt you to configure an email account. You'll ne
 - `↑/↓` - Navigate contact suggestions (when typing in "To" field)
 - `Esc` - Save draft and exit
 
-## Updating Matcha
+## CLI Commands
+
+Matcha includes several CLI subcommands that work without launching the TUI.
+
+### Send Email
+
+Send an email directly from the command line:
+
+```bash
+matcha send --to user@example.com --subject "Hello" --body "Hi there"
+```
+
+This is useful for scripts, automation, and [AI agent integration](./Features/AI_AGENTS.md). See the full [CLI reference](./Features/CLI.md) for all options.
+
+### Update
 
 Check for updates and install the latest version:
 
@@ -72,3 +86,13 @@ This command will:
 1. Check for the latest release on GitHub
 2. Detect your installation method (Homebrew, Snap, or binary)
 3. Update using the appropriate method
+
+### Gmail OAuth2
+
+Manage Gmail OAuth2 authorization:
+
+```bash
+matcha gmail auth <email>     # Authorize a Gmail account
+matcha gmail token <email>    # Print a fresh access token
+matcha gmail revoke <email>   # Revoke stored tokens
+```

main.go 🔗

@@ -6,6 +6,7 @@ import (
 	"compress/gzip"
 	"encoding/base64"
 	"encoding/json"
+	"flag"
 	"fmt"
 	"io"
 	"log"
@@ -2291,6 +2292,170 @@ func runGmailOAuthCLI(args []string) {
 	}
 }
 
+// stringSliceFlag implements flag.Value to allow repeated --attach flags.
+type stringSliceFlag []string
+
+func (s *stringSliceFlag) String() string { return strings.Join(*s, ", ") }
+func (s *stringSliceFlag) Set(val string) error {
+	*s = append(*s, val)
+	return nil
+}
+
+// runSendCLI implements the CLI entrypoint for `matcha send`.
+// It sends an email non-interactively using configured accounts.
+func runSendCLI(args []string) {
+	fs := flag.NewFlagSet("send", flag.ExitOnError)
+
+	to := fs.String("to", "", "Recipient(s), comma-separated (required)")
+	cc := fs.String("cc", "", "CC recipient(s), comma-separated")
+	bcc := fs.String("bcc", "", "BCC recipient(s), comma-separated")
+	subject := fs.String("subject", "", "Email subject (required)")
+	body := fs.String("body", "", `Email body (Markdown supported). Use "-" to read from stdin`)
+	from := fs.String("from", "", "Sender account email (defaults to first configured account)")
+	withSignature := fs.Bool("signature", true, "Append default signature")
+	signSMIME := fs.Bool("sign-smime", false, "Sign with S/MIME")
+	encryptSMIME := fs.Bool("encrypt-smime", false, "Encrypt with S/MIME")
+	signPGP := fs.Bool("sign-pgp", false, "Sign with PGP")
+
+	var attachments stringSliceFlag
+	fs.Var(&attachments, "attach", "Attachment file path (can be repeated)")
+
+	fs.Usage = func() {
+		fmt.Fprintln(os.Stderr, "Usage: matcha send [flags]")
+		fmt.Fprintln(os.Stderr, "")
+		fmt.Fprintln(os.Stderr, "Send an email non-interactively using a configured account.")
+		fmt.Fprintln(os.Stderr, "")
+		fmt.Fprintln(os.Stderr, "Flags:")
+		fs.PrintDefaults()
+		fmt.Fprintln(os.Stderr, "")
+		fmt.Fprintln(os.Stderr, "Examples:")
+		fmt.Fprintln(os.Stderr, `  matcha send --to user@example.com --subject "Hello" --body "Hi there"`)
+		fmt.Fprintln(os.Stderr, `  echo "Body text" | matcha send --to user@example.com --subject "Hello" --body -`)
+		fmt.Fprintln(os.Stderr, `  matcha send --to user@example.com --subject "Report" --body "See attached" --attach report.pdf`)
+	}
+
+	if err := fs.Parse(args); err != nil {
+		os.Exit(1)
+	}
+
+	if *to == "" || *subject == "" {
+		fmt.Fprintln(os.Stderr, "Error: --to and --subject are required")
+		fs.Usage()
+		os.Exit(1)
+	}
+
+	// Read body from stdin if "-"
+	emailBody := *body
+	if emailBody == "-" {
+		data, err := io.ReadAll(os.Stdin)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err)
+			os.Exit(1)
+		}
+		emailBody = string(data)
+	}
+
+	// Load config
+	cfg, err := config.LoadConfig()
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Error loading config: %v\n", err)
+		os.Exit(1)
+	}
+	if !cfg.HasAccounts() {
+		fmt.Fprintln(os.Stderr, "Error: no accounts configured. Run matcha to set up an account first.")
+		os.Exit(1)
+	}
+
+	// Resolve account
+	var account *config.Account
+	if *from != "" {
+		account = cfg.GetAccountByEmail(*from)
+		if account == nil {
+			// Also try matching against FetchEmail
+			for i := range cfg.Accounts {
+				if strings.EqualFold(cfg.Accounts[i].FetchEmail, *from) {
+					account = &cfg.Accounts[i]
+					break
+				}
+			}
+		}
+		if account == nil {
+			fmt.Fprintf(os.Stderr, "Error: no account found matching %q\n", *from)
+			os.Exit(1)
+		}
+	} else {
+		account = cfg.GetFirstAccount()
+	}
+
+	// Use account S/MIME/PGP defaults unless explicitly set
+	if !isFlagSet(fs, "sign-smime") {
+		*signSMIME = account.SMIMESignByDefault
+	}
+	if !isFlagSet(fs, "sign-pgp") {
+		*signPGP = account.PGPSignByDefault
+	}
+
+	// Append signature
+	if *withSignature {
+		if sig, err := config.LoadSignature(); err == nil && sig != "" {
+			emailBody = emailBody + "\n\n" + sig
+		}
+	}
+
+	// Process inline images (same logic as TUI sendEmail)
+	images := make(map[string][]byte)
+	re := regexp.MustCompile(`!\[.*?\]\((.*?)\)`)
+	matches := re.FindAllStringSubmatch(emailBody, -1)
+	for _, match := range matches {
+		imgPath := match[1]
+		imgData, err := os.ReadFile(imgPath)
+		if err != nil {
+			log.Printf("Could not read image file %s: %v", imgPath, err)
+			continue
+		}
+		cid := fmt.Sprintf("%s%s@%s", uuid.NewString(), filepath.Ext(imgPath), "matcha")
+		images[cid] = []byte(base64.StdEncoding.EncodeToString(imgData))
+		emailBody = strings.Replace(emailBody, imgPath, "cid:"+cid, 1)
+	}
+
+	htmlBody := markdownToHTML([]byte(emailBody))
+
+	// Process attachments
+	attachMap := make(map[string][]byte)
+	for _, attachPath := range attachments {
+		fileData, err := os.ReadFile(attachPath)
+		if err != nil {
+			fmt.Fprintf(os.Stderr, "Error reading attachment %s: %v\n", attachPath, err)
+			os.Exit(1)
+		}
+		attachMap[filepath.Base(attachPath)] = fileData
+	}
+
+	// Send
+	recipients := splitEmails(*to)
+	ccList := splitEmails(*cc)
+	bccList := splitEmails(*bcc)
+
+	err = sender.SendEmail(account, recipients, ccList, bccList, *subject, emailBody, string(htmlBody), images, attachMap, "", nil, *signSMIME, *encryptSMIME, *signPGP, false)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+		os.Exit(1)
+	}
+
+	fmt.Println("Email sent successfully.")
+}
+
+// isFlagSet returns true if the named flag was explicitly provided on the command line.
+func isFlagSet(fs *flag.FlagSet, name string) bool {
+	found := false
+	fs.Visit(func(f *flag.Flag) {
+		if f.Name == name {
+			found = true
+		}
+	})
+	return found
+}
+
 func runUpdateCLI() error {
 	const api = "https://api.github.com/repos/floatpane/matcha/releases/latest"
 	resp, err := http.Get(api)
@@ -2632,6 +2797,12 @@ func main() {
 		os.Exit(0)
 	}
 
+	// Send email CLI subcommand: matcha send --to <email> --subject <subject> [flags]
+	if len(os.Args) > 1 && os.Args[1] == "send" {
+		runSendCLI(os.Args[2:])
+		os.Exit(0)
+	}
+
 	cfg, err := config.LoadConfig()
 	if err == nil && cfg.Theme != "" {
 		theme.SetTheme(cfg.Theme)