Navigace
ELOWEN · Dokumentace

Poslední aktualizace: 27. října 2026

Procházet dokumentaci · Email tool execution and confirmation

Tento článek zatím není přeložen. Je dostupný v angličtině.

Email tool execution and confirmation

Registration and ownership

Foundation calls registerEmailTools(ctx, store, factory) from src/tools.ts once. The supplied EmailStore and MailBackendFactory are the same owners used by the API; there is no separate tool store, credential path or backend registry. Register the exact manifest names EmailSearch, EmailRead, EmailCompose and EmailOrganize; declare only the first two in provides.readOnlyTools. The plugin needs reads: ['db', 'alerts', 'project-files'] and the host must admit email to its existing Project-file transfer consumers. No fallback to direct filesystem access is permitted.

Each execute resolves ctx.currentAccountUserId(); null rejects. Account lookup always uses that owner and the mailbox UUID. parseMessageId locates a message's account, then the store authorizes it; decodeMessageId checks same-account compose sources. Provider mismatches reject before backend access. Secrets remain entirely inside the factory/adapter path. A factory receives only an owner-checked account.

Each backend operation opens and closes its connection in finally, including errors. Source-message reads finish and close before a send question parks the turn. No backend connection is held while waiting for a human. Errors propagate; there is no automatic retry or conversion of failed confirmation into sending.

Search and read

EmailSearch({}) lists personal account IDs, addresses, display names, provider kinds and access levels, without server settings or credentials. Supplying search filters requires an explicit account. A UUID searches one mailbox; account: 'all' searches each owned mailbox sequentially. Defaults are folder: 'INBOX', page: 0 and pageSize: 20; page size is per mailbox and bounded to 100. Choose folder or label, never both. Filters are text, from, unread, flagged, hasAttachment and ISO date-time since. Results retain {accounts:[{account,messages,total,searchLimit?}]}; each account uses the requested zero-based page independently, not a merged global cursor. With only account (including 'all'), each page also contains folders and labels:{supported,folderBacked,labels}. There are no cursor or limit parameters. A failure rejects the search rather than inventing a successful empty page.

