agent-jmap-mcp 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_jmap_mcp/__init__.py +25 -0
- agent_jmap_mcp/cli.py +257 -0
- agent_jmap_mcp/client.py +755 -0
- agent_jmap_mcp/models.py +133 -0
- agent_jmap_mcp/server.py +225 -0
- agent_jmap_mcp/triage.py +102 -0
- agent_jmap_mcp-0.2.0.dist-info/METADATA +290 -0
- agent_jmap_mcp-0.2.0.dist-info/RECORD +11 -0
- agent_jmap_mcp-0.2.0.dist-info/WHEEL +4 -0
- agent_jmap_mcp-0.2.0.dist-info/entry_points.txt +3 -0
- agent_jmap_mcp-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""agent-jmap-mcp: Stateless JMAP client and FastMCP server for AI agents."""
|
|
2
|
+
|
|
3
|
+
from agent_jmap_mcp.client import JMAPClient
|
|
4
|
+
from agent_jmap_mcp.models import (
|
|
5
|
+
EmailAddress,
|
|
6
|
+
EmailHeader,
|
|
7
|
+
EmailMessage,
|
|
8
|
+
EmailThread,
|
|
9
|
+
MailboxInfo,
|
|
10
|
+
TriageCategory,
|
|
11
|
+
TriageResult,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__version__ = "0.2.0"
|
|
15
|
+
__all__ = [
|
|
16
|
+
"EmailAddress",
|
|
17
|
+
"EmailHeader",
|
|
18
|
+
"EmailMessage",
|
|
19
|
+
"EmailThread",
|
|
20
|
+
"JMAPClient",
|
|
21
|
+
"MailboxInfo",
|
|
22
|
+
"TriageCategory",
|
|
23
|
+
"TriageResult",
|
|
24
|
+
"__version__",
|
|
25
|
+
]
|
agent_jmap_mcp/cli.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Command line interface (CLI) for agent-jmap operations and server execution."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from agent_jmap_mcp.client import JMAPClient
|
|
12
|
+
from agent_jmap_mcp.server import mcp
|
|
13
|
+
from agent_jmap_mcp.triage import triage_inbox
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_client_from_args(args: argparse.Namespace) -> JMAPClient:
|
|
19
|
+
session_url = args.session_url or os.environ.get("JMAP_SESSION_URL")
|
|
20
|
+
api_token = args.token or os.environ.get("JMAP_API_TOKEN")
|
|
21
|
+
account_id = args.account or os.environ.get("JMAP_ACCOUNT_ID")
|
|
22
|
+
|
|
23
|
+
if not session_url or not api_token:
|
|
24
|
+
console.print(
|
|
25
|
+
"[bold red]Error:[/bold red] Missing JMAP session URL or API token.\n"
|
|
26
|
+
"Set JMAP_SESSION_URL and JMAP_API_TOKEN env vars or pass --session-url and --token."
|
|
27
|
+
)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
return JMAPClient(session_url=session_url, token=api_token, account_id=account_id)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
34
|
+
parser = argparse.ArgumentParser(
|
|
35
|
+
prog="agent-jmap",
|
|
36
|
+
description="Stateless JMAP Email Client and Model Context Protocol Server for AI Agents.",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument("--session-url", help="JMAP session discovery URL")
|
|
39
|
+
parser.add_argument("--token", help="JMAP bearer API token")
|
|
40
|
+
parser.add_argument("--account", help="Specific account ID (optional)")
|
|
41
|
+
|
|
42
|
+
subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
|
|
43
|
+
|
|
44
|
+
# serve
|
|
45
|
+
subparsers.add_parser("serve", help="Run the FastMCP stdio server")
|
|
46
|
+
|
|
47
|
+
# mailboxes
|
|
48
|
+
subparsers.add_parser("mailboxes", help="List all mailboxes/folders")
|
|
49
|
+
|
|
50
|
+
# list
|
|
51
|
+
list_p = subparsers.add_parser("list", help="List emails in a mailbox")
|
|
52
|
+
list_p.add_argument("--mailbox", default="INBOX", help="Target mailbox (default: INBOX)")
|
|
53
|
+
list_p.add_argument("--limit", type=int, default=15, help="Max results (default: 15)")
|
|
54
|
+
list_p.add_argument("--unread", action="store_true", help="Filter unread emails only")
|
|
55
|
+
list_p.add_argument("--query", help="Search text query")
|
|
56
|
+
|
|
57
|
+
# get
|
|
58
|
+
get_p = subparsers.add_parser("get", help="Get full details of an email by ID")
|
|
59
|
+
get_p.add_argument("email_id", help="JMAP Email ID")
|
|
60
|
+
get_p.add_argument("--read", action="store_true", help="Mark email as read")
|
|
61
|
+
|
|
62
|
+
# thread
|
|
63
|
+
thread_p = subparsers.add_parser("thread", help="View full conversation thread by Thread ID")
|
|
64
|
+
thread_p.add_argument("thread_id", help="JMAP Thread ID")
|
|
65
|
+
|
|
66
|
+
# download
|
|
67
|
+
dl_p = subparsers.add_parser("download", help="Download an email attachment by blob ID")
|
|
68
|
+
dl_p.add_argument("blob_id", help="JMAP Binary Blob ID")
|
|
69
|
+
dl_p.add_argument("--name", required=True, help="Target filename for saving")
|
|
70
|
+
dl_p.add_argument("--output", help="Output directory path (default: current directory)")
|
|
71
|
+
|
|
72
|
+
# send
|
|
73
|
+
send_p = subparsers.add_parser("send", help="Send an email atomically or save to Drafts")
|
|
74
|
+
send_p.add_argument("--to", required=True, nargs="+", help="Recipient email address(es)")
|
|
75
|
+
send_p.add_argument("--subject", required=True, help="Email subject")
|
|
76
|
+
send_p.add_argument("--body", required=True, help="Email body text")
|
|
77
|
+
send_p.add_argument("--from-addr", help="Sender email address")
|
|
78
|
+
send_p.add_argument("--cc", nargs="+", help="CC recipient address(es)")
|
|
79
|
+
send_p.add_argument("--bcc", nargs="+", help="BCC recipient address(es)")
|
|
80
|
+
send_p.add_argument("--draft", action="store_true", help="Save to Drafts instead of sending")
|
|
81
|
+
|
|
82
|
+
# triage
|
|
83
|
+
triage_p = subparsers.add_parser("triage", help="Run automated inbox triage digest")
|
|
84
|
+
triage_p.add_argument("--mailbox", default="INBOX", help="Mailbox to triage (default: INBOX)")
|
|
85
|
+
triage_p.add_argument("--limit", type=int, default=20, help="Number of emails to analyze")
|
|
86
|
+
triage_p.add_argument("--all", action="store_true", help="Include read emails in analysis")
|
|
87
|
+
|
|
88
|
+
return parser
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main() -> int:
|
|
92
|
+
parser = build_parser()
|
|
93
|
+
args = parser.parse_args()
|
|
94
|
+
|
|
95
|
+
if not args.command:
|
|
96
|
+
parser.print_help()
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
if args.command == "serve":
|
|
100
|
+
mcp.run()
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
client = get_client_from_args(args)
|
|
105
|
+
|
|
106
|
+
if args.command == "mailboxes":
|
|
107
|
+
mailboxes = client.get_mailboxes()
|
|
108
|
+
table = Table(title="JMAP Mailboxes", show_header=True, header_style="bold cyan")
|
|
109
|
+
table.add_column("ID", style="dim")
|
|
110
|
+
table.add_column("Name", style="bold")
|
|
111
|
+
table.add_column("Role", style="magenta")
|
|
112
|
+
table.add_column("Unread", justify="right", style="bold red")
|
|
113
|
+
table.add_column("Total", justify="right")
|
|
114
|
+
|
|
115
|
+
for mb in mailboxes:
|
|
116
|
+
table.add_row(
|
|
117
|
+
mb.id,
|
|
118
|
+
mb.name,
|
|
119
|
+
mb.role or "-",
|
|
120
|
+
str(mb.unread_emails),
|
|
121
|
+
str(mb.total_emails),
|
|
122
|
+
)
|
|
123
|
+
console.print(table)
|
|
124
|
+
|
|
125
|
+
elif args.command == "list":
|
|
126
|
+
headers = client.list_emails(
|
|
127
|
+
mailbox_name=args.mailbox,
|
|
128
|
+
limit=args.limit,
|
|
129
|
+
unread_only=args.unread,
|
|
130
|
+
query=args.query,
|
|
131
|
+
)
|
|
132
|
+
table = Table(
|
|
133
|
+
title=f"Emails in {args.mailbox} ({len(headers)})",
|
|
134
|
+
show_header=True,
|
|
135
|
+
header_style="bold cyan",
|
|
136
|
+
)
|
|
137
|
+
table.add_column("ID", style="dim")
|
|
138
|
+
table.add_column("From", style="green")
|
|
139
|
+
table.add_column("Subject", style="bold")
|
|
140
|
+
table.add_column("Received", style="blue")
|
|
141
|
+
table.add_column("Status")
|
|
142
|
+
|
|
143
|
+
for h in headers:
|
|
144
|
+
sender = h.from_addr[0].format_string() if h.from_addr else "Unknown"
|
|
145
|
+
status = "[bold red]UNREAD[/bold red]" if h.unread else "[dim]READ[/dim]"
|
|
146
|
+
table.add_row(h.id, sender, h.subject, h.received_at[:10], status)
|
|
147
|
+
console.print(table)
|
|
148
|
+
|
|
149
|
+
elif args.command == "get":
|
|
150
|
+
msg = client.get_email(email_id=args.email_id, mark_as_read=args.read)
|
|
151
|
+
if not msg:
|
|
152
|
+
console.print(f"[bold red]Error:[/bold red] Email {args.email_id} not found.")
|
|
153
|
+
return 1
|
|
154
|
+
|
|
155
|
+
sender = msg.from_addr[0].format_string() if msg.from_addr else "Unknown"
|
|
156
|
+
recipients = ", ".join(t.format_string() for t in msg.to)
|
|
157
|
+
header_info = f"[bold]From:[/bold] {sender}\n[bold]To:[/bold] {recipients}\n[bold]Date:[/bold] {msg.received_at}\n[bold]Thread ID:[/bold] {msg.thread_id or '-'}"
|
|
158
|
+
if msg.attachments:
|
|
159
|
+
att_names = ", ".join(a.get("name", "unnamed") for a in msg.attachments)
|
|
160
|
+
header_info += f"\n[bold yellow]Attachments:[/bold yellow] {att_names}"
|
|
161
|
+
|
|
162
|
+
console.print(
|
|
163
|
+
Panel(
|
|
164
|
+
f"{header_info}\n\n{msg.body_text}",
|
|
165
|
+
title=f"[bold]{msg.subject}[/bold]",
|
|
166
|
+
border_style="cyan",
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
elif args.command == "thread":
|
|
171
|
+
thread = client.get_thread(thread_id=args.thread_id)
|
|
172
|
+
if not thread:
|
|
173
|
+
console.print(f"[bold red]Error:[/bold red] Thread {args.thread_id} not found.")
|
|
174
|
+
return 1
|
|
175
|
+
|
|
176
|
+
console.print(
|
|
177
|
+
f"[bold cyan]Thread:[/bold cyan] {thread.subject} ({thread.message_count} messages, Senders: {', '.join(thread.senders)})"
|
|
178
|
+
)
|
|
179
|
+
for i, msg in enumerate(thread.messages, 1):
|
|
180
|
+
sender = msg.from_addr[0].format_string() if msg.from_addr else "Unknown"
|
|
181
|
+
console.print(
|
|
182
|
+
Panel(
|
|
183
|
+
f"[bold]From:[/bold] {sender} | [bold]Date:[/bold] {msg.received_at}\n\n{msg.body_text}",
|
|
184
|
+
title=f"Message #{i} ({msg.id})",
|
|
185
|
+
border_style="blue",
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
elif args.command == "download":
|
|
190
|
+
out_dir = args.output or os.getcwd()
|
|
191
|
+
content = client.download_attachment(
|
|
192
|
+
blob_id=args.blob_id,
|
|
193
|
+
filename=args.name,
|
|
194
|
+
output_path=out_dir,
|
|
195
|
+
)
|
|
196
|
+
console.print(
|
|
197
|
+
f"[bold green]Saved:[/bold green] {args.name} ({len(content)} bytes) to {out_dir}"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
elif args.command == "send":
|
|
201
|
+
res = client.send_email(
|
|
202
|
+
to=args.to,
|
|
203
|
+
subject=args.subject,
|
|
204
|
+
body=args.body,
|
|
205
|
+
from_addr=args.from_addr,
|
|
206
|
+
cc=args.cc,
|
|
207
|
+
bcc=args.bcc,
|
|
208
|
+
draft_only=args.draft,
|
|
209
|
+
)
|
|
210
|
+
console.print(
|
|
211
|
+
f"[bold green]Success:[/bold green] {res.get('status')} "
|
|
212
|
+
f"(Email ID: {res.get('emailId')}, Submission ID: {res.get('submissionId')})"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
elif args.command == "triage":
|
|
216
|
+
res = triage_inbox(
|
|
217
|
+
client=client,
|
|
218
|
+
mailbox_name=args.mailbox,
|
|
219
|
+
limit=args.limit,
|
|
220
|
+
unread_only=not args.all,
|
|
221
|
+
)
|
|
222
|
+
table = Table(
|
|
223
|
+
title=f"Triage Digest for {res.mailbox} ({res.analyzed_count} analyzed)",
|
|
224
|
+
show_header=True,
|
|
225
|
+
header_style="bold yellow",
|
|
226
|
+
)
|
|
227
|
+
table.add_column("Prio", justify="center", style="bold")
|
|
228
|
+
table.add_column("Category", style="cyan")
|
|
229
|
+
table.add_column("From", style="green")
|
|
230
|
+
table.add_column("Subject", style="bold")
|
|
231
|
+
table.add_column("Action Summary")
|
|
232
|
+
|
|
233
|
+
for cat in res.categories:
|
|
234
|
+
prio_style = (
|
|
235
|
+
"bold red" if cat.priority <= 2 else "yellow" if cat.priority == 3 else "dim"
|
|
236
|
+
)
|
|
237
|
+
table.add_row(
|
|
238
|
+
f"[{prio_style}]P{cat.priority}[/{prio_style}]",
|
|
239
|
+
cat.category,
|
|
240
|
+
cat.from_address,
|
|
241
|
+
cat.subject[:40],
|
|
242
|
+
cat.summary,
|
|
243
|
+
)
|
|
244
|
+
console.print(table)
|
|
245
|
+
if res.recommended_actions:
|
|
246
|
+
console.print("\n[bold yellow]Recommended Action Items:[/bold yellow]")
|
|
247
|
+
for act in res.recommended_actions:
|
|
248
|
+
console.print(f" • {act}")
|
|
249
|
+
|
|
250
|
+
return 0
|
|
251
|
+
except Exception as e: # noqa: BLE001
|
|
252
|
+
console.print(f"[bold red]Execution Error:[/bold red] {e}")
|
|
253
|
+
return 1
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
if __name__ == "__main__":
|
|
257
|
+
sys.exit(main())
|