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

View File

@@ -3,7 +3,7 @@
"language": "python", "language": "python",
"name": "mailu-openapi-agent", "name": "mailu-openapi-agent",
"description": "Mailu administration agent generated from the live Mailu OpenAPI spec.", "description": "Mailu administration agent generated from the live Mailu OpenAPI spec.",
"version": "1.2", "version": "1.3",
"entrypoint": { "entrypoint": {
"module": "agent", "module": "agent",
"class_name": "MailuOpenapiAgent", "class_name": "MailuOpenapiAgent",
@@ -84,7 +84,7 @@
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"email": { "mailbox_email": {
"type": "string" "type": "string"
}, },
"to": { "to": {
@@ -134,16 +134,10 @@
"type": "null" "type": "null"
} }
] ]
},
"smtp_host": {
"type": "string"
},
"smtp_port": {
"type": "integer"
} }
}, },
"required": [ "required": [
"email", "mailbox_email",
"to", "to",
"subject", "subject",
"text_body" "text_body"
@@ -185,7 +179,7 @@
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"email": { "mailbox_email": {
"type": "string" "type": "string"
}, },
"mailbox": { "mailbox": {
@@ -202,16 +196,10 @@
}, },
"max_body_chars": { "max_body_chars": {
"type": "integer" "type": "integer"
},
"imap_host": {
"type": "string"
},
"imap_port": {
"type": "integer"
} }
}, },
"required": [ "required": [
"email" "mailbox_email"
], ],
"additionalProperties": false "additionalProperties": false
}, },
@@ -250,7 +238,7 @@
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"email": { "mailbox_email": {
"type": "string" "type": "string"
}, },
"mailbox": { "mailbox": {
@@ -261,16 +249,10 @@
}, },
"max_body_chars": { "max_body_chars": {
"type": "integer" "type": "integer"
},
"imap_host": {
"type": "string"
},
"imap_port": {
"type": "integer"
} }
}, },
"required": [ "required": [
"email" "mailbox_email"
], ],
"additionalProperties": false "additionalProperties": false
}, },
@@ -309,7 +291,7 @@
"input_schema": { "input_schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"email": { "mailbox_email": {
"type": "string" "type": "string"
}, },
"uid": { "uid": {
@@ -317,16 +299,10 @@
}, },
"mailbox": { "mailbox": {
"type": "string" "type": "string"
},
"imap_host": {
"type": "string"
},
"imap_port": {
"type": "integer"
} }
}, },
"required": [ "required": [
"email", "mailbox_email",
"uid" "uid"
], ],
"additionalProperties": false "additionalProperties": false
@@ -2578,7 +2554,7 @@
"source": "python-a2a-pack", "source": "python-a2a-pack",
"project_manifest": { "project_manifest": {
"name": "mailu-openapi-agent", "name": "mailu-openapi-agent",
"version": "1.2", "version": "1.3",
"entrypoint": "agent:MailuOpenapiAgent" "entrypoint": "agent:MailuOpenapiAgent"
} }
} }

View File

