Repository navigation
Add an EMAIL connector type: IMAP and SMTP (#1344) - #1348
Merged
Merged
Conversation
A new ConnectorType EMAIL (migration adds the enum value) and EmailEngine,
which runs a fixed set of built-in tools against a mailbox: list folders,
search, get message, get attachment, set flags, move, delete to Trash,
save draft (IMAP APPEND) and send (SMTP, reply threading, copy to Sent).
- imapflow for IMAP, mailparser for HTML bodies and headers, the existing
nodemailer for SMTP; one IMAP connection per call, 15 s connect and
60 s socket timeouts, no IDLE
- hosts pass the SSRF guard and the socket goes to the checked address
(resolveSafeOutboundHost), with the name kept for TLS
- Cloud: TLS only, valid certificates, standard ports; self-hosted may
allow plaintext and self-signed certificates for internal servers
- passwords are scrubbed from every error and log line
- settings in config.email (catalog {{VARS}} resolved per call), login in
the encrypted authConfig; an empty password on edit keeps the stored one
- tool annotations per method; send is open-world, delete destructive
- Email type on the new-connector page: provider presets, IMAP and SMTP servers with port and security, login, optional SMTP login, sender, folders; plaintext and self-signed only offered self-hosted - connector page: edit the same settings (empty passwords keep the stored ones), server summary, no import for the built-in tool set - Email badge and filter in the connector list, email methods in the tool editor, strings in en/de/it, Playwright smoke test
intl/imap-smtp installs an EMAIL connector with the nine email built-ins under the imap_smtp prefix and a static guide. Its instructions carry the hosts and ports of common providers and where to create app passwords. The validator and catalog spec accept the EMAIL type; adapter count 573.
docs/connectors/email.md: setup, built-in tools, settings, how it connects, Cloud vs self-hosted behaviour and what is not supported yet. README source tables, the guides index and the tool-definition reference list the new type; llms.txt regenerated.
# Conflicts: # CITATION.cff # README.de.md # README.ja.md # README.md # README.zh-CN.md # glama.json # llms-full.txt # llms.txt # package.json # server.json
# Conflicts: # CITATION.cff # README.de.md # README.ja.md # README.md # README.zh-CN.md # glama.json # llms-full.txt # llms.txt # package.json # server.json
- Cloud: the hosting provider blocks outgoing SMTP on 25, 465 and 587. A send there gives up after 5 s with an smtp_blocked error that says so and points to save_draft or a provider on port 2525; Test connection reports IMAP working and SMTP not reachable instead of failing - Cloud: SMTP port 25 is no longer allowed (direct delivery from our address risks blocklisting); self-hosted keeps it - Cloud: SMTP login is forced, so nothing goes out through an open relay unauthenticated - send_message and delete_messages are created switched off, for hand-made connectors and catalog installs; save_draft stays the default way to answer - reading tools and their results say that message content is untrusted data whose instructions must not be followed - adapter instructions, guide, setup help and docs updated
- mailparser 3.9.43 pins nodemailer 10.1.4, the version the backend already uses, so the nested nodemailer 10.1.1 with three advisories is gone from the lockfile - the from address is checked by splitting instead of a regular expression that backtracks polynomially on long runs of dots - the email login merge runs on the connector's type alone; the request body no longer guards the branch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A new connector type,
EMAIL, for any mailbox that speaks IMAP and SMTP: IONOS, Strato, GMX, web.de, T-Online, Hetzner, all-inkl, Aruba, Gmail/Google Workspace with an app password, Yahoo, iCloud, Fastmail, self-hosted Dovecot/Postfix. Today the catalog has only provider APIs (Gmail, Microsoft 365, Zoho Mail), so a search for "IMAP", "SMTP" or "email" finds nothing for the large share of small businesses whose mail runs at a hoster. This was discussed with the maintainer before starting, as AGENTS.md asks for a new connector type; the schema change is described in #1344.Like OData, an Email connector has no endpoints to map: it ships a fixed tool set (list folders, search, get message, get attachment, set flags, move, delete to Trash, save draft, send), created with a hand-made connector and injected into catalog adapters.
Changes
ConnectorTypegainsEMAIL(20261030100000_add_email_connector_type,ALTER TYPE ... ADD VALUE). No new tables or columns.connectors/engines/email.engine.ts):imapflow(MIT) for IMAP,mailparser(MIT) for HTML bodies and header parsing, the existingnodemailerfor SMTP; outgoing messages reusemime-message.util.ts(header-injection checks, RFC 2047). One IMAP connection per tool call (connect, work, logout), 15 s connect / 60 s socket timeouts, no IDLE, bounded literal/response sizes. Bodies and attachments are downloaded part by part withBODY.PEEK(reading does not mark as read); bodies cut with a marker, attachments capped at 4 MB and returned as text or as an MCP file (existing binary-response path). Bcc only in the SMTP envelope, Sent copy keeps it; Gmail is left to file its own copy; a failed Sent copy after SMTP accepted the mail is a warning, not an error. Delete moves to Trash (special-use) and never expunges; a server with neither MOVE nor UIDPLUS is refused for moves because its fallback EXPUNGE would hit unrelated messages.resolveSafeOutboundHostreturns the checked addresses so the socket connects to exactly those (name kept for SNI/certificate). Passwords (and their base64 / AUTH PLAIN forms) are scrubbed from every error and log line; the clients' own loggers are off.config.email(catalog{{VARS}}resolved per call, like OData settings), login in the encryptedauthConfig(BASIC_AUTH:username,password, optionalsmtpUsername/smtpPassword). On edit, an empty username/password keeps the stored value.authConfig.mechanismis the extension point for XOAUTH2.connectors.service(test connection: IMAP login + LIST, SMTPverify(); Run Test / install probe),dynamic-mcp-tools,tool-annotations(reads read-only and closed-world,sendopen-world,deletedestructive), controller create (built-ins, settings check) and update (auth merge),validateBaseUrl(imap:///imaps://), catalogwithEmailBuiltins, validator and catalog spec.intl/imap-smtp("Email (IMAP/SMTP)", categoryemail, aliases IMAP/SMTP/E-Mail/GMX/…): provider table with hosts and ports, app-password instructions, tool list, limits, Cloud vs self-hosted; static guide tool; generic envelope logo; adapter count 573 (README ×4, glama, CITATION, server.json, package.json, llms.txt regenerated).connectors/new(provider presets, servers/ports/security, login, optional SMTP login, sender, folders, "accept self-signed" and plaintext only when self-hosted); connector page edit + summary (catalog installs are edited through their variables); list badge and filter; email methods in the tool editor; en/de/it strings.docs/connectors/email.md, README source tables (4 languages),docs/guides.md,docs/tool-definition.md.Cloud and self-hosted
SSRF_ALLOWED_HOSTS); Test connection offers the allowlist link as for other connectorsThese rules are enforced in
resolveEmailSettings(create/update API and every call) and reflected in the form.Hardening (commit 0741258)
smtp_blockederror: the provider blocks the port, nothing was sent, usesave_draftor a provider on 2525. Test connection on Cloud reports "IMAP login accepted, N folders; SMTP not reachable: …" instead of failing. Stated in the adapter instructions, guide tool,EMAIL_SMTP_PORThelp, the form's Cloud note anddocs/connectors/email.md.forceAuth): a server that does not advertise AUTH fails instead of relaying unauthenticated. Integration test against an smtp-server open relay: refused on Cloud, accepted (control) self-hosted.send_messageanddelete_messagesare created switched off (isEnabled: falsefor hand-made connectors,enabled: falsein the catalog, same mechanism as other adapters' risky tools);save_draftstays on as the default way to answer. Reading tools' descriptions and every result with message content (contentNotice) say the content is untrusted data and its instructions must not be followed.Type
Testing
New tests:
email.engine.spec.ts(38, scripted imapflow/nodemailer): search criteria mapping, limit cap, newest first, snippets, body truncation marker, HTML to text, attachment cap (refused before and during download), text vs binary attachments, reply headers (In-Reply-To, References,Re:, Reply-To recipient,\Answered), Bcc envelope-only, Sent-folder rules (generic, Gmail,never, failed append after send), SSRF guard on Cloud (loopback, RFC 1918, metadata IP, SMTP host) and self-hosted (allowlisted host, pinned address with servername), plaintext refused on Cloud before any socket, STARTTLS required, MOVE/UIDPLUS guard, no password or AUTH PLAIN blob in errors or log lines, TLS error wording per deployment.email.engine.integration.spec.ts(13): the same flows against real servers in-process, no Docker: ImapKit (MIT, in-memory IMAP) and smtp-server (MIT-0), including TLS with a self-signed certificate refused by default and accepted with the self-hosted switch. Both are devDependencies only.email-settings.spec.ts,email-message.util.spec.ts,connectors.controller.email.spec.ts,connectors.service.email.spec.ts,dynamic-mcp-tools.email.spec.ts, additions totool-annotations.spec.ts,base-url.util.spec.ts,catalog.spec.ts,validate-adapters.test.mjs;adapters/intl/imap-smtp.live.spec.ts(static assertions; read-only live part gated behindRUN_IMAP_SMTP_LIVE=1, not run).tests/e2e/connector-email.spec.ts(creates an Email connector from the form, mocked API).Commands and results (2026-10-30, macOS, Node 22):
packages/backendnpx tsc --noEmit -p tsconfig.jsonpackages/backendnpm run lint--fixrewritespackages/backendnpx jest src/connectors/engines/email src/connectors/email src/connectors/connectors.controller.email.spec.ts src/connectors/connectors.service.email.spec.ts src/mcp-server/dynamic-mcp-tools.email.spec.ts src/adapters/intl/imap-smtp.live.spec.tspackages/backendnpm testpackages/frontendnpm run lintpackages/frontendnpx tsc --noEmit -p tsconfig.jsonpackages/frontendnpx playwright test tests/e2e/connector-email.spec.tsnode scripts/validate-adapters.mjs --warnimap-smtp; the rest pre-existing)node scripts/regenerate-catalog.mjscatalog.tsdiff committednode scripts/adapter-count.mjs --checknode scripts/generate-llms-txt.mjs --checknode --test scripts/validate-adapters.test.mjs scripts/adapter-new.test.mjs scripts/generate-llms-txt.test.mjsnode scripts/check-runtime-deps.mjsManual end-to-end check (local, no real mailbox): ImapKit on 127.0.0.1:11143 and smtp-server on 127.0.0.1:11025 as standalone processes; backend from this branch with a fresh database (
prisma migrate deployapplied the new migration; enum listsEMAIL) andSSRF_ALLOWED_HOSTS=127.0.0.1(the self-hosted allowlist path).POST /api/connectors(type EMAIL): 201, nineoffice_mail_*tools, attached to the default MCP server;authConfignot in the response.auth_failed, password not in the message. Restoring with an empty username kept the stored one.send_message: the SMTP server receivedIn-Reply-To/References/Re:and the right envelope, the copy landed in Sent, the original got\Answered.tools/listshows the annotations above;tools/calllist_folders and save_draft succeed; an unknown UID returnsisErrorwith a clear message.GET /api/adapters?search=IMAP|SMTP|email|GMXall findimap-smtp;POST /api/adapters/imap-smtp/importcreates 10 tools and its probe (imap_smtp_list_folders) passes; Test connection on the installed connector passes.Checklist
npm test)Not in this PR
authConfig.mechanismis the extension point; both clients accept access tokens.X-GM-RAWsearch.Related Issues
Closes #1344