Sending and Receiving Email
Complete guide to the email lifecycle in AgentPost
This guide covers the full email lifecycle: creating inboxes, sending messages, handling inbound email, processing attachments, and implementing reply and forward patterns. By the end, you will have a complete understanding of how to build an email-powered agent.
The email lifecycle
Create Inbox --> Send Email --> Receive Replies --> Process & Respond
| |
v v
SES delivers SES receives
to recipient via SNS notificationCreating an inbox
Every email workflow starts with an inbox. Create one with a descriptive username:
import AgentPost from 'agentpost';
const client = new AgentPost({ apiKey: 'ap_sk_...' });
const inbox = await client.inboxes.create({
username: 'support',
display_name: 'AcmeCo Customer Support',
client_id: 'support-inbox-prod', // Idempotent -- safe to retry
});
console.log(`Support inbox: ${inbox.email}`);from agentpost import AgentPost
client = AgentPost(api_key="ap_sk_...")
inbox = client.inboxes.create(
username="support",
display_name="AcmeCo Customer Support",
client_id="support-inbox-prod",
)
print(f"Support inbox: {inbox.email}")curl -X POST https://api.agent-post.dev/api/v1/inboxes \
-H "Authorization: Bearer $AGENTPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"username": "support",
"display_name": "AcmeCo Customer Support",
"client_id": "support-inbox-prod"
}'Sending email
Basic send
const message = await client.messages.send(inbox.id, {
to: [{ email: 'customer@example.com', name: 'Jane Smith' }],
subject: 'Your support ticket #4821 has been received',
text_body: 'Hi Jane,\n\nWe have received your support request and a team member will respond within 2 hours.\n\nTicket ID: #4821\nSubject: Login issues on mobile app\n\nBest,\nAcmeCo Support',
html_body: '<p>Hi Jane,</p><p>We have received your support request and a team member will respond within 2 hours.</p><p><strong>Ticket ID:</strong> #4821<br><strong>Subject:</strong> Login issues on mobile app</p><p>Best,<br>AcmeCo Support</p>',
});message = client.messages.send(
inbox_id=inbox.id,
to=[{"email": "customer@example.com", "name": "Jane Smith"}],
subject="Your support ticket #4821 has been received",
text_body="Hi Jane,\n\nWe have received your support request...",
html_body="<p>Hi Jane,</p><p>We have received your support request...</p>",
)curl -X POST "https://api.agent-post.dev/api/v1/inboxes/$INBOX_ID/messages" \
-H "Authorization: Bearer $AGENTPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": [{"email": "customer@example.com", "name": "Jane Smith"}],
"subject": "Your support ticket #4821 has been received",
"text_body": "Hi Jane, We have received your support request..."
}'With CC and BCC
const message = await client.messages.send(inbox.id, {
to: [{ email: 'customer@example.com', name: 'Jane Smith' }],
cc: [{ email: 'team-lead@acmeco.com', name: 'Team Lead' }],
bcc: [{ email: 'support-log@acmeco.com' }],
subject: 'Resolution: Ticket #4821',
text_body: 'Hi Jane, the login issue has been resolved...',
});message = client.messages.send(
inbox_id=inbox.id,
to=[{"email": "customer@example.com", "name": "Jane Smith"}],
cc=[{"email": "team-lead@acmeco.com", "name": "Team Lead"}],
bcc=[{"email": "support-log@acmeco.com"}],
subject="Resolution: Ticket #4821",
text_body="Hi Jane, the login issue has been resolved...",
)curl -X POST "https://api.agent-post.dev/api/v1/inboxes/$INBOX_ID/messages" \
-H "Authorization: Bearer $AGENTPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": [{"email": "customer@example.com", "name": "Jane Smith"}],
"cc": [{"email": "team-lead@acmeco.com", "name": "Team Lead"}],
"bcc": [{"email": "support-log@acmeco.com"}],
"subject": "Resolution: Ticket #4821",
"text_body": "Hi Jane, the login issue has been resolved..."
}'Receiving email
Polling for new messages
The simplest approach is to periodically poll for new inbound messages:
async function pollForMessages(inboxId: string) {
const messages = await client.messages.list(inboxId, {
direction: 'inbound',
limit: 10,
});
for (const msg of messages.data) {
console.log(`New message from ${msg.from_address.email}`);
console.log(`Subject: ${msg.subject}`);
console.log(`Content: ${msg.extracted_text}`);
// Process the message (classify, respond, label, etc.)
await processInboundMessage(msg);
}
}def poll_for_messages(inbox_id: str):
messages = client.messages.list(
inbox_id=inbox_id,
direction="inbound",
limit=10,
)
for msg in messages.data:
print(f"New message from {msg.from_address['email']}")
print(f"Subject: {msg.subject}")
process_inbound_message(msg)curl "https://api.agent-post.dev/api/v1/inboxes/$INBOX_ID/messages?direction=inbound&limit=10" \
-H "Authorization: Bearer $AGENTPOST_API_KEY"Real-time with webhooks
For production use, configure a webhook endpoint to receive instant notifications:
// 1. Create a webhook endpoint
const endpoint = await client.webhookEndpoints.create({
url: 'https://your-agent.example.com/webhooks/agentpost',
events: ['message.received'],
});
// 2. Handle the webhook in your server
app.post('/webhooks/agentpost', async (req, res) => {
const event = req.body;
if (event.type === 'message.received') {
const message = event.data;
console.log(`New email from ${message.from_address.email}`);
// Auto-classify and respond
const classification = await classifyMessage(message.extracted_text);
await client.messages.addLabels(message.id, {
labels: [classification.category],
});
if (classification.auto_reply) {
await client.messages.reply(message.id, {
text_body: classification.response,
});
}
}
res.status(200).json({ received: true });
});# 1. Create a webhook endpoint
endpoint = client.webhook_endpoints.create(
url="https://your-agent.example.com/webhooks/agentpost",
events=["message.received"],
)
# 2. Handle the webhook in your server (Flask example)
@app.route("/webhooks/agentpost", methods=["POST"])
def handle_webhook():
event = request.json
if event["type"] == "message.received":
message = event["data"]
classification = classify_message(message["extracted_text"])
client.messages.add_labels(
message_id=message["id"],
labels=[classification["category"]],
)
return {"received": True}, 200# Create a webhook endpoint
curl -X POST https://api.agent-post.dev/api/v1/webhook-endpoints \
-H "Authorization: Bearer $AGENTPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-agent.example.com/webhooks/agentpost",
"events": ["message.received"]
}'Reply and forward patterns
Replying to a message
Replies stay in the same thread and automatically set the correct email headers:
// Reply adds "Re:" prefix and continues the thread
const reply = await client.messages.reply(inboundMessage.id, {
text_body: 'Hi Jane,\n\nI have looked into the login issue. The root cause was an expired session token. I have reset your session -- please try logging in again.\n\nLet me know if this resolves the issue.\n\nBest,\nAcmeCo Support',
});reply = client.messages.reply(
message_id=inbound_message.id,
text_body="Hi Jane,\n\nI have looked into the login issue...",
)curl -X POST "https://api.agent-post.dev/api/v1/messages/$MESSAGE_ID/reply" \
-H "Authorization: Bearer $AGENTPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text_body": "Hi Jane, I have looked into the login issue..."}'Forwarding a message
Forwarding creates a new thread with the original message content:
// Forward creates a new thread with "Fwd:" prefix
const forwarded = await client.messages.forward(inboundMessage.id, {
to: [{ email: 'mobile-team@acmeco.com', name: 'Mobile Team' }],
text_body: 'Can the mobile team investigate this login issue? Customer reports it only happens on iOS 18.',
});forwarded = client.messages.forward(
message_id=inbound_message.id,
to=[{"email": "mobile-team@acmeco.com", "name": "Mobile Team"}],
text_body="Can the mobile team investigate this login issue?",
)curl -X POST "https://api.agent-post.dev/api/v1/messages/$MESSAGE_ID/forward" \
-H "Authorization: Bearer $AGENTPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": [{"email": "mobile-team@acmeco.com", "name": "Mobile Team"}],
"text_body": "Can the mobile team investigate this login issue?"
}'Processing attachments
Handling inbound attachments
// Check if the message has attachments
if (message.has_attachments) {
// Get attachment details
const attachment = await client.attachments.get(message.attachment_ids[0]);
// Download the file
const response = await fetch(attachment.download_url);
const buffer = await response.arrayBuffer();
// Process the attachment (e.g., extract text from PDF)
const text = await extractTextFromPDF(Buffer.from(buffer));
console.log(`Extracted ${text.length} characters from ${attachment.filename}`);
}if message.has_attachments:
attachment = client.attachments.get(message.attachment_ids[0])
import requests
response = requests.get(attachment.download_url)
# Process the attachment
text = extract_text_from_pdf(response.content)
print(f"Extracted {len(text)} characters from {attachment.filename}")# Get attachment details
curl "https://api.agent-post.dev/api/v1/attachments/$ATTACHMENT_ID" \
-H "Authorization: Bearer $AGENTPOST_API_KEY"
# Download from the presigned URL (returned in the response)
curl -o "downloaded-file.pdf" "$DOWNLOAD_URL"Building a support ticket agent
Here is a complete example of a support ticket agent that receives email, classifies it, and responds:
import AgentPost from 'agentpost';
const client = new AgentPost({ apiKey: 'ap_sk_...' });
async function handleSupportTicket(message: any) {
// 1. Label the message as received
await client.messages.addLabels(message.id, {
labels: ['received', 'needs-triage'],
});
// 2. Classify the message content
const category = await classifyWithLLM(message.extracted_text);
await client.messages.addLabels(message.id, {
labels: [category], // e.g., "billing", "technical", "account"
});
await client.messages.removeLabels(message.id, {
labels: ['needs-triage'],
});
// 3. Generate and send a response
const response = await generateResponseWithLLM(
message.extracted_text,
category,
);
await client.messages.reply(message.id, {
text_body: response,
html_body: `<p>${response.replace(/\n/g, '<br>')}</p>`,
});
// 4. Label as responded
await client.messages.addLabels(message.id, {
labels: ['auto-responded'],
});
console.log(`Handled ticket from ${message.from_address.email} [${category}]`);
}from agentpost import AgentPost
client = AgentPost(api_key="ap_sk_...")
def handle_support_ticket(message):
# 1. Label as received
client.messages.add_labels(
message_id=message.id,
labels=["received", "needs-triage"],
)
# 2. Classify
category = classify_with_llm(message.extracted_text)
client.messages.add_labels(message_id=message.id, labels=[category])
client.messages.remove_labels(message_id=message.id, labels=["needs-triage"])
# 3. Generate and send response
response = generate_response_with_llm(message.extracted_text, category)
client.messages.reply(
message_id=message.id,
text_body=response,
)
# 4. Mark as responded
client.messages.add_labels(message_id=message.id, labels=["auto-responded"])# This workflow is best implemented in code using the SDK.
# The cURL equivalent would require multiple sequential requests:
# 1. Label as received
curl -X POST "https://api.agent-post.dev/api/v1/messages/$MSG_ID/labels" \
-H "Authorization: Bearer $AGENTPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"labels": ["received", "needs-triage"]}'
# 2. Reply to the message
curl -X POST "https://api.agent-post.dev/api/v1/messages/$MSG_ID/reply" \
-H "Authorization: Bearer $AGENTPOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text_body": "Thank you for contacting support..."}'Tips
- Always provide both
text_bodyandhtml_body-- some email clients only render one format - Use
extracted_text(nothtml_body) for AI processing of inbound messages - Subject prefixes (
Re:,Fwd:) are added automatically -- do not add them yourself - Use
client_idon inbox creation to safely handle agent restarts - For high-volume workflows, use webhooks instead of polling
- Check
send_pausedon your inbox before sending -- it indicates a delivery issue