This commit is contained in:
a2a-platform
2026-06-04 22:43:04 +00:00
parent f733d482d7
commit 8dae9ba1f3
3 changed files with 47 additions and 100 deletions

101
agent.py
View File

@@ -42,6 +42,9 @@ ROOT_SECURITY = json.loads("[\n {\n \"Bearer\": []\n }\n]")
SECURITY_SCHEMES = json.loads("{\n \"Bearer\": {\n \"in\": \"header\",\n \"name\": \"Authorization\",\n \"type\": \"apiKey\"\n }\n}")
SECURITY_FIELDS = json.loads("{\n \"Bearer\": {\n \"field\": \"AUTHORIZATION\",\n \"kind\": \"apiKey\",\n \"location\": \"header\",\n \"name\": \"Authorization\"\n }\n}")
PATH_PARAMETER_RE = re.compile(r"{([^}/]+)}")
MAIL_HOST = "mail.a2acloud.io"
SMTP_SUBMISSION_PORT = 587
IMAPS_PORT = 993
class OperationInput(BaseModel):
@@ -53,49 +56,41 @@ class OperationInput(BaseModel):
class SendEmailInput(BaseModel):
email: str = Field(description="Mailbox email address to send as. A temporary Mailu token is minted internally.")
mailbox_email: str = Field(description="Mailbox email address to send as. A temporary Mailu token is minted internally.")
to: list[str] = Field(description="Recipient email addresses.")
subject: str = Field(description="Message subject.")
text_body: str = Field(description="Plain text message body.")
cc: list[str] = Field(default_factory=list, description="CC recipients.")
bcc: list[str] = Field(default_factory=list, description="BCC recipients.")
reply_to: str | None = Field(default=None, description="Optional Reply-To address.")
smtp_host: str = Field(default="mail.a2acloud.io", description="SMTP submission host.")
smtp_port: int = Field(default=587, description="SMTP port. Use 587 for STARTTLS or 465 for implicit TLS.")
class ReadInboxInput(BaseModel):
email: str = Field(description="Mailbox email address to read. A temporary Mailu token is minted internally.")
mailbox_email: str = Field(description="Mailbox email address to read. A temporary Mailu token is minted internally.")
mailbox: str = Field(default="INBOX", description="IMAP mailbox name.")
limit: int = Field(default=10, ge=1, le=50, description="Maximum number of newest messages to return.")
unread_only: bool = Field(default=True, description="Only return unread messages.")
mark_seen: bool = Field(default=False, description="Mark returned messages as read.")
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 ReadSentInput(BaseModel):
email: str = Field(description="Mailbox email address to read. A temporary Mailu token is minted internally.")
mailbox_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.")
mailbox_email: str = Field(description="Mailbox email address. A temporary Mailu token is minted internally.")
uid: str = Field(description="IMAP UID returned by read_inbox.")
mailbox: str = Field(default="INBOX", description="IMAP mailbox name.")
imap_host: str = Field(default="mail.a2acloud.io", description="IMAPS host.")
imap_port: int = Field(default=993, description="IMAPS port.")
class MailuOpenapiAgent(A2AAgent):
name = "mailu-openapi-agent"
description = "Mailu administration agent generated from the live Mailu OpenAPI spec."
version = "1.2"
version = "1.3"
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),
@@ -173,22 +168,20 @@ class MailuOpenapiAgent(A2AAgent):
async def send_email(
self,
ctx: RunContext,
email: str,
mailbox_email: str,
to: list[str],
subject: str,
text_body: str,
cc: list[str] | None = None,
bcc: list[str] | None = None,
reply_to: str | None = None,
smtp_host: str = "mail.a2acloud.io",
smtp_port: int = 587,
) -> dict[str, Any]:
if not to:
return {"ok": False, "error": "recipient_required"}
async with self._temporary_mail_token(ctx, email, "send_email") as token_info:
async with self._temporary_mail_token(ctx, mailbox_email, "send_email") as token_info:
return await asyncio.to_thread(
_send_email_sync,
email,
mailbox_email,
token_info["token"],
to,
subject,
@@ -196,8 +189,8 @@ class MailuOpenapiAgent(A2AAgent):
cc or [],
bcc or [],
reply_to,
smtp_host,
smtp_port,
MAIL_HOST,
SMTP_SUBMISSION_PORT,
)
@skill(
@@ -209,27 +202,25 @@ class MailuOpenapiAgent(A2AAgent):
async def read_inbox(
self,
ctx: RunContext,
email: str,
mailbox_email: str,
mailbox: str = "INBOX",
limit: int = 10,
unread_only: bool = True,
mark_seen: bool = False,
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_inbox") as token_info:
async with self._temporary_mail_token(ctx, mailbox_email, "read_inbox") as token_info:
return await asyncio.to_thread(
_read_inbox_sync,
email,
mailbox_email,
token_info["token"],
mailbox,
limit,
unread_only,
mark_seen,
max_body_chars,
imap_host,
imap_port,
MAIL_HOST,
IMAPS_PORT,
)
@skill(
@@ -241,25 +232,23 @@ class MailuOpenapiAgent(A2AAgent):
async def read_sent(
self,
ctx: RunContext,
email: str,
mailbox_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:
async with self._temporary_mail_token(ctx, mailbox_email, "read_sent") as token_info:
return await asyncio.to_thread(
_read_mailbox_sync,
email,
mailbox_email,
token_info["token"],
mailbox,
limit,
"ALL",
False,
max_body_chars,
imap_host,
imap_port,
MAIL_HOST,
IMAPS_PORT,
)
@skill(
@@ -271,21 +260,19 @@ class MailuOpenapiAgent(A2AAgent):
async def mark_email_read(
self,
ctx: RunContext,
email: str,
mailbox_email: str,
uid: str,
mailbox: str = "INBOX",
imap_host: str = "mail.a2acloud.io",
imap_port: int = 993,
) -> dict[str, Any]:
async with self._temporary_mail_token(ctx, email, "mark_email_read") as token_info:
async with self._temporary_mail_token(ctx, mailbox_email, "mark_email_read") as token_info:
return await asyncio.to_thread(
_mark_email_read_sync,
email,
mailbox_email,
token_info["token"],
uid,
mailbox,
imap_host,
imap_port,
MAIL_HOST,
IMAPS_PORT,
)
@skill(
@@ -1012,83 +999,67 @@ class MailuOpenapiAgent(A2AAgent):
def _operation_tools(self, ctx: RunContext) -> list[StructuredTool]:
async def send_email_tool(
email: str,
mailbox_email: str,
to: list[str],
subject: str,
text_body: str,
cc: list[str] | None = None,
bcc: list[str] | None = None,
reply_to: str | None = None,
smtp_host: str = "mail.a2acloud.io",
smtp_port: int = 587,
) -> dict[str, Any]:
return await self.send_email(
ctx,
email=email,
mailbox_email=mailbox_email,
to=to,
subject=subject,
text_body=text_body,
cc=cc,
bcc=bcc,
reply_to=reply_to,
smtp_host=smtp_host,
smtp_port=smtp_port,
)
async def read_inbox_tool(
email: str,
mailbox_email: str,
mailbox: str = "INBOX",
limit: int = 10,
unread_only: bool = True,
mark_seen: bool = False,
max_body_chars: int = 4000,
imap_host: str = "mail.a2acloud.io",
imap_port: int = 993,
) -> dict[str, Any]:
return await self.read_inbox(
ctx,
email=email,
mailbox_email=mailbox_email,
mailbox=mailbox,
limit=limit,
unread_only=unread_only,
mark_seen=mark_seen,
max_body_chars=max_body_chars,
imap_host=imap_host,
imap_port=imap_port,
)
async def read_sent_tool(
email: str,
mailbox_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_email=mailbox_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,
mailbox_email: str,
uid: str,
mailbox: str = "INBOX",
imap_host: str = "mail.a2acloud.io",
imap_port: int = 993,
) -> dict[str, Any]:
return await self.mark_email_read(
ctx,
email=email,
mailbox_email=mailbox_email,
uid=uid,
mailbox=mailbox,
imap_host=imap_host,
imap_port=imap_port,
)
tools: list[StructuredTool] = [