@@ -1,5 +1,5 @@
name: mailu-openapi-agent name: mailu-openapi-agent
version: '1.2' version: '1.3'
entrypoint: agent:MailuOpenapiAgent entrypoint: agent:MailuOpenapiAgent
description: Mailu administration agent generated from the live Mailu OpenAPI spec. description: Mailu administration agent generated from the live Mailu OpenAPI spec.
runtime: runtime:

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_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}") 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"{([^}/]+)}") PATH_PARAMETER_RE = re.compile(r"{([^}/]+)}")
MAIL_HOST = "mail.a2acloud.io"
SMTP_SUBMISSION_PORT = 587
IMAPS_PORT = 993
class OperationInput(BaseModel): class OperationInput(BaseModel):
@@ -53,49 +56,41 @@ class OperationInput(BaseModel):
class SendEmailInput(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.") to: list[str] = Field(description="Recipient email addresses.")
subject: str = Field(description="Message subject.") subject: str = Field(description="Message subject.")
text_body: str = Field(description="Plain text message body.") text_body: str = Field(description="Plain text message body.")
cc: list[str] = Field(default_factory=list, description="CC recipients.") cc: list[str] = Field(default_factory=list, description="CC recipients.")
bcc: list[str] = Field(default_factory=list, description="BCC recipients.") bcc: list[str] = Field(default_factory=list, description="BCC recipients.")
reply_to: str | None = Field(default=None, description="Optional Reply-To address.") 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): 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.") 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.") 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.") unread_only: bool = Field(default=True, description="Only return unread messages.")
mark_seen: bool = Field(default=False, description="Mark returned messages as read.") 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.") 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): 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.") 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.") 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.") 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): 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.") uid: str = Field(description="IMAP UID returned by read_inbox.")
mailbox: str = Field(default="INBOX", description="IMAP mailbox name.") 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): class MailuOpenapiAgent(A2AAgent):
name = "mailu-openapi-agent" name = "mailu-openapi-agent"
description = "Mailu administration agent generated from the live Mailu OpenAPI spec." description = "Mailu administration agent generated from the live Mailu OpenAPI spec."
version = "1.2" version = "1.3"
consumer_setup = ConsumerSetup.from_fields( 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.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), 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( async def send_email(
self, self,
ctx: RunContext, ctx: RunContext,
email: str, mailbox_email: str,
to: list[str], to: list[str],
subject: str, subject: str,
text_body: str, text_body: str,
cc: list[str] | None = None, cc: list[str] | None = None,
bcc: list[str] | None = None, bcc: list[str] | None = None,
reply_to: str | None = None, reply_to: str | None = None,
smtp_host: str = "mail.a2acloud.io",
smtp_port: int = 587,
) -> dict[str, Any]: ) -> dict[str, Any]:
if not to: if not to:
return {"ok": False, "error": "recipient_required"} 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( return await asyncio.to_thread(
_send_email_sync, _send_email_sync,
email, mailbox_email,
token_info["token"], token_info["token"],
to, to,
subject, subject,
@@ -196,8 +189,8 @@ class MailuOpenapiAgent(A2AAgent):
cc or [], cc or [],
bcc or [], bcc or [],
reply_to, reply_to,
smtp_host, MAIL_HOST,
smtp_port, SMTP_SUBMISSION_PORT,
) )
@skill( @skill(
@@ -209,27 +202,25 @@ class MailuOpenapiAgent(A2AAgent):
async def read_inbox( async def read_inbox(
self, self,
ctx: RunContext, ctx: RunContext,
email: str, mailbox_email: str,
mailbox: str = "INBOX", mailbox: str = "INBOX",
limit: int = 10, limit: int = 10,
unread_only: bool = True, unread_only: bool = True,
mark_seen: bool = False, mark_seen: bool = False,
max_body_chars: int = 4000, max_body_chars: int = 4000,
imap_host: str = "mail.a2acloud.io",
imap_port: int = 993,
) -> dict[str, Any]: ) -> 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( return await asyncio.to_thread(
_read_inbox_sync, _read_inbox_sync,
email, mailbox_email,
token_info["token"], token_info["token"],
mailbox, mailbox,
limit, limit,
unread_only, unread_only,
mark_seen, mark_seen,
max_body_chars, max_body_chars,
imap_host, MAIL_HOST,
imap_port, IMAPS_PORT,
) )
@skill( @skill(
@@ -241,25 +232,23 @@ class MailuOpenapiAgent(A2AAgent):
async def read_sent( async def read_sent(
self, self,
ctx: RunContext, ctx: RunContext,
email: str, mailbox_email: str,
mailbox: str = "Sent", mailbox: str = "Sent",
limit: int = 10, limit: int = 10,
max_body_chars: int = 4000, max_body_chars: int = 4000,
imap_host: str = "mail.a2acloud.io",
imap_port: int = 993,
) -> dict[str, Any]: ) -> 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( return await asyncio.to_thread(
_read_mailbox_sync, _read_mailbox_sync,
email, mailbox_email,
token_info["token"], token_info["token"],
mailbox, mailbox,
limit, limit,
"ALL", "ALL",
False, False,
max_body_chars, max_body_chars,
imap_host, MAIL_HOST,
imap_port, IMAPS_PORT,
) )
@skill( @skill(
@@ -271,21 +260,19 @@ class MailuOpenapiAgent(A2AAgent):
async def mark_email_read( async def mark_email_read(
self, self,
ctx: RunContext, ctx: RunContext,
email: str, mailbox_email: str,
uid: str, uid: str,
mailbox: str = "INBOX", mailbox: str = "INBOX",
imap_host: str = "mail.a2acloud.io",
imap_port: int = 993,
) -> dict[str, Any]: ) -> 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( return await asyncio.to_thread(
_mark_email_read_sync, _mark_email_read_sync,
email, mailbox_email,
token_info["token"], token_info["token"],
uid, uid,
mailbox, mailbox,
imap_host, MAIL_HOST,
imap_port, IMAPS_PORT,
) )
@skill( @skill(
@@ -1012,83 +999,67 @@ class MailuOpenapiAgent(A2AAgent):
def _operation_tools(self, ctx: RunContext) -> list[StructuredTool]: def _operation_tools(self, ctx: RunContext) -> list[StructuredTool]:
async def send_email_tool( async def send_email_tool(
email: str, mailbox_email: str,
to: list[str], to: list[str],
subject: str, subject: str,
text_body: str, text_body: str,
cc: list[str] | None = None, cc: list[str] | None = None,
bcc: list[str] | None = None, bcc: list[str] | None = None,
reply_to: str | None = None, reply_to: str | None = None,
smtp_host: str = "mail.a2acloud.io",
smtp_port: int = 587,
) -> dict[str, Any]: ) -> dict[str, Any]:
return await self.send_email( return await self.send_email(
ctx, ctx,
email=email, mailbox_email=mailbox_email,
to=to, to=to,
subject=subject, subject=subject,
text_body=text_body, text_body=text_body,
cc=cc, cc=cc,
bcc=bcc, bcc=bcc,
reply_to=reply_to, reply_to=reply_to,
smtp_host=smtp_host,
smtp_port=smtp_port,
) )
async def read_inbox_tool( async def read_inbox_tool(
email: str, mailbox_email: str,
mailbox: str = "INBOX", mailbox: str = "INBOX",
limit: int = 10, limit: int = 10,
unread_only: bool = True, unread_only: bool = True,
mark_seen: bool = False, mark_seen: bool = False,
max_body_chars: int = 4000, max_body_chars: int = 4000,
imap_host: str = "mail.a2acloud.io",
imap_port: int = 993,
) -> dict[str, Any]: ) -> dict[str, Any]:
return await self.read_inbox( return await self.read_inbox(
ctx, ctx,
email=email, mailbox_email=mailbox_email,
mailbox=mailbox, mailbox=mailbox,
limit=limit, limit=limit,
unread_only=unread_only, unread_only=unread_only,
mark_seen=mark_seen, mark_seen=mark_seen,
max_body_chars=max_body_chars, max_body_chars=max_body_chars,
imap_host=imap_host,
imap_port=imap_port,
) )
async def read_sent_tool( async def read_sent_tool(
email: str, mailbox_email: str,
mailbox: str = "Sent", mailbox: str = "Sent",
limit: int = 10, limit: int = 10,
max_body_chars: int = 4000, max_body_chars: int = 4000,
imap_host: str = "mail.a2acloud.io",
imap_port: int = 993,
) -> dict[str, Any]: ) -> dict[str, Any]:
return await self.read_sent( return await self.read_sent(
ctx, ctx,
email=email, mailbox_email=mailbox_email,
mailbox=mailbox, mailbox=mailbox,
limit=limit, limit=limit,
max_body_chars=max_body_chars, max_body_chars=max_body_chars,
imap_host=imap_host,
imap_port=imap_port,
) )
async def mark_email_read_tool( async def mark_email_read_tool(
email: str, mailbox_email: str,
uid: str, uid: str,
mailbox: str = "INBOX", mailbox: str = "INBOX",
imap_host: str = "mail.a2acloud.io",
imap_port: int = 993,
) -> dict[str, Any]: ) -> dict[str, Any]:
return await self.mark_email_read( return await self.mark_email_read(
ctx, ctx,
email=email, mailbox_email=mailbox_email,
uid=uid, uid=uid,
mailbox=mailbox, mailbox=mailbox,
imap_host=imap_host,
imap_port=imap_port,
) )
tools: list[StructuredTool] = [ tools: list[StructuredTool] = [