From 9b78b2fa4da9184f98d2118d1d30fd7a9fdc000d Mon Sep 17 00:00:00 2001 From: Drew Smirnoff Date: Mon, 6 Apr 2026 20:40:56 +0400 Subject: [PATCH] feat: one-turn sending and SKILL (#459) --- 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(-) create mode 100644 SKILLS/send-email/SKILL.md create mode 100644 docs/docs/Features/AI_AGENTS.md create mode 100644 docs/docs/Features/CLI.md diff --git a/README.md b/README.md index 9c5a1fbf484545bef6e2ef1bc0fcd60e4e7e6fbd..3a811f4e3ebf71871ec85e862e36c6a62dbd33db 100644 --- a/README.md +++ b/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 diff --git a/SKILLS/send-email/SKILL.md b/SKILLS/send-email/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..d372ccd2a2cb078310227b3ea7468c80aee08373 --- /dev/null +++ b/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 --subject --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 `. Otherwise omit it to use the default account. +4. **Attachments**: If the user mentions files to attach, use `--attach ` 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. diff --git a/docs/docs/Features/AI_AGENTS.md b/docs/docs/Features/AI_AGENTS.md new file mode 100644 index 0000000000000000000000000000000000000000..98110456ece03d0688ac87dbd5b236e00f446d65 --- /dev/null +++ b/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. diff --git a/docs/docs/Features/CLI.md b/docs/docs/Features/CLI.md new file mode 100644 index 0000000000000000000000000000000000000000..bcb8be7b3e7eae9a3762e6e8d6374b8dc4416d08 --- /dev/null +++ b/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 --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 # Authorize a Gmail account (opens browser) +matcha gmail token # Print a fresh access token +matcha gmail revoke # 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 +``` diff --git a/docs/docs/index.md b/docs/docs/index.md index 86166b92ed9721e716a759cdff6cf3d2f9409589..8d4e9747fa1470a83b1490fd848d443e7ce29ba4 100644 --- a/docs/docs/index.md +++ b/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 diff --git a/docs/docs/usage.md b/docs/docs/usage.md index f79049222fcafc98132a3ee3656a5f24ec3c861b..65d2d2ce8f1cd087818a42e4438e7e359e6d8dcd 100644 --- a/docs/docs/usage.md +++ b/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 # Authorize a Gmail account +matcha gmail token # Print a fresh access token +matcha gmail revoke # Revoke stored tokens +``` diff --git a/main.go b/main.go index e02114aac67643bde0dd94c324625ecf2e43a3f4..bb9ee342e7f097c690b7f2bf58af377120ae30c2 100644 --- a/main.go +++ b/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 --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)