Skip to content

Commit e1fbc16

Browse files
committed
Merge email accounts into beta
Signed-off-by: Tiberiu Socaci <tsocaci@makeitfuture.com>
2 parents 5c1b1ca + aed6f99 commit e1fbc16

37 files changed

Lines changed: 3145 additions & 27 deletions

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ product overview.
1818
1919
## Unreleased
2020

21+
- **Email accounts.** Connect an ordinary mailbox — IMAP for reading, SMTP for sending — to a
22+
conversation or to yourself by entering its server settings once on one form: in the admin UI
23+
(the conversation's Connections, or a person under Users), in Slack's ⚙️ Settings (MCP page for
24+
the conversation's, You page for your own), in Teams' Settings → MCPs, or through Google Chat's
25+
private settings form. Both logins are tested before the account is saved, passwords are
26+
write-only everywhere, and the mail clients run in the gateway, so a channel container never
27+
holds a mailbox password. The agent reads, searches, files, downloads attachments from and
28+
sends with the account through new `gateway` tools (`list_email_accounts`, `email_search`,
29+
`email_read`, `email_download_attachment`, `email_move`, `email_set_flag`, `email_send`);
30+
personal accounts exist only in their owner's turns. Gmail, Outlook and other Composio apps stay
31+
on Composio. Adds the `nodemailer` and `imapflow` dependencies.
32+
2133
- Price GPT-6.1 Sol at its September 29 Standard rates, including per-request long-context
2234
multipliers. Preserve pre-July 30 GPT-5.6 Terra/Luna rates during historical previews and
2335
refreshes. Defer this pricing basis's automatic historical refresh until an audited,

‎FEATURES.md‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,59 @@ control-plane status.
415415
`--strict-mcp-config`, Codex `--ignore-user-config`).
416416
→ TEST-PLAN: Custom MCP connections.
417417

418+
## Email accounts (IMAP + SMTP, per person and per conversation)
419+
420+
- A mailbox is connected **once, on one form** — name, address, optional display name, IMAP
421+
server/port/user/password, SMTP server/port/user/password — to a conversation (channel scope,
422+
shared by everyone who talks there) or to one person (personal scope, usable only in runs they
423+
author). Each scope holds at most five. Ports default to 993 (IMAP) and 587 (SMTP); security is
424+
derived from the port (993/465 implicit TLS, anything else a **mandatory** STARTTLS upgrade —
425+
there is no cleartext option). Blank SMTP user/password reuse the IMAP ones. Rules in
426+
`src/gateway/email-accounts.js`, writes in `email-account-store.js`.
427+
- **Tested before stored.** Every save logs in to the IMAP server and the SMTP server (no message
428+
is sent) and refuses the record naming the failing side (`IMAP login failed (host:port): …`).
429+
The daemon dials each server through the same public-address check as custom MCP relays, pins
430+
the connection to the vetted address and verifies TLS against the configured host name:
431+
loopback, private, link-local and metadata destinations are refused at save time and again at
432+
every dial. A server that does not upgrade to TLS fails the connection.
433+
- **Where to connect one.** Admin UI → the conversation → Connections → *Email accounts*, or
434+
Users → the person → *Personal email accounts* (`/api/channels/:id/email-accounts/:name`,
435+
`/api/users/:id/email-accounts/:name`). In Slack: ⚙️ Settings → **MCP** → *Email accounts*
436+
(the conversation's; channel managers and admins) and **You** → *Email accounts* (the viewer's
437+
own) as inline forms, with the save acknowledged at once and the connection test allowed up to
438+
45 s. In Teams: Settings → **MCPs** → *Email accounts* (channel block and *My connections*). In
439+
Google Chat: Settings → *Open private settings* — the private browser form, since a public card
440+
cannot take a password. All surfaces call the same store; audit events
441+
`channel_email_account_set/_removed` and `user_email_account_set/_removed` carry the name, the
442+
address and the server hosts, never a password.
443+
- **Write-only.** Every listing (`GET /api/channels`, `/api/users`, the per-scope routes, the
444+
Slack/Teams/Chat views, the browser form) returns servers, ports, security, user names,
445+
`hasPassword` and who/when. There is no reveal path. Saving an existing name with the password
446+
boxes empty keeps the stored passwords; a new IMAP password with a shared SMTP password rotates
447+
both. Passwords are also pushed into every run's output redactor (`emailSecrets` from
448+
`resolveRunIntegrations`), so no reply, stream or job log can echo one.
449+
- **The agent uses it through gateway tools, in the daemon.** `list_email_accounts`,
450+
`email_mailboxes`, `email_search`, `email_read`, `email_download_attachment` (into
451+
`uploads/email/<account>/`, ≤ 50 MB, no-follow writes), `email_move`, `email_set_flag` and
452+
`email_send` (From is the account's address; attachments are opened through the confined-file
453+
opener, 10 files / 25 MB; `in_reply_to` threads; a copy is appended to the `\Sent` mailbox
454+
when the server exposes one; refused when the account's *allow sending* is off). ImapFlow and
455+
Nodemailer run in the daemon process; a container never holds a mailbox password and the
456+
`.claude/settings.json` allowlist pre-approves the tools like every other gateway tool. The
457+
memory reviewer's reduced toolset does not get them.
458+
- **Scope resolution is explicit.** A turn sees the conversation's accounts plus the VERIFIED
459+
author's own; the HTTP run API's named author is attribution only and gets the channel scope
460+
alone, and another person's turn in the same conversation never sees someone's personal account.
461+
`account` takes a name, `personal/<name>`, `channel/<name>` or the address; a bare name present
462+
in both scopes is refused until qualified; with exactly one account it may be omitted. The
463+
`gateway-usage` guide (`references/email.md`) carries the pronoun rule ("my" = personal, "your /
464+
the team's" = channel), the confirm-before-send rule, the shared-thread privacy note and the
465+
instruction to send people to Settings instead of asking for a password in chat.
466+
- **Not a Composio replacement.** Gmail, Outlook and the other apps a Composio account already
467+
holds stay on Composio; email accounts cover the mailboxes it does not — custom domains, app
468+
passwords, shared inboxes, transactional SMTP relays.
469+
→ TEST-PLAN: Email accounts.
470+
418471
## Platform activation emoji settings
419472

420473
- Admin Settings → Agent defaults → **Message activation reactions** has independent Slack,

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ catalog, including edge cases and links to regression coverage.
155155
agent account. When both have the requested app and the account is ambiguous, the agent asks.
156156
- **Custom MCP servers:** maintain an admin catalog of granted servers and non-interactive
157157
credentials; supported transports and optional integrations depend on the engine.
158+
- **Email accounts:** connect an ordinary mailbox (IMAP + SMTP) to a conversation or to yourself
159+
from the admin UI or your chat platform's settings; the agent reads, files and sends with it
160+
through gateway tools while the passwords stay in the gateway, never in a container.
158161
- **Enterprise SDK provisioning (Beta):** an organization Composio SDK key provisions separate
159162
Slack user/channel identities and reusable thread sessions. Standard MCP mode remains available
160163
without Enterprise entitlement.

‎TEST-PLAN.md‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,74 @@
11
# ChannelGate — Test Plan
22

3+
## Email accounts (2026-10-22)
4+
5+
### Regression
6+
- [x] Automated: `node --test test/email-accounts.test.js`.
7+
- One-form rules: port defaults, security by port, SMTP credentials falling back to IMAP's,
8+
name/address/port/user validation, loopback/private/link-local/metadata hosts refused by
9+
name and literal.
10+
- Updates: blank passwords keep the stored ones, a rotated IMAP password follows into a shared
11+
SMTP password and leaves a distinct one alone. Upsert by name, the five-entry cap, remove.
12+
The masked list and `emailAccountSecrets` never disagree; malformed rows are dropped.
13+
- Run resolution: channel accounts always, personal accounts only for a trusted principal;
14+
`selectEmailAccount` with bare names, `personal/` / `channel/` / `my/` prefixes, addresses,
15+
the single-account default and the cross-scope name clash.
16+
- Store: the connection test receives the MERGED record (stored passwords on an update), a
17+
failing test stores nothing, the interactive `authorize` re-check runs after it, audit rows
18+
carry no password; personal accounts on the user record.
19+
- Tools (fake mail client): every tool registered and in `GATEWAY_TOOL_NAMES`; the verified
20+
author sees channel + own, an untrusted principal and another person do not see the personal
21+
account; search/read/move/flag pass the chosen account through and render compactly;
22+
attachment downloads land under `uploads/email/<account>/` with a sanitized name; `email_send`
23+
attaches only confined files (a symlink out of the folder and a `../` path are refused) and
24+
refuses an account with sending off before any client call.
25+
- `resolveRunIntegrations` returns every channel and personal password as `emailSecrets`, and no
26+
personal password for an untrusted principal.
27+
- Admin routes: channel and user PUT/GET/DELETE, the failing-login 400 without the password,
28+
the loopback refusal, `GET /api/channels` and `/api/users` listings without a password.
29+
- [x] Automated: `node --test test/slack-email-account-settings.test.js` — the Slack Settings
30+
inline forms (`em-add:<scope>`, `em:<scope>:<name>`), the MCP and You page sections, manager
31+
gating, masked rendering, the password-field error mapping.
32+
- [x] Automated: `node --test test/teams-email-account-settings.test.js
33+
test/conversation-settings-email.test.js` — the Teams MCPs page blocks and
34+
`settings.email.save/remove`, public-card refusal, the confirm round-trip, the private browser
35+
form's save/remove and masked listing.
36+
- [x] `test/folders-settings.test.js` tracks the new tool module in the permission allowlist;
37+
`test/progress-report-availability.test.js` still proves the integration resolver's shape.
38+
39+
### Live gates (Claude AND Codex) — UNEXECUTED
40+
Fixture: a disposable conversation in Worker mode. A mailbox on a public IMAP/SMTP host you own
41+
(any provider that issues app passwords, or a test domain on a transactional relay with an IMAP
42+
inbox). Record the address and the servers; never the password.
43+
- [ ] Connect (dashboard): admin UI → the conversation → Connections → *Email accounts*: name
44+
`qa`, the address, IMAP host/user/password, SMTP host (ports blank). Pass: the hint says both
45+
logins were verified, the row shows `qa`, the address, `imap…:993` and `smtp…:587`;
46+
`GET /api/channels` contains no password. A wrong password answers "IMAP login failed (…)" and
47+
stores nothing. `127.0.0.1` or a private host name as the IMAP server is refused.
48+
- [ ] Connect (Slack): ⚙️ Settings → **MCP** → *+ Connect email account* as a channel manager
49+
(name `qa2`); a non-manager sees the list but no button. ⚙️ Settings → **You** → connect
50+
`mine`. Pass: the modal acknowledges at once, the row appears masked, no password in the view
51+
payload (`views.update` body) or in Slack history.
52+
- [ ] Connect (Teams and Google Chat): Teams Settings → MCPs → *Email accounts* save; Google Chat
53+
Settings → *Open private settings* → the browser form. Pass: the account appears masked on the
54+
card and the page; the Chat card itself has no password input.
55+
- [ ] Read, Claude then Codex (`codex …` directive): "List my newest 5 emails in qa and read the
56+
first one; save its attachment." Pass: `email_search` output, `email_read` with headers and
57+
body, `email_download_attachment` saves under `uploads/email/qa/` and the reply names the
58+
file with a 📄 button. No password in the reply or the artifact dir (`grep -r <password>`).
59+
- [ ] Send, both engines: "Send an email from qa to <your address>, subject QA, attaching that
60+
file, replying to the message you read." Pass: the agent confirms recipients first (or the
61+
prompt gave them), the mail arrives threaded (`In-Reply-To` set), with the attachment, From the
62+
account's address and display name; a copy sits in the Sent folder where the server has one;
63+
the reply names the account used. With *allow sending* off, the tool refuses and the agent
64+
reports it without trying another account.
65+
- [ ] Scope: the personal account `mine` is used by its owner's turn, absent from another approved
66+
user's turn in the same conversation and from a `POST /api/runs` turn; a bare name present in
67+
both scopes makes the agent ask which one. Lean mode does not hide the tools (they are gateway
68+
tools) but the memory reviewer's run has none.
69+
- [ ] Rotation and removal: change the IMAP password with the SMTP box blank (shared password) —
70+
the next send still works; remove the account — the next turn reports it absent.
71+
372
## Registered inventory caps and Enterprise harness approvals (2026-10-22)
473

574
- [x] Automated `test/license-registration.test.js`: signed Registered and no-key fixtures admit

‎documentation/src/content/docs/controls/accounts-and-secrets.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,24 @@ Approve an exact host for a hidden secret awaiting destination approval.
138138

139139
**Restrictions:** Model API hosts are refused. Does not enable arbitrary network access or override known destination restrictions. Useful when a background job/SSH request posted no approval card.
140140

141+
## Email account tools
142+
143+
An email account is a mailbox's IMAP and SMTP settings entered once on one form — in the admin UI (a conversation's Connections, or a person under Users), in Slack's ⚙️ Settings (the **MCP** page for the conversation's account, the **You** page for your own), in Teams' Settings → MCPs, or through Google Chat's private settings form. Both logins are tested before the account is stored, passwords are write-only, and the mail clients run in the gateway. There is no tool that connects an account: a password typed into chat would stay in the conversation's history.
144+
145+
| Tool | Purpose |
146+
| --- | --- |
147+
| `list_email_accounts` | The accounts this turn may use: the conversation's (`channel/<name>`) and the requester's own (`personal/<name>`), with address and whether sending is enabled. |
148+
| `email_mailboxes` | Folders and their roles. |
149+
| `email_search` | Newest-first listing with `query`, `from`, `to`, `subject`, `since`, `before`, `unseen`, `flagged` and `limit` (≤ 50). |
150+
| `email_read` | One message by uid: headers, text body (HTML converted), attachments with part ids. |
151+
| `email_download_attachment` | Saves one attachment under `uploads/email/<account>/` in the working folder (≤ 50 MB). |
152+
| `email_move` / `email_set_flag` | File a message elsewhere; mark seen, unseen, flagged, unflagged or answered. |
153+
| `email_send` | Send as the account, with attachments from the working folder (10 files, 25 MB total) and `in_reply_to` threading; a copy goes to the Sent folder when the server has one. |
154+
155+
**Authority and scope:** channel accounts are usable by anyone authorized in the conversation; personal accounts exist only in runs their owner authored — never in another person's turn, a scheduled run or an HTTP run-API turn. `account` takes a name, `personal/<name>`, `channel/<name>` or the address, and a bare name present in both scopes is refused until qualified.
156+
157+
**Restrictions:** sending is refused on an account whose *allow sending* is off. Replies, streams and job output are redacted of every stored password.
158+
141159
## Related guides
142160

143161
[Accounts](/docs/features/connected-accounts), [environment configuration](/docs/configuration/environment-variables), [secrets](/docs/features/secrets), [Composio SDK](/docs/features/composio-sdk), and [Toolbox/Make](/docs/features/toolbox-and-make).

0 commit comments

Comments
 (0)