Skip to content

Commit 396dcac

Browse files
committed
feat: Add verify_ssl support for IMAP connections
1 parent 9a1e992 commit 396dcac

File tree

3 files changed

+28
-9
lines changed

3 files changed

+28
-9
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ You can also configure the email server using environment variables, which is pa
7373
| `MCP_EMAIL_SERVER_IMAP_HOST` | IMAP server host | - | Yes |
7474
| `MCP_EMAIL_SERVER_IMAP_PORT` | IMAP server port | `993` | No |
7575
| `MCP_EMAIL_SERVER_IMAP_SSL` | Enable IMAP SSL | `true` | No |
76+
| `MCP_EMAIL_SERVER_IMAP_VERIFY_SSL` | Verify IMAP SSL certificates (disable for self-signed) | `true` | No |
7677
| `MCP_EMAIL_SERVER_SMTP_HOST` | SMTP server host | - | Yes |
7778
| `MCP_EMAIL_SERVER_SMTP_PORT` | SMTP server port | `465` | No |
7879
| `MCP_EMAIL_SERVER_SMTP_SSL` | Enable SMTP SSL | `true` | No |
@@ -162,6 +163,7 @@ If you're using a local mail server with self-signed certificates (like ProtonMa
162163
"command": "uvx",
163164
"args": ["mcp-email-server@latest", "stdio"],
164165
"env": {
166+
"MCP_EMAIL_SERVER_IMAP_VERIFY_SSL": "false",
165167
"MCP_EMAIL_SERVER_SMTP_VERIFY_SSL": "false"
166168
}
167169
}
@@ -176,6 +178,9 @@ Or in TOML configuration:
176178
account_name = "protonmail"
177179
# ... other settings ...
178180

