8000 feat(chat): the assistant's own email address, over IMAP and SMTP by adeelahmad · Pull Request #314 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content

feat(chat): the assistant's own email address, over IMAP and SMTP - #314

Merged
adeelahmad merged 25 commits into
mainfrom
claude/project-thread-sr9ptd
Oct 11, 2026
Merged

adeelahmad merged 25 commits into
mainfrom
claude/project-thread-sr9ptd

Conversation

@adeelahmad
@adeelahmad adeelahmad commented Oct 10, 2026 •
Copy link
Copy Markdown
Owner

Requested by Adeel · project thread

Description

Before: the assistant could be reached in Chat, chat rooms (Matterbridge), Telegram and Slack, but not by email. Email only came in as an import source.

After: Settings › Assistant email: enter an address and its app password, turn on Answer email, then email it. You show under Wrote to it, not let in. Click Let in, and from then on the assistant answers your email in the same thread (In-Reply-To/References). It imports attachments into the namespace it picks; one it would have to create waits for approval. A forwarded email is held whole as an .eml to import. Its text reaches the model marked as third-party data (#302), and once the model has read it, changes ask first, the import included (#309). Approving by replying "yes", /clear and /compact work as in chat rooms. Answers list the excerpts they cite, with links back into Lens, and are plain text. Check the mailbox signs in to IMAP and SMTP without reading or sending anything.

Who it answers: only addresses in People it answers whose provider vouched for the From address. That means DMARC passed, or DKIM or SPF passed for the sender's domain, read from the topmost Authentication-Results header (or the one signed by Trust checks by). Strangers get nothing back and wait on the settings page. Auto-replies, bounces, mailing lists and its own mail are never answered. Replies go only to From, never Reply-To.

Which namespace: a plus address (lens+pods@…), then Where mail goes rules (from: *@acme.com = acme, subject: newsletter = skip, the grammar of mail_routes.py), then the same picking as an open chat room.

How: app/domain/mailbox.py is a channels.Channel adapter next to Matterbridge, Telegram and Slack. The lease means one process reads it, every poll_seconds (30 by default). It reads with the IMAP code of email sources (feeds.connect, _select, _messages, new read), with a UIDVALIDITY-keyed cursor that starts at UIDNEXT, so old mail isn't answered. It sends with smtplib, using Auto-Submitted: auto-replied (RFC 3834). bridge.handle gains four channel hooks. The first is a channel-set namespace. The second is Channel.acts (act_on): the import tool runs without asking when imports are automatic and the namespace exists. The third is Channel.outside, which says where a message's text came from outside Lens: any room with no user list, as before (the default), and forwarded mail. The fourth is a cited-sources footer for channels that ask for one. convert._message is now public parse_email, and mail_routes gains a one-line rule grammar. Endpoints: GET /settings/mailbox, POST /settings/mailbox/test, plus the usual PUT /settings/mailbox. The password is a sealed secret.

Motivation and Context

Adeel asked for a dedicated IMAP email address for the agent, so people can email or forward things to it.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change; ! in the title)

Checklist:

  • The title is a Conventional Commit.
  • My code follows the code style of this project (ruff 0.12.2, mypy, eslint, prettier clean).
  • I added or updated tests: tests/api/test_mailbox.py (11, against new tests/fake_smtp.py and an extended tests/fake_imap.py) and tests/domain/test_mailbox.py (4).
  • My change requires documentation updates, and I have updated docs/ accordingly: new docs/assistant-email.md, plus links from docs/chat-rooms.md, the README and the mkdocs nav. mkdocs build --strict is clean.
  • Users need more than the title to understand it, so I wrote it under ## Unreleased in CHANGELOG.md.

Done questions (AGENTS.md)

  • Goal: email in, answer by email in the thread, attachments imported, allowed senders only, chat-room routing and approvals, untrusted forwarded text, mail routing rules. It merges on green.
  • Agents: settings, status and the check are API endpoints (/settings/mailbox, /settings/mailbox/test). The assistant can change them with approval (change_settings mailbox), and the mailbox is itself a way for any agent or person to reach the assistant.
  • Zero setup: any existing mailbox works. There is no public address, mail server, tunnel or new dependency (stdlib imaplib and smtplib), and servers are guessed from the address.
  • Fewest clicks: address, password, one toggle, then one Let in click. Servers, routing and approvals are pre-filled.
  • Configurable: every field is in Settings › Assistant email with a default. It is off by default and not in the setup wizard.
  • Light: one polling thread while on and nothing while off. It runs fine on a Pi.
  • Private and secure: the password is sealed, and replies go only to verified, allowed senders. Strangers are never told the address is read, answers are plain text, and forwarded text is marked as data. Threat model updated: new input row, the Outbound family and N-BRIDGE.
  • Cost: no model call happens unless a verified allowed sender writes. Answers use the chat path and its activity ledger and budgets, and IMAP and SMTP are free.
  • User decides: imports into a new namespace, entity edits and settings still wait for approval, and Imported once approved makes every import wait. No existing default changes.
  • Upgrades: this adds a new settings section only, with no change to stored data.
  • Honest: answers list their cited excerpts, and the docs describe only what's built.
  • Tests, docs and generated files: make openapi regenerated openapi.json and the client, and the threat-model YAML hash was recomputed and validated.
  • Open standards: IMAP, SMTP, RFC 3834, RFC 8601 Authentication-Results and DMARC. No new dependencies.
  • Reported in the project thread. Refine later (in the mailbox.py docstring and the docs): IMAP IDLE, a copy of answers in Sent, autoconfig for more providers, letting in whole domains, and answers as audio attachments.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VdrxhwvmLjFsfzQZ5pRJGC

