Sync Client API Reference¶
This document describes the synchronous wrapper clients that provide a simpler interface for non-async applications.
Overview¶
Simple Email Gateway provides both async and sync APIs:
Async clients (IMAPClient, SMTPClient): Recommended for async applications
Sync clients (SyncIMAPClient, SyncSMTPClient): Wrapper clients for simpler synchronous code
Both client types provide identical functionality - sync clients are thin wrappers around async clients using a dedicated event loop in a background thread.
When to Use Sync vs Async¶
Use async clients when:
Building async applications (FastAPI, Quart, etc.)
Running in async context (asyncio event loop already running)
Need maximum performance (no thread overhead)
Already using async/await throughout your codebase
Use sync clients when:
Building synchronous applications (scripts, CLI tools)
No existing async context
Simpler code is more important than performance
Integrating with legacy synchronous code
Sync Clients Architecture¶
Sync clients use Strategy 2: dedicated event loop in a background thread.
How it works:
Each sync client creates its own event loop
A daemon thread runs the event loop
Methods use
asyncio.run_coroutine_threadsafe()to delegate to async clientContext manager ensures proper cleanup (stop loop, join thread)
Benefits:
Connection pooling works correctly
No nested event loop issues
Thread-safe (each client has its own loop/thread)
Proper cleanup on exceptions
SyncIMAPClient¶
- class SyncIMAPClient(account: EmailAccount)¶
Synchronous wrapper around IMAPClient using dedicated event loop.
- __init__(account: EmailAccount)¶
Initialize sync wrapper and start background event loop.
- Parameters:
account – Email account configuration
- __enter__() SyncIMAPClient¶
Context manager entry.
- Returns:
Self for use in with statements
- __exit__(exc_type, exc_val, exc_tb) None¶
Context manager exit - cleanup resources (stop loop, join thread).
- connect() IMAP4_SSL¶
Establish IMAP connection.
- Returns:
IMAP4_SSL connection object
- Raises:
RuntimeError – If connection fails
- list_folders() list[dict[str, Any]]¶
List all folders/mailboxes.
- Returns:
List of folder dictionaries with ‘name’, ‘flags’, and ‘delimiter’ keys
- Raises:
RuntimeError – If operation fails
- select_folder(folder: str = 'INBOX') dict[str, int]¶
Select a folder and return message count.
- Parameters:
folder – Folder name to select (default: “INBOX”)
- Returns:
Dict with ‘folder’ and ‘count’ keys
- Raises:
RuntimeError – If operation fails
- search(folder: str = 'INBOX', criteria: str = 'ALL', limit: int = 50) list[str]¶
Search for messages matching criteria.
- Parameters:
folder – Folder to search (default: “INBOX”)
criteria – IMAP search criteria (default: “ALL”)
limit – Maximum number of results (default: 50)
- Returns:
List of message ID strings
- Raises:
RuntimeError – If operation fails
ValueError – If criteria is invalid
- fetch_message(message_id: str, folder: str = 'INBOX') dict[str, Any]¶
Fetch a single message by ID.
- Parameters:
message_id – Message ID to fetch
folder – Folder containing the message (default: “INBOX”)
- Returns:
Dict with message details (id, folder, subject, from, to, body, attachments)
- Raises:
RuntimeError – If operation fails
ValueError – If message_id is invalid
- move_message(message_id: str, source_folder: str, dest_folder: str) bool¶
Move a message between folders.
- Parameters:
message_id – Message ID to move
source_folder – Source folder name
dest_folder – Destination folder name
- Returns:
True if successful
- Raises:
RuntimeError – If operation fails
ValueError – If message_id is invalid
- delete_message(message_id: str, folder: str = 'INBOX', expunge: bool = True) bool¶
Delete a message.
- Parameters:
message_id – Message ID to delete
folder – Folder containing the message (default: “INBOX”)
expunge – Whether to expunge after deletion (default: True)
- Returns:
True if successful
- Raises:
RuntimeError – If operation fails
ValueError – If message_id is invalid
- mark_message(message_id: str, folder: str, flag: str, action: str = 'add') bool¶
Add or remove flags from a message.
- Parameters:
message_id – Message ID to mark
folder – Folder containing the message
flag – IMAP flag to add/remove (e.g., “\Seen”)
action – “add” or “remove” (default: “add”)
- Returns:
True if successful
- Raises:
RuntimeError – If operation fails
ValueError – If message_id is invalid
- download_attachment(message_id: str, folder: str, filename: str, output_dir: str) str¶
Download an attachment from a message.
- Parameters:
message_id – Message ID containing the attachment
folder – Folder containing the message
filename – Attachment filename to download
output_dir – Directory to save the attachment
- Returns:
Absolute path to downloaded file
- Raises:
RuntimeError – If operation fails
ValueError – If message_id or filename is invalid
FileNotFoundError – If attachment not found
SecurityError – If download escapes workspace confinement
SyncSMTPClient¶
- class SyncSMTPClient(account: EmailAccount)¶
Synchronous wrapper around SMTPClient using dedicated event loop.
- __init__(account: EmailAccount)¶
Initialize sync wrapper and start background event loop.
- Parameters:
account – Email account configuration
- __enter__() SyncSMTPClient¶
Context manager entry.
- Returns:
Self for use in with statements
- send_email(to: list[str], subject: str, body: str, cc: list[str] | None = None, bcc: list[str] | None = None, html_body: str | None = None, attachments: list[str] | None = None) dict[str, str]¶
Send an email message.
- Parameters:
to – List of recipient email addresses
subject – Email subject line
body – Plain text email body
cc – Optional list of CC recipients
bcc – Optional list of BCC recipients
html_body – Optional HTML version of the body
attachments – Optional list of file paths to attach
- Returns:
Dict with ‘status’, ‘recipients’, and ‘message’ keys
- Raises:
RuntimeError – If send fails
ValueError – If email addresses are invalid
WhitelistError – If recipients not in whitelist
FileNotFoundError – If attachment file not found
- reply_email(to: str, subject: str, body: str, in_reply_to: str, references: list[str] | None = None, html_body: str | None = None) dict[str, str]¶
Reply to an email message.
- Parameters:
to – Recipient email address
subject – Email subject line
body – Plain text email body
in_reply_to – Message-ID being replied to
references – Optional list of message IDs for References header
html_body – Optional HTML version of the body
- Returns:
Dict with ‘status’, ‘recipients’, and ‘message’ keys
- Raises:
RuntimeError – If send fails
ValueError – If email addresses are invalid
WhitelistError – If recipient not in whitelist
- forward_email(to: list[str], subject: str, original_from: str, original_date: str, original_body: str) dict[str, str]¶
Forward an email message.
- Parameters:
to – List of recipient email addresses
subject – Email subject line
original_from – Original sender email address
original_date – Original message date
original_body – Original message body
- Returns:
Dict with ‘status’, ‘recipients’, and ‘message’ keys
- Raises:
RuntimeError – If send fails
ValueError – If email addresses are invalid
WhitelistError – If recipients not in whitelist
Usage Examples¶
Basic Sync Usage¶
from simple_email_gw import SyncIMAPClient, SyncSMTPClient, EmailAccount
# Create account
account = EmailAccount(
name="work",
imap_host="imap.gmail.com",
smtp_host="smtp.gmail.com",
username="user@gmail.com",
password="app-password"
)
# Use IMAP client
with SyncIMAPClient(account) as client:
folders = client.list_folders()
messages = client.search(folder="INBOX", criteria="UNSEEN")
for msg_id in messages:
msg = client.fetch_message(msg_id)
print(f"From: {msg['from']}, Subject: {msg['subject']}")
# Use SMTP client
with SyncSMTPClient(account) as client:
result = client.send_email(
to=["recipient@example.com"],
subject="Test Email",
body="Hello from sync client!"
)
print(f"Sent: {result}")
Error Handling¶
from simple_email_gw import SyncIMAPClient, EmailAccount
account = EmailAccount(name="work", imap_host="imap.gmail.com", ...)
with SyncIMAPClient(account) as client:
try:
folders = client.list_folders()
except RuntimeError as e:
print(f"Connection/operation error: {e}")
except ValueError as e:
print(f"Invalid parameter: {e}")
Multiple Operations¶
from simple_email_gw import SyncIMAPClient, EmailAccount
account = EmailAccount(name="work", imap_host="imap.gmail.com", ...)
with SyncIMAPClient(account) as client:
# Multiple operations reuse same connection
folders = client.list_folders()
client.select_folder("INBOX")
messages = client.search(criteria="UNSEEN")
for msg_id in messages:
msg = client.fetch_message(msg_id)
# Process message...
client.mark_message(msg_id, "INBOX", "\\Seen", action="add")
Thread Safety¶
Each sync client instance has its own event loop and background thread:
Thread-safe: Multiple clients can run concurrently
Isolated: Each client’s event loop is separate
No shared state: No race conditions between instances
# Safe to use multiple clients concurrently
client1 = SyncIMAPClient(account1)
client2 = SyncIMAPClient(account2)
# Each has its own thread + event loop
# No interference between clients
Performance Considerations¶
Sync clients have minimal overhead compared to async clients:
Thread overhead: ~1 MB per client (stack + event loop)
Context switching: Negligible for email operations (network-bound)
Connection pooling: Fully preserved (async client manages pool)
Best practices:
Reuse client instances when possible (context manager pattern)
Use async clients in high-concurrency async applications
Sync clients are ideal for scripts, CLI tools, simple applications
Limitations¶
Do NOT use sync clients:
Inside async functions (causes nested loop error)
In async frameworks (FastAPI, Quart, etc.)
In applications already running event loop
Error example:
# ❌ WRONG: Using sync client in async context
async def my_async_function():
with SyncIMAPClient(account) as client: # Error: nested event loop
messages = client.search()
# ✅ CORRECT: Use async client in async context
async def my_async_function():
async with IMAPClient(account) as client:
messages = await client.search()
Migration Guide¶
Migrating from async to sync:
# Before (async)
async with IMAPClient(account) as client:
folders = await client.list_folders()
messages = await client.search(folder="INBOX")
# After (sync)
with SyncIMAPClient(account) as client:
folders = client.list_folders()
messages = client.search(folder="INBOX")
The only changes needed:
Replace
IMAPClientwithSyncIMAPClientReplace
SMTPClientwithSyncSMTPClientRemove
async with/awaitkeywords
All methods, parameters, and return values remain identical.