Skip to content

Commit a5627df

Browse files
Environment variable support for email server configuration (#48)
* Environment variable support for email server configuration * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add comprehensive tests for environment variable configuration - Test missing email/password scenarios - Test missing IMAP/SMTP hosts with logger warnings - Test exception handling for invalid port values - Test Settings initialization with and without env vars - Test account override vs new account addition - Test boolean parsing for SSL settings - Test masked() method for security These tests ensure proper coverage of the environment variable configuration feature added for CI/CD deployments. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix linting issues in test_env_config_coverage.py - Add noqa comments for false positive password warnings - Fix formatting and trailing whitespace - Ensure file ends with newline --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 33a7b5b commit a5627df

File tree

3 files changed

+449
-0
lines changed

3 files changed

+449
-0
lines changed

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,54 @@ This package is available on PyPI, so you can install it using `pip install mcp-
3535

3636
After that, configure your email server using the ui: `mcp-email-server ui`
3737

38+
### Environment Variable Configuration
39+
40+
You can also configure the email server using environment variables, which is particularly useful for CI/CD environments like Jenkins. zerolib-email supports both UI configuration (via TOML file) and environment variables, with environment variables taking precedence.
41+
42+
```json
43+
{
44+
"mcpServers": {
45+
"zerolib-email": {
46+
"command": "uvx",
47+
"args": ["mcp-email-server@latest", "stdio"],
48+
"env": {
49+
"MCP_EMAIL_SERVER_ACCOUNT_NAME": "work",
50+
"MCP_EMAIL_SERVER_FULL_NAME": "John Doe",
51+
"MCP_EMAIL_SERVER_EMAIL_ADDRESS": "john@example.com",
52+
"MCP_EMAIL_SERVER_USER_NAME": "john@example.com",
53+
"MCP_EMAIL_SERVER_PASSWORD": "your_password",
54+
"MCP_EMAIL_SERVER_IMAP_HOST": "imap.gmail.com",
55+
"MCP_EMAIL_SERVER_IMAP_PORT": "993",
56+
"MCP_EMAIL_SERVER_SMTP_HOST": "smtp.gmail.com",
57+
"MCP_EMAIL_SERVER_SMTP_PORT": "465"
58+
}
59+
}
60+
}
61+
}
62+
```
63+
64+
#### Available Environment Variables
65+
66+
| Variable | Description | Default | Required |
67+
| --------------------------------- | ------------------ | ------------- | -------- |
68+
| `MCP_EMAIL_SERVER_ACCOUNT_NAME` | Account identifier | `"default"` | No |
69+
| `MCP_EMAIL_SERVER_FULL_NAME` | Display name | Email prefix | No |
70+
| `MCP_EMAIL_SERVER_EMAIL_ADDRESS` | Email address | - | Yes |
71+
| `MCP_EMAIL_SERVER_USER_NAME` | Login username | Same as email | No |
72+
| `MCP_EMAIL_SERVER_PASSWORD` | Email password | - | Yes |
73+
| `MCP_EMAIL_SERVER_IMAP_HOST` | IMAP server host | - | Yes |
74+
| `MCP_EMAIL_SERVER_IMAP_PORT` | IMAP server port | `993` | No |
75+
| `MCP_EMAIL_SERVER_IMAP_SSL` | Enable IMAP SSL | `true` | No |
76+
| `MCP_EMAIL_SERVER_SMTP_HOST` | SMTP server host | - | Yes |
77+
| `MCP_EMAIL_SERVER_SMTP_PORT` | SMTP server port | `465` | No |
78+
| `MCP_EMAIL_SERVER_SMTP_SSL` | Enable SMTP SSL | `true` | No |
79+
| `MCP_EMAIL_SERVER_SMTP_START_SSL` | Enable STARTTLS | `false` | No |
80+
81+
For separate IMAP/SMTP credentials, you can also use:
82+
83+
- `MCP_EMAIL_SERVER_IMAP_USER_NAME` / `MCP_EMAIL_SERVER_IMAP_PASSWORD`
84+
- `MCP_EMAIL_SERVER_SMTP_USER_NAME` / `MCP_EMAIL_SERVER_SMTP_PASSWORD`
85+
3886
Then you can try it in [Claude Desktop](https://claude.ai/download). If you want to intergrate it with other mcp client, run `$which mcp-email-server` for the path and configure it in your client like:
3987

4088
```json

mcp_email_server/config.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import datetime
44
import os
55
from pathlib import Path
6+
from typing import Any
67

78
import tomli_w
89
from pydantic import BaseModel, Field, model_validator
@@ -111,6 +112,72 @@ def init(
111112
),
112113
)
113114

