@@ -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.
@@ -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.
@@ -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
+```
@@ -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)