Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 23 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"
}
}
Expand All @@ -176,6 +178,9 @@ Or in TOML configuration:
account_name = "protonmail"
# ... other settings ...

[emails.incoming]
verify_ssl = false

[emails.outgoing]
verify_ssl = false
```
Expand Down
4 changes: 4 additions & 0 deletions mcp_email_server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand Down
34 changes: 24 additions & 10 deletions mcp_email_server/emails/classic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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]:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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 = []

Expand Down
Loading