115+
@classmethod
116+
def from_env(cls) -> EmailSettings | None:
117+
"""Create EmailSettings from environment variables.
118+
119+
Expected environment variables:
120+
- MCP_EMAIL_SERVER_ACCOUNT_NAME (default: "default")
121+
- MCP_EMAIL_SERVER_FULL_NAME
122+
- MCP_EMAIL_SERVER_EMAIL_ADDRESS
123+
- MCP_EMAIL_SERVER_USER_NAME
124+
- MCP_EMAIL_SERVER_PASSWORD
125+
- MCP_EMAIL_SERVER_IMAP_HOST
126+
- MCP_EMAIL_SERVER_IMAP_PORT (default: 993)
127+
- MCP_EMAIL_SERVER_IMAP_SSL (default: true)
128+
- MCP_EMAIL_SERVER_SMTP_HOST
129+
- MCP_EMAIL_SERVER_SMTP_PORT (default: 465)
130+
- MCP_EMAIL_SERVER_SMTP_SSL (default: true)
131+
- MCP_EMAIL_SERVER_SMTP_START_SSL (default: false)
132+
"""
133+
# Check if minimum required environment variables are set
134+
email_address = os.getenv("MCP_EMAIL_SERVER_EMAIL_ADDRESS")
135+
password = os.getenv("MCP_EMAIL_SERVER_PASSWORD")
136+
137+
if not email_address or not password:
138+
return None
139+
140+
# Parse boolean values
141+
def parse_bool(value: str | None, default: bool = True) -> bool:
142+
if value is None:
143+
return default
144+
return value.lower() in ("true", "1", "yes", "on")
145+
146+
# Get all environment variables with defaults
147+
account_name = os.getenv("MCP_EMAIL_SERVER_ACCOUNT_NAME", "default")
148+
full_name = os.getenv("MCP_EMAIL_SERVER_FULL_NAME", email_address.split("@")[0])
149+
user_name = os.getenv("MCP_EMAIL_SERVER_USER_NAME", email_address)
150+
imap_host = os.getenv("MCP_EMAIL_SERVER_IMAP_HOST")
151+
smtp_host = os.getenv("MCP_EMAIL_SERVER_SMTP_HOST")
152+
153+
# Required fields check
154+
if not imap_host or not smtp_host:
155+
logger.warning("Missing required email configuration environment variables (IMAP_HOST or SMTP_HOST)")
156+
return None
157+
158+
try:
159+
return cls.init(
160+
account_name=account_name,
161+
full_name=full_name,
162+
email_address=email_address,
163+
user_name=user_name,
164+
password=password,
165+
imap_host=imap_host,
166+
imap_port=int(os.getenv("MCP_EMAIL_SERVER_IMAP_PORT", "993")),
167+
imap_ssl=parse_bool(os.getenv("MCP_EMAIL_SERVER_IMAP_SSL"), True),
168+
smtp_host=smtp_host,
169+
smtp_port=int(os.getenv("MCP_EMAIL_SERVER_SMTP_PORT", "465")),
170+
smtp_ssl=parse_bool(os.getenv("MCP_EMAIL_SERVER_SMTP_SSL"), True),
171+
smtp_start_ssl=parse_bool(os.getenv("MCP_EMAIL_SERVER_SMTP_START_SSL"), False),
172+
smtp_user_name=os.getenv("MCP_EMAIL_SERVER_SMTP_USER_NAME", user_name),
173+
smtp_password=os.getenv("MCP_EMAIL_SERVER_SMTP_PASSWORD", password),
174+
imap_user_name=os.getenv("MCP_EMAIL_SERVER_IMAP_USER_NAME", user_name),
175+
imap_password=os.getenv("MCP_EMAIL_SERVER_IMAP_PASSWORD", password),
176+
)
177+
except (ValueError, TypeError) as e:
178+
logger.error(f"Failed to create email settings from environment variables: {e}")
179+
return None
180+
114181
def masked(self) -> EmailSettings:
115182
return self.model_copy(
116183
update={
@@ -135,6 +202,29 @@ class Settings(BaseSettings):
135202

136203
model_config = SettingsConfigDict(toml_file=CONFIG_PATH, validate_assignment=True, revalidate_instances="always")
137204

205+
def __init__(self, **data: Any) -> None:
206+
"""Initialize Settings with support for environment variables."""
207+
super().__init__(**data)
208+
209+
# Check for email configuration from environment variables
210+
env_email = EmailSettings.from_env()
211+
if env_email:
212+
# Check if this account already exists (from TOML)
213+
existing_account = None
214+
for i, email in enumerate(self.emails):
215+
if email.account_name == env_email.account_name:
216+
existing_account = i
217+
break
218+
219+
if existing_account is not None:
220+
# Replace existing account with env configuration
221+
self.emails[existing_account] = env_email
222+
logger.info(f"Overriding email account '{env_email.account_name}' with environment variables")
223+
else:
224+
# Add new account from env
225+
self.emails.insert(0, env_email)
226+
logger.info(f"Added email account '{env_email.account_name}' from environment variables")
227+
138228
def add_email(self, email: EmailSettings) -> None:
139229
"""Use re-assigned for validation to work."""
140230
self.emails = [email, *self.emails]

0 commit comments

Comments
 (0)