Development Guide¶
This document provides information for developers contributing to simple-email-gw.
Note: This package provides both async and sync APIs. Async clients (IMAPClient, SMTPClient) are the primary implementation. Sync wrapper clients (SyncIMAPClient, SyncSMTPClient) delegate to async clients using dedicated event loops.
Prerequisites¶
Python 3.11 or higher
uv package manager
Getting Started¶
Clone and Install¶
git clone https://github.com/christophevg/simple-email-gw.git
cd simple-email-gw
# Install development dependencies
make dev-env
Run Tests¶
make test
Run Linter¶
make lint
Type Check¶
make typecheck
Run All Checks¶
make all
Project Structure¶
simple-email-gw/
├── docs/ # Documentation
│ ├── api.md # API reference
│ ├── configuration.md # Configuration options
│ ├── development.md # Development guide
│ ├── installation.md # Installation guide
│ ├── mcp-tools.md # MCP tools reference
│ └── security.md # Security features
├── src/simple_email_gw/ # Source code
│ ├── __init__.py
│ ├── config.py # Configuration handling
│ ├── mcp.py # MCP server
│ ├── imap/ # IMAP client
│ │ └── client.py
│ ├── smtp/ # SMTP client
│ │ └── client.py
│ ├── connections/ # Connection pooling
│ │ └── pool.py
│ └── safety/ # Security features
│ ├── audit.py
│ ├── rate_limiter.py
│ └── sanitize.py
├── tests/ # Test suite
│ ├── conftest.py
│ ├── test_config.py
│ ├── test_rate_limiter.py
│ ├── test_sanitize.py
│ └── test_whitelist.py
├── pyproject.toml
├── Makefile
└── README.md
Code Style¶
Formatting¶
Use two spaces for indentation
Maximum line length: 100 characters
Use
ruff formatto auto-format
make format
Linting¶
We use ruff for linting. Run it with:
make lint
Type Annotations¶
All public functions should have type annotations. We use mypy for type checking:
make typecheck
Testing¶
Running Tests¶
# Run all tests
make test
# Run with coverage
make test-cov
# Run specific test file
uv run pytest tests/test_sanitize.py
# Run specific test
uv run pytest tests/test_sanitize.py::test_sanitize_subject
Writing Tests¶
We use pytest with the following patterns:
import pytest
from simple_email_gw import sanitize_subject
class TestSanitizeSubject:
"""Tests for subject line sanitization."""
def test_removes_crlf(self):
"""CRLF sequences should be replaced with spaces."""
result = sanitize_subject("Hello\r\nWorld")
assert result == "Hello World"
def test_removes_cr_only(self):
"""CR characters should be replaced with spaces."""
result = sanitize_subject("Hello\rWorld")
assert result == "Hello World"
def test_handles_normal_text(self):
"""Normal text should pass through unchanged."""
result = sanitize_subject("Normal subject")
assert result == "Normal subject"
Test Conventions¶
Use descriptive test names
Group related tests in classes
Use
autouse=Truefixtures for setupTest both success and error paths
MCP Server Development¶
Running the Server¶
# Create .env file with credentials
cat > .env << EOF
EMAIL_IMAP_HOST=imap.gmail.com
EMAIL_SMTP_HOST=smtp.gmail.com
EMAIL_USERNAME=your-email@gmail.com
EMAIL_PASSWORD=your-app-password
EOF
# Run the MCP server
make email-gw-mcp-server
Adding New Tools¶
Add new MCP tools in src/simple_email_gw/mcp.py:
@mcp.tool
async def new_tool(
account: Annotated[str, Field(description="Account name")],
ctx: Context = None,
) -> dict[str, str]:
"""Description of the new tool.
Args:
account: The account name.
Returns:
Dictionary with result.
"""
if ctx:
await ctx.info("Performing operation")
try:
pool = await get_pool()
client = await pool.get_imap_client(account)
# Do something
return {"status": "ok"}
except ValueError:
raise ToolError(f"Account not found: {account}")
except Exception:
raise ToolError("Operation failed. Check server logs for details.")
Building and Publishing¶
Build Distribution¶
make build
This creates:
dist/simple_email_gw-0.1.0-py3-none-any.whldist/simple_email_gw-0.1.0.tar.gz
Publish to PyPI¶
make publish
Requires PyPI credentials to be configured.
Debugging¶
Enable Debug Logging¶
import logging
logging.basicConfig(level=logging.DEBUG)
IMAP Debug Mode¶
# Enable IMAP protocol logging
import aioimaplib
aioimaplib.log.setLevel(logging.DEBUG)
Common Issues¶
Import Errors¶
If you see import errors after adding new modules:
# Reinstall the package
make dev-env
Test Failures¶
If tests fail after changes:
# Clean and reinstall
make clean
make dev-env
make test
Type Checking Errors¶
If mypy reports errors:
Ensure all imports are properly typed
Check that return types match annotations
Run
uv run mypy src/ --show-error-codesfor details
Contributing¶
Fork the repository
Create a feature branch:
git checkout -b feature/my-featureMake changes and add tests
Run all checks:
make allCommit with conventional format:
git commit -m "feat: add new feature"Push and create a pull request
Commit Message Format¶
We use conventional commits:
feat:New featuresfix:Bug fixesdocs:Documentation changestest:Test changesrefactor:Code refactoringchore:Maintenance tasks
Release Process¶
Update version in
pyproject.tomland__init__.pyUpdate CHANGELOG (if exists)
Run
make allto verifyBuild and publish:
make build publishCreate git tag:
git tag v0.x.xPush tag:
git push origin v0.x.x