# Send Email

> Send email over SMTP using only the Python standard library, with plain-text and HTML bodies, file attachments, cc/bcc, reply-to, a dry-run preview, connection verification, and redacted config inspection.

- Skill: `hoanganhduc/send-email` (Agent Skill)
- Install (CLI): `npx skillmds@latest add hoanganhduc/send-email`
- Raw SKILL.md: https://api.skillmd.com/api/skills/hoanganhduc/send-email/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: hoanganhduc (https://skillmd.com/u/hoanganhduc)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/hoanganhduc/send-email

---


# Send Email

Send email over SMTP from any install target on any operating system. The runtime
is pure Python standard library (`smtplib`, `ssl`, `email`), so there is nothing
to install beyond Python 3.10+. It composes plain-text and HTML messages with
attachments, cc/bcc, and reply-to, previews a message with `--dry-run`, checks a
server with `verify`, and exposes the resolved settings (password redacted) with
`show-config`.

Use this skill when the user wants to send a message, mail a file, deliver a
report or notification by email, or test SMTP credentials. For document delivery
over Telegram use `vnu-eoffice`; this skill is email/SMTP only.

Credentials are never hardcoded, printed, or committed. Managed launches read
them only from a dedicated send-email JSON authority and redact them from errors;
ambient `SMTP_*`, generic AAS secret pointers, Python startup hooks, and dynamic
loader variables are removed before interpreter selection.

## Windows Runtime Commands

On native Windows, use the managed Windows runner and the native runtime command
target. Set `$runtime` to the installed runtime root. Multi-agent installs
usually use `%LOCALAPPDATA%\ai-agents-skills\runtime`. Then run:

```powershell
$runtime = if ($env:AAS_RUNTIME_ROOT) { $env:AAS_RUNTIME_ROOT } else { "$env:LOCALAPPDATA\ai-agents-skills\runtime" }
& "$runtime\run_skill.ps1" "skills/send-email/run_send_email.ps1" <args>
```

POSIX examples below use `run_skill.sh` and the `.sh` command target; use the
Windows command target above on native Windows.

## Configuration

The managed runtime selects the secrets file in this order:
`--secrets-file PATH`, `SEND_EMAIL_SECRETS_FILE`, then send-email's documented
dedicated default locations. It never falls back to `AAS_SECRETS_FILE`,
`OPENCLAW_SECRETS_FILE`, or a shared workspace secrets document. Non-secret CLI
identity/message options may override file values; do not put passwords on argv.

- Connection fields: `host`, `port`, `user`, `password`, `from`, `security`
  (`ssl` | `starttls` | `plain`), and `timeout`.
- Pre-defined sender identity fields (all optional): `from_name`, `reply_to`,
  `cc`, `bcc`, `signature`, `signature_html`, `reply_to_self`, and `bcc_self`.
A ready-to-edit sample ships at `skills/send-email/send-email.example.json`
(installed to `<runtime_root>/workspace/skills/send-email/send-email.example.json`).
Copy its `smtp` (or `accounts`) block into your secrets file and replace the
`<placeholders>`.

- Secrets file: put an `smtp` object (or top-level `SMTP_*` keys) in a
  send-email-specific JSON file holding both the connection settings and the
  identity defaults:

```json
{
  "smtp": {
    "host": "smtp.example.com",
    "port": 587,
    "security": "starttls",
    "user": "<smtp-username>",
    "password": "<app-password>",
    "from": "<sender-address>",
    "from_name": "Your Name",
    "reply_to": "<reply-address>",
    "cc": ["<standing-cc>"],
    "bcc": ["<standing-bcc>"],
    "signature": "--\nYour Name\nYour Lab",
    "signature_html": "<p>Your Name<br>Your Lab</p>",
    "reply_to_self": true,
    "bcc_self": true
  }
}
```

The identity fields are optional and not secret, but they live in the same
`smtp` object for one-file configuration; only `user`/`password` are sensitive.
`from_name` is combined with `from` to send as `Your Name <addr>` (or embed the
name directly in `from`). The `signature` (and optional `signature_html`) is
appended after the standard `-- ` delimiter; if only a text signature is set it is
also wrapped into the HTML alternative.

By default **Reply-To and Bcc are set to the sender address** (so replies come
back to you and you keep a copy). Set `reply_to_self`/`bcc_self` to `false`, or
pass `--no-reply-to-self` / `--no-bcc-self`, to disable. An explicit `reply_to`
(or `--reply-to`) overrides the self-default; `--cc`/`--bcc` add to the standing
lists.

