An AI-powered email assistant that polls an O365 shared mailbox via Microsoft Graph API, processes messages through AWS Bedrock AgentCore and the Strands agent framework, and sends HTML replies — all deployed on serverless AWS infrastructure with Terraform.
- O365 inbox polling: EventBridge-scheduled Lambda polls via Graph API delta query for efficient incremental sync
- Idempotent processing: DynamoDB tracks processed emails and delta tokens to prevent duplicate replies
- Bedrock AgentCore worker: Containerized Strands agent handles thread context, knowledge base retrieval, and reply generation
- Email-safe HTML replies: Markdown responses converted to inline-styled HTML compatible with Outlook, Gmail, and Apple Mail
- MCP tool integrations: GitHub, Atlassian, PagerDuty (via Gateway), Azure, AWS CLI, Splunk, and Atlan
- File attachments and charts: Agent can generate downloadable files and charts as email attachments
- Cross-platform memory: AgentCore Memory persists user preferences across sessions
- Recipient filtering: Reply-all filtered to internal domains only — no accidental external replies
- Audit logging: Optional CloudWatch audit trail with OpenTelemetry trace correlation
- Bedrock Guardrails: Content safety guardrail on all model invocations
EventBridge (schedule) → Poller Lambda → Invoker Lambda → AgentCore Worker
↓ ↓
DynamoDB Microsoft Graph
(Delta Tokens + (Send Email Reply)
Processed Emails)
| Component | Purpose |
|---|---|
| Poller | EventBridge-triggered Lambda; polls inbox via Graph API delta query; invokes Invoker per new email |
| Invoker | Bridge Lambda; forwards email payload to AgentCore runtime |
| Worker | Bedrock AgentCore container; builds thread context, runs Strands agent, sends reply |
Mermaid architecture diagram
graph TD
EB[EventBridge Schedule] --> PL[Poller Lambda]
PL --> DDB1[(DynamoDB\nDelta Tokens)]
PL --> DDB2[(DynamoDB\nProcessed Emails)]
PL --> IL[Invoker Lambda]
IL --> AC[AgentCore Runtime\nWorker Container]
AC --> KB[Bedrock Knowledge Base]
AC --> MEM[AgentCore Memory]
AC --> GW[MCP Gateway\nPagerDuty etc.]
AC --> GH[GitHub MCP]
AC --> AZ[Azure MCP]
AC --> AWS[AWS CLI MCP]
AC --> SP[Splunk MCP]
AC --> AT[Atlan MCP]
AC --> GRAPH[Microsoft Graph API\nSend Reply]
- Azure Portal → Azure Active Directory → App registrations → New registration
- Name:
EmailBot-{env} - API Permissions (Application, not Delegated):
Mail.Read,Mail.ReadWrite,Mail.Send
- Grant admin consent
- Create a client secret (Certificates & secrets → New client secret)
Record:
AZURE_TENANT_ID: Directory (tenant) IDAZURE_CLIENT_ID: Application (client) IDAZURE_CLIENT_SECRET: Client secret value
Separate registration for querying Azure resources via MCP.
- Assign Reader role on subscriptions/resource groups the bot needs to query
- Record:
AZURE_MCP_CLIENT_ID,AZURE_MCP_CLIENT_SECRET(sharesAZURE_TENANT_ID)
Create a shared mailbox in Microsoft 365 Admin Center. No license required.
Restrict mailbox access via PowerShell (recommended):
Connect-ExchangeOnline
New-DistributionGroup -Name "EmailBot-AllowedMailboxes" -Type Security
Add-DistributionGroupMember -Identity "EmailBot-AllowedMailboxes" -Member "bot@yourdomain.com"
New-ApplicationAccessPolicy `
-AppId "{AZURE_CLIENT_ID}" `
-PolicyScopeGroupId "EmailBot-AllowedMailboxes" `
-AccessRight RestrictAccess `
-Description "Restrict EmailBot to specific mailboxes"Create a secret containing all credentials:
{
"AZURE_TENANT_ID": "your-tenant-id",
"AZURE_CLIENT_ID": "your-client-id",
"AZURE_CLIENT_SECRET": "your-client-secret",
"AZURE_MCP_CLIENT_ID": "your-azure-mcp-client-id",
"AZURE_MCP_CLIENT_SECRET": "your-azure-mcp-client-secret",
"GITHUB_TOKEN": "ghp_your-github-token",
"GATEWAY_CLIENT_SECRET": "your-gateway-client-secret",
"ATLAN_API_KEY": "your-atlan-api-key",
"ATLAN_BASE_URL": "https://your-org.atlan.com",
"SPLUNK_TOKEN": "your-splunk-token"
}The worker requires a Bedrock Knowledge Base, AgentCore Memory, Bedrock Guardrail, and an AgentCore Gateway (for PagerDuty and other MCP providers). These can be shared with other bots in your organization.
1. Configure tfvars (copy from data/tf-aws-agentic-email-bot-example.tfvars):
environment = "prod"
email_mailbox = "bot@yourdomain.com"
secret_name = "mybot/secrets/EMAILBOT_SECRETS_JSON"
knowledge_base_id = "YOUR_KB_ID"
memory_id = "YOUR_MEMORY_ID"
guardrails_id = "YOUR_GUARDRAIL_ID"
gateway_url = "https://YOUR_GATEWAY.gateway.bedrock-agentcore.us-east-1.amazonaws.com/mcp"2. Deploy infrastructure:
terraform init
terraform plan -var-file=data/tf-aws-agentic-email-bot-example.tfvars
terraform apply -var-file=data/tf-aws-agentic-email-bot-example.tfvars3. Build and push worker container:
# Authenticate to ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <account>.dkr.ecr.us-east-1.amazonaws.com
# Build for ARM64 (Lambda architecture)
docker buildx build --platform linux/arm64 -t <ecr-url>:latest ./worker --pushEdit worker/src/worker_inputs.py to customize the bot's persona, tool instructions, and internal knowledge base guidance for your organization.
Set the INTERNAL_EMAIL_DOMAINS environment variable (comma-separated) to control which email domains receive bot replies. Defaults to yourdomain.com.
Each MCP client in worker/src/worker_mcp_client_*.py fails silently if credentials are missing — disable integrations by simply omitting their credentials from the secret.
├── main.tf # Module composition
├── locals.tf # Naming conventions
├── variables.tf # Input variables
├── data.tf # Data sources
├── secrets.tf # Secrets Manager reference
├── ecr.tf # Worker container registry
├── dynamodb_delta.tf # Delta tokens + processed emails
├── poller/ # Poll inbox Lambda
│ └── src/poller.py
├── invoker/ # AgentCore invoker Lambda
│ └── src/invoker.py
├── worker/ # AgentCore container runtime
│ ├── Dockerfile
│ ├── requirements.txt
│ └── src/
│ ├── worker_agentcore.py # BedrockAgentCoreApp entry point
│ ├── worker_conversation.py # Thread context builder
│ ├── worker_email_graph.py # Graph API client (OAuth, delta, reply-all)
│ ├── worker_agent.py # Strands agent executor
│ ├── worker_inputs.py # Config, system prompt, constants
│ ├── worker_knowledge_base.py # Custom KB search tool
│ ├── worker_memory_tools.py # Memory management tools
│ ├── worker_audit.py # CloudWatch audit logging
│ ├── worker_token_cache.py # Container-level token cache
│ └── worker_mcp_client_*.py # MCP integrations
└── data/ # Example tfvars
CloudWatch Log Groups:
- Poller:
/aws/lambda/{prefix}EmailPoller - Invoker:
/aws/lambda/{prefix}EmailInvoker - Worker:
/aws/bedrock-agentcore/...(AgentCore managed, includes OTEL traces)
Log Emoji Indicators:
- 🟢 Success/completion
- 🟡 Warning/info
- 🔴 Error/failure
- 🚮 Skipped/filtered
- 🚫 Fatal error