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:
$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), andtimeout.Pre-defined sender identity fields (all optional):
from_name,reply_to,cc,bcc,signature,signature_html,reply_to_self, andbcc_self. A ready-to-edit sample ships atskills/send-email/send-email.example.json(installed to<runtime_root>/workspace/skills/send-email/send-email.example.json). Copy itssmtp(oraccounts) block into your secrets file and replace the<placeholders>.Secrets file: put an
smtpobject (or top-levelSMTP_*keys) in a send-email-specific JSON file holding both the connection settings and the identity defaults:
{
"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.
{ "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):
# 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-fileand/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-- withsend, 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:
# 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 nogpg-agentis available (otherwise rely on the agent).--no-signoverrides a per-account default. sendreports"signed": true/false. Example:
… 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 ": run
sendwith--attach; preview with--dry-runfirst if the recipient or content is unconfirmed. After sending, offer to save anynew_recipientsto the address book. - "sign this email" / "send signed": add
--sign(needs gpg + your key). - "send from my work account": run
send --account work(seeaccounts). - "who do I have saved?" / "look up 's email": run
contacts/contacts --search <query>. - "does my SMTP setup work?": run
verify(add--accountto test a specific one). - "what mail settings are configured?": run
show-config/accounts.
Security notes
- TLS certificates are validated (
ssl.create_default_context); prefersslorstarttlsoverplain. - 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--securityresolves toplain). - 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.