Skip to content

feat: add IMAP STARTTLS support with RFC 8314 security enum#120

Closed
codefuturist wants to merge 1 commit intoai-zerolab:mainfrom
codefuturist:feat/imap-starttls
Closed

feat: add IMAP STARTTLS support with RFC 8314 security enum#120
codefuturist wants to merge 1 commit intoai-zerolab:mainfrom
codefuturist:feat/imap-starttls

Conversation

@codefuturist
Copy link
Contributor

@codefuturist codefuturist commented Feb 14, 2026

Summary

Adds proper IMAP STARTTLS support by replacing the ambiguous use_ssl/start_ssl boolean pair with a clean ConnectionSecurity enum (tls/starttls/none) per RFC 8314.

Related to #87 (Proton Mail Bridge compatibility)

Problem

The start_ssl config field on EmailServer is only wired for SMTP, never for IMAP. When configuring use_ssl = false + start_ssl = true (e.g., Proton Mail Bridge on port 1143), the IMAP connection remains unencrypted — STARTTLS is never issued. This likely contributes to IMAP failures with Proton Bridge.

Solution

New security enum field

Mode Description IMAP Port SMTP Port
tls Implicit TLS — encrypted from the first byte 993 465
starttls STARTTLS — connect plaintext, then upgrade to TLS 143 587
none No encryption (not recommended) 143 25

Changes

config.py:

  • Added ConnectionSecurity enum with RFC 8314 docstring
  • Added security field on EmailServer (default: tls)
  • mode="before" model validator derives security from legacy fields; explicit security takes precedence
  • New env vars: MCP_EMAIL_SERVER_IMAP_SECURITY, MCP_EMAIL_SERVER_SMTP_SECURITY
  • Added imap_verify_ssl support (was only on SMTP before)
  • Extracted _parse_security_env() and _resolve_security_env() helpers

emails/classic.py:

  • Implemented _imap_starttls() — sends STARTTLS command, upgrades transport via asyncio.loop.start_tls() (same pattern as aiosmtplib)
  • Added _create_imap_connection() factory handling TLS/STARTTLS/plaintext
  • Replaced all direct imap_class() calls with _connect_imap()
  • Added _create_imap_ssl_context() for verify_ssl=false support

ui.py:

  • Replaced Use SSL / Start SSL checkboxes with Connection Security dropdown (tls/starttls/none)
  • Added Verify SSL Certificate checkbox for both IMAP and SMTP

Tests: 39 new tests covering enum, backward compat, validator precedence, STARTTLS flow, SSL context, env vars

README: Documented security modes, env vars, ProtonMail Bridge example

Backward Compatibility

  • Existing TOML configs with use_ssl/start_ssl continue to work unchanged
  • Existing env vars still work; new *_SECURITY vars take precedence when set
  • Default behavior unchanged (security = "tls")

ProtonMail Bridge Example

[emails.incoming]
host = "127.0.0.1"
port = 1143
security = "starttls"
verify_ssl = false

[emails.outgoing]
host = "127.0.0.1"
port = 1025
security = "starttls"
verify_ssl = false

Testing

  • All 172 tests pass (133 existing + 39 new)
  • make check passes (ruff, prettier, pre-commit, deptry)
  • Tested with Proton Mail Bridge v3.x on macOS

Copilot AI review requested due to automatic review settings February 14, 2026 14:01
@codefuturist codefuturist force-pushed the feat/imap-starttls branch 2 times, most recently from d92244f to 0e9ed77 Compare February 14, 2026 14:07
@codecov
Copy link

codecov bot commented Feb 14, 2026

Codecov Report

❌ Patch coverage is 88.52459% with 14 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
mcp_email_server/config.py 92.1% 2 Missing and 4 partials ⚠️
mcp_email_server/ui.py 0.0% 5 Missing ⚠️
mcp_email_server/emails/classic.py 92.6% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds comprehensive IMAP STARTTLS support to the email server and introduces a clean ConnectionSecurity enum (tls/starttls/none) that replaces the ambiguous use_ssl/start_ssl boolean pair, aligning with RFC 8314 security recommendations. The existing start_ssl config field was only wired for SMTP and never worked for IMAP - this PR fixes that gap while maintaining full backward compatibility with existing configurations.

