This commit is contained in:
a2a-platform
2026-06-04 22:39:51 +00:00
parent eb4abbf0eb
commit f733d482d7
4 changed files with 153 additions and 6 deletions

View File

@@ -75,6 +75,15 @@ class ReadInboxInput(BaseModel):
imap_port: int = Field(default=993, description="IMAPS port.")
class ReadSentInput(BaseModel):
email: str = Field(description="Mailbox email address to read. A temporary Mailu token is minted internally.")
mailbox: str = Field(default="Sent", description="IMAP sent-mail mailbox name. Override if the client uses another folder.")
limit: int = Field(default=10, ge=1, le=50, description="Maximum number of newest sent messages to return.")
max_body_chars: int = Field(default=4000, ge=0, le=20000, description="Maximum plain text body characters per message.")
imap_host: str = Field(default="mail.a2acloud.io", description="IMAPS host.")
imap_port: int = Field(default=993, description="IMAPS port.")
class MarkEmailReadInput(BaseModel):
email: str = Field(description="Mailbox email address. A temporary Mailu token is minted internally.")
uid: str = Field(description="IMAP UID returned by read_inbox.")
@@ -86,7 +95,7 @@ class MarkEmailReadInput(BaseModel):
class MailuOpenapiAgent(A2AAgent):
name = "mailu-openapi-agent"
description = "Mailu administration agent generated from the live Mailu OpenAPI spec."
version = "1.1"
version = "1.2"
consumer_setup = ConsumerSetup.from_fields(
ConsumerSetupField.config("OPENAPI_BASE_URL", label="API base URL", description="Override the default API server (https://mail.a2acloud.io/api/v1).", required=False, input_type="url"),
ConsumerSetupField.secret("AUTHORIZATION", label="Bearer API key", description="Sent as header parameter 'Authorization'. Also used to mint temporary per-user Mailu mailbox tokens for SMTP/IMAP skills.", required=True),
@@ -223,6 +232,36 @@ class MailuOpenapiAgent(A2AAgent):
imap_port,
)
@skill(
name="read_sent",
description="Mint a temporary Mailu token for a mailbox, read sent messages over IMAPS, then delete the token.",
tags=("mailbox", "imap", "email"),
timeout_seconds=120,
)
async def read_sent(
self,
ctx: RunContext,
email: str,
mailbox: str = "Sent",
limit: int = 10,
max_body_chars: int = 4000,
imap_host: str = "mail.a2acloud.io",
imap_port: int = 993,
) -> dict[str, Any]:
async with self._temporary_mail_token(ctx, email, "read_sent") as token_info:
return await asyncio.to_thread(
_read_mailbox_sync,
email,
token_info["token"],
mailbox,
limit,
"ALL",
False,
max_body_chars,
imap_host,
imap_port,
)
@skill(
name="mark_email_read",
description="Mint a temporary Mailu token for a mailbox, mark one IMAP UID as read, then delete the token.",
@@ -1018,6 +1057,24 @@ class MailuOpenapiAgent(A2AAgent):
imap_port=imap_port,
)
async def read_sent_tool(
email: str,
mailbox: str = "Sent",
limit: int = 10,
max_body_chars: int = 4000,
imap_host: str = "mail.a2acloud.io",
imap_port: int = 993,
) -> dict[str, Any]:
return await self.read_sent(
ctx,
email=email,
mailbox=mailbox,
limit=limit,
max_body_chars=max_body_chars,
imap_host=imap_host,
imap_port=imap_port,
)
async def mark_email_read_tool(
email: str,
uid: str,
@@ -1047,6 +1104,12 @@ class MailuOpenapiAgent(A2AAgent):
description="Read Mailu mailbox messages through IMAPS. Provide the mailbox email; the tool mints and deletes a temporary Mailu user token internally.",
args_schema=ReadInboxInput,
),
StructuredTool.from_function(
coroutine=read_sent_tool,
name="read_sent",
description="Read Mailu sent-folder messages through IMAPS. Provide the mailbox email; the tool mints and deletes a temporary Mailu user token internally.",
args_schema=ReadSentInput,
),
StructuredTool.from_function(
coroutine=mark_email_read_tool,
name="mark_email_read",
@@ -1083,13 +1146,14 @@ class MailuOpenapiAgent(A2AAgent):
"You operate an API through generated OpenAPI tools.",
"Call tools to get real results. Do not invent API responses.",
"For write, update, or delete operations, explain the intended action before calling the tool.",
"For mailbox tasks, use send_email, read_inbox, and mark_email_read. These tools mint temporary Mailu user tokens internally; never ask the user for or reveal mailbox tokens.",
"For mailbox tasks, use send_email, read_inbox, read_sent, and mark_email_read. These tools mint temporary Mailu user tokens internally; never ask the user for or reveal mailbox tokens.",
"If an operation reports missing consumer setup, tell the user which setup field is required.",
"If an operation returns 404, 405, 410, or a schema/validation error that suggests the live API no longer matches these tools, tell the user this generated agent may need to be refreshed from the latest OpenAPI spec and ask whether they want to refresh it.",
"",
"Mailbox tools:",
"- send_email: WRITE SMTP submission using a temporary per-user token.",
"- read_inbox: READ IMAPS mailbox messages using a temporary per-user token.",
"- read_sent: READ sent-folder messages using a temporary per-user token.",
"- mark_email_read: WRITE IMAP seen flag using a temporary per-user token.",
"",
"Available operations:",
@@ -1374,6 +1438,30 @@ def _read_inbox_sync(
max_body_chars: int,
imap_host: str,
imap_port: int,
) -> dict[str, Any]:
return _read_mailbox_sync(
email_address,
token,
mailbox,
limit,
"UNSEEN" if unread_only else "ALL",
mark_seen,
max_body_chars,
imap_host,
imap_port,
)
def _read_mailbox_sync(
email_address: str,
token: str,
mailbox: str,
limit: int,
criteria: str,
mark_seen: bool,
max_body_chars: int,
imap_host: str,
imap_port: int,
) -> dict[str, Any]:
limit = max(1, min(limit, 50))
messages: list[dict[str, Any]] = []
@@ -1382,7 +1470,6 @@ def _read_inbox_sync(
status, _ = imap.select(mailbox, readonly=not mark_seen)
if status != "OK":
return {"ok": False, "error": "mailbox_select_failed", "mailbox": mailbox}
criteria = "UNSEEN" if unread_only else "ALL"
status, data = imap.uid("search", None, criteria)
if status != "OK":
return {"ok": False, "error": "search_failed", "mailbox": mailbox}