Skip to content
Open
81 changes: 62 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,24 +63,36 @@ 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_SECURITY` | IMAP connection security: `tls`, `starttls`, or `none` | `tls` | No |
| `MCP_EMAIL_SERVER_IMAP_VERIFY_SSL` | Verify IMAP SSL certificates | `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_SECURITY` | SMTP connection security: `tls`, `starttls`, or `none` | `tls` | No |
| `MCP_EMAIL_SERVER_SMTP_VERIFY_SSL` | Verify SMTP SSL certificates | `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 |

> **Deprecated:** `MCP_EMAIL_SERVER_IMAP_SSL`, `MCP_EMAIL_SERVER_SMTP_SSL`, and `MCP_EMAIL_SERVER_SMTP_START_SSL` still work for backward compatibility but are superseded by the `*_SECURITY` variables above.

#### Connection Security Modes

The `security` field (or `*_SECURITY` env var) controls how the connection to the mail server is encrypted, per [RFC 8314](https://tools.ietf.org/html/rfc8314):

| 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** — plaintext only (not recommended) | 143 | 25 |

### Enabling Attachment Downloads

Expand Down Expand Up @@ -153,7 +165,7 @@ sent_folder_name = "INBOX.Sent"

### Self-Signed Certificates (e.g., ProtonMail Bridge)

If you're using a local mail server with self-signed certificates (like ProtonMail Bridge), you'll need to disable SSL certificate verification:
If you're using a local mail server with self-signed certificates (like ProtonMail Bridge), you'll need to disable SSL certificate verification for both IMAP and SMTP:

```json
{
Expand All @@ -162,6 +174,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,7 +189,37 @@ Or in TOML configuration:
account_name = "protonmail"
# ... other settings ...

[emails.incoming]
verify_ssl = false

[emails.outgoing]
verify_ssl = false
```

#### ProtonMail Bridge Example

ProtonMail Bridge uses STARTTLS on local ports with self-signed certificates:

```toml
[[emails]]
account_name = "protonmail"
full_name = "Your Name"
email_address = "you@proton.me"

[emails.incoming]
host = "127.0.0.1"
port = 1143
user_name = "you@proton.me"
password = "your-bridge-password"
security = "starttls"
verify_ssl = false

[emails.outgoing]
host = "127.0.0.1"
port = 1025
user_name = "you@proton.me"
password = "your-bridge-password"
security = "starttls"
verify_ssl = false
```

Expand Down
215 changes: 170 additions & 45 deletions mcp_email_server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import datetime
import os
from enum import Enum
from pathlib import Path
from typing import Any
from zoneinfo import ZoneInfo
Expand Down Expand Up @@ -30,15 +31,77 @@ def _parse_bool_env(value: str | None, default: bool = False) -> bool:
CONFIG_PATH = Path(os.getenv("MCP_EMAIL_SERVER_CONFIG_PATH", DEFAULT_CONFIG_PATH)).expanduser().resolve()


class ConnectionSecurity(str, Enum):
"""Connection security mode per RFC 8314.

- TLS: Implicit TLS — encrypted from the first byte (IMAP port 993, SMTP port 465)
- STARTTLS: Connect plaintext, then upgrade via STARTTLS command (IMAP port 143, SMTP port 587)
- NONE: No encryption (not recommended, only for trusted local connections)
"""

TLS = "tls"
STARTTLS = "starttls"
NONE = "none"


def _parse_security_env(value: str | None, default: ConnectionSecurity | None = None) -> ConnectionSecurity | None:
"""Parse ConnectionSecurity from environment variable string."""
if value is None:
return default
try:
return ConnectionSecurity(value.lower())
except ValueError:
logger.warning(f"Invalid security value '{value}', using default")
return default


class EmailServer(BaseModel):
user_name: str
password: str
host: str
port: int
use_ssl: bool = True # Usually port 465
start_ssl: bool = False # Usually port 587
security: ConnectionSecurity = ConnectionSecurity.TLS
verify_ssl: bool = True # Set to False for self-signed certificates (e.g., ProtonMail Bridge)

# Deprecated: use `security` instead. Kept for backward compatibility with existing configs.
use_ssl: bool | None = None
start_ssl: bool | None = None

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

If the new `security` field is explicitly set, it takes precedence.
If only legacy fields are set, `security` is derived from them.
"""
if not isinstance(data, dict):
return data

has_security = "security" in data
use_ssl = data.get("use_ssl")
start_ssl = data.get("start_ssl")

# Only derive from legacy fields when `security` was NOT explicitly provided
if not has_security and (use_ssl is not None or start_ssl is not None):
use_ssl_val = use_ssl if use_ssl is not None else False
start_ssl_val = start_ssl if start_ssl is not None else False

if use_ssl_val and start_ssl_val:
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_val:
data["security"] = ConnectionSecurity.TLS
elif start_ssl_val:
data["security"] = ConnectionSecurity.STARTTLS
else:
data["security"] = ConnectionSecurity.NONE

return data

def masked(self) -> EmailServer:
return self.model_copy(update={"password": "********"})

Expand Down Expand Up @@ -101,36 +164,57 @@ def init(
imap_user_name: str | None = None,
imap_password: str | None = None,
imap_port: int = 993,
imap_ssl: bool = True,
imap_security: ConnectionSecurity = ConnectionSecurity.TLS,
imap_verify_ssl: bool = True,
smtp_port: int = 465,
smtp_ssl: bool = True,
smtp_start_ssl: bool = False,
smtp_security: ConnectionSecurity = ConnectionSecurity.TLS,
smtp_verify_ssl: bool = True,
smtp_user_name: str | None = None,
smtp_password: str | None = None,
save_to_sent: bool = True,
sent_folder_name: str | None = None,
# Deprecated parameters for backward compatibility
imap_ssl: bool | None = None,
smtp_ssl: bool | None = None,
smtp_start_ssl: bool | None = None,
) -> EmailSettings:
# Build incoming server config
incoming_kwargs: dict[str, Any] = {
"user_name": imap_user_name or user_name,
"password": imap_password or password,
"host": imap_host,
"port": imap_port,
"verify_ssl": imap_verify_ssl,
}
if imap_ssl is not None:
# Legacy path: use_ssl was explicitly passed
incoming_kwargs["use_ssl"] = imap_ssl
else:
incoming_kwargs["security"] = imap_security

# Build outgoing server config
outgoing_kwargs: dict[str, Any] = {
"user_name": smtp_user_name or user_name,
"password": smtp_password or password,
"host": smtp_host,
"port": smtp_port,
"verify_ssl": smtp_verify_ssl,
}
if smtp_ssl is not None or smtp_start_ssl is not None:
# Legacy path: use_ssl/start_ssl were explicitly passed
if smtp_ssl is not None:
outgoing_kwargs["use_ssl"] = smtp_ssl
if smtp_start_ssl is not None:
outgoing_kwargs["start_ssl"] = smtp_start_ssl
else:
outgoing_kwargs["security"] = smtp_security

return cls(
account_name=account_name,
full_name=full_name,
email_address=email_address,
incoming=EmailServer(
user_name=imap_user_name or user_name,
password=imap_password or password,
host=imap_host,
port=imap_port,
use_ssl=imap_ssl,
),
outgoing=EmailServer(
user_name=smtp_user_name or user_name,
password=smtp_password or password,
host=smtp_host,
port=smtp_port,
use_ssl=smtp_ssl,
start_ssl=smtp_start_ssl,
verify_ssl=smtp_verify_ssl,
),
incoming=EmailServer(**incoming_kwargs),
outgoing=EmailServer(**outgoing_kwargs),
save_to_sent=save_to_sent,
sent_folder_name=sent_folder_name,
)
Expand All @@ -147,14 +231,19 @@ def from_env(cls) -> EmailSettings | None:
- MCP_EMAIL_SERVER_PASSWORD
- MCP_EMAIL_SERVER_IMAP_HOST
- MCP_EMAIL_SERVER_IMAP_PORT (default: 993)
- MCP_EMAIL_SERVER_IMAP_SSL (default: true)
- MCP_EMAIL_SERVER_IMAP_SECURITY (default: "tls") — "tls", "starttls", or "none"
- 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)
- MCP_EMAIL_SERVER_SMTP_START_SSL (default: false)
- MCP_EMAIL_SERVER_SMTP_SECURITY (default: "tls") — "tls", "starttls", or "none"
- MCP_EMAIL_SERVER_SMTP_VERIFY_SSL (default: true)
- MCP_EMAIL_SERVER_SAVE_TO_SENT (default: true)
- MCP_EMAIL_SERVER_SENT_FOLDER_NAME (default: auto-detect)

Deprecated (still supported for backward compatibility):
- MCP_EMAIL_SERVER_IMAP_SSL → use MCP_EMAIL_SERVER_IMAP_SECURITY instead
- MCP_EMAIL_SERVER_SMTP_SSL → use MCP_EMAIL_SERVER_SMTP_SECURITY instead
- MCP_EMAIL_SERVER_SMTP_START_SSL → use MCP_EMAIL_SERVER_SMTP_SECURITY instead
"""
# Check if minimum required environment variables are set
email_address = os.getenv("MCP_EMAIL_SERVER_EMAIL_ADDRESS")
Expand All @@ -176,31 +265,67 @@ def from_env(cls) -> EmailSettings | None:
return None

try:
return cls.init(
account_name=account_name,
full_name=full_name,
email_address=email_address,
user_name=user_name,
password=password,
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),
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),
smtp_start_ssl=_parse_bool_env(os.getenv("MCP_EMAIL_SERVER_SMTP_START_SSL"), False),
smtp_verify_ssl=_parse_bool_env(os.getenv("MCP_EMAIL_SERVER_SMTP_VERIFY_SSL"), True),
smtp_user_name=os.getenv("MCP_EMAIL_SERVER_SMTP_USER_NAME", user_name),
smtp_password=os.getenv("MCP_EMAIL_SERVER_SMTP_PASSWORD", password),
imap_user_name=os.getenv("MCP_EMAIL_SERVER_IMAP_USER_NAME", user_name),
imap_password=os.getenv("MCP_EMAIL_SERVER_IMAP_PASSWORD", password),
save_to_sent=_parse_bool_env(os.getenv("MCP_EMAIL_SERVER_SAVE_TO_SENT"), True),
sent_folder_name=os.getenv("MCP_EMAIL_SERVER_SENT_FOLDER_NAME"),
)
imap_port = int(os.getenv("MCP_EMAIL_SERVER_IMAP_PORT", "993"))
smtp_port = int(os.getenv("MCP_EMAIL_SERVER_SMTP_PORT", "465"))
except ValueError as e:
logger.error(f"Invalid port configuration: {e}")
return None