Multiple accounts: instead of (or alongside) a single `smtp` block, define an
`accounts` map of named profiles and an optional `default_account`; each profile
holds the same keys (connection + identity). Select one with `--account NAME` (or
`SMTP_ACCOUNT`); `accounts` lists the names.

```json
{ "default_account": "work",
  "accounts": {
    "work": {"host": "smtp.work.example", "user": "<u>", "password": "<p>", "from": "<work-from-address>", "from_name": "You (Work)", "pgp_sign": true, "pgp_key": "<gpg-key-id-or-email>"},
    "lab":  {"host": "smtp.gmail.com", "port": 587, "user": "<u>", "password": "<p>", "from": "<lab-from-address>"}
  } }
```

If `--port`/`--security` are omitted, port 465 implies `ssl`, port 25 implies
`plain`, and the default is `starttls` on port 587. Common hosts: `smtp.gmail.com`
and `smtp.office365.com` (use an app password, not the account password).

### Where to put the config across install targets

Use a send-email-specific secrets file when one SMTP configuration should serve
all install targets. Recommended defaults:

- Linux / WSL: `${XDG_CONFIG_HOME:-~/.config}/send-email/secrets.json`
- macOS: `$XDG_CONFIG_HOME/send-email/secrets.json`, then
  `~/Library/Application Support/send-email/secrets.json`, then
  `~/.config/send-email/secrets.json`
- Windows: `%APPDATA%\send-email\secrets.json`, then
  `%LOCALAPPDATA%\send-email\secrets.json`

For a custom location, pass `--secrets-file PATH` for one command or set
`SEND_EMAIL_SECRETS_FILE=PATH` in the environment used for send-email. These are
send-email-specific and do not redirect other runtime-backed skills.

Do not put a send-email-only path in global `AAS_SECRETS_FILE`: the launcher
unsets it, along with every other ambient secrets pointer, before it runs a
skill, so a global setting has no effect here. Use `SEND_EMAIL_SECRETS_FILE`,
`--secrets-file`, or a dedicated default file. On POSIX the selected authority
must be a bounded regular, single-link file owned by the current user with no
group/world permission bits; it is opened without following links and its
identity is checked before and after the bounded read. Native Windows admission
is owned by the managed PowerShell authority/DACL gate.

Never write a real address, password, or token into a tracked file; pass them via
the environment or the secrets file. Use `show-config` to confirm what resolved;
it redacts credentials but still shows diagnostic metadata such as sender,
default cc/bcc, and the selected config path.

## Commands

Run via the managed runner (POSIX shown; see Windows Runtime Commands above):

```bash
# Credential-bearing lanes must launch from a root-owned AAS component
# generation; the per-user runtime copy is refused by the credential gate.
# The generation directory is named for a git commit, so its name carries no
# ordering -- pick the newest publish, which the store records as its mtime.
launcher="${AAS_RUNTIME_ROOT:-$HOME/.local/share/ai-agents-skills/runtime}/run_skill.sh"
newest=0
for gen in /usr/local/libexec/coding-system/components/ai-agents-skills/*/; do
  gen="${gen%/}"
  [ -f "$gen/manifest/credential-runtime.json" ] || continue
  [ -x "$gen/canonical/runtime/runners/run_skill.sh" ] || continue
  stamp="$(stat -c %Y "$gen" 2>/dev/null || stat -f %m "$gen" 2>/dev/null)" || continue
  [ "${stamp:-0}" -gt "$newest" ] || continue
  newest="$stamp"
  launcher="$gen/canonical/runtime/runners/run_skill.sh"
done
bash "$launcher" skills/send-email/run_send_email.sh <command> [args...]
```

- `send` -- compose and send. Recipients (`--to`, `--cc`, `--bcc`) are repeatable
  and may be comma-separated. Body is `--body`/`--body-file` and/or
  `--html`/`--html-file` (both present become a multipart/alternative). Attach
  files with repeated `--attach`. Identity overrides: `--from-name`, `--reply-to`,
  `--signature`/`--signature-file`/`--signature-html-file`, `--no-signature`,
  `--no-reply-to-self`, `--no-bcc-self`.
- `--dry-run` -- with `send`, compose and report the message (from, reply-to,
  recipients, cc, subject, html flag, attachments, byte size) without connecting
  or sending.
- `verify` -- connect and authenticate to the server, then disconnect; sends no
  message. Use it to test credentials.
- `show-config` -- print the resolved account/host/port/security/user/from/identity,
  selected secrets file/source, and whether a password is set; the password
  itself is never printed.
- `accounts` -- list configured named SMTP accounts (names only, no secrets).
- `contacts` -- the address book: lists saved contacts by default; `--add ADDR
  [--name N]`, `--remove ADDR`, `--search QUERY`.
