Skip to content

Commit 5d97637

Browse files
feat: add save_to_sent option to save sent emails to IMAP Sent folder (#82)
* feat: add save_to_sent option to save sent emails to IMAP Sent folder - Add save_to_sent config option (default: true) to EmailSettings - Add sent_folder_name config option for custom Sent folder names - Implement IMAP APPEND after successful SMTP send - Auto-detect common Sent folder names across providers - Support env vars MCP_EMAIL_SERVER_SAVE_TO_SENT and MCP_EMAIL_SERVER_SENT_FOLDER_NAME - Update README with documentation for new feature Closes #81 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: exclude None values when serializing to TOML TOML doesn't support None values, so use exclude_none=True in model_dump() * test: add comprehensive tests for save_to_sent feature - Test save_to_sent and sent_folder_name config options - Test environment variable configuration - Test ClassicEmailHandler initialization with save_to_sent - Test send_email calls append_to_sent when enabled - Test append_to_sent with various scenarios (success, failure, auto-detect) * style: fix linting and formatting in test file --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent e3b9d32 commit 5d97637

File tree

4 files changed

+576
-17
lines changed

4 files changed

+576
-17
lines changed

README.md

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,21 +63,23 @@ You can also configure the email server using environment variables, which is pa
6363

6464
#### Available Environment Variables
6565

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-
| `MCP_EMAIL_SERVER_ENABLE_ATTACHMENT_DOWNLOAD` | Enable attachment download | `false` | No |
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+
| `MCP_EMAIL_SERVER_ENABLE_ATTACHMENT_DOWNLOAD` | Enable attachment download | `false` | No |
81+
| `MCP_EMAIL_SERVER_SAVE_TO_SENT` | Save sent emails to IMAP Sent folder | `true` | No |
82+
| `MCP_EMAIL_SERVER_SENT_FOLDER_NAME` | Custom Sent folder name (auto-detect if not set) | - | No |
8183

8284
### Enabling Attachment Downloads
8385

@@ -112,6 +114,42 @@ enable_attachment_download = true
112114

113115
Once enabled, you can use the `download_attachment` tool to save email attachments to a specified path.
114116

117+
### Saving Sent Emails to IMAP Sent Folder
118+
119+
By default, sent emails are automatically saved to your IMAP Sent folder. This ensures that emails sent via the MCP server appear in your email client (Thunderbird, webmail, etc.).
120+
121+
The server auto-detects common Sent folder names: `Sent`, `INBOX.Sent`, `Sent Items`, `Sent Mail`, `[Gmail]/Sent Mail`.
122+
123+
**To specify a custom Sent folder name** (useful for providers with non-standard folder names):
124+
125+
**Option 1: Environment Variable**
126+
127+
```json
128+
{
129+
"mcpServers": {
130+
"zerolib-email": {
131+
"command": "uvx",
132+
"args": ["mcp-email-server@latest", "stdio"],
133+
"env": {
134+
"MCP_EMAIL_SERVER_SENT_FOLDER_NAME": "INBOX.Sent"
135+
}
136+
}
137+
}
138+
}
139+
```
140+
141+
**Option 2: TOML Configuration**
142+
143+
```toml
144+
[[emails]]
145+
account_name = "work"
146+
save_to_sent = true
147+
sent_folder_name = "INBOX.Sent"
148+
# ... rest of your email configuration
149+
```
150+
151+
**To disable saving to Sent folder**, set `MCP_EMAIL_SERVER_SAVE_TO_SENT=false` or `save_to_sent = false` in your TOML config.
152+
115153
For separate IMAP/SMTP credentials, you can also use:
116154

117155
- `MCP_EMAIL_SERVER_IMAP_USER_NAME` / `MCP_EMAIL_SERVER_IMAP_PASSWORD`

mcp_email_server/config.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ class EmailSettings(AccountAttributes):
7575
email_address: str
7676
incoming: EmailServer
7777
outgoing: EmailServer
78+
save_to_sent: bool = True # Save sent emails to IMAP Sent folder
79+
sent_folder_name: str | None = None # Override Sent folder name (auto-detect if None)
7880

7981
@classmethod
8082
def init(
@@ -96,6 +98,8 @@ def init(
9698
smtp_start_ssl: bool = False,
9799
smtp_user_name: str | None = None,
98100
smtp_password: str | None = None,
101+
save_to_sent: bool = True,
102+
sent_folder_name: str | None = None,
99103
) -> EmailSettings:
100104
return cls(
101105
account_name=account_name,
@@ -116,6 +120,8 @@ def init(
116120
use_ssl=smtp_ssl,
117121
start_ssl=smtp_start_ssl,
118122
),
123+
save_to_sent=save_to_sent,
124+
sent_folder_name=sent_folder_name,
119125
)
120126

121127
@classmethod
@@ -135,6 +141,8 @@ def from_env(cls) -> EmailSettings | None:
135141
- MCP_EMAIL_SERVER_SMTP_PORT (default: 465)
136142
- MCP_EMAIL_SERVER_SMTP_SSL (default: true)
137143
- MCP_EMAIL_SERVER_SMTP_START_SSL (default: false)
144+
- MCP_EMAIL_SERVER_SAVE_TO_SENT (default: true)
145+
- MCP_EMAIL_SERVER_SENT_FOLDER_NAME (default: auto-detect)
138146
"""
139147
# Check if minimum required environment variables are set
140148
email_address = os.getenv("MCP_EMAIL_SERVER_EMAIL_ADDRESS")
@@ -179,6 +187,8 @@ def parse_bool(value: str | None, default: bool = True) -> bool:
179187
smtp_password=os.getenv("MCP_EMAIL_SERVER_SMTP_PASSWORD", password),
180188
imap_user_name=os.getenv("MCP_EMAIL_SERVER_IMAP_USER_NAME", user_name),
181189
imap_password=os.getenv("MCP_EMAIL_SERVER_IMAP_PASSWORD", password),
190+
save_to_sent=parse_bool(os.getenv("MCP_EMAIL_SERVER_SAVE_TO_SENT"), True),
191+
sent_folder_name=os.getenv("MCP_EMAIL_SERVER_SENT_FOLDER_NAME"),
182192
)
183193
except (ValueError, TypeError) as e:
184194
logger.error(f"Failed to create email settings from environment variables: {e}")
@@ -303,7 +313,7 @@ def settings_customise_sources(
303313
return (TomlConfigSettingsSource(settings_cls),)
304314

305315
def _to_toml(self) -> str:
306-
data = self.model_dump()
316+
data = self.model_dump(exclude_none=True)
307317
return tomli_w.dumps(data)
308318

309319
def store(self) -> None:

mcp_email_server/emails/classic.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,91 @@ async def send_email(
574574

575575
await smtp.send_message(msg, recipients=all_recipients)
576576

577+
# Return the message for potential saving to Sent folder
578+
return msg
579+
580+
async def append_to_sent(
581+
self,
582+
msg: MIMEText | MIMEMultipart,
583+
incoming_server: EmailServer,
584+
sent_folder_name: str | None = None,
585+
) -> bool:
586+
"""Append a sent message to the IMAP Sent folder.
587+
588+
Args:
589+
msg: The email message that was sent
590+
incoming_server: IMAP server configuration for accessing Sent folder
591+
sent_folder_name: Override folder name, or None for auto-detection
592+
593+
Returns:
594+
True if successfully saved, False otherwise
595+
"""
596+
imap_class = aioimaplib.IMAP4_SSL if incoming_server.use_ssl else aioimaplib.IMAP4
597+
imap = imap_class(incoming_server.host, incoming_server.port)
598+
599+
# Common Sent folder names across different providers
600+
sent_folder_candidates = [
601+
sent_folder_name, # User-specified override (if provided)
602+
"Sent",
603+
"INBOX.Sent",
604+
"Sent Items",
605+
"Sent Mail",
606+
"[Gmail]/Sent Mail",
607+
"INBOX/Sent",
608+
]
609+
# Filter out None values
610+
sent_folder_candidates = [f for f in sent_folder_candidates if f]
611+
612+
try:
613+
await imap._client_task
614+
await imap.wait_hello_from_server()
615+
await imap.login(incoming_server.user_name, incoming_server.password)
616+
617+
# Try to find and use the Sent folder
618+
for folder in sent_folder_candidates:
619+
try:
620+
logger.debug(f"Trying Sent folder: '{folder}'")
621+
# Try to select the folder to verify it exists
622+
result = await imap.select(folder)
623+
logger.debug(f"Select result for '{folder}': {result}")
624+
625+
# aioimaplib returns (status, data) where status is a string like 'OK' or 'NO'
626+
status = result[0] if isinstance(result, tuple) else result
627+
if str(status).upper() == "OK":
628+
# Folder exists, append the message
629+
msg_bytes = msg.as_bytes()
630+
logger.debug(f"Appending message to '{folder}'")
631+
# aioimaplib.append signature: (message_bytes, mailbox, flags, date)
632+
append_result = await imap.append(
633+
msg_bytes,
634+
mailbox=folder,
635+
flags=r"(\Seen)",
636+
)
637+
logger.debug(f"Append result: {append_result}")
638+
append_status = append_result[0] if isinstance(append_result, tuple) else append_result
639+
if str(append_status).upper() == "OK":
640+
logger.info(f"Saved sent email to '{folder}'")
641+
return True
642+
else:
643+
logger.warning(f"Failed to append to '{folder}': {append_status}")
644+
else:
645+
logger.debug(f"Folder '{folder}' select returned: {status}")
646+
except Exception as e:
647+
logger.debug(f"Folder '{folder}' not available: {e}")
648+
continue
649+
650+
logger.warning("Could not find a valid Sent folder to save the message")
651+
return False
652+
653+
except Exception as e:
654+
logger.error(f"Error saving to Sent folder: {e}")
655+
return False
656+
finally:
657+
try:
658+
await imap.logout()
659+
except Exception as e:
660+
logger.debug(f"Error during logout: {e}")
661+
577662
async def delete_emails(self, email_ids: list[str], mailbox: str = "INBOX") -> tuple[list[str], list[str]]:
578663
"""Delete emails by their UIDs. Returns (deleted_ids, failed_ids)."""
579664
imap = self.imap_class(self.email_server.host, self.email_server.port)
@@ -612,6 +697,8 @@ def __init__(self, email_settings: EmailSettings):
612697
email_settings.outgoing,
613698
sender=f"{email_settings.full_name} <{email_settings.email_address}>",
614699
)
700+
self.save_to_sent = email_settings.save_to_sent
701+
self.sent_folder_name = email_settings.sent_folder_name
615702

616703
async def get_emails_metadata(
617704
self,
@@ -686,7 +773,15 @@ async def send_email(
686773
html: bool = False,
687774
attachments: list[str] | None = None,
688775
) -> None:
689-
await self.outgoing_client.send_email(recipients, subject, body, cc, bcc, html, attachments)
776+
msg = await self.outgoing_client.send_email(recipients, subject, body, cc, bcc, html, attachments)
777+
778+
# Save to Sent folder if enabled
779+
if self.save_to_sent and msg:
780+
await self.outgoing_client.append_to_sent(
781+
msg,
782+
self.email_settings.incoming,
783+
self.sent_folder_name,
784+
)
690785

691786
async def delete_emails(self, email_ids: list[str], mailbox: str = "INBOX") -> tuple[list[str], list[str]]:
692787
"""Delete emails by their UIDs. Returns (deleted_ids, failed_ids)."""

0 commit comments

Comments
 (0)