Email guide (AWS SES)

BYO email channel: connect your own AWS SES verified domain so your agent replies from your address. Inbound mail arrives via SNS; outbound is sent through SES v2, preserving observed reply headers.

How it works

  1. SES receipt rule — AWS stores incoming MIME mail in your S3 bucket and sends its object reference to an SNS topic. Legacy inline SNS receipts remain available for small mail.
  2. SNS HTTPS subscription — you subscribe that topic to the returned webhook URL; when mail arrives the SNS notification is POSTed to /webhooks/email/{appId}. AgentSky confirms the signed subscription challenge after verifying its topic and AWS endpoint.
  3. Thread mapping — the In-Reply-To / References headers are used to map replies to the same conversation thread. New conversations start a fresh thread.
  4. Outbound — your agent's reply is sent via SES v2 SendEmail with the correct threading headers so email clients show the conversation inline.

Credentials

FieldDescription
AWS RegionThe region where your SES identity is verified (e.g. us-east-1).
Access Key IDIAM access key with ses:SendEmail permission for your verified sender.
Secret Access KeyThe corresponding IAM secret.
From addressThe SES-verified sender address (e.g. agent@yourdomain.com).
SNS Topic ARNThe ARN of the SNS topic your SES receipt rule publishes to.

For ordinary attachments, supply s3_bucket, s3_prefix and s3_bucket_owner together. The bucket must be in the configured region; the nonempty prefix ends in / (for example inbound/astra/); the owner is the bucket account’s 12-digit AWS ID. Use a dedicated mailbox prefix. Leaving all three absent preserves legacy inline SNS receiving.

Optional credentials: from_name supplies the sender display name (Unicode and quoted names are MIME-encoded; control characters are rejected), while from_address remains the bare SES-verified mailbox. Omitting the name sends a bare address. inbound_address selects an additional receiving mailbox, allow_subaddressing accepts plus-tagged receiving addresses, and reply_to_sender_only ignores a sender's alternate MIME Reply-To. The two flags are strings "true" or "false", defaulting to false. Once any connection exists, changing sender-only mode is rejected because it changes conversation scope; ordinary IAM credential rotation remains available.

AWS setup

1 — Verify your sender domain or address

Open the AWS SES console and verify the domain or address you will send from.

2 — Create an SNS topic

In Amazon SNS → Topics, create a Standard topic (e.g. ses-inbound-agentsky). Note the Topic ARN.

3 — Create an SES receipt rule

In SES → Email receiving → Rule sets, match the exact receiving mailbox and enable spam and virus scanning. Use a Deliver to S3 action with the configured bucket and prefix, and select the SNS topic from step 2 in that same action. SES stores raw MIME at the prefix followed by its message ID. Do not add a second SNS-content action for the same message.

Grant the SES service permission to write only this prefix and publish to this topic, constrained by your source account and receipt-rule ARN. Use ordinary S3 server-side encryption; the SES action’s separate Message encryption option uses client-side encryption and is not supported by this reader. For SSE-KMS, the runtime identity also needs decrypt permission on that exact key. See AWS receiving permissions.

Retain objects beyond your delivery/retry window using a lifecycle rule limited to the owned prefix. AgentSky never deletes receipt objects. Versioned buckets pin the original version; unversioned buckets require an unchanged ETag, and both verify the MIME digest when queued attachments resume. Missing or changed objects refuse delivery rather than substituting bytes.

Legacy mode: an SNS action with UTF-8 content encoding still works when all three S3 fields are absent. It accepts at most 150 KB including headers; AWS bounces larger mail. Base64 receipt actions are unsupported.

4 — Create an IAM user

Create an IAM user with an inline policy granting:

json
{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow", "Action": "ses:SendEmail", "Resource": "arn:aws:ses:REGION:ACCOUNT_ID:identity/YOUR_VERIFIED_DOMAIN" },
    { "Effect": "Allow", "Action": ["s3:GetObject", "s3:GetObjectVersion"], "Resource": "arn:aws:s3:::YOUR_BUCKET/inbound/astra/*" }
  ]
}

