OpenClaw Mailer
Governed email sending for OpenClaw agents — fail-closed recipient allowlist, one TLS trust model with agent-shield, port 587 + MAILER_CA_CERT behind AV interception.
OpenClaw Mailer
Let your OpenClaw agent send email — without letting it send to anyone, leak secrets, or quietly disable TLS.
OpenClaw Mailer is a small, self-contained MCP server that exposes a single tool, send_email (built on nodemailer). It is deliberately separate from agent-shield: agent-shield decides whether an action is allowed; the mailer only sends. governance_check runs before every send, so the decision and the delivery are two clean, auditable steps.
Why not the built-in / CLI mail path? A pure Node sender shares one trust model with agent-shield: the same --use-system-ca / certificate handling for the gateway and for mail. A second TLS stack (e.g. a Rust CLI mailer) does not read the OS certificate store and fails behind antivirus HTTPS inspection — the very problem this page solves.
How It Works
OpenClaw Agent → governance_check (agent-shield) → {{brand}} Gateway → Policy Check
↓
✅ ALLOW / ✏️ MODIFY → send_email (openclaw-mailer) → SMTP
🚫 BLOCK → no sendThe agent first calls governance_check. Only on ALLOW (or MODIFY, with PII already masked) does it call send_email. Each step produces its own tamper-evident trace.
Secure Defaults
- Recipient allowlist (fail-closed). Every recipient must appear in
MAILER_ALLOWED_RECIPIENTS, or nothing is sent. This is defense-in-depth on top ofgovernance_check: even a compromised or prompt-injected agent cannot mail arbitrary addresses. - Certificate verification is never disabled.
rejectUnauthorizedstaystrue. Trust is added (via the OS store orMAILER_CA_CERT), never switched off. - Secrets only via env.
SMTP_PASSis read from the environment, never logged and never placed into error messages or hints.
Setup / MCP Registration
Run the mailer with command: node and --use-system-ca as the first argument, then the absolute path to the server. Supply all configuration through the env block — placeholders only, never real secrets in a repo:
{
"mcpServers": {
"openclaw-mailer": {
"command": "node",
"args": ["--use-system-ca", "C:/Projekte/openclaw-mailer/bin/openclaw-mailer-mcp.mjs"],
"env": {
"SMTP_HOST": "smtp.gmail.com",
"SMTP_PORT": "587",
"SMTP_SECURE": "false",
"SMTP_USER": "[email protected]",
"SMTP_PASS": "your-app-password",
"SMTP_FROM": "Your Name <[email protected]>",
"MAILER_ALLOWED_RECIPIENTS": "[email protected]",
"MAILER_CA_CERT": "C:/Users/you/inspecting-ca.pem"
}
}
}
}The app password belongs in the runtime env (or your MCP host's secret store) only — never in version control. The package is published privately as @{{brand_scope}}/openclaw-mailer and is invoked by absolute path rather than npx.
Behind antivirus / firewall HTTPS inspection, use port 587 (STARTTLS), not 465 — and set
MAILER_CA_CERT. See TLS behind AV interception below for why.
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
SMTP_HOST | yes | — | SMTP server hostname. |
SMTP_PORT | no | 465 | 587 = STARTTLS submission (recommended behind AV), 465 = implicit TLS. |
SMTP_SECURE | no | true (false at port 587) | true/1/yes ⇒ implicit TLS. At port 587 it defaults to false so nodemailer upgrades via STARTTLS. |
SMTP_USER | yes | — | SMTP username (usually the full address). |
SMTP_PASS | yes | — | SMTP password / app password. Never logged, never in the repo. |
SMTP_FROM | no | SMTP_USER | Envelope/From. Accepts Display Name <addr>. |
MAILER_ALLOWED_RECIPIENTS | yes | — | Comma-separated recipient allowlist. Mandatory (fail-closed). |
MAILER_CA_CERT | no | — | Absolute path to a PEM with one or more extra root CAs (e.g. an AV inspecting CA). Read in-process and trusted in addition to the built-in roots. Falls back to NODE_EXTRA_CA_CERTS if unset. |
DEBUG_LOG_PATH | no | — | If set, append-only secret-safe JSONL diagnostics (same convention as agent-shield). |
Tool: send_email
| Field | Type | Required |
|---|---|---|
to | string or string[] | yes |
subject | string | yes |
text | string | yes |
html | string | no |
The result is returned as JSON inside the MCP text content:
- Success:
{ "ok": true, "messageId": "<id>" } - Failure:
{ "ok": false, "reason": "<reason>", "hint": "<actionable hint>" }(a generic send failure uses"message"instead of"hint")
reason | Meaning |
|---|---|
recipient_not_allowlisted | A recipient is not in MAILER_ALLOWED_RECIPIENTS — nothing was sent. |
smtp_tls_untrusted | The SMTP certificate chain could not be verified (AV interception). |
ca_cert_unreadable | MAILER_CA_CERT is set but the file is missing or not a valid PEM. |
missing_config | A required env variable is unset. |
invalid_input | to / subject / text missing or malformed. |
smtp_send_failed | The send failed for another reason (secret-free message included). |
TLS behind AV interception
Antivirus/firewall HTTPS inspection (e.g. Norton) re-signs TLS with its own root CA. The crucial, verified detail: such products use two different roots, and which one you get depends on the port.
| Endpoint | Issuer presented | Trustable? |
|---|---|---|
| HTTPS (443) | Scanning Root (in OS store) | ✅ yes |
smtp.gmail.com:587 (STARTTLS) | Scanning Root (in OS store) | ✅ yes |
smtp.gmail.com:465 (implicit TLS) | Untrusted Root (in no store) | ❌ no — active distrust |
imap.gmail.com:993 | Untrusted Root (in no store) | ❌ no — active distrust |
The Untrusted Root (its OU literally reads "generated by … for untrusted server certificates") is the AV's signal that it could not verify the upstream server. It is deliberately kept out of every trust store — it is not a missing-CA problem you can fix by trusting it.
1. Use port 587 + STARTTLS. Set SMTP_PORT=587 and SMTP_SECURE=false. Port 587 is intercepted with the trustable Scanning Root, so the connection can be verified properly. Avoid implicit TLS on 465 behind such AV.
2. Trust the scanning CA via MAILER_CA_CERT. This is the most reliable route because some MCP runtimes (e.g. OpenClaw's embedded runtime) do not forward --use-system-ca or NODE_OPTIONS to the spawned process. MAILER_CA_CERT is read in-process and trusted in addition to the built-in roots, independent of how the runtime spawns Node — rejectUnauthorized stays true.
Export the AV root to a PEM (Windows PowerShell example for Norton):
Get-ChildItem Cert:\LocalMachine\Root | Where-Object { $_.Subject -match 'Norton' } |
ForEach-Object { "-----BEGIN CERTIFICATE-----`n" +
[Convert]::ToBase64String($_.RawData, 'InsertLineBreaks') +
"`n-----END CERTIFICATE-----" } |
Set-Content -Encoding ascii C:\Users\you\inspecting-ca.pemThen point MAILER_CA_CERT at that file.
What the reasons mean. If the chain still cannot be verified, send_email returns smtp_tls_untrusted with the hint "TLS certificate could not be verified — most likely an antivirus or firewall performing HTTPS inspection with a root CA that Node does not trust by default. Start Node with --use-system-ca (Node ≥22) so it trusts the OS certificate store, or set NODE_EXTRA_CA_CERTS to the inspecting CA. This does NOT disable certificate verification." — plus a note that MAILER_CA_CERT is the most reliable fix. If MAILER_CA_CERT points at a missing or non-PEM file, you get ca_cert_unreadable (fail-closed — it never silently falls back to default trust).
Never add the Untrusted Root to a trust store or to
MAILER_CA_CERT, and never setrejectUnauthorized: falseorNODE_TLS_REJECT_UNAUTHORIZED=0. Trusting the Untrusted Root means trusting connections the AV itself could not verify — a real MITM hole. The correct fix is port 587 + the Scanning Root; verification stays on.
This is the SMTP-specific counterpart to the gateway's gateway_tls_untrusted — same trust philosophy, one model. For the gateway side, see OpenClaw Integration.
Allowlist: test vs. production
Keep MAILER_ALLOWED_RECIPIENTS as tight as possible. During setup and testing, use a single test address. Widen it deliberately for production, one address at a time — the allowlist is the last line of defense against an injected agent emailing arbitrary parties.
# Test / setup
[email protected]
# Production (explicit, reviewed)
[email protected],[email protected]Viewing Traces
Every send is governed like every other action: the preceding governance_check and its outcome (Recommendation + Outcome, PII findings, optional Flare attestation) appear as a trace in the dashboard — searchable and filterable, exactly like the OpenClaw governance traces. See the Traces API for programmatic access.
Troubleshooting
Symptom (reason) | Cause | Fix |
|---|---|---|
recipient_not_allowlisted | Recipient missing from the allowlist. | Add the address to MAILER_ALLOWED_RECIPIENTS (comma-separated) and restart the server. |
smtp_tls_untrusted | AV/firewall HTTPS inspection; the chain can't be verified. | Switch to port 587 (SMTP_PORT=587, SMTP_SECURE=false) and set MAILER_CA_CERT to the scanning CA PEM. Never disable verification. |
ca_cert_unreadable | MAILER_CA_CERT is set but the file is missing or not a valid PEM. | Fix the path / re-export the PEM, or unset the variable to use default trust. |
missing_config | A required env variable is unset. | Set SMTP_HOST, SMTP_USER, SMTP_PASS and MAILER_ALLOWED_RECIPIENTS. |
Next Steps
- OpenClaw Integration — the governance side (
governance_check, gateway TLS). - Create your own policies to govern what your agent may email.
- Set up approval workflows for high-risk sends.