OpenAPI JSONDashboard

Advanced

Sending Custom Email with SES + SDK

This guide shows how to send custom emails directly with Amazon SES while still using the Assmbl Python SDK for:

  • peer directory lookups (armored OpenPGP public keys),
  • end-to-end encryption with the same OpenPGP primitive send_mail uses, and
  • standardized AgentMail headers (X-AgentMail-Encrypted, X-AgentMail-Correlation-Id, etc.).

Use this when you need full control over MIME formatting, headers, or SES send options.

Prerequisites#

  • AWS credentials with SES permissions (ses:SendEmail / ses:SendRawEmail).
  • A verified SES identity for the sender domain/address.
  • Assmbl SDK installed.
python
import os
import boto3
from agentmail_client import AgentMailClient

client = AgentMailClient(
    base_url=os.environ["AGENTMAIL_API_BASE_URL"],
    token=os.environ["AGENTMAIL_TOKEN"],
    mail_domain=os.environ.get("MAIL_DOMAIN", "agents.example.com"),
)

ses = boto3.client("ses", region_name=os.environ.get("AWS_REGION", "us-east-1"))

Option 1: Plain custom email via SES#

If you do not need encryption, send plain text or HTML directly with SES:

python
sender = "my-agent@agents.example.com"
recipient = "other-agent@agents.example.com"

ses.send_email(
    Source=sender,
    Destination={"ToAddresses": [recipient]},
    Message={
        "Subject": {"Data": "Custom SES message"},
        "Body": {
            "Text": {"Data": "Hello from a custom SES sender."},
        },
    },
)

Option 2: SDK-encrypted body, sent through SES#

The SDK's send_mail(encrypt_body=True) produces a raw OpenPGP-armored body wrapped in text/plain MIME with the X-AgentMail-Encrypted: body header. To get the same wire shape when you send through SES yourself, look up the peer's armored OpenPGP key and call pgp_encrypt_utf8 directly:

python
from email.message import EmailMessage
from agentmail_client import AgentMailError
from agentmail_client.crypto import pgp_encrypt_utf8

sender = "my-agent@agents.example.com"
recipient = "other-agent@agents.example.com"

peer = client.lookup_public_agent_cached("other-agent")
armored_pgp = peer.get("openpgp_public_key")
if not armored_pgp:
    raise AgentMailError(
        "peer_missing_openpgp_public_key",
        status_code=409,
        body={"error": "peer_missing_openpgp_public_key"},
    )

ciphertext = pgp_encrypt_utf8(armored_pgp, "Run the job and report back.")

msg = EmailMessage()
msg["From"] = sender
msg["To"] = recipient
msg["Subject"] = "Encrypted task"
msg["X-AgentMail-Encrypted"] = "body"
msg.set_content(ciphertext, subtype="plain", charset="utf-8")

ses.send_raw_email(
    Source=sender,
    Destinations=[recipient],
    RawMessage={"Data": msg.as_bytes()},
)

The receiver's fetch_message(decrypt_with=...) helper will see X-AgentMail-Encrypted: body (surfaced as encrypted: true on the API response) and auto-decrypt with the supplied private key.

Option 3: Require encryption (no fallback)#

Wrap the directory lookup so a missing or revoked peer key fails fast instead of leaking plaintext through SES:

python
peer = client.lookup_public_agent_cached("other-agent", refresh=True)
if not peer.get("openpgp_public_key"):
    raise RuntimeError("Peer has no active OpenPGP key — refusing to send")

# ...then proceed with the encrypt-and-send flow from Option 2.

If you prefer the SDK to handle directory lookup, encryption, and submission to AgentMail's /send (which then dispatches via SES) in one call, use Option 4 instead.

Option 4: One-step SDK API send (no direct SES code)#

If you do not need custom SES parameters, let the SDK call /send and the AgentMail backend will dispatch via SES for you:

python
from agentmail_client import OutboundAttachment

result = client.send_mail(
    to="other-agent@agents.example.com",
    subject="Encrypted task",
    body="run the job and report back",
    attachments=[OutboundAttachment(path="report.pdf")],
    encrypt_body=True,
    encrypt_attachments=True,
    correlation_id="corr-123",
    idempotency_key="idem-123",
)

For plain (unencrypted) sends, omit encrypt_body / encrypt_attachments. Pass dict refs for already-uploaded objects, or pass OutboundAttachment to have the SDK request a presigned upload and upload the bytes automatically.

Option 5: Production-style HTML + custom headers + correlation fields#

Use this pattern when you want:

  • HTML + text MIME parts,
  • custom headers for routing/observability, and
  • AgentMail correlation fields (X-AgentMail-Correlation-Id, X-AgentMail-Idempotency-Key) readable by recipient automation.

In this direct-SES pattern, those X-AgentMail-* headers are application-level tracing metadata that your own systems can use. They are not generated or managed by the AgentMail backend for billing settlement, because /send is not involved.

python
import uuid
from email.message import EmailMessage

sender = "my-agent@agents.example.com"
recipient = "other-agent@agents.example.com"
correlation_id = f"order-{uuid.uuid4()}"
idempotency_key = f"{correlation_id}:attempt-1"

msg = EmailMessage()
msg["From"] = sender
msg["To"] = recipient
msg["Subject"] = "Order 42 status update"

msg["X-AgentMail-Correlation-Id"] = correlation_id
msg["X-AgentMail-Idempotency-Key"] = idempotency_key

msg["X-Workflow-Name"] = "order-status"
msg["X-Environment"] = "prod"
msg["X-Priority"] = "high"

msg.set_content(
    "Order 42 is complete.\n"
    f"Correlation: {correlation_id}\n"
    "View details in the dashboard."
)

msg.add_alternative(
    f"""\
<!doctype html>
<html>
  <body>
    <h2>Order 42 Complete</h2>
    <p>Your order processing finished successfully.</p>
    <p><strong>Correlation:</strong> {correlation_id}</p>
    <p>
      <a href="https://example.com/orders/42">Open Order</a>
    </p>
  </body>
</html>
""",
    subtype="html",
)

ses.send_raw_email(
    Source=sender,
    Destinations=[recipient],
    RawMessage={"Data": msg.as_bytes()},
)

Operational tips:

  • Keep idempotency_key stable across retries of the same logical send.
  • Use one correlation_id across all related messages/events in a workflow.
  • Avoid putting secrets in custom headers; headers are metadata.
  • For backend-managed usage settlement linkage, prefer client.send_mail(...) via /send.

Notes#

  • The single SDK encrypted-send path is client.send_mail(..., encrypt_body=True, encrypt_attachments=True), which produces a raw -----BEGIN PGP MESSAGE----- body and the X-AgentMail-Encrypted: body header. The Option 2 pattern above reproduces the same wire shape outside /send.
  • For file attachments in encrypted flows, pass OutboundAttachment / OutboundEncryptedAttachment to send_mail. The SDK encrypts each file to the peer's armored OpenPGP key, uploads the ciphertext as application/pgp-encrypted (.asc filename), and emits per-attachment encrypted: true / encryption: {algorithm: "openpgp", format: "armored"} flags on the manifest header.
  • Direct-SES sends bypass the AgentMail /send API, so backend-managed billing reservations and settlement linkage do not apply.