The simplest way to work with email in Python. No boilerplate, no low-level IMAP commands, no headaches.
Just connect, read, search, send, backup, and restore β with one class. Or hand the same account to Claude, Cursor or any MCP client and let the model do it.
from email_profile import Email
with Email("user@gmail.com", "app_password") as app:
for msg in app.inbox.where().messages():
print(f"{msg.date} | {msg.from_} | {msg.subject}")That's it. No server configuration needed β email-profile auto-discovers your IMAP server from your email address.
- Documentation: linux-profile.github.io/email-profile
- Source Code: github.com/linux-profile/email-profile
- PyPI: pypi.org/project/email-profile
Contents β Install Β· Why Β· Quick Start Β· MCP Server Β· Features Β· Providers Β· Environment
pip install email-profile # library
pip install email-profile[mcp] # + MCP server for AI clientsMost Python email libraries make you deal with imaplib directly, parse raw bytes, manage connections manually, and write dozens of lines just to read your inbox.
email-profile gives you a clean, human API:
- Write
Email("user@gmail.com", "pw")instead of configuring IMAP servers manually - Write
app.inbox.where(Q.unseen()).first()instead of raw IMAP search commands - Write
app.sync()instead of building your own backup system - Write
app.send(to="...", subject="...", body="...")instead of constructing MIME messages - Run
email-profile-mcpand ask Claude "what needs an answer today?" instead of writing an agent
It combines IMAP + SMTP + storage + sync + MCP in a single library. No other Python package does this.
Three ways to connect β pick the one that fits:
from email_profile import Email
# Just email + password (auto-discovers the server)
with Email("user@gmail.com", "app_password") as app:
print(app.mailboxes())
# From .env file (great for production)
with Email.from_env() as app:
print(app.mailboxes())
# Explicit server (when you need full control)
with Email("imap.gmail.com", "user@gmail.com", "app_password") as app:
print(app.mailboxes())with Email.from_env() as app:
# How many emails?
print(app.inbox.where().count())
# Read them
for msg in app.inbox.where().messages():
print(f"{msg.date} | {msg.from_} | {msg.subject}")
# Just the first one
msg = app.inbox.where().first()
# Only headers (much faster for large mailboxes)
for msg in app.inbox.where().messages(mode="headers"):
print(msg.subject)Find exactly what you need with composable queries:
from email_profile import Email, Q
from datetime import date
with Email.from_env() as app:
# Combine conditions with & (AND), | (OR), ~ (NOT)
q = Q.subject("meeting") & Q.unseen()
print(app.inbox.where(q).count())
# From Alice or Bob
q = Q.from_("alice@x.com") | Q.from_("bob@x.com")
# Everything except seen emails
q = ~Q.seen()
# Emails from 2025, larger than 1MB
q = Q.since(date(2025, 1, 1)) & Q.before(date(2025, 12, 31)) & Q.larger(1_000_000)Or use validated kwargs if you prefer:
from email_profile import Query
query = Query(subject="report", unseen=True, since=date(2025, 1, 1))
query = Query(subject="report").exclude(subject="spam").or_(subject="urgent")Built-in shortcuts for common searches:
app.unread().count()
app.recent(days=7).count()
app.search("invoice").count()Send, reply, and forward β with automatic SMTP discovery:
with Email.from_env() as app:
# Simple
app.send(to="recipient@x.com", subject="Hello", body="Hi there!")
# HTML + attachments + CC
app.send(
to=["alice@x.com", "bob@x.com"],
subject="Report",
body="See attached.",
html="<h1>Report</h1>",
attachments=["report.pdf"],
cc="manager@x.com",
)
# Reply to an email (preserves threading)
msg = app.inbox.where().first()
app.reply(msg, body="Thanks!")
# Forward
app.forward(msg, to="colleague@x.com", body="FYI")Sync your entire mailbox to a local SQLite database. Incremental β only downloads new emails. Parallel β multiple mailboxes at once. With progress bars.
with Email.from_env() as app:
# Backup everything (compares by Message-ID, skips duplicates)
result = app.sync()
print(f"{result.inserted} new, {result.skipped} skipped")
# Backup one mailbox
result = app.sync(mailbox="INBOX")
# Force re-download (skip duplicate check)
result = app.sync(skip_duplicates=False)
# Restore to server (e.g. after migrating)
count = app.restore()with Email.from_env() as app:
# Built-in folder shortcuts (auto-detected across languages)
app.inbox # INBOX
app.sent # Sent / Enviados / Enviadas
app.trash # Trash / Lixeira / Papelera
app.drafts # Drafts / Rascunhos
app.spam # Spam / Junk / Lixo EletrΓ΄nico
# Any folder by name
work = app.mailbox("INBOX.Work")
# Message operations
work.mark_seen(uid)
work.move(uid, "INBOX.Archive")
work.delete(uid)Storage is lazily initialized β email.db is only created when sync() or restore() is first called.
from email_profile import Email, StorageSQLite
# Default: saves to ./email.db on first sync
with Email.from_env() as app:
app.sync()
# Custom path
with Email.from_env() as app:
app.storage = StorageSQLite("./backup.db")
app.sync()The same account, as tools for an AI client. Install the extra, point Claude Code, Claude Desktop or Cursor at it, and ask:
"What came in today that needs an answer?" "Find the invoice Alice sent last month and save the PDF." "Draft a reply saying Thursday works β show me before you send."
pip install "email-profile[mcp]"
email-profile-mcp # read-only
email-profile-mcp --allow-send # + send, reply, forward- Read-only by default.
send_email,reply_messageandforward_messageexist only with--allow-send;delete_messageonly with--allow-delete. - Irreversible tools are annotated destructive, so clients that support it ask you before calling them.
- Credentials never pass through the model. They come from the
environment or
.env; no tool accepts a password. - Bodies are truncated (4000 chars by default) and attachment bytes never
cross the wire β
save_attachmentwrites only underEMAIL_MCP_ATTACHMENTS_DIRand returns the path. - One message per call.
uidaccepts a single id; IMAP ranges like1:*are refused, and search strings are escaped before reaching the server.
Claude Code
claude mcp add email \
--env EMAIL_USERNAME=you@gmail.com --env EMAIL_PASSWORD=app-password \
-- uvx --from "email-profile[mcp]" email-profile-mcp --allow-sendOr install the repository as a plugin β the server plus six skills that keep the model from sending before you approve:
claude plugin marketplace add linux-profile/email-profile
claude plugin install email-profile@email-profileClaude Desktop
~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"email": {
"command": "uvx",
"args": ["--from", "email-profile[mcp]", "email-profile-mcp", "--allow-send"],
"env": { "EMAIL_USERNAME": "you@gmail.com", "EMAIL_PASSWORD": "app-password" }
}
}
}Cursor
Same shape in .cursor/mcp.json β copy examples/cursor.json.
From Python
from email_profile import Email
from email_profile.mcp import Settings, build
server = build(
Settings(allow_send=True),
email_factory=lambda: Email("imap.example.com", "user", "pw"),
)
server.run() # stdio
server.run(transport="streamable-http") # or HTTP| Tool | Hint | What it does |
|---|---|---|
list_mailboxes |
read-only | Server-side folder names |
search_messages |
read-only | Filter one mailbox by sender, subject, text, dates, flags β headers only, newest first, paginated |
read_message |
read-only | Headers, body and attachment metadata for one (mailbox, uid) |
list_attachments |
read-only | Name, type and size of each attachment |
save_attachment |
reversible | Write one attachment to disk |
mark_seen / mark_unseen |
reversible | Read state |
flag_message / unflag_message |
reversible | Star |
move_message |
reversible | Move to another mailbox |
send_email |
destructive | New message over SMTP β --allow-send |
reply_message |
destructive | Reply keeping thread headers β --allow-send |
forward_message |
destructive | Forward with attachments β --allow-send |
delete_message |
destructive | Flag, or expunge with expunge=true β --allow-delete |
Four prompts put the tools in the order a task needs: triage_inbox,
find_message, draft_reply, summarize_thread. The plugin ships the same
guidance as skills under skills/.
| Flag | Env | Default |
|---|---|---|
--allow-send |
EMAIL_MCP_ALLOW_SEND |
off |
--allow-delete |
EMAIL_MCP_ALLOW_DELETE |
off |
--max-chars |
EMAIL_MCP_MAX_CHARS |
4000 |
| β | EMAIL_MCP_LIMIT |
20 |
| β | EMAIL_MCP_DEFAULT_MAILBOX |
INBOX |
| β | EMAIL_MCP_ATTACHMENTS_DIR |
. β the only place save_attachment writes |
--http --host --port |
β | stdio |
Full reference: MCP Server docs.
| Feature | Description |
|---|---|
| Auto-discovery | Detects IMAP/SMTP servers from email domain (50+ providers) |
| Unified API | IMAP + SMTP in a single Email class |
| Query Builder | Composable search with Q (AND, OR, NOT) and validated Query kwargs |
| Sync & Restore | Incremental backup to SQLite, restore to any server |
| Parallel | Multi-threaded sync and restore with configurable workers |
| Progress | Rich progress bars with per-mailbox status |
| Retry | Exponential backoff on transient failures |
| Send | Send, reply, forward with HTML, attachments, CC/BCC |
| Storage | Pluggable storage backend (SQLite default) |
| Flags | Read/unread, flag, delete, move, copy operations |
| Context Manager | with Email(...) as app: for automatic cleanup |
| MCP Server | 14 tools + 4 prompts for Claude Code, Claude Desktop, Cursor β read-only until you opt in |
| Plugin | Claude Code plugin with skills that gate sending behind your approval |
Auto-discovery works out of the box. Just use your email and password β no server configuration needed.
Any server with DNS SRV or MX records is also detected automatically.
EMAIL_USERNAME=user@example.com
EMAIL_PASSWORD=app_password
EMAIL_SERVER=imap.example.com # optional, auto-discovered
EMAIL_MCP_ALLOW_SEND=false # MCP server only
EMAIL_MCP_ALLOW_DELETE=falseGmail, Outlook and iCloud require an app password, not the account password.
Issues and pull requests welcome β see CONTRIBUTING.md. Security reports: SECURITY.md.
MIT
