diff --git a/.a2a/agent.dsl.json b/.a2a/agent.dsl.json index 02ee9c7..700ad25 100644 --- a/.a2a/agent.dsl.json +++ b/.a2a/agent.dsl.json @@ -3,7 +3,7 @@ "language": "python", "name": "mailu-openapi-agent", "description": "Mailu administration agent generated from the live Mailu OpenAPI spec.", - "version": "1.2", + "version": "1.3", "entrypoint": { "module": "agent", "class_name": "MailuOpenapiAgent", @@ -84,7 +84,7 @@ "input_schema": { "type": "object", "properties": { - "email": { + "mailbox_email": { "type": "string" }, "to": { @@ -134,16 +134,10 @@ "type": "null" } ] - }, - "smtp_host": { - "type": "string" - }, - "smtp_port": { - "type": "integer" } }, "required": [ - "email", + "mailbox_email", "to", "subject", "text_body" @@ -185,7 +179,7 @@ "input_schema": { "type": "object", "properties": { - "email": { + "mailbox_email": { "type": "string" }, "mailbox": { @@ -202,16 +196,10 @@ }, "max_body_chars": { "type": "integer" - }, - "imap_host": { - "type": "string" - }, - "imap_port": { - "type": "integer" } }, "required": [ - "email" + "mailbox_email" ], "additionalProperties": false }, @@ -250,7 +238,7 @@ "input_schema": { "type": "object", "properties": { - "email": { + "mailbox_email": { "type": "string" }, "mailbox": { @@ -261,16 +249,10 @@ }, "max_body_chars": { "type": "integer" - }, - "imap_host": { - "type": "string" - }, - "imap_port": { - "type": "integer" } }, "required": [ - "email" + "mailbox_email" ], "additionalProperties": false }, @@ -309,7 +291,7 @@ "input_schema": { "type": "object", "properties": { - "email": { + "mailbox_email": { "type": "string" }, "uid": { @@ -317,16 +299,10 @@ }, "mailbox": { "type": "string" - }, - "imap_host": { - "type": "string" - }, - "imap_port": { - "type": "integer" } }, "required": [ - "email", + "mailbox_email", "uid" ], "additionalProperties": false @@ -2578,7 +2554,7 @@ "source": "python-a2a-pack", "project_manifest": { "name": "mailu-openapi-agent", - "version": "1.2", + "version": "1.3", "entrypoint": "agent:MailuOpenapiAgent" } } diff --git a/a2a.yaml b/a2a.yaml index 374bfe5..b526fcb 100644 --- a/a2a.yaml +++ b/a2a.yaml @@ -1,5 +1,5 @@ name: mailu-openapi-agent -version: '1.2' +version: '1.3' entrypoint: agent:MailuOpenapiAgent description: Mailu administration agent generated from the live Mailu OpenAPI spec. runtime: diff --git a/agent.py b/agent.py index eb1c327..f97e991 100644 --- a/agent.py +++ b/agent.py @@ -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] = [