Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## [Unreleased]

### Added

- `kagi mail send` submits plain-text email through Kagi Mail's SMTP endpoint, with separate environment-only credentials and required verified STARTTLS.

## [0.20.1]

### Fixed
Expand Down
75 changes: 71 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ reqwest = { version = "0.12.15", default-features = false, features = ["brotli",
futures-util = "0.3.34"
fs2 = "0.4"
jsonc-parser = { version = "0.33.1", features = ["serde"] }
lettre = { version = "0.11.23", default-features = false, features = ["builder", "smtp-transport", "tokio1-rustls", "ring", "webpki-roots"] }
scraper = "0.27.0"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.151"
Expand All @@ -45,4 +46,5 @@ toon = "0.1.2"

[dev-dependencies]
httpmock = "0.8.3"
parking_lot = "0.12.5"
tempfile = "3.27.0"
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,11 +149,31 @@ kagi mail read MESSAGE_ID --format pretty
kagi mail read --thread THREAD_ID --new-text-only
```

Mail uses its own OAuth credentials. It supports `--profile`, JSON by default,
and `--format compact|toon|pretty`. The MCP exposes read operations only;
messages are not added to the local history or response cache.
Read-only MCP access uses separate OAuth credentials and supports `--profile`.
Mail defaults to JSON and supports `--format compact|toon|pretty`. Messages are
not added to the local history or response cache.

To send plain-text mail, supply SMTP credentials only through the environment
variables `KAGI_MAIL_SMTP_USERNAME` and `KAGI_MAIL_SMTP_PASSWORD`, then run:

```bash
kagi mail send \
--from sender@example.com \
--to recipient@example.com \
--subject "Meeting notes" \
--body "The meeting starts at 10."
```

All four flags, `--from`, `--to`, `--subject`, and `--body`, are required.
Sending uses `mail.kagimail.com:587` with required, certificate-verified STARTTLS,
with no plaintext or unverified TLS fallback. HTML and attachments are not
supported. SMTP credentials are env-only, not command-line flags or saved
config. The read-only MCP OAuth token is separate and is never reused for
sending; `kagi mail login` does not supply SMTP credentials. SMTP errors do not
echo credentials, message content, or remote response text.

See the [mail command reference](docs/content/docs/commands/mail.mdx) for setup,
filters, and token handling.
filters, sending, and token handling.

## auth model

Expand All @@ -163,6 +183,7 @@ filters, and token handling.
| `KAGI_API_KEY` | current `/api/v1` Search API and Extract API with `Bearer` auth |
| `KAGI_API_TOKEN` | legacy `/api/v0` public `summarize`, `fastgpt`, `enrich web`, and `enrich news` with `Bot` auth |
| saved mail OAuth tokens or `KAGI_MAIL_ACCESS_TOKEN` | `mail boxes`, `mail search`, and `mail read` |
| `KAGI_MAIL_SMTP_USERNAME` and `KAGI_MAIL_SMTP_PASSWORD` | `mail send` only, through SMTP |
| none | `news`, `smallweb`, `auth status`, `mail status`, `--help` |

example config:
Expand Down Expand Up @@ -211,7 +232,7 @@ for the full command-to-token matrix, use the [`auth-matrix`](https://kagi.micr.
| `kagi skills` | list and load embedded, version-matched agent skills with `skills get kagi-usage` as the agent starting point |
| `kagi batch` | run multiple searches in parallel with JSON, TOON, compact, pretty, markdown, or csv output and shared filters |
| `kagi auth` | launch the auth wizard, or inspect, validate, and save credentials |
| `kagi mail` | list mailboxes, search mail, and read messages or threads with separate OAuth login |
| `kagi mail` | list mailboxes, search and read mail with separate OAuth login, or send plain-text mail through SMTP |
| `kagi completion` | generate or install shell completions for bash, zsh, fish, or PowerShell |
| `kagi summarize` | use the paid public summarizer API or the subscriber summarizer with `--subscriber` |
| `kagi extract` | extract a page's full content as markdown through the current paid API, using `KAGI_API_KEY` directly |
Expand Down
52 changes: 43 additions & 9 deletions docs/content/docs/commands/mail.mdx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
---
title: mail
description: Search and read Kagi Mail from the terminal.
description: Search, read, and send Kagi Mail from the terminal.
---

`kagi mail` lists mailboxes, searches messages, and reads messages or threads
through the mail MCP. These operations do not send, delete, move, or mark mail
as read. Responses are not stored in local history or the response cache.
through the read-only mail MCP. These operations do not send, delete, move, or
mark mail as read. `kagi mail send` submits plain-text mail through SMTP instead.
Messages and responses are not stored in local history or the response cache.

## Setup
## Read-only MCP setup

Add the MCP endpoint and OAuth client ID supplied by your mail service to your
private `~/.config/kagi-cli/config.toml`:
Expand Down Expand Up @@ -68,18 +69,51 @@ YYYY-MM-DD or RFC 3339. `--after` is inclusive and `--before` is exclusive.
oldest first. `--new-text-only` drops quoted reply history. Attachment metadata
is included when available; attachment download is not exposed by the MCP.

## Send mail

`kagi mail send` requires all four flags: `--from`, `--to`, `--subject`, and
`--body`. Use one bare email address for each of `--from` and `--to`, without
display names. The subject must not contain control characters.

```bash
kagi mail send \
--from sender@example.com \
--to recipient@example.com \
--subject "Meeting notes" \
--body "The meeting starts at 10."
```

Supply SMTP credentials only through the environment variables
`KAGI_MAIL_SMTP_USERNAME` and `KAGI_MAIL_SMTP_PASSWORD`. There are no credential
flags or saved-config credentials for sending. Do not put credentials on the
command line.

Sending uses `mail.kagimail.com:587` with required, certificate-verified STARTTLS.
It never falls back to plaintext or unverified TLS. Mail is plain-text only;
HTML and attachments are not supported.

The read-only MCP OAuth token is separate and is never reused for SMTP.
`kagi mail login`, saved profiles, and `KAGI_MAIL_ACCESS_TOKEN` do not supply
sending credentials.

Successful submission returns `{"status":"submitted"}`. SMTP acceptance does
not prove recipient delivery. If submission fails, delivery may be unknown;
check your mailbox before retrying to avoid duplicate mail. SMTP errors do not
echo credentials, message content, or remote response text. Authentication
rejections identify the SMTP environment variables, not the MCP login flow.

## Output and errors

All commands default to `--format json`. Use `compact` for minified JSON,
`toon` for agent context, or `pretty` for plain terminal text. The format flag
works before or after the mail subcommand. JSON/TOON retain the server's tool
result, including IDs, URLs, notes, and truncation flags, without the MCP wrapper.
Empty `emails` or `mailboxes` arrays may be `null`, as returned by the service.
Pretty output shows message IDs and thread IDs for the next command.
works before or after the mail subcommand. For MCP reads, JSON/TOON retain the
server's tool result, including IDs, URLs, notes, and truncation flags, without
the MCP wrapper. Empty `emails` or `mailboxes` arrays may be `null`, as returned
by the service. Pretty output shows message IDs and thread IDs for the next command.

Success exits 0, argument parsing errors exit 2, and runtime failures exit 1. Runtime
errors go to stderr and support the global `--error-format json`. Authentication
failures suggest `kagi mail status` and `kagi mail login`. Temporary token-service
failures for MCP reads suggest `kagi mail status` and `kagi mail login`. Temporary token-service
failures during login or refresh, including HTTP 429 and server errors, remain
retryable and leave saved tokens intact. Error diagnostics do not echo remote
response bodies, credentials, or private endpoints. Login removes control
Expand Down
2 changes: 1 addition & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ pub enum Commands {
/// Inspect account plan, AI allowance, renewal, and calendar-month usage
#[command(visible_alias = "billing")]
Usage(UsageArgs),
/// Search and read Kagi Mail
/// Search, read, or explicitly send Kagi Mail
Mail(crate::mail::MailCommand),
/// Summarize a URL or text with Kagi's public API or subscriber web Summarizer
Summarize(SummarizeArgs),
Expand Down
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ pub enum KagiError {
#[error("authentication error: {0}")]
MailAuth(String),

/// SMTP submission failed; retrying could duplicate a message.
#[error("mail send error: {0}")]
MailSend(String),

/// A data parsing or deserialization failure.
#[error("parse error: {0}")]
Parse(String),
Expand Down
1 change: 1 addition & 0 deletions src/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ fn write_json_locked<T: Serialize>(path: &Path, value: &T) -> Result<(), KagiErr
fn open_locked_append(path: &Path) -> Result<File, KagiError> {
let file = fs::OpenOptions::new()
.create(true)
.read(true)
.append(true)
.open(path)
.map_err(|error| {
Expand Down
Loading
Loading