init_kwargs: dict[str, Any] = {
"account_name": account_name,
"full_name": full_name,
"email_address": email_address,
"user_name": user_name,
"password": password,
"imap_host": imap_host,
"imap_port": imap_port,
"imap_verify_ssl": _parse_bool_env(os.getenv("MCP_EMAIL_SERVER_IMAP_VERIFY_SSL"), True),
"smtp_host": smtp_host,
"smtp_port": smtp_port,
"smtp_verify_ssl": _parse_bool_env(os.getenv("MCP_EMAIL_SERVER_SMTP_VERIFY_SSL"), True),
"smtp_user_name": os.getenv("MCP_EMAIL_SERVER_SMTP_USER_NAME", user_name),
"smtp_password": os.getenv("MCP_EMAIL_SERVER_SMTP_PASSWORD", password),
"imap_user_name": os.getenv("MCP_EMAIL_SERVER_IMAP_USER_NAME", user_name),
"imap_password": os.getenv("MCP_EMAIL_SERVER_IMAP_PASSWORD", password),
"save_to_sent": _parse_bool_env(os.getenv("MCP_EMAIL_SERVER_SAVE_TO_SENT"), True),
"sent_folder_name": os.getenv("MCP_EMAIL_SERVER_SENT_FOLDER_NAME"),
}