- `selftest` -- offline smoke (no network): builds, serializes, and re-parses
  messages in memory to validate message construction.

Account and identity selection: `--account NAME` (or `SMTP_ACCOUNT`) picks a named
account; otherwise `default_account`, otherwise the single `smtp` block. Per-send
CLI flags override the chosen account. `--secrets-file PATH` overrides the
configured/default secrets file for `send`, `verify`, `show-config`, and
`accounts`.

Examples:

```bash
# preview without sending
bash "$launcher" skills/send-email/run_send_email.sh \
  send --to <recipient> --subject "Report" --body "See attached." --attach ~/report.pdf --dry-run

# send a text + HTML message to several recipients
bash "$launcher" skills/send-email/run_send_email.sh \
  send --to <recipient> --cc <reviewer> --subject "Update" \
  --body "Plain fallback." --html "<p>Rich <b>body</b>.</p>"

# test the server and credentials
bash "$launcher" skills/send-email/run_send_email.sh verify
```

Every command prints a single JSON object. On success it includes `"ok": true`;
on failure it includes `"ok": false` with an `error_code` and a redacted message,
and the process exits non-zero.

## Address book (save recipients for reuse)

`send` reports `new_recipients` -- the addresses in this message that are not yet
in the address book. **After a successful send, if `new_recipients` is non-empty,
ask the user whether to save those addresses for later; if they agree, run
`contacts --add <address> [--name <name>]` for each** (or send with
`--save-recipients` to save them automatically). The book is a JSON file at
`<runtime_workspace>/.address-book.json` (override with `SEND_EMAIL_ADDRESS_BOOK`);
it is personal state and should be backed up alongside the secrets file. Use
`contacts --search` to look up a saved address before composing.

## PGP signing (optional)

Messages can be PGP/MIME signed (RFC 3156: `multipart/signed` with a detached
signature, which works with HTML and attachments). This needs **GnuPG (`gpg`)** on
PATH with your secret key in the keyring; it adds no Python dependency, and
unsigned sending still works if `gpg` is absent.

- Enable per send with `--sign`, or per account with `"pgp_sign": true`.
- The signing key defaults to the sender address; override with `--pgp-key <id>`
  or `"pgp_key"`. Use `--gnupg-home`/`"gnupg_home"` for a non-default keyring, and
  `"pgp_passphrase"` in the secrets file only if the key needs a passphrase and no
  `gpg-agent` is available (otherwise rely on the agent). `--no-sign` overrides a
  per-account default.
- `send` reports `"signed": true/false`. Example:

```bash
… run_send_email.sh send --account work --to <recipient> --subject Hi --body Hi --sign
```

The passphrase is passed to gpg over a pipe (never on the command line) and is
never printed.

For a passphrase-protected key, prefer `"pgp_passphrase"` in the secrets file for
**non-interactive/automated** signing: the skill then uses gpg loopback and needs
no agent or terminal, so it works headlessly (this is the reliable path for an
agent sending mail). Relying on a cached `gpg-agent` instead is fragile here: a
non-interactive run has no TTY, so on a cache miss gpg fails with
`Inappropriate ioctl for device` (it cannot show a pinentry prompt). The agent
cache also only populates from an interactive terminal that has `export
GPG_TTY=$(tty)` set, and it expires after gpg-agent's cache TTL (default ~10
minutes) — so treat gpg-agent as a convenience for your own terminal, not for
unattended signing.

## Natural-language routing

- "email this file to <recipient>": run `send` with `--attach`; preview with
  `--dry-run` first if the recipient or content is unconfirmed. After sending,
  offer to save any `new_recipients` to the address book.
- "sign this email" / "send signed": add `--sign` (needs gpg + your key).
- "send from my work account": run `send --account work` (see `accounts`).
- "who do I have saved?" / "look up <name>'s email": run `contacts` /
  `contacts --search <query>`.
- "does my SMTP setup work?": run `verify` (add `--account` to test a specific one).
- "what mail settings are configured?": run `show-config` / `accounts`.

## Security notes

- TLS certificates are validated (`ssl.create_default_context`); prefer `ssl` or
  `starttls` over `plain`.
- Authenticating over an unencrypted (`plain`) connection is refused unless you
  pass `--allow-insecure-auth`, so credentials are not sent in the clear by
  accident (note port 25 with no `--security` resolves to `plain`).
- Header values are rejected if they contain newlines, preventing header
  injection.
- Passwords are read only from the environment or the secrets file and are
  redacted from all output and errors.

