Every business runs on email, and most of it is repetitive: confirmations, status requests, forwarding to the right person, skimming newsletters for one relevant paragraph. An AI agent email integration built on IMAP and SMTP handles that work directly on your infrastructure — no third-party SaaS middleware, no per-seat pricing, no data leaving your servers.
This guide walks through the full stack: connecting to a mailbox, reading and classifying messages, generating draft replies, and sending outbound email — all from a self-hosted agent you control.
Why IMAP + SMTP (and Not a Vendor API)
When people think "email automation," they often reach for a SaaS connector — Zapier, Make, or the Gmail/Outlook API. Those work, but they come with strings: OAuth app review, per-seat costs, rate limits you don't control, and data that passes through someone else's servers.
IMAP (Internet Message Access Protocol) is the standard protocol for reading email from a mail server. SMTP (Simple Mail Transfer Protocol) is the standard for sending. Both are vendor-neutral, universally supported, and require zero API keys from a middleman.
The IMAP/SMTP approach has concrete advantages:
- Universal. Works with Google Workspace, Microsoft 365, ProtonMail Bridge, self-hosted Postfix/Dovecot, Fastmail, Zoho — any provider that offers standard mailbox access.
- No vendor lock-in. Switch email providers without rewriting a single line of agent code.
- Data stays yours. Credentials and message content never touch a third-party SaaS platform.
- No per-automation billing. You pay your mail provider (already paid) and LLM token costs. No "tasks per month" metering.
The trade-off is that you handle the plumbing yourself. But as we'll see, the plumbing is well-understood and not especially complicated.
Setting Up IMAP Access for an AI Agent
Prerequisites
You need three things before writing any code:
1. A mailbox — a dedicated address like [email protected] or a shared inbox like [email protected]. 2. App-specific credentials — not your human login password. Google, Microsoft, and most providers let you generate an app password scoped to IMAP/SMTP only. 3. IMAP server details — typically imap.yourprovider.com on port 993 (TLS) and smtp.yourprovider.com on port 587 (STARTTLS).
Connecting with Python (imaplib)
Python's standard library includes imaplib. Here's a minimal, production-viable connection that reads unread messages from the inbox:
import imaplib
import email
from email.header import decode_header
IMAP_HOST = "imap.yourcompany.com"
IMAP_PORT = 993
EMAIL = "[email protected]"
APP_PASSWORD = "your-app-password"
def fetch_unread(limit=20):
mail = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT)
mail.login(EMAIL, APP_PASSWORD)
mail.select("INBOX")
status, data = mail.search(None, "UNSEEN")
msg_ids = data[0].split()
messages = []
for uid in msg_ids[-limit:]: # last N unread
_, msg_data = mail.fetch(uid, "(RFC822)")
raw = msg_data[0][1]
msg = email.message_from_bytes(raw)
subject = decode_header(msg["Subject"])
subject = subject[0][0]
if isinstance(subject, bytes):
subject = subject.decode()
body = _extract_text(msg)
messages.append({
"uid": uid.decode(),
"from": msg["From"],
"to": msg["To"],
"subject": subject,
"body": body,
"date": msg["Date"],
"message_id": msg["Message-ID"],
})
mail.logout()
return messages
def _extract_text(msg):
if msg.is_multipart():
for part in msg.walk():
if part.get_content_type() == "text/plain":
payload = part.get_payload(decode=True)
charset = part.get_content_charset() or "utf-8"
return payload.decode(charset, errors="replace")
else:
payload = msg.get_payload(decode=True)
charset = msg.get_content_charset() or "utf-8"
return payload.decode(charset, errors="replace")
return ""
A few nuances that matter in practice:
UNSEENvsALL: UseUNSEENto avoid re-processing. Mark messages as read (mail.store(uid, '+FLAGS', '\\Seen')) only after successful processing, so a crash doesn't lose work.- Message-ID: Capture it — you'll need it to reply in-thread (the
In-Reply-ToandReferencesheaders depend on it). - HTML-only emails: Some senders omit
text/plain. Add a fallback that strips HTML tags or uses a library likebeautifulsoup4orhtml2textto extract readable text. - Attachments: Walk multipart messages, check
Content-Disposition, and decide whether your agent should process them (e.g., extract text from PDF invoices).
IMAP IDLE for Near-Real-Time Delivery
Polling every 30 seconds works for most triage, but if you need sub-second reaction times (e.g., monitoring a time-sensitive support queue), use IMAP IDLE — the server pushes notifications when new mail arrives.
import imaplib
def idle_loop(callback):
mail = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT)
mail.login(EMAIL, APP_PASSWORD)
mail.select("INBOX")
while True:
mail.send(b"A01 IDLE\r\n")
response = mail.readline() # waits for server push
if b"EXISTS" in response:
mail.send(b"DONE\r\n")
new_msgs = fetch_unread(limit=5)
for msg in new_msgs:
callback(msg)
mail.send(b"A01 IDLE\r\n")
In practice, IDLE requires maintaining a persistent TCP connection. Use a connection manager with automatic reconnect (connections drop after ~29 minutes on most servers, so send a periodic NOOP to keepalive).
Sending Email via SMTP: Drafting and Auto-Reply
Reading email is half the job. The agent also needs to send: forwarding summaries, posting draft replies for human review, or — with guardrails — responding directly.
import smtplib
from email.mime.text import MIMEText
SMTP_HOST = "smtp.yourcompany.com"
SMTP_PORT = 587
def send_email(to, subject, body, in_reply_to=None, references=None):
msg = MIMEText(body, "plain", "utf-8")
msg["From"] = EMAIL
msg["To"] = to
msg["Subject"] = subject
if in_reply_to:
msg["In-Reply-To"] = in_reply_to
msg["References"] = references or in_reply_to
with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
server.starttls()
server.login(EMAIL, APP_PASSWORD)
server.send_message(msg)
Threading is critical: without In-Reply-To and References, your reply appears as a new conversation rather than a continuation. Always preserve the original Message-ID.
Auto-Reply Guardrails
Never let an LLM send email autonomously without constraints. A safe pattern:
1. Confidence threshold. The LLM classifies the email and returns a confidence score (0–1). Only auto-reply above 0.9. 2. Allow-lists. Auto-reply only to known internal addresses or pre-approved sender domains. 3. Draft-first mode. For external senders, generate a draft and queue it for human approval rather than sending immediately. 4. Rate limits. Cap outbound auto-replies at N per hour to prevent runaway loops (someone sets an out-of-office, your agent replies, their OOO replies back…).
Inbox Triage: Classification, Routing, Prioritization
This is where the LLM earns its keep. Raw inbox triage means classifying every incoming message and deciding what happens next.
Classification Schema
Define a fixed set of categories the LLM must choose from:
TRIAGE_SCHEMA = {
"categories": [
"urgent_customer_issue",
"internal_question",
"invoice_or_payment",
"newsletter_or_promo",
"meeting_request",
"spam_or_phishing",
"needs_human_review"
]
}
Feed each email to the LLM with a system prompt:
You are an email triage agent. Classify the following email into exactly one
category from the list. Also extract: sender_name, urgency (low/medium/high),
and a one-sentence summary. Return valid JSON only.
Categories: urgent_customer_issue, internal_question, invoice_or_payment,
newsletter_or_promo, meeting_request, spam_or_phishing, needs_human_review
The LLM returns structured JSON:
{
"category": "urgent_customer_issue",
"sender_name": "Maria Chen",
"urgency": "high",
"summary": "Customer reports production API returning 500 errors since 9am."
}
Routing Actions
Each category maps to an action:
| Category | Action |
|---|---|
urgent_customer_issue | Forward to #support Slack channel + send acknowledgment to sender |
internal_question | Route to relevant team member by keyword matching |
invoice_or_payment | Extract amount/due date, log to accounting spreadsheet |
newsletter_or_promo | Archive + extract key takeaway to weekly digest |
meeting_request | Parse date/time, check calendar, send tentative confirmation |
spam_or_phishing | Move to Junk, flag sender domain |
needs_human_review | Queue in review dashboard for a person to triage manually |
Reducing LLM Costs
You don't need GPT-4 for every email. A smart routing layer saves money:
1. Rule-based pre-filter. If the sender is in your contacts database and the subject matches [INVOICE], skip the LLM entirely — route straight to accounting. 2. Cheap model for classification. Use a small, fast model (GPT-4o-mini, Claude Haiku, or a local 7B model) for the classification step. Save the expensive model for reply drafting. 3. Batch processing. Accumulate 5–10 emails and classify them in one LLM call with multiple inputs, reducing per-message overhead. 4. Local models for extraction. Entity extraction (dates, amounts, names) can run on a small local model with zero API cost.
Architecture: Polling Loop, State Machine, and Idempotency
A production email agent needs more than a script — it needs an architecture that handles failure gracefully.
The Polling Loop
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Fetch IMAP │────▶│ Classify │────▶│ Route/Act │
│ (UNSEEN) │ │ (LLM) │ │ (send/fwd/ │
│ │ │ │ │ archive) │
└──────────────┘ └──────────────┘ └──────────────┘
│ │
└──────── Mark SEEN on success ◀──────────┘
Idempotency is non-negotiable. If the agent crashes between classification and action, it must not re-classify or re-send on restart. Solutions:
- Store processed UIDs in a lightweight database (SQLite is fine).
- Mark IMAP messages as
\Seenonly after the full pipeline succeeds. - Use
Message-IDas the deduplication key, not IMAP UID (UIDs can change on some servers after compaction).
State Storage
A simple SQLite schema tracks everything:
CREATE TABLE processed_emails (
message_id TEXT PRIMARY KEY,
uid TEXT,
from_addr TEXT,
subject TEXT,
category TEXT,
urgency TEXT,
summary TEXT,
action_taken TEXT,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
This also gives you a searchable audit log of every decision the agent made — invaluable for debugging and compliance.
Security and Credential Management
An AI agent with email access is powerful and dangerous. Lock it down:
- App-specific passwords with IMAP+SMTP only (no calendar, no contacts, no admin access).
- Dedicated mailbox, not someone's personal account. If the agent is compromised, blast radius is limited.
- Secrets in environment variables, never hardcoded. Use Docker secrets or a
.envfile excluded from version control. - TLS everywhere. IMAP on port 993 (implicit TLS), SMTP on port 587 (STARTTLS). Never plaintext.
- IP allowlisting. If your mail provider supports it, restrict IMAP/SMTP access to your VPS IP.
- Read-only mode for monitoring. If the agent only needs to triage (not reply), use a read-only IMAP permission.
Handling Edge Cases That Will Hit You
Real-world email is messy. Plan for these:
- Encoding hell. Senders use ISO-8859-1, Windows-1252, UTF-8, and occasionally broken encodings. Always decode with
errors="replace". - Nested multipart MIME. Some emails have 5+ levels of multipart nesting (forwarded emails containing forwarded emails). Recurse carefully.
- Auto-reply loops. Your agent sends a reply, the recipient's OOO replies back, your agent replies again. Guard with a header check (
X-Auto-Response-Suppress,Auto-Submittedheader) and per-sender rate limits. - Large attachments. A 50MB attachment can choke your processing pipeline. Set a size limit and skip (or defer) messages above a threshold.
- Calendar invites (.ics). These are multipart messages with a
text/calendarpart. Parse them separately. - Phishing in the agent's inbox. If your agent processes phishing emails, it might follow malicious links in the body. Never have the agent click links or download attachments without explicit allow-lists.
If you want a ready-made AI team that handles email triage out of the box — reading incoming mail on your domain, drafting replies, routing to the right person — a self-hosted AI team like OfficeForge ships with a secretary agent that does exactly this, running entirely on your VPS. No per-seat subscription, no data leaving your infrastructure, and you bring your own model key so you pay the LLM provider directly at their base rate.
Get OfficeForge — $199Monitoring and Observability
An email agent operating silently is an agent you can't trust. Build in observability from day one:
- Structured logging. Log every IMAP fetch (count, errors), every LLM classification (category, confidence), and every outbound action. Use JSON logs so you can grep/filter.
- Metrics. Track emails processed per hour, classification distribution (pie chart of categories), auto-reply rate, human-override rate.
- Alerting. If the agent fails to connect to IMAP for 10 minutes, or if
needs_human_reviewbacklog exceeds N items, fire an alert to Slack/PagerDuty/Discord. - Weekly digest. Have the agent summarize its own activity: "This week I triaged 342 emails, auto-replied to 47, forwarded 89 to the team, and archived 206 newsletters."
Putting It All Together: A Minimal Viable Agent
The simplest useful version of this system is roughly 200 lines of Python:
1. Poll IMAP every 30 seconds for UNSEEN messages. 2. Pre-filter by sender/subject rules — skip known noise. 3. Classify remaining messages with a cheap LLM. 4. Route: archive promos, forward urgent issues, queue drafts for human review. 5. Log everything to SQLite. 6. Alert via webhook if something needs immediate human attention.
You can run this as a single Docker container alongside your existing infrastructure. No SaaS subscription, no data leaving your network, and total control over what the agent does and doesn't do.
From there, you expand: add attachment processing, integrate with your project management tool (create a ticket when an urgent issue arrives), connect to your calendar for meeting requests, build a human-review dashboard. The foundation stays the same — IMAP to read, LLM to decide, SMTP to act — and every piece stays on hardware you own.
Email isn't going away. An AI agent email integration built on open protocols means your automation works today, works when you switch providers next year, and works without paying someone else a cut of every message processed.
FAQ
Can an AI agent read and send emails without a third-party email API?
Yes. Standard IMAP lets you read mailboxes and SMTP lets you send — both are open protocols supported by every mail server. No vendor SDK required.
How do I keep credentials safe when an AI agent accesses email?
Use app-specific passwords with minimal scopes, restrict the agent to a dedicated mailbox or sub-address, rotate credentials quarterly, and run the agent on infrastructure you control.
What is the best architecture for polling vs real-time email delivery to an AI agent?
IMAP IDLE gives near-real-time push notifications; polling every 30–60 seconds is simpler and works everywhere. For most triage workloads, 30-second polling is sufficient and more resilient.
Can I connect an AI agent to Google Workspace or Microsoft 365 email?
Both support IMAP/SMTP with app passwords or OAuth2. Google requires "Less Secure Apps" or OAuth2; Microsoft uses OAuth2 or IMAP with modern auth. Both work fine with a self-hosted agent.
How does an AI agent decide which emails to reply to automatically?
You define triage rules: sender allow-lists, subject-pattern matching, confidence thresholds from the LLM classifier, and escalation paths for uncertain cases. Never auto-send without a guardrail.
Is it cheaper to self-host an email-capable AI agent than to pay for a SaaS inbox tool?
Self-hosting eliminates per-seat SaaS fees. You pay only for compute (often an existing VPS) and LLM tokens at provider cost — typically 80–95% cheaper for a team of any size.
