diff --git a/README.md b/README.md index 7db0a35..0e403f5 100644 --- a/README.md +++ b/README.md @@ -63,24 +63,25 @@ You can also configure the email server using environment variables, which is pa #### Available Environment Variables -| Variable | Description | Default | Required | -| --------------------------------------------- | ------------------------------------------------- | ------------- | -------- | -| `MCP_EMAIL_SERVER_ACCOUNT_NAME` | Account identifier | `"default"` | No | -| `MCP_EMAIL_SERVER_FULL_NAME` | Display name | Email prefix | No | -| `MCP_EMAIL_SERVER_EMAIL_ADDRESS` | Email address | - | Yes | -| `MCP_EMAIL_SERVER_USER_NAME` | Login username | Same as email | No | -| `MCP_EMAIL_SERVER_PASSWORD` | Email password | - | Yes | -| `MCP_EMAIL_SERVER_IMAP_HOST` | IMAP server host | - | Yes | -| `MCP_EMAIL_SERVER_IMAP_PORT` | IMAP server port | `993` | No | -| `MCP_EMAIL_SERVER_IMAP_SSL` | Enable IMAP SSL | `true` | No | -| `MCP_EMAIL_SERVER_SMTP_HOST` | SMTP server host | - | Yes | -| `MCP_EMAIL_SERVER_SMTP_PORT` | SMTP server port | `465` | No | -| `MCP_EMAIL_SERVER_SMTP_SSL` | Enable SMTP SSL | `true` | No | -| `MCP_EMAIL_SERVER_SMTP_START_SSL` | Enable STARTTLS | `false` | No | -| `MCP_EMAIL_SERVER_SMTP_VERIFY_SSL` | Verify SSL certificates (disable for self-signed) | `true` | No | -| `MCP_EMAIL_SERVER_ENABLE_ATTACHMENT_DOWNLOAD` | Enable attachment download | `false` | No | -| `MCP_EMAIL_SERVER_SAVE_TO_SENT` | Save sent emails to IMAP Sent folder | `true` | No | -| `MCP_EMAIL_SERVER_SENT_FOLDER_NAME` | Custom Sent folder name (auto-detect if not set) | - | No | +| Variable | Description | Default | Required | +| --------------------------------------------- | ------------------------------------------------------ | ------------- | -------- | +| `MCP_EMAIL_SERVER_ACCOUNT_NAME` | Account identifier | `"default"` | No | +| `MCP_EMAIL_SERVER_FULL_NAME` | Display name | Email prefix | No | +| `MCP_EMAIL_SERVER_EMAIL_ADDRESS` | Email address | - | Yes | +| `MCP_EMAIL_SERVER_USER_NAME` | Login username | Same as email | No | +| `MCP_EMAIL_SERVER_PASSWORD` | Email password | - | Yes | +| `MCP_EMAIL_SERVER_IMAP_HOST` | IMAP server host | - | Yes | +| `MCP_EMAIL_SERVER_IMAP_PORT` | IMAP server port | `993` | No | +| `MCP_EMAIL_SERVER_IMAP_SSL` | Enable IMAP SSL | `true` | No | +| `MCP_EMAIL_SERVER_IMAP_VERIFY_SSL` | Verify IMAP SSL certificates (disable for self-signed) | `true` | No | +| `MCP_EMAIL_SERVER_SMTP_HOST` | SMTP server host | - | Yes | +| `MCP_EMAIL_SERVER_SMTP_PORT` | SMTP server port | `465` | No | +| `MCP_EMAIL_SERVER_SMTP_SSL` | Enable SMTP SSL | `true` | No | +| `MCP_EMAIL_SERVER_SMTP_START_SSL` | Enable STARTTLS | `false` | No | +| `MCP_EMAIL_SERVER_SMTP_VERIFY_SSL` | Verify SSL certificates (disable for self-signed) | `true` | No | +| `MCP_EMAIL_SERVER_ENABLE_ATTACHMENT_DOWNLOAD` | Enable attachment download | `false` | No | +| `MCP_EMAIL_SERVER_SAVE_TO_SENT` | Save sent emails to IMAP Sent folder | `true` | No | +| `MCP_EMAIL_SERVER_SENT_FOLDER_NAME` | Custom Sent folder name (auto-detect if not set) | - | No | ### Enabling Attachment Downloads @@ -162,6 +163,7 @@ If you're using a local mail server with self-signed certificates (like ProtonMa "command": "uvx", "args": ["mcp-email-server@latest", "stdio"], "env": { + "MCP_EMAIL_SERVER_IMAP_VERIFY_SSL": "false", "MCP_EMAIL_SERVER_SMTP_VERIFY_SSL": "false" } } @@ -176,6 +178,9 @@ Or in TOML configuration: account_name = "protonmail" # ... other settings ... +[emails.incoming] +verify_ssl = false + [emails.outgoing] verify_ssl = false ``` diff --git a/mcp_email_server/config.py b/mcp_email_server/config.py index 879f35f..28c11ce 100644 --- a/mcp_email_server/config.py +++ b/mcp_email_server/config.py @@ -102,6 +102,7 @@ def init( imap_password: str | None = None, imap_port: int = 993, imap_ssl: bool = True, + imap_verify_ssl: bool = True, smtp_port: int = 465, smtp_ssl: bool = True, smtp_start_ssl: bool = False, @@ -121,6 +122,7 @@ def init( host=imap_host, port=imap_port, use_ssl=imap_ssl, + verify_ssl=imap_verify_ssl, ), outgoing=EmailServer( user_name=smtp_user_name or user_name, @@ -148,6 +150,7 @@ def from_env(cls) -> EmailSettings | None: - MCP_EMAIL_SERVER_IMAP_HOST - MCP_EMAIL_SERVER_IMAP_PORT (default: 993) - MCP_EMAIL_SERVER_IMAP_SSL (default: true) + - MCP_EMAIL_SERVER_IMAP_VERIFY_SSL (default: true) - MCP_EMAIL_SERVER_SMTP_HOST - MCP_EMAIL_SERVER_SMTP_PORT (default: 465) - MCP_EMAIL_SERVER_SMTP_SSL (default: true) @@ -185,6 +188,7 @@ def from_env(cls) -> EmailSettings | None: imap_host=imap_host, imap_port=int(os.getenv("MCP_EMAIL_SERVER_IMAP_PORT", "993")), imap_ssl=_parse_bool_env(os.getenv("MCP_EMAIL_SERVER_IMAP_SSL"), True), + imap_verify_ssl=_parse_bool_env(os.getenv("MCP_EMAIL_SERVER_IMAP_VERIFY_SSL"), True), smtp_host=smtp_host, smtp_port=int(os.getenv("MCP_EMAIL_SERVER_SMTP_PORT", "465")), smtp_ssl=_parse_bool_env(os.getenv("MCP_EMAIL_SERVER_SMTP_SSL"), True), diff --git a/mcp_email_server/emails/classic.py b/mcp_email_server/emails/classic.py index 618c959..81b66a0 100644 --- a/mcp_email_server/emails/classic.py +++ b/mcp_email_server/emails/classic.py @@ -79,8 +79,8 @@ async def _send_imap_id(imap: aioimaplib.IMAP4 | aioimaplib.IMAP4_SSL) -> None: logger.warning(f"IMAP ID command failed: {e!s}") -def _create_smtp_ssl_context(verify_ssl: bool) -> ssl.SSLContext | None: - """Create SSL context for SMTP connections. +def _create_ssl_context(verify_ssl: bool) -> ssl.SSLContext | None: + """Create SSL context for SMTP/IMAP connections. Returns None for default verification, or permissive context for self-signed certificates when verify_ssl=False. @@ -93,6 +93,10 @@ def _create_smtp_ssl_context(verify_ssl: bool) -> ssl.SSLContext | None: return ctx +# Backwards-compatible alias +_create_smtp_ssl_context = _create_ssl_context + + class EmailClient: def __init__(self, email_server: EmailServer, sender: str | None = None): self.email_server = email_server @@ -104,9 +108,16 @@ def __init__(self, email_server: EmailServer, sender: str | None = None): self.smtp_start_tls = self.email_server.start_ssl self.smtp_verify_ssl = self.email_server.verify_ssl + def _imap_connect(self) -> aioimaplib.IMAP4_SSL | aioimaplib.IMAP4: + """Create a new IMAP connection with the configured SSL context.""" + if self.email_server.use_ssl: + imap_ssl_context = _create_ssl_context(self.email_server.verify_ssl) + return self.imap_class(self.email_server.host, self.email_server.port, ssl_context=imap_ssl_context) + return self.imap_class(self.email_server.host, self.email_server.port) + def _get_smtp_ssl_context(self) -> ssl.SSLContext | None: """Get SSL context for SMTP connections based on verify_ssl setting.""" - return _create_smtp_ssl_context(self.smtp_verify_ssl) + return _create_ssl_context(self.smtp_verify_ssl) @staticmethod def _parse_recipients(email_message) -> list[str]: @@ -403,7 +414,7 @@ async def get_email_count( flagged: bool | None = None, answered: bool | None = None, ) -> int: - imap = self.imap_class(self.email_server.host, self.email_server.port) + imap = self._imap_connect() try: # Wait for the connection to be established await imap._client_task @@ -449,7 +460,7 @@ async def get_emails_metadata_stream( flagged: bool | None = None, answered: bool | None = None, ) -> AsyncGenerator[dict[str, Any], None]: - imap = self.imap_class(self.email_server.host, self.email_server.port) + imap = self._imap_connect() try: # Wait for the connection to be established await imap._client_task @@ -563,7 +574,7 @@ async def _fetch_email_with_formats(self, imap, email_id: str) -> list | None: return None async def get_email_body_by_id(self, email_id: str, mailbox: str = "INBOX") -> dict[str, Any] | None: - imap = self.imap_class(self.email_server.host, self.email_server.port) + imap = self._imap_connect() try: # Wait for the connection to be established await imap._client_task @@ -618,7 +629,7 @@ async def download_attachment( Returns: A dictionary with download result information. """ - imap = self.imap_class(self.email_server.host, self.email_server.port) + imap = self._imap_connect() try: await imap._client_task await imap.wait_hello_from_server() @@ -853,8 +864,11 @@ async def append_to_sent( Returns: True if successfully saved, False otherwise """ - imap_class = aioimaplib.IMAP4_SSL if incoming_server.use_ssl else aioimaplib.IMAP4 - imap = imap_class(incoming_server.host, incoming_server.port) + if incoming_server.use_ssl: + imap_ssl_context = _create_ssl_context(incoming_server.verify_ssl) + imap = aioimaplib.IMAP4_SSL(incoming_server.host, incoming_server.port, ssl_context=imap_ssl_context) + else: + imap = aioimaplib.IMAP4(incoming_server.host, incoming_server.port) # Common Sent folder names across different providers sent_folder_candidates = [ @@ -928,7 +942,7 @@ async def append_to_sent( async def delete_emails(self, email_ids: list[str], mailbox: str = "INBOX") -> tuple[list[str], list[str]]: """Delete emails by their UIDs. Returns (deleted_ids, failed_ids).""" - imap = self.imap_class(self.email_server.host, self.email_server.port) + imap = self._imap_connect() deleted_ids = [] failed_ids = []