EmailRead({id}) returns plain message text, headers and attachment metadata, never the HTML source. Plain reads also return labels, flagged state, threadKey and otherThreadIds, derived through the oldest-first adapter thread read. Message, thread and search reads never set read flags or send mail. With part, textual MIME types (text/*, JSON and XML, including structured suffixes) decode through Node's standard MIMEType and fatal TextDecoder using the declared charset or UTF-8. Other types require file_path; unknown parts, invalid MIME/charset data and failed decoding reject rather than fabricate text. With part and file_path, bytes go to ctx.host.projectFiles().write create-only, and the result reports the actual saved path, MIME type and byte count. No remote filename becomes a destination implicitly. In read-only turns the host denies saving files, while plain reads remain available.

Compose and confirmation

EmailCompose requires a mailbox UUID, action: 'draft' | 'send', mode: 'new' | 'reply' | 'replyAll' | 'forward', plain text and language: 'cs' | 'sk' | 'en'. The agent selects the current user's language, as the question seam has no locale accessor. New mail requires an explicit subject and recipients; replies/forwards require a stable source ID from that same mailbox. Recipient values are address/name objects, not ad-hoc comma-separated strings.

Replies default to Reply-To or From. Reply-all adds the original To/Cc recipients without duplicate addresses or the connected mailbox itself. Explicit To/Cc/Bcc override those defaults. Reply/forward subjects default to Re:/Fwd: plus the source subject, without adding an already present prefix. The caller supplies the complete outgoing text, including any desired quotations or forwarded body. The tool does not copy source attachment parts; save and attach them explicitly when needed. Adapters receive the typed source action to derive threading/provider operations. The adapter contract requires the supplied attachment list to be the complete inventory, including forwards: provider-inherited copies must not be retained implicitly.

Compose attachments require file_path, display filename and MIME contentType. Bytes are read before confirmation using ctx.host.projectFiles().read, with the host transfer ceiling of 250 MiB per file. Host, managed and computer paths, authorization, consent and chunking remain native Project-file responsibilities. Adapters receive bytes, never paths. MIME types use the standard parser.

Read access refuses every compose action. Draft access refuses a send request, even when the turn could only save a draft. Sending access allows either action. Every send request checks ctx.canAskUser() at execution time:

  • With a sink, ctx.askUser receives one single-select, options-only question with Send / Save draft / Cancel translated into the requested language. The question text is only the short localized "send this email?" sentence; the email rides the host's plain-text question detail (AskQuestionDetail, core 0.30.38 and later): localized From, To, Cc, Bcc, Subject and Attachments rows (empty rows omitted, recipients as Name <address>, attachment sizes on the host's 1024 ladder) and the complete outgoing text as body, exactly the text that is sent. Every host surface draws it verbatim, never as markdown or HTML, so mail content cannot restyle or hide part of the confirmation. Compose stays within what that detail shows whole (1,000,000 characters per row and body): the schema bounds text to 1,000,000 characters, addresses to 254 and names to 256 characters, and execution refuses more than 1,000 To/Cc/Bcc recipients and applies the same address and name bounds to recipients taken from a reply or reply-all source, before asking.
  • Send invokes backend.send({kind: 'message', message}) only after that explicit choice. Save draft invokes only saveDraft. Cancel returns {status: 'cancelled', sent: false} without writing anything.
  • An expired, absent, custom or unknown answer rejects without sending or saving. A thrown question or cancellation also never sends or silently saves.
  • Without a sink, the request becomes a draft and raises an owner-scoped draftWaiting.title/body alert linked to the draft. The result says status: 'draft', sent: false, reason: 'confirmation_unavailable', with its ID and URL. Alert keys are draft: plus the shared SHA-256 hashText of the complete stable message ID. They stay within the host's 200-character key limit, so long Graph IDs with a common prefix do not share one alert's dismissal state. Alert failure throws with the saved draft URL and states that nothing was sent.

After preparation/confirmation, execution rechecks the current owner, current mailbox access for the original request and the confirmed sender before opening a write backend. Abort signals are checked before work, after confirmation and after opening the write backend, before invoking send/saveDraft. They are also passed to native Project-file operations. The adapter contract does not expose in-flight mail cancellation.

Draft URLs use /p/email/<accountId>/<encoded full stable message ID>, matching the account/message page contract; the last segment is not a raw provider reference. A successful draft result always includes that URL. A successful send result is {status: 'sent', sent: true}; a draft is never described as sent. A send/transport failure is not retried automatically because delivery may be uncertain.

Organize

EmailOrganize requires a mailbox UUID and an action. Message actions accept 1–100 unique, same-account/provider ids:

  • label / unlabel: one native label ID, applied without moving mail;
  • move: a destination folder ID;
  • archive / trash: the adapter's role-folder operation;
  • markRead / markUnread: explicit read flags;
  • star / unstar: explicit flagged state.

createFolder instead takes name and optional parentId; it has no message IDs. The tool schema is one root object, as required by the native host; execution validates action-dependent IDs, label, destination and name before opening a backend. The result is {changed:number} or {folder:MailFolder}. Read account access refuses all organize actions with the required draft access named in the error. Draft and send access allow them without a send question. The tool cannot delete folders or permanently delete mail, and is not declared read-only. Every ID is checked before opening the backend; batch size/uniqueness and abort checks apply before a remote write. Adapters remain responsible for stale IDs and native folder/label protection. There is no automatic retry.

Verification boundary

tests/emailTools.test.ts covers execute-time ownership, search/list/read, every access combination, same-account sources, localized choices, unattended drafts and alerts, failure cleanup and native Project-file calls. The focused fixture uses mock backends/store/files; it proves delegation, not real IMAP/SMTP, host/managed confinement or the host's consumer gate. Core tests own that gate and native transfer isolation. A real Telegram question adapter round-trips each choice with fake network I/O, and Discord's production renderer receives the same question and three buttons; the platform sinks' rendering of the detail is covered in tests/askQuestionDetail.test.ts. This is not a live chat-platform, browser or real elicitation integration test.