Paste a bot token from @Botfather in Settings > Telegram and the assistant
answers there through getUpdates long polling: no Matterbridge, no public
address. It answers only people let in (one click from the settings page),
transcribes voice notes, holds files for the assistant to import, and keeps
everything Matterbridge rooms do (namespace routing, approving by reply).

channels.py is the adapter layer: Matterbridge and Telegram are adapters with
their own thread, lease and status, and bridge.handle serves both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194jTK9wbki29B4LcS7aKHG
…at channel

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194jTK9wbki29B4LcS7aKHG
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194jTK9wbki29B4LcS7aKHG
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194jTK9wbki29B4LcS7aKHG
People let in email or forward things to the assistant's mailbox; it answers
in the same thread and imports what's attached. Mail is answered only from
allowed senders whose provider vouched for From (Authentication-Results);
strangers are held on the settings page until let in. Forwarded text is
marked as third-party data. Plus addresses and mail routing rules pick the
namespace. Settings > Assistant email, with a Check button.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdrxhwvmLjFsfzQZ5pRJGC
@adeelahmad adeelahmad self-assigned this Oct 10, 2026
10000 claude added 13 commits October 10, 2026 21:29
…to claude/project-thread-sr9ptd

# Conflicts:
#	CHANGELOG.md
#	threat-model.yaml
…-sr9ptd

# Conflicts:
#	CHANGELOG.md
#	README.md
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194jTK9wbki29B4LcS7aKHG
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194jTK9wbki29B4LcS7aKHG
…to claude/project-thread-sr9ptd

# Conflicts:
#	CHANGELOG.md
#	fastapi_backend/app/domain/ai_tools.py
#	threat-model.yaml
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194jTK9wbki29B4LcS7aKHG
…tside text (#309)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdrxhwvmLjFsfzQZ5pRJGC
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194jTK9wbki29B4LcS7aKHG
…to claude/project-thread-sr9ptd

# Conflicts:
#	CHANGELOG.md
#	fastapi_backend/app/domain/settings.py
#	threat-model.yaml
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdrxhwvmLjFsfzQZ5pRJGC
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdrxhwvmLjFsfzQZ5pRJGC
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194jTK9wbki29B4LcS7aKHG
The cloud-model journey picks OpenAI from Settings › LLM provider's cloud
providers (#334) instead of typing the address and model, and both
baselines come from fresh runs: computer 3 clicks + 2 keys (was 5 + 40),
phone 5 taps + 1 key + 1 swipe (was 7 + 39 + 2).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bmpwkzx9nwb1vcDxNMagtu
(cherry picked from commit d1d6cfd)
Copy link
Copy Markdown
Owner Author

Clicks and keystrokes per feature failed on 14d9948 in the phone run's cloud-model journey (locator.waitFor: Timeout 20000ms exceeded). That failure is not this PR's. #334's cloud presets replaced the plain Base URL and Model boxes that #325's journey types into, and the same check is red on main at b77d4a3.

I ported #350's fix into this PR as 1dbe789: the journey now picks OpenAI from the presets, and the baselines are refreshed. When #350 merges, that commit makes no further change here.


Generated by Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0194jTK9wbki29B4LcS7aKHG
…d-sr9ptd

# Conflicts:
#	fastapi_backend/app/domain/bridge.py
#	fastapi_backend/app/domain/settings.py
#	threat-model.yaml
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdrxhwvmLjFsfzQZ5pRJGC
The open-room check moves from Matterbridge to Channel.outside, so Slack
without a user list still makes changes ask first, as on main.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdrxhwvmLjFsfzQZ5pRJGC
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VdrxhwvmLjFsfzQZ5pRJGC
@adeelahmad
adeelahmad merged commit 7a66b16 into main Oct 11, 2026
11 checks passed
@adeelahmad
adeelahmad deleted the claude/project-thread-sr9ptd branch October 11, 2026 03:16
@adeelahmad adeelahmad mentioned this pull request Oct 11, 2026
17 of 19 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0