Replace the region, account ID, verified domain, bucket and exact prefix in the policy. Omit GetObjectVersion for an unversioned bucket and omit both S3 actions for legacy inline mode. The reader needs no ListBucket, PutObject or DeleteObject permission. Existing send-only keys need this narrow read grant before S3 receiving works. Generate an access key and copy both values. Read-only ses:GetAccount on Resource * is optional and lets AgentSky check whether account sending is enabled.

Connect the app

In the developer console fill in the five required fields and the three S3 fields for attachment receiving, then click Create app. AgentSky first checks the account with read-only GetAccount. If AWS denies that read, it authenticates the same credentials with STS GetCallerIdentity instead. It returns the SNS webhook URL without sending a test email. A successful connection verifies credentials; it does not probe the bucket or prove sending permission, sender identity, S3 read permission or receiving configuration:

text
setup: { "webhook_url": "<returned webhook URL>" }

Add an HTTPS subscription on your SNS topic pointing to this URL. AgentSky auto-confirms it.

Connect a conversation

sh
curl -X POST https://api.agentsky.dev/v1/channels/connections \
  -H "Authorization: Bearer <token>" \
  -d '{"platform": "email", "label": "Support inbox", "app": "<appId>"}'
# -> { "id": "conn_...", "status": "CONNECTED" }

Bind an agent session:

sh
curl -X PUT https://api.agentsky.dev/v1/channels/connections/<conn_id>/binding \
  -H "Authorization: Bearer <token>" \
  -d '{"destination": {"session": "sess-..."}}'

Consumer replies and files

Normal BYO apps retain one mailbox connection, email:{appId}. Sender-only apps isolate each administratively verified recipient at email:{appId}:{recipient}; use the existing admin-scoped adopt connection flow with a matching author_id. Threads in both modes use email:{appId}:{recipient}:{16-character-root-hash}. Never construct the retired five-segment wrapper.

POST /api/v1/channels/threads/{thread_id}/messages accepts email: { subject, reply_to } beside parts and idempotency_key. These headers cannot change the conversation recipient. A replay returns the accepted receipt; an uncertain send is not automatically repeated. Incoming consumer events include email.to, email.subject, the observed RFC email.message_id, and email.provider_message_id.

Inbound MIME attachments become scoped public Files with name, MIME type and byte size in attachments. S3 receiving supports raw MIME up to the service’s 40 MiB bound, with at most 32 MiB total decoded attachment bytes; AWS may impose a lower message limit. Oversized objects stop while streaming and do not create Files. SNS carries metadata only, and the busy-conversation queue stores pinned object/part references instead of attachment base64. Restoration rechecks the current ACTIVE app, owner, workspace, mailbox, topic and S3 namespace before reading. The existing queue expires logically after 90 seconds; S3 storage does not extend that queue window. Legacy inline SNS remains limited to 150 KB including headers and MIME encoding. Outbound file parts resolve authorized Files and send real MIME attachments with their bytes and names. Files must total at most 32 MiB before encoding, and the full SES v2 MIME message must fit 40 MiB after encoding. Cross-workspace files are refused before sending.

Troubleshooting

  • S3 receipt returns 503 — check the exact key exists, the current IAM key can read it (and its version), the bucket owner matches, and any KMS permission is present. Retryable reads never fall back to another bucket or credentials. Registration alone does not check this.
  • S3 receipt returns 403 — compare the configured bucket/prefix/topic against the signed SES action. The reader accepts only prefix + SES message ID in that owned namespace.
  • SNS sends Confirmation but webhook returns 403 — the TopicArn in the SNS notification doesn't match the one stored in the app credentials; verify the app was registered with the correct ARN.
  • Replies start a new thread — inspect the received In-Reply-To / References headers and the 30-day state lifetime. Outbound replies reference message IDs actually received; SES response IDs are tracked as aliases rather than fabricated email headers.
  • Mail not processed — the signed SES receipt must have a passing DMARC verdict and name the configured receiving address. MIME authentication headers alone are not trusted.
  • Spam or virus verdict FAIL — inbound messages that fail SES spam or virus checks are silently dropped. Check your SES receipt rule's spam action if you need to handle these differently.