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_mailuses, 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.
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:
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:
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:
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:
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.
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_keystable across retries of the same logical send. - Use one
correlation_idacross 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 theX-AgentMail-Encrypted: bodyheader. The Option 2 pattern above reproduces the same wire shape outside/send. - For file attachments in encrypted flows, pass
OutboundAttachment/OutboundEncryptedAttachmenttosend_mail. The SDK encrypts each file to the peer's armored OpenPGP key, uploads the ciphertext asapplication/pgp-encrypted(.ascfilename), and emits per-attachmentencrypted: true/encryption: {algorithm: "openpgp", format: "armored"}flags on the manifest header. - Direct-SES sends bypass the AgentMail
/sendAPI, so backend-managed billing reservations and settlement linkage do not apply.