Skip to content

Commit d4d29e3

Browse files
committed
Support ui and fix some bugs
1 parent 3a0dce3 commit d4d29e3

File tree

13 files changed

+1326
-101
lines changed

13 files changed

+1326
-101
lines changed

mcp_email_server/app.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from datetime import datetime
2+
13
from mcp.server.fastmcp import FastMCP
24

35
from mcp_email_server.config import (
@@ -6,6 +8,8 @@
68
ProviderSettings,
79
get_settings,
810
)
11+
from mcp_email_server.emails.dispatcher import dispatch_handler
12+
from mcp_email_server.emails.models import EmailPageResponse
913

1014
mcp = FastMCP("email")
1115

@@ -27,3 +31,24 @@ async def add_email_account(email: EmailSettings) -> None:
2731
settings = get_settings()
2832
settings.add_email(email)
2933
settings.store()
34+
35+
36+
@mcp.tool()
37+
async def page_email(
38+
account_name: str,
39+
page: int = 1,
40+
page_size: int = 10,
41+
before: datetime | None = None,
42+
after: datetime | None = None,
43+
include: str | None = None,
44+
) -> EmailPageResponse:
45+
handler = dispatch_handler(account_name)
46+
47+
return await handler.get_emails(page=page, page_size=page_size, before=before, after=after, include=include)
48+
49+
50+
@mcp.tool()
51+
async def send_email(account_name: str, recipient: str, subject: str, body: str) -> None:
52+
handler = dispatch_handler(account_name)
53+
await handler.send_email(recipient, subject, body)
54+
return

mcp_email_server/cli.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ def sse(
2323

2424
@app.command()
2525
def ui():
26-
typer.echo("🚧 UI not implemented yet")
26+
from mcp_email_server.ui import main as ui_main
27+
28+
ui_main()
2729

2830

2931
@app.command()

mcp_email_server/config.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ def init(
8686
imap_ssl: bool = True,
8787
smtp_port: int = 465,
8888
smtp_ssl: bool = True,
89+
smtp_start_ssl: bool = False,
8990
smtp_user_name: str | None = None,
9091
smtp_password: str | None = None,
9192
) -> EmailSettings:
@@ -98,14 +99,15 @@ def init(
9899
password=imap_password or password,
99100
host=imap_host,
100101
port=imap_port,
101-
ssl=imap_ssl,
102+
use_ssl=imap_ssl,
102103
),
103104
outgoing=EmailServer(
104105
user_name=smtp_user_name or user_name,
105106
password=smtp_password or password,
106107
host=smtp_host,
107108
port=smtp_port,
108-
ssl=smtp_ssl,
109+
use_ssl=smtp_ssl,
110+
start_ssl=smtp_start_ssl,
109111
),
110112
)
111113

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import abc
2+
from datetime import datetime
3+
from typing import TYPE_CHECKING
4+
5+
if TYPE_CHECKING:
6+
from mcp_email_server.emails.models import EmailPageResponse
7+
8+
9+
class EmailHandler(abc.ABC):
10+
@abc.abstractmethod
11+
async def get_emails(
12+
self,
13+
page: int = 1,
14+
page_size: int = 10,
15+
before: datetime | None = None,
16+
after: datetime | None = None,
17+
include: str | None = None,
18+
) -> "EmailPageResponse":
19+
"""
20+
Get emails
21+
"""
22+
23+
@abc.abstractmethod
24+
async def send_email(self, recipient: str, subject: str, body: str) -> None:
25+
"""
26+
Send email
27+
"""

mcp_email_server/emails/classic.py

Lines changed: 22 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,10 @@
88

99
import aioimaplib
1010
import aiosmtplib
11-
from pydantic import BaseModel
1211

1312
from mcp_email_server.config import EmailServer, EmailSettings
14-
15-
16-
class EmailData(BaseModel):
17-
subject: str
18-
sender: str
19-
body: str
20-
date: datetime
21-
attachments: list[str]
22-
23-
@classmethod
24-
def from_email(cls, email: dict[str, Any]):
25-
return cls(
26-
subject=email["subject"],
27-
sender=email["from"],
28-
body=email["body"],
29-
date=email["date"],
30-
attachments=email["attachments"],
31-
)
32-
33-
34-
class EmailPageResponse(BaseModel):
35-
page: int
36-
page_size: int
37-
before: datetime | None
38-
after: datetime | None
39-
include: str | None
40-
emails: list[EmailData]
41-
total: int
13+
from mcp_email_server.emails import EmailHandler
14+
from mcp_email_server.emails.models import EmailData, EmailPageResponse
4215

