Development Guide

This document provides information for developers contributing to simple-email-gw.

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

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 format to 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=True fixtures for setup

  • Test 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 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.whl

  • dist/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

Test Failures

If tests fail after changes:

# Clean and reinstall
make clean
make dev
make test

Type Checking Errors

If mypy reports errors:

  1. Ensure all imports are properly typed

  2. Check that return types match annotations

  3. Run uv run mypy src/ --show-error-codes for details

Contributing

  1. Fork the repository

  2. Create a feature branch: git checkout -b feature/my-feature

  3. Make changes and add tests

  4. Run all checks: make all

  5. Commit with conventional format: git commit -m "feat: add new feature"

  6. Push and create a pull request

Commit Message Format

We use conventional commits:

  • feat: New features

  • fix: Bug fixes

  • docs: Documentation changes

  • test: Test changes

  • refactor: Code refactoring

  • chore: Maintenance tasks

Release Process

  1. Update version in pyproject.toml and __init__.py

  2. Update CHANGELOG (if exists)

  3. Run make all to verify

  4. Build and publish: make build publish

  5. Create git tag: git tag v0.x.x

  6. Push tag: git push origin v0.x.x