diff --git a/EMAIL_ASSISTANT_E2E_GUIDE.md b/EMAIL_ASSISTANT_E2E_GUIDE.md
deleted file mode 100644
index a72a18c..0000000
--- a/EMAIL_ASSISTANT_E2E_GUIDE.md
+++ /dev/null
@@ -1,164 +0,0 @@
-# Nanobot Email Assistant: End-to-End Guide
-
-This guide explains how to run nanobot as a real email assistant with explicit user permission and optional automatic replies.
-
-## 1. What This Feature Does
-
-- Read unread emails via IMAP.
-- Let the agent analyze/respond to email content.
-- Send replies via SMTP.
-- Enforce explicit owner consent before mailbox access.
-- Let you toggle automatic replies on or off.
-
-## 2. Permission Model (Required)
-
-`channels.email.consentGranted` is the hard permission gate.
-
-- `false`: nanobot must not access mailbox content and must not send email.
-- `true`: nanobot may read/send based on other settings.
-
-Only set `consentGranted: true` after the mailbox owner explicitly agrees.
-
-## 3. Auto-Reply Mode
-
-`channels.email.autoReplyEnabled` controls outbound automatic email replies.
-
-- `true`: inbound emails can receive automatic agent replies.
-- `false`: inbound emails can still be read/processed, but automatic replies are skipped.
-
-Use `autoReplyEnabled: false` when you want analysis-only mode.
-
-## 4. Required Account Setup (Gmail Example)
-
-1. Enable 2-Step Verification in Google account security settings.
-2. Create an App Password.
-3. Use this app password for both IMAP and SMTP auth.
-
-Recommended servers:
-- IMAP host/port: `imap.gmail.com:993` (SSL)
-- SMTP host/port: `smtp.gmail.com:587` (STARTTLS)
-
-## 5. Config Example
-
-Edit `~/.nanobot/config.json`:
-
-```json
-{
- "channels": {
- "email": {
- "enabled": true,
- "consentGranted": true,
- "imapHost": "imap.gmail.com",
- "imapPort": 993,
- "imapUsername": "you@gmail.com",
- "imapPassword": "${NANOBOT_EMAIL_IMAP_PASSWORD}",
- "imapMailbox": "INBOX",
- "imapUseSsl": true,
- "smtpHost": "smtp.gmail.com",
- "smtpPort": 587,
- "smtpUsername": "you@gmail.com",
- "smtpPassword": "${NANOBOT_EMAIL_SMTP_PASSWORD}",
- "smtpUseTls": true,
- "smtpUseSsl": false,
- "fromAddress": "you@gmail.com",
- "autoReplyEnabled": true,
- "pollIntervalSeconds": 30,
- "markSeen": true,
- "allowFrom": ["trusted.sender@example.com"]
- }
- }
-}
-```
-
-## 6. Set Secrets via Environment Variables
-
-In the same shell before starting gateway:
-
-```bash
-read -s "NANOBOT_EMAIL_IMAP_PASSWORD?IMAP app password: "
-echo
-read -s "NANOBOT_EMAIL_SMTP_PASSWORD?SMTP app password: "
-echo
-export NANOBOT_EMAIL_IMAP_PASSWORD
-export NANOBOT_EMAIL_SMTP_PASSWORD
-```
-
-If you use one app password for both, enter the same value twice.
-
-## 7. Run and Verify
-
-Start:
-
-```bash
-cd /Users/kaijimima1234/Desktop/nanobot
-PYTHONPATH=/Users/kaijimima1234/Desktop/nanobot .venv/bin/nanobot gateway
-```
-
-Check channel status:
-
-```bash
-PYTHONPATH=/Users/kaijimima1234/Desktop/nanobot .venv/bin/nanobot channels status
-```
-
-Expected behavior:
-- `enabled=true + consentGranted=true + autoReplyEnabled=true`: read + auto reply.
-- `enabled=true + consentGranted=true + autoReplyEnabled=false`: read only, no auto reply.
-- `consentGranted=false`: no read, no send.
-
-## 8. Commands You Can Tell Nanobot
-
-Once gateway is running and email consent is enabled:
-
-1. Summarize yesterday's emails:
-
-```text
-summarize my yesterday email
-```
-
-or
-
-```text
-!email summary yesterday
-```
-
-2. Send an email to a friend:
-
-```text
-!email send friend@example.com | Subject here | Body here
-```
-
-or
-
-```text
-send email to friend@example.com subject: Subject here body: Body here
-```
-
-Notes:
-- Sending command always performs a direct send (manual action by you).
-- If `consentGranted` is `false`, send/read are blocked.
-- If `autoReplyEnabled` is `false`, automatic replies are disabled, but direct send command above still works.
-
-## 9. End-to-End Test Plan
-
-1. Send a test email from an allowed sender to your mailbox.
-2. Confirm nanobot receives and processes it.
-3. If `autoReplyEnabled=true`, confirm a reply is delivered.
-4. Set `autoReplyEnabled=false`, send another test email.
-5. Confirm no auto-reply is sent.
-6. Set `consentGranted=false`, send another test email.
-7. Confirm nanobot does not read/send.
-
-## 10. Security Notes
-
-- Never commit real passwords/tokens into git.
-- Prefer environment variables for secrets.
-- Keep `allowFrom` restricted whenever possible.
-- Rotate app passwords immediately if leaked.
-
-## 11. PR Checklist
-
-- [ ] `consentGranted` gating works for read/send.
-- [ ] `autoReplyEnabled` toggle works as documented.
-- [ ] README updated with new fields.
-- [ ] Tests pass (`pytest`).
-- [ ] No real credentials in tracked files.
diff --git a/README.md b/README.md
index 502a42f..8f7c1a2 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@
⚡️ Delivers core agent functionality in just **~4,000** lines of code — **99% smaller** than Clawdbot's 430k+ lines.
-📏 Real-time line count: **3,448 lines** (run `bash core_agent_lines.sh` to verify anytime)
+📏 Real-time line count: **3,479 lines** (run `bash core_agent_lines.sh` to verify anytime)
## 📢 News
@@ -166,7 +166,7 @@ nanobot agent -m "Hello from my local LLM!"
## 💬 Chat Apps
-Talk to your nanobot through Telegram, Discord, WhatsApp, or Feishu — anytime, anywhere.
+Talk to your nanobot through Telegram, Discord, WhatsApp, Feishu, DingTalk, or Email — anytime, anywhere.
| Channel | Setup |
|---------|-------|
@@ -174,6 +174,8 @@ Talk to your nanobot through Telegram, Discord, WhatsApp, or Feishu — anytime,
| **Discord** | Easy (bot token + intents) |
| **WhatsApp** | Medium (scan QR) |
| **Feishu** | Medium (app credentials) |
+| **DingTalk** | Medium (app credentials) |
+| **Email** | Medium (IMAP/SMTP credentials) |
Telegram (Recommended)
@@ -372,6 +374,55 @@ nanobot gateway
+
+Email
+
+Uses **IMAP** polling for inbound + **SMTP** for outbound. Requires explicit consent before accessing mailbox data.
+
+**1. Get credentials (Gmail example)**
+- Enable 2-Step Verification in Google account security
+- Create an [App Password](https://myaccount.google.com/apppasswords)
+- Use this app password for both IMAP and SMTP
+
+**2. Configure**
+
+> [!TIP]
+> Set `"autoReplyEnabled": false` if you only want to read/analyze emails without sending automatic replies.
+
+```json
+{
+ "channels": {
+ "email": {
+ "enabled": true,
+ "consentGranted": true,
+ "imapHost": "imap.gmail.com",
+ "imapPort": 993,
+ "imapUsername": "you@gmail.com",
+ "imapPassword": "your-app-password",
+ "imapUseSsl": true,
+ "smtpHost": "smtp.gmail.com",
+ "smtpPort": 587,
+ "smtpUsername": "you@gmail.com",
+ "smtpPassword": "your-app-password",
+ "smtpUseTls": true,
+ "fromAddress": "you@gmail.com",
+ "allowFrom": ["trusted@example.com"]
+ }
+ }
+}
+```
+
+> `consentGranted`: Must be `true` to allow mailbox access. Set to `false` to disable reading and sending entirely.
+> `allowFrom`: Leave empty to accept emails from anyone, or restrict to specific sender addresses.
+
+**3. Run**
+
+```bash
+nanobot gateway
+```
+
+
+
## ⚙️ Configuration
Config file: `~/.nanobot/config.json`
@@ -542,7 +593,7 @@ PRs welcome! The codebase is intentionally small and readable. 🤗
- [ ] **Multi-modal** — See and hear (images, voice, video)
- [ ] **Long-term memory** — Never forget important context
- [ ] **Better reasoning** — Multi-step planning and reflection
-- [ ] **More integrations** — Discord, Slack, email, calendar
+- [ ] **More integrations** — Slack, calendar, and more
- [ ] **Self-improvement** — Learn from feedback and mistakes
### Contributors
diff --git a/nanobot/channels/email.py b/nanobot/channels/email.py
index 029c00d..0e47067 100644
--- a/nanobot/channels/email.py
+++ b/nanobot/channels/email.py
@@ -55,7 +55,8 @@ class EmailChannel(BaseChannel):
self.config: EmailConfig = config
self._last_subject_by_chat: dict[str, str] = {}
self._last_message_id_by_chat: dict[str, str] = {}
- self._processed_uids: set[str] = set()
+ self._processed_uids: set[str] = set() # Capped to prevent unbounded growth
+ self._MAX_PROCESSED_UIDS = 100000
async def start(self) -> None:
"""Start polling IMAP for inbound emails."""
@@ -301,6 +302,9 @@ class EmailChannel(BaseChannel):
if dedupe and uid:
self._processed_uids.add(uid)
+ # mark_seen is the primary dedup; this set is a safety net
+ if len(self._processed_uids) > self._MAX_PROCESSED_UIDS:
+ self._processed_uids.clear()
if mark_seen:
client.store(imap_id, "+FLAGS", "\\Seen")