cls._resolve_security_env(init_kwargs)

try:
return cls.init(**init_kwargs)
except (ValueError, TypeError) as e:
logger.error(f"Failed to create email settings from environment variables: {e}")
return None

@staticmethod
def _resolve_security_env(init_kwargs: dict[str, Any]) -> None:
"""Resolve IMAP/SMTP security from env vars, preferring new over legacy."""
imap_security_env = os.getenv("MCP_EMAIL_SERVER_IMAP_SECURITY")
smtp_security_env = os.getenv("MCP_EMAIL_SERVER_SMTP_SECURITY")

if imap_security_env is not None:
security = _parse_security_env(imap_security_env)
if security is not None:
init_kwargs["imap_security"] = security
else:
imap_ssl_env = os.getenv("MCP_EMAIL_SERVER_IMAP_SSL")
if imap_ssl_env is not None:
init_kwargs["imap_ssl"] = _parse_bool_env(imap_ssl_env, True)

if smtp_security_env is not None:
security = _parse_security_env(smtp_security_env)
if security is not None:
init_kwargs["smtp_security"] = security
else:
smtp_ssl_env = os.getenv("MCP_EMAIL_SERVER_SMTP_SSL")
smtp_start_ssl_env = os.getenv("MCP_EMAIL_SERVER_SMTP_START_SSL")
if smtp_ssl_env is not None:
init_kwargs["smtp_ssl"] = _parse_bool_env(smtp_ssl_env, True)
if smtp_start_ssl_env is not None:
init_kwargs["smtp_start_ssl"] = _parse_bool_env(smtp_start_ssl_env, False)

def masked(self) -> EmailSettings:
return self.model_copy(
update={
Expand Down
Loading
Loading