181+
[emails.incoming]
182+
verify_ssl = false
183+
179184
[emails.outgoing]
180185
verify_ssl = false
181186
```

mcp_email_server/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def init(
102102
imap_password: str | None = None,
103103
imap_port: int = 993,
104104
imap_ssl: bool = True,
105+
imap_verify_ssl: bool = True,
105106
smtp_port: int = 465,
106107
smtp_ssl: bool = True,
107108
smtp_start_ssl: bool = False,
@@ -121,6 +122,7 @@ def init(
121122
host=imap_host,
122123
port=imap_port,
123124
use_ssl=imap_ssl,
125+
verify_ssl=imap_verify_ssl,
124126
),
125127
outgoing=EmailServer(
126128
user_name=smtp_user_name or user_name,
@@ -148,6 +150,7 @@ def from_env(cls) -> EmailSettings | None:
148150
- MCP_EMAIL_SERVER_IMAP_HOST
149151
- MCP_EMAIL_SERVER_IMAP_PORT (default: 993)
150152
- MCP_EMAIL_SERVER_IMAP_SSL (default: true)
153+
- MCP_EMAIL_SERVER_IMAP_VERIFY_SSL (default: true)
151154
- MCP_EMAIL_SERVER_SMTP_HOST
152155
- MCP_EMAIL_SERVER_SMTP_PORT (default: 465)
153156
- MCP_EMAIL_SERVER_SMTP_SSL (default: true)
@@ -185,6 +188,7 @@ def from_env(cls) -> EmailSettings | None:
185188
imap_host=imap_host,
186189
imap_port=int(os.getenv("MCP_EMAIL_SERVER_IMAP_PORT", "993")),
187190
imap_ssl=_parse_bool_env(os.getenv("MCP_EMAIL_SERVER_IMAP_SSL"), True),
191+
imap_verify_ssl=_parse_bool_env(os.getenv("MCP_EMAIL_SERVER_IMAP_VERIFY_SSL"), True),
188192
smtp_host=smtp_host,
189193
smtp_port=int(os.getenv("MCP_EMAIL_SERVER_SMTP_PORT", "465")),
190194
smtp_ssl=_parse_bool_env(os.getenv("MCP_EMAIL_SERVER_SMTP_SSL"), True),

mcp_email_server/emails/classic.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ async def _send_imap_id(imap: aioimaplib.IMAP4 | aioimaplib.IMAP4_SSL) -> None:
7979
logger.warning(f"IMAP ID command failed: {e!s}")
8080

8181

82-
def _create_smtp_ssl_context(verify_ssl: bool) -> ssl.SSLContext | None:
83-
"""Create SSL context for SMTP connections.
82+
def _create_ssl_context(verify_ssl: bool) -> ssl.SSLContext | None:
83+
"""Create SSL context for SMTP/IMAP connections.
8484
8585
Returns None for default verification, or permissive context
8686
for self-signed certificates when verify_ssl=False.
@@ -93,20 +93,29 @@ def _create_smtp_ssl_context(verify_ssl: bool) -> ssl.SSLContext | None:
9393
return ctx
9494

9595

96+
# Backwards-compatible alias
97+
_create_smtp_ssl_context = _create_ssl_context
98+
99+
96100
class EmailClient:
97101
def __init__(self, email_server: EmailServer, sender: str | None = None):
98102
self.email_server = email_server
99103
self.sender = sender or email_server.user_name
100104

101105
self.imap_class = aioimaplib.IMAP4_SSL if self.email_server.use_ssl else aioimaplib.IMAP4
106+
self.imap_ssl_context = _create_ssl_context(self.email_server.verify_ssl)
102107

103108
self.smtp_use_tls = self.email_server.use_ssl
104109
self.smtp_start_tls = self.email_server.start_ssl
105110
self.smtp_verify_ssl = self.email_server.verify_ssl
106111

112+
def _imap_connect(self) -> aioimaplib.IMAP4_SSL | aioimaplib.IMAP4:
113+
"""Create a new IMAP connection with the configured SSL context."""
114+
return self.imap_class(self.email_server.host, self.email_server.port, ssl_context=self.imap_ssl_context)
115+
107116
def _get_smtp_ssl_context(self) -> ssl.SSLContext | None:
108117
"""Get SSL context for SMTP connections based on verify_ssl setting."""
109-
return _create_smtp_ssl_context(self.smtp_verify_ssl)
118+
return _create_ssl_context(self.smtp_verify_ssl)
110119

111120
@staticmethod
112121
def _parse_recipients(email_message) -> list[str]:
@@ -403,7 +412,7 @@ async def get_email_count(
403412
flagged: bool | None = None,
404413
answered: bool | None = None,
405414
) -> int:
406-
imap = self.imap_class(self.email_server.host, self.email_server.port)
415+
imap = self._imap_connect()
407416
try:
408417
# Wait for the connection to be established
409418
await imap._client_task
@@ -449,7 +458,7 @@ async def get_emails_metadata_stream(
449458
flagged: bool | None = None,
450459
answered: bool | None = None,
451460
) -> AsyncGenerator[dict[str, Any], None]:
452-
imap = self.imap_class(self.email_server.host, self.email_server.port)
461+
imap = self._imap_connect()
453462
try:
454463
# Wait for the connection to be established
455464
await imap._client_task
@@ -563,7 +572,7 @@ async def _fetch_email_with_formats(self, imap, email_id: str) -> list | None:
563572
return None
564573

565574
async def get_email_body_by_id(self, email_id: str, mailbox: str = "INBOX") -> dict[str, Any] | None:
566-
imap = self.imap_class(self.email_server.host, self.email_server.port)
575+
imap = self._imap_connect()
567576
try:
568577
# Wait for the connection to be established
569578
await imap._client_task
@@ -618,7 +627,7 @@ async def download_attachment(
618627
Returns:
619628
A dictionary with download result information.
620629
"""
621-
imap = self.imap_class(self.email_server.host, self.email_server.port)
630+
imap = self._imap_connect()
622631
try:
623632
await imap._client_task
624633
await imap.wait_hello_from_server()
@@ -854,7 +863,8 @@ async def append_to_sent(
854863
True if successfully saved, False otherwise
855864
"""
856865
imap_class = aioimaplib.IMAP4_SSL if incoming_server.use_ssl else aioimaplib.IMAP4
857-
imap = imap_class(incoming_server.host, incoming_server.port)
866+
imap_ssl_context = _create_ssl_context(incoming_server.verify_ssl)
867+
imap = imap_class(incoming_server.host, incoming_server.port, ssl_context=imap_ssl_context)
858868

859869
# Common Sent folder names across different providers
860870
sent_folder_candidates = [
@@ -928,7 +938,7 @@ async def append_to_sent(
928938

929939
async def delete_emails(self, email_ids: list[str], mailbox: str = "INBOX") -> tuple[list[str], list[str]]:
930940
"""Delete emails by their UIDs. Returns (deleted_ids, failed_ids)."""
931-
imap = self.imap_class(self.email_server.host, self.email_server.port)
941+
imap = self._imap_connect()
932942
deleted_ids = []
933943
failed_ids = []
934944

0 commit comments

Comments
 (0)