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:

  1. Each sync client creates its own event loop

  2. A daemon thread runs the event loop

  3. Methods use asyncio.run_coroutine_threadsafe() to delegate to async client

  4. Context 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

disconnect() None

Close IMAP connection and stop background event loop.

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:
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:
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:
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:
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:
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

has_capability(name: str) bool

Check if server supports a capability (synchronous property access).

Parameters:

name – Capability name to check

Returns:

True if capability is supported

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

__exit__(exc_type, exc_val, exc_tb) None

Context manager exit - stop loop and join thread.

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:
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:

  1. Replace IMAPClient with SyncIMAPClient

  2. Replace SMTPClient with SyncSMTPClient

  3. Remove async with / await keywords

All methods, parameters, and return values remain identical.