4316

4417
class EmailClient:
@@ -128,16 +101,20 @@ async def get_emails_stream( # noqa: C901
128101
await imap.select("INBOX")
129102

130103
# Build search criteria
131-
search_criteria = "ALL"
104+
search_criteria = []
132105
if before:
133-
search_criteria = f'(BEFORE "{before.isoformat()}")'
106+
search_criteria.extend(["BEFORE", before.isoformat()])
134107
if after:
135-
search_criteria = f'{search_criteria} (AFTER "{after.isoformat()}")'
108+
search_criteria.extend(["AFTER", after.isoformat()])
136109
if include:
137-
search_criteria = f'{search_criteria} (TEXT "{include}")'
110+
search_criteria.extend(["TEXT", include])
111+
112+
# If no specific criteria, search for ALL
113+
if not search_criteria:
114+
search_criteria = ["ALL"]
138115

139116
# Search for messages
140-
_, messages = await imap.search(search_criteria)
117+
_, messages = await imap.search(*search_criteria)
141118
message_ids = messages[0].split()
142119
start = (page - 1) * page_size
143120
end = start + page_size
@@ -203,16 +180,20 @@ async def get_email_count(
203180
await imap.select("INBOX")
204181

205182
# Build search criteria
206-
search_criteria = "ALL"
183+
search_criteria = []
207184
if before:
208-
search_criteria = f'(BEFORE "{before.isoformat()}")'
185+
search_criteria.extend(["BEFORE", before.isoformat()])
209186
if after:
210-
search_criteria = f'{search_criteria} (AFTER "{after.isoformat()}")'
187+
search_criteria.extend(["AFTER", after.isoformat()])
211188
if include:
212-
search_criteria = f'{search_criteria} (TEXT "{include}")'
189+
search_criteria.extend(["TEXT", include])
190+
191+
# If no specific criteria, search for ALL
192+
if not search_criteria:
193+
search_criteria = ["ALL"]
213194

214195
# Search for messages and count them
215-
_, messages = await imap.search(search_criteria)
196+
_, messages = await imap.search(*search_criteria)
216197
return len(messages[0].split())
217198
finally:
218199
# Ensure we logout properly
@@ -237,7 +218,7 @@ async def send_email(self, recipient: str, subject: str, body: str):
237218
await smtp.send_message(msg)
238219

239220

240-
class ClassicEmailHandler:
221+
class ClassicEmailHandler(EmailHandler):
241222
def __init__(self, email_settings: EmailSettings):
242223
self.email_settings = email_settings
243224
self.incoming_client = EmailClient(email_settings.incoming)
@@ -268,5 +249,5 @@ async def get_emails(
268249
total=total,
269250
)
270251

271-
async def send_email(self, recipient: str, subject: str, body: str):
252+
async def send_email(self, recipient: str, subject: str, body: str) -> None:
272253
await self.outgoing_client.send_email(recipient, subject, body)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from mcp_email_server.config import EmailSettings, ProviderSettings, get_settings
6+
from mcp_email_server.emails.classic import ClassicEmailHandler
7+
8+
if TYPE_CHECKING:
9+
from mcp_email_server.emails import EmailHandler
10+
11+
12+
def dispatch_handler(account_name: str) -> EmailHandler:
13+
settings = get_settings()
14+
account = settings.get_account(account_name)
15+
if isinstance(account, ProviderSettings):
16+
raise NotImplementedError
17+
if isinstance(account, EmailSettings):
18+
return ClassicEmailHandler(account)

mcp_email_server/emails/models.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from datetime import datetime
2+
from typing import Any
3+
4+
from pydantic import BaseModel
5+
6+
7+
class EmailData(BaseModel):
8+
subject: str
9+
sender: str
10+
body: str
11+
date: datetime
12+
attachments: list[str]
13+
14+
@classmethod
15+
def from_email(cls, email: dict[str, Any]):
16+
return cls(
17+
subject=email["subject"],
18+
sender=email["from"],
19+
body=email["body"],
20+
date=email["date"],
21+
attachments=email["attachments"],
22+
)
23+
24+
25+
class EmailPageResponse(BaseModel):
26+
page: int
27+
page_size: int
28+
before: datetime | None
29+
after: datetime | None
30+
include: str | None
31+
emails: list[EmailData]
32+
total: int

mcp_email_server/tools/__init__.py

Whitespace-only changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mcpServers": {
3+
"zerolib-email": {
4+
"command": "{{ ENTRYPOINT }}",
5+
"args": ["stdio"]
6+
}
7+
}
8+
}

0 commit comments

Comments
 (0)