Changes:

  • Introduced ConnectionSecurity enum with three modes: tls (implicit TLS), starttls (upgrade via STARTTLS command), and none (plaintext)
  • Implemented IMAP STARTTLS functionality using asyncio.loop.start_tls() pattern matching aiosmtplib
  • Added backward compatibility layer that transparently converts legacy use_ssl/start_ssl booleans to the new enum
  • Enhanced configuration with new environment variables (MCP_EMAIL_SERVER_IMAP_SECURITY, MCP_EMAIL_SERVER_SMTP_SECURITY) while maintaining support for legacy ones

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
mcp_email_server/config.py Added ConnectionSecurity enum, backward-compatible validator, new security parameters in EmailSettings.init(), and environment variable resolution with precedence handling
mcp_email_server/emails/classic.py Implemented _imap_starttls() function, _create_imap_connection() factory, _create_imap_ssl_context(), replaced imap_class attribute with _connect_imap() method throughout EmailClient
tests/test_imap_starttls.py Added 30+ comprehensive tests covering enum values, backward compatibility, STARTTLS flow, SSL context creation, environment variable resolution, and TOML config loading
tests/test_email_client.py Updated mocks to use _connect_imap instead of imap_class
tests/test_email_attachments.py Updated mocks to use _connect_imap and removed redundant asyncio mock setup now handled by the connection factory
README.md Documented new security field, connection modes table, environment variables, and added ProtonMail Bridge STARTTLS example

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +69 to +96

@model_validator(mode="after")
@classmethod
def resolve_security_from_legacy(cls, obj: EmailServer) -> EmailServer:
"""Derive `security` from deprecated `use_ssl`/`start_ssl` for backward compatibility.

If both old and new fields are set, `security` takes precedence.
If only old fields are set, derive `security` from them.
"""
# If legacy fields are explicitly set and security was not explicitly provided
# (i.e. it's at default), derive security from legacy fields
if obj.use_ssl is not None or obj.start_ssl is not None:
use_ssl = obj.use_ssl if obj.use_ssl is not None else False
start_ssl = obj.start_ssl if obj.start_ssl is not None else False

if use_ssl and start_ssl:
raise ValueError(
"Invalid configuration: 'use_ssl' and 'start_ssl' cannot both be true. "
"Use 'security = \"tls\"' for implicit TLS or 'security = \"starttls\"' for STARTTLS."
)

if use_ssl:
obj.security = ConnectionSecurity.TLS
elif start_ssl:
obj.security = ConnectionSecurity.STARTTLS
else:
obj.security = ConnectionSecurity.NONE

Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backward compatibility validator doesn't actually give security precedence over legacy fields as documented. If both are explicitly set (e.g., security="starttls", use_ssl=True), the validator will override security to TLS based on use_ssl=True, contrary to the docstring.

To fix this, you should check if security was explicitly provided before overriding it. In Pydantic v2, you can check if a field was explicitly set by comparing against the default value or by tracking the fields that were set during initialization. Consider checking if the incoming security value differs from the default before applying the legacy logic.

Copilot uses AI. Check for mistakes.
Replace ambiguous use_ssl/start_ssl boolean pair with a clean
ConnectionSecurity enum (tls/starttls/none) per RFC 8314.

Changes:
- Add ConnectionSecurity enum to config.py
- Add model_validator for backward compat with use_ssl/start_ssl
- Implement IMAP STARTTLS transport upgrade via asyncio.loop.start_tls()
- Add _create_imap_connection() factory for TLS/STARTTLS/plaintext
- Add IMAP verify_ssl support for self-signed certificates
- Wire SMTP flags from security enum in EmailClient
- Add MCP_EMAIL_SERVER_IMAP_SECURITY/SMTP_SECURITY env vars
- Update existing tests to use _connect_imap instead of imap_class
- Add comprehensive tests for new security features
- Update README with security modes, env vars, and ProtonMail Bridge example

Existing configs with use_ssl/start_ssl continue to work unchanged.
The start_ssl field was previously ignored for IMAP — this fixes that.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants