7 min read

AI agents sending email through an API: safe controls

AI agents sending email through an API need recipient boundaries, final-message approvals, delivery handling, and records that survive an incident.

AI agents sending email through an API: safe controls

An agent that can call an email API can contact your customers, vendors, and partners at machine speed. That capability is useful for routine notices and operational follow-up. It also turns a small prompt error, stale record, or compromised agent session into an external communication problem before anyone reads a draft.

The safe design does not begin with a better prompt. It begins by making the send path refuse unsafe recipients, require approval when a message crosses a defined risk line, and preserve enough evidence to reconstruct each decision. If your answer to "Who could this agent email?" is "anyone in the CRM," you have not built a boundary. You have handed it an address book.

The email credential must never define the agent's authority

The credential that calls the provider API proves that a service may submit mail. It does not prove that a particular agent process should contact a particular person for a particular purpose. Teams often collapse those questions because provider tokens are easy to issue and difficult to inspect after they leak.

Keep the provider token in a component that executes the send. The agent should submit an intent, not hold a reusable bearer token. That component can identify the agent run, resolve recipient IDs, inspect the message, require a decision when needed, and call the provider only after those checks pass.

This distinction matters during failure. Suppose an agent receives an instruction copied from a ticket: "Send the updated agreement to my new address." If the agent owns the email credential, it can send immediately to whatever address appears in the text. If it submits a request to a controlled sender, the sender can reject the unrecognized address, ask for approval, or require a contact-record update from a human.

A raw API token also makes revocation clumsy. You can revoke the token, but that may interrupt every legitimate worker that shares it. Give each agent process a session identity instead. End the session when the process ends. If the process behaves badly, revoke that session and keep other work running.

Do not put an email API token in an agent's environment variables, project files, shell history, tool configuration, or prompt. Redaction does not repair this design. Once a model or a tool process has read a secret, you cannot reliably prove where it traveled.

For teams using autonomous coding agents on a Mac, Sallyport can execute an HTTP email API call without exposing the credential to the agent. That solves credential custody, but it does not replace the recipient and content rules described below.

Recipient boundaries need records, not string matching

A recipient boundary should answer whether this specific address may receive this class of message from this sender. A domain allowlist alone cannot answer that. A vendor may use a personal mailbox, a customer may have several contacts, and a typo can still point at a real address on an allowed domain.

Make a recipient registry the source of truth. Every external recipient gets a stable ID and an address, plus information your send service needs to judge a request: relationship, owner, permitted message purposes, consent status where relevant, and whether a human must review every send. The agent requests contact_4821, not [email protected].

Your sender resolves the ID only after it verifies the record. The request can carry a display name for rendering, but it must not override the address held in the registry. This prevents a subtle failure that shows up often in agent integrations: developers validate a contact ID, then trust a free-form to field from the same request.

Use separate lists for these cases:

  • customers who opted in to a defined class of notices
  • active vendor operational contacts owned by a named employee
  • internal test recipients used during rollout
  • exception recipients that always require a human decision

Do not accept to, cc, bcc, or reply-to as unconstrained strings in the agent-facing interface. Bcc deserves special treatment. It is useful for a narrow set of compliance or case-management workflows, but it also creates a hidden disclosure path. Disable it by default. Require a documented reason and an explicit approval for every Bcc recipient that you allow.

Reply-to can cause just as much trouble as the recipient list. An agent can send a harmless notice with a reply-to address that routes customer information into an unattended inbox. Resolve reply-to values from a short sender profile list rather than accepting them from the agent.

Treat contact data as mutable. A vendor contact may leave, an account may close, consent may change, and a customer may ask that messages stop. The send path must check current status at send time, not only when an agent first planned the message. Cached addresses are convenient until they are the reason an old employee receives a contract update.

Sender identities should make the message's purpose obvious

An agent should send from a dedicated organizational identity, not an employee mailbox and never an executive address. Recipients need a clear signal about what sort of mailbox contacted them and where a reply will go.

Set up sender profiles such as billing-notices, service-status, or vendor-operations. Each profile should specify a From address, a reply destination, permitted message classes, and the templates it may use. The agent chooses from profile IDs. It does not write arbitrary From or reply-to headers.

This separation limits both damage and confusion. If an agent that handles support case updates can also use accounts-payable, it can make a payment request look legitimate. If all operational messages use one broad address, staff cannot tell whether an unexpected message came from a monitored process or a human.

