You finally got your AI agent doing something useful — scheduling meetings, sending reports, responding to support tickets. Then you check the logs and realize half its emails are bouncing or rotting in spam folders. Sound familiar?
I spent the better part of last month debugging this exact problem for an automated workflow system, and the root causes are surprisingly consistent. Let me walk you through why programmatic email is harder than it looks and how to actually fix it.
The Core Problem: Email Wasn't Built for Machines
SMTP was designed in 1982 for humans sending messages to other humans. Four decades later, we're asking AI agents to participate in email conversations, and the entire anti-spam ecosystem is designed to make that difficult.
When your agent sends an email, it has to pass through multiple layers of validation that were specifically built to stop automated senders:
- SPF (Sender Policy Framework) — verifies the sending IP is authorized
- DKIM (DomainKeys Identified Mail) — cryptographically signs messages
- DMARC (Domain-based Message Authentication) — ties SPF and DKIM together with a policy
Miss any one of these, and your agent's carefully crafted email goes straight to junk. Or worse, silently disappears.
Step 1: Set Up DNS Records Properly
This is where most people trip up. You spin up a mail-sending service, fire off a test email, it works in dev, and you move on. Then production traffic starts and deliverability craters.
Here's what your DNS needs at minimum:
; SPF record — authorize your sending IPs
; Replace with your actual mail server IPs
yourdomain.com. IN TXT "v=spf1 ip4:203.0.113.10 ip4:198.51.100.0/24 -all"
; DKIM selector record — public key for signature verification
mail._domainkey.yourdomain.com. IN TXT "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBA..."
; DMARC policy — what to do when checks fail
_dmarc.yourdomain.com. IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc-reports@yourdomain.com; pct=100"The -all in the SPF record is important. I see people use ~all (soft fail) thinking it's safer. It is — for getting your emails flagged. Use -all and explicitly list every IP that sends on your behalf.
For the DMARC policy, start with p=none while you're monitoring, then graduate to p=reject once you're confident your alignment is correct. The rua tag sends you aggregate reports so you can see what's passing and failing.
Step 2: Generate and Rotate DKIM Keys
DKIM is the one that trips up automated systems the most. You need to generate a key pair, configure your mail server to sign outgoing messages with the private key, and publish the public key in DNS.
import dkim
import smtplib
from email.mime.text import MIMEText
def send_signed_email(sender, recipient, subject, body):
msg = MIMEText(body)
msg["From"] = sender
msg["To"] = recipient
msg["Subject"] = subject
# Convert to bytes for DKIM signing
msg_bytes = msg.as_bytes()
# Sign with your private key
# selector and domain must match your DNS records
with open("/etc/dkim/private.key", "rb") as f:
private_key = f.read()
signature = dkim.sign(
msg_bytes,
b"mail", # DKIM selector
b"yourdomain.com", # signing domain
private_key,
include_headers=[ # headers to include in signature
b"From", b"To", b"Subject", b"Date", b"Message-ID"
]
)
# Prepend the signature header to the message
signed_msg = signature + msg_bytes
with smtplib.SMTP("localhost", 587) as server:
server.starttls()
server.send_message(msg) # send the signed version
return TrueOne gotcha I ran into: rotate your DKIM keys regularly. If a key is compromised or if you've been using the same 1024-bit key since 2019, upgrade to 2048-bit and set up rotation every 6-12 months. You can run two selectors simultaneously during rotation to avoid downtime.
Step 3: Handle Inbound Email for Agent Responses
Sending is only half the battle. If your agent needs to receive replies (and most useful agents do), you need to handle inbound email processing.
The most reliable approach I've found is using MX records pointed at a server that parses incoming messages and routes them to your agent:
import email
from email import policy
import asyncio
from aiosmtpd.controller import Controller
class AgentMailHandler:
async def handle_RCPT(self, server, session, envelope, address, rcpt_options):
# Only accept mail for your domain
if not address.endswith("@yourdomain.com"):
return "550 not relaying to that domain"
envelope.rcpt_tos.append(address)
return "250 OK"
async def handle_DATA(self, server, session, envelope):
msg = email.message_from_bytes(
envelope.content,
policy=policy.default
)
# Extract the plain text body
body = msg.get_body(preferencelist=("plain",))
text_content = body.get_content() if body else ""
# Route to your agent based on the recipient address
# e.g., support-agent@yourdomain.com -> support handler
recipient = envelope.rcpt_tos[0]
agent_id = recipient.split("@")[0]
await route_to_agent(agent_id, {
"from": envelope.mail_from,
"subject": msg["subject"],
"body": text_content,
"message_id": msg["message-id"],
"in_reply_to": msg.get("in-reply-to"), # for threading
})
return "250 Message accepted"
# Start the SMTP server
controller = Controller(AgentMailHandler(), hostname="0.0.0.0", port=25)
controller.start()The In-Reply-To and References headers are critical for maintaining conversation threading. Without them, every reply from your agent starts a new thread in the recipient's inbox, which is confusing and unprofessional.
Step 4: Implement Rate Limiting and Warm-Up
This is the part nobody tells you about until you're already in trouble. If your agent starts sending 500 emails a day from a fresh domain, every major email provider will throttle or block you.
IP warm-up schedule (roughly):- Week 1: 50 emails/day
- Week 2: 100 emails/day
- Week 3: 250 emails/day
- Week 4: 500 emails/day
- Increase by ~50% each week after that
Build rate limiting directly into your agent's email-sending logic:
import time
from collections import defaultdict
class EmailRateLimiter:
def __init__(self, max_per_hour=100, max_per_day=500):
self.max_per_hour = max_per_hour
self.max_per_day = max_per_day
self.hourly_counts = defaultdict(int)
self.daily_counts = defaultdict(int)
def can_send(self, domain: str) -> bool:
current_hour = int(time.time() // 3600)
current_day = int(time.time() // 86400)
if self.hourly_counts[current_hour] >= self.max_per_hour:
return False
if self.daily_counts[current_day] >= self.max_per_day:
return False
return True
def record_send(self):
current_hour = int(time.time() // 3600)
current_day = int(time.time() // 86400)
self.hourly_counts[current_hour] += 1
self.daily_counts[current_day] += 1Also: always include a List-Unsubscribe header for any automated email. It's not just polite — major email providers will penalize your deliverability if bulk-ish email doesn't include one. Google and Yahoo both started enforcing this aggressively in 2024.
Step 5: Monitor Bounce Rates and Feedback Loops
Set up monitoring from day one. You need to track:
- Hard bounces — invalid addresses, remove them immediately
- Soft bounces — temporary failures, retry with exponential backoff
- Spam complaints — if this exceeds 0.1%, you have a serious problem
- DMARC reports — parse the aggregate reports to catch alignment issues early
Sign up for feedback loops (FBLs) with major providers. When someone marks your agent's email as spam, the FBL notifies you so you can suppress that address.
Prevention: Things I Wish I'd Done From the Start
- Use a subdomain for agent-generated email (
agent.yourdomain.com). This isolates your agent's reputation from your main domain. If deliverability tanks, it doesn't drag down your human team's email. - Set up BIMI (Brand Indicators for Message Identification) if you have a verified trademark. It puts your logo next to your emails in supporting clients, which builds trust.
- Log everything. Every SMTP response code, every bounce, every delivery confirmation. When deliverability drops (and it will), you need the data to diagnose why.
- Test before you ship. Tools like
mail-tester.comwill score your setup and flag issues. Run your agent's emails through it during development, not after you've already annoyed 1,000 recipients.
The Bigger Picture
As AI agents become more autonomous, email is becoming a critical interface for them to interact with the world. But the email ecosystem has decades of anti-abuse infrastructure that assumes senders are either humans or spammers. The agents that succeed at email will be the ones that play by the rules — proper authentication, gradual warm-up, and genuine respect for recipients' inboxes.
Get the fundamentals right, and your agent's emails will actually get read. Skip them, and you're just building an elaborate system for generating spam complaints.
