feat: add IMAP STARTTLS support with RFC 8314 security enum#120
feat: add IMAP STARTTLS support with RFC 8314 security enum#120codefuturist wants to merge 1 commit intoai-zerolab:mainfrom
Conversation
d92244f to
0e9ed77
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
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
ConnectionSecurityenum with three modes:tls(implicit TLS),starttls(upgrade via STARTTLS command), andnone(plaintext) - Implemented IMAP STARTTLS functionality using
asyncio.loop.start_tls()pattern matching aiosmtplib - Added backward compatibility layer that transparently converts legacy
use_ssl/start_sslbooleans 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.
|
|
||
| @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 | ||
|
|
There was a problem hiding this comment.
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.
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>
0e9ed77 to
87051dc
Compare
Summary
Adds proper IMAP STARTTLS support by replacing the ambiguous
use_ssl/start_sslboolean pair with a cleanConnectionSecurityenum (tls/starttls/none) per RFC 8314.Related to #87 (Proton Mail Bridge compatibility)
Problem
The
start_sslconfig field onEmailServeris only wired for SMTP, never for IMAP. When configuringuse_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
securityenum fieldtlsstarttlsnoneChanges
config.py:ConnectionSecurityenum with RFC 8314 docstringsecurityfield onEmailServer(default:tls)mode="before"model validator derivessecurityfrom legacy fields; explicitsecuritytakes precedenceMCP_EMAIL_SERVER_IMAP_SECURITY,MCP_EMAIL_SERVER_SMTP_SECURITYimap_verify_sslsupport (was only on SMTP before)_parse_security_env()and_resolve_security_env()helpersemails/classic.py:_imap_starttls()— sends STARTTLS command, upgrades transport viaasyncio.loop.start_tls()(same pattern as aiosmtplib)_create_imap_connection()factory handling TLS/STARTTLS/plaintextimap_class()calls with_connect_imap()_create_imap_ssl_context()forverify_ssl=falsesupportui.py:Use SSL/Start SSLcheckboxes withConnection Securitydropdown (tls/starttls/none)Verify SSL Certificatecheckbox for both IMAP and SMTPTests: 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
use_ssl/start_sslcontinue to work unchanged*_SECURITYvars take precedence when setsecurity = "tls")ProtonMail Bridge Example
Testing
make checkpasses (ruff, prettier, pre-commit, deptry)