Authenticate each sending identity. SPF tells receiving systems which infrastructure may send mail for a domain. DKIM attaches a domain signature to a message. DMARC publishes the domain's requested handling when SPF and DKIM fail alignment. None of those records decide whether your agent chose the right recipient. They protect domain reputation and help receivers judge authenticity.

RFC 5322 defines the Internet Message Format and separates fields such as From, Sender, Reply-To, To, Cc, and Bcc. The standard permits many forms that an email client can display gracefully. Your agent interface should be far narrower than the format permits. Flexibility in the mail format is not permission for agents to manufacture mailboxes, headers, or recipient lists.

Keep display names conservative. A message from "Accounts Payable" <billing-notices@...> can mislead a vendor when it asks for sensitive banking changes. Reserve names for the actual operating function and ban language that claims a named employee sent or reviewed the message unless that occurred.

Approval must inspect the final message, not the agent's summary

A human approval works only if the reviewer sees the exact decision that the system will execute. "Agent wants to notify the customer about an invoice" is not an approval object. It omits the customer, amount, sender, wording, reply path, and attachment set.

Build the message first, resolve every recipient, render every template variable, and create an immutable proposed-send record. Then present that record for review. The final send call must refer to the approved record by ID and reject any change after approval.

A proposed record should include at least this shape:

{
  "request_id": "req_01J...",
  "agent_session": "sess_01J...",
  "purpose": "vendor_invoice_query",
  "sender_profile": "vendor-operations",
  "to": [{"contact_id": "vendor_4821", "address": "[email protected]"}],
  "cc": [],
  "bcc": [],
  "subject": "Question about invoice INV-1048",
  "body_sha256": "6af1...",
  "attachment_sha256": [],
  "approval_required": true
}

Store the rendered content in protected storage or store a digest plus a durable copy under your retention rules. A digest alone proves that bytes did not change only if you can still retrieve the bytes someone claims you sent. For sensitive messages, preserve both the rendered MIME message and its digest.

Approval triggers should reflect harm, not a vague confidence score. Confidence scores are attractive because they sound adaptive, but they leave reviewers trying to understand why one 0.74 message sent and another 0.71 message stopped. Use plain conditions that an operator can inspect.

Require approval when a request does any of the following:

  • introduces a recipient not previously approved for that purpose
  • sends to an external party outside a routine notice class
  • changes payment, account access, contract, pricing, delivery, or legal terms
  • includes an attachment or a Bcc recipient
  • exceeds the known recipient count for that message class

Also require approval when the agent creates the body from open-ended instructions rather than a constrained template. A reminder that says "Your scheduled maintenance begins tomorrow" has a bounded purpose. A message drafted from a long support conversation may contain claims, promises, or personal data the agent pulled from the wrong case.

Do not approve an agent session for a day and call it review. That grants a broad capability while hiding each consequence. Session approval can authorize the agent to prepare requests. Per-message approval should decide external communication when the message falls outside a low-risk, pre-approved class.

Templates reduce variation, but they do not grant permission

Keep email keys out of agents
Sallyport injects the email API credential for HTTP calls, so the agent never receives it.

Templates are useful because they constrain wording and make inspection easier. They do not make a message safe if the agent can pick any recipient, populate fields with unchecked data, or select a template whose purpose does not match the event.

Each template needs a stated message class, allowed sender profiles, permitted recipient relationship, required data fields, and a maximum recipient count. The template renderer should reject unknown variables rather than silently leaving placeholders or accepting arbitrary HTML.

Consider a service-maintenance notice. The agent may populate a customer name, a maintenance window, and a support route from records associated with an active account. It should not populate a free-text explanation pulled from a ticket, include another customer's identifier, or add an attachment because the agent thinks it would be helpful.

A small policy representation can make these checks auditable without pretending that a rules engine solves judgment:

message_class: scheduled_maintenance
sender_profile: service-status
recipient_relationship: active_customer
max_recipients: 1
allowed_template: maintenance_notice_v3
approval:
  required_if:
    - attachment_present
    - recipient_status_not_active
    - maintenance_window_changed_after_render

The failure this prevents is not theoretical. A common workflow renders a template, stores a draft, then lets the agent update the maintenance window immediately before sending. The approval screen still shows the old window. Bind approval to the rendered content digest and invalidate it when any recipient, header, variable, or attachment changes.

Keep templates out of the agent's authority boundary as well. An agent may request a template ID and structured values. The sending service should load the template and escape values according to the output context. If the agent submits complete HTML, it can hide extra text with markup, include tracking material you did not approve, or alter the visual meaning of the message.

Do not use templates to disguise outreach. Transactional notices and marketing messages have different consent, frequency, and unsubscribe expectations. If you cannot classify the message clearly, route it to a human instead of forcing it through a convenient template.

Attachments and quoted threads carry the data you forget to review

Attachments turn a controlled text send into a file-disclosure workflow. The agent may find an old proposal, export a ticket, or generate a spreadsheet that includes columns nobody intended to share. A reviewer who reads only the email body will miss the most damaging part of the send.

Require attachments to enter a staging area owned by the sending service. Scan the file according to your organization's security process, calculate a digest, label its source, and bind that exact file to the proposed send. The reviewer should be able to open the staged copy or see a reliable preview before deciding.

Never allow a request like attach: "/Users/shared/contracts/latest.pdf" from an agent. "Latest" is not a record, and a filesystem path does not prove the document's audience. Let a human or an approved document workflow create an attachment record with a classification, owner, filename, digest, and expiration.

Quoted email threads need the same suspicion. Forwarding a thread can reveal internal notes, prior recipients, copied headers, and unrelated case details. If the agent needs context, provide the relevant structured facts. If it must send a prior message, treat the forwarded content as an attachment-like artifact that requires review.

Images and generated PDFs merit a direct check. Text extraction can help reviewers search for account numbers or personal data, but it cannot reliably catch everything in a visual layout. A staged preview is slower than blind automation. It is much faster than explaining why a vendor received another vendor's invoice.

Delivery events are evidence, not permission to retry forever

Put an action gateway in front
Route email requests through the bundled sp mcp shim, then let Sallyport execute the HTTP call.

The email API response usually means that the provider accepted your request. It does not mean that the recipient mailbox accepted the message, that a person read it, or that a reply will reach a monitored queue. Preserve the provider message ID and connect later events to your internal request ID.

RFC 5321 describes SMTP's transfer behavior, including the distinction between acceptance by one server and later delivery outcomes. API providers wrap that transport in a friendlier response, but they cannot erase the distinction. Treat a successful API response as submission evidence.

Record delivery, hard bounce, soft bounce, complaint, unsubscribe, and provider rejection events where your provider supplies them. Use those events to update recipient eligibility. A hard-bouncing contact should stop receiving automated operational mail until an owner fixes the record. A complaint should remove the address from the relevant class immediately, not after the agent's next campaign-like run.

Retry logic needs a ceiling and an owner. Transient failures can justify a limited retry using the same approved message record. Do not ask the agent to rewrite and resend a rejected message on its own. A rewritten retry can evade duplicate protections and can turn one failed notification into several inconsistent emails.

Deduplicate before the provider call. Derive an idempotency value from the approved send ID, not from the mutable subject line or the agent's current task. If a network timeout occurs after submission, the agent must query the send record rather than assume failure and submit another request.

A provider webhook can arrive late, twice, or out of order. Store it as an event with its provider ID and process it idempotently. Do not let a duplicate delivery event trigger a second internal workflow or persuade an agent that it should send a follow-up.

An audit record must answer the uncomfortable questions

Separate runs from sends
Sallyport separates the agent run in Sessions from each individual HTTP call in Activity.

When a customer says, "Why did you send this to me?", you need more than a dashboard line saying sent. You need to determine which agent run requested the send, which account or workflow initiated it, which recipient record resolved to the address, what exact content went out, who approved it, and what the provider accepted.

Keep two related trails. A run journal tracks the identity and lifetime of the agent process, its authorization, and revocation. A call journal tracks each proposed send, validation decision, approval, provider submission, and delivery event. Joining them should take an identifier, not detective work across application logs.

Make audit events append-only and protect them from the component that makes the send. If a worker can delete or rewrite its own record, the audit trail will fail when you need it most. Hash chaining gives you a practical tamper check: each event includes the digest of the prior event and its own serialized data. Verify the chain independently.

A minimal event sequence looks like this:

2025-04-03T09:12:04Z proposed  req_01J... sess_01J... digest=6af1...
2025-04-03T09:12:10Z approved  req_01J... reviewer=user_17 digest=6af1...
2025-04-03T09:12:11Z submitted req_01J... provider_id=msg_92...
2025-04-03T09:12:14Z delivered req_01J... provider_event=evt_44...

The timestamps alone do not make a record trustworthy. Protect the event store, record the actor for every state transition, and verify integrity separately from the service that writes events. Retention also matters. Decide how long you need message content, metadata, and attachments before an incident forces an answer you no longer have.

Sallyport keeps separate session and activity views from one encrypted, hash-chained audit log, and sp audit verify checks the chain offline without a vault key. That kind of independent verification is useful when a team must investigate whether a record changed after an agent run.

A controlled rollout exposes bad assumptions before customers do

Do not begin by letting an agent email all active contacts. Begin with an internal recipient registry and a single message class that has no financial, contractual, access-control, or legal consequence. Your goal is to find mismatches between the records you think you have and the records the send path actually uses.

Run an internal pilot with people who agreed to receive test messages. Deliberately try the failures your interface must reject: an unknown address, an extra Cc recipient, a Bcc request, a changed template variable after approval, an attachment from an unstaged path, and a resend after a simulated timeout. Record whether the system rejected each request and whether the audit trail explains the rejection.

Then add one external use case with a small, owned recipient set. Keep approvals on for every send until you have reviewed enough real records to understand the exception patterns. Do not remove approvals because the messages look repetitive. Remove them only when the recipient source, template, sender profile, data fields, and retry behavior are all constrained and monitored.

Someone should own the recipient registry and the message classes. Automation often fails at handoff boundaries: sales thinks support owns the address, support thinks finance owns the wording, and the agent sees only a contact row. A named owner can correct a stale record and decide whether a new purpose belongs in the automatic path.

Give humans an immediate stop control that blocks future sends from a session and prevents queued proposals from crossing the final submission check. Then test it while a queue has pending requests. A revoke button that works only before work starts is a comforting decoration.

The first action worth taking is simple: list every external email an agent could send today, then identify the exact recipient record, sender profile, approval rule, content artifact, and audit event for each one. Any row with a blank answer is still an unbounded API call, regardless of how polished the agent workflow looks.

FAQ

Is it safe to let an AI agent send emails automatically?

It can be safe when the agent can act only through a narrow sending account, a controlled recipient registry, and a review path that matches the message's risk. Giving an agent unrestricted API access to a normal employee mailbox is not a safe design. Treat the sender identity, recipients, content, and attachment set as separate controls.

What email address should an AI agent use to send messages?

Use an owned domain or subdomain with a sender address reserved for agent-assisted operations, such as notices or vendor operations. Do not let the agent impersonate individual employees or use an executive address. Configure SPF, DKIM, and DMARC for that sending identity before any external send.

How do I stop an AI agent from emailing the wrong customer?

Make customer and vendor contact lists records with stable IDs, approved addresses, ownership, purpose, and status. The agent should request a recipient ID, then the sending service should resolve the address after it checks the record. Free-form email addresses belong in an exception flow with human approval.

What should a human approve before an agent sends an email?

Approval should cover the final rendered message, resolved recipients, sender identity, attachments, and purpose. A generic approval for "send emails" is too broad because it leaves the high-risk facts outside the decision. Store the exact rendered bytes or a cryptographic digest with the approval record.

Which AI-generated emails need human approval?

Use automatic sending only for narrow, repeatable, low-impact notices with deterministic recipients and templates. Put sales outreach, payment changes, account access matters, legal claims, support escalations, and any message with attachments behind review. Start stricter than you think you need, then relax rules only after reviewing real message records.

What records should I keep for AI-sent emails?

Record the request ID, agent session, human or system initiator, sender, resolved recipients, template or source prompt reference, rendered subject and body digest, attachments, approval decision, provider response, and delivery events. Capture Bcc separately and keep it tightly restricted because it hides recipients from ordinary message views. An audit trail that says only "email sent" will not help when someone disputes what happened.

Does a successful email API response prove delivery?

No. A provider's acceptance response proves that it accepted a request, not that a recipient received, opened, or understood the message. Keep provider message IDs and ingest delivery, bounce, complaint, and unsubscribe events where your provider offers them.

Can an AI agent attach files to customer emails?

Do not let an agent choose arbitrary attachments from a shared filesystem, issue tracker, or cloud drive. Generate approved documents in a staging area, scan them, bind them to the send request by digest, and show them in the approval view. Attachments create privacy and data disclosure risks that a text-only review will miss.

How do I revoke an AI agent's email access quickly?

Give each agent run its own session identity and expire that identity when the run exits. Authorize the action gateway, not a raw provider token handed to the model. Revocation must stop future sends immediately, including queued requests that have not crossed the final send check.

Can I test agent email sending with a sandbox account?

No, not for production contact with customers or vendors. A sandbox can verify payload shape and template rendering, but it cannot test permission boundaries or real-world recipient mistakes. Use a controlled internal pilot after the sandbox, with recipients who know they are part of the test.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov