jarvis-assist/docs/superpowers/plans/2026-09-13-email-integratio...

678 lines
28 KiB
Markdown

# JARVIS Email Integration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Give JARVIS read/send access to the `kontakt@mbo-tech-it.de` mailbox: chat-driven live reading and confirmed sending, plus an n8n-cached "new emails" dashboard widget.
**Architecture:** `imaplib`/`smtplib` (Python stdlib, no new dependency) power two new Claude tools in `run_chat_completion` (`list_recent_emails`, `send_email`). A separate n8n workflow using the built-in `Email Trigger (IMAP)` node caches new-message metadata into a new `email_cache` table for a dashboard widget, independent of the chat tools.
**Tech Stack:** `imaplib`, `smtplib`, `email` (all Python stdlib), FastAPI (existing patterns), Anthropic tool use (existing `run_chat_completion` loop), n8n REST API, React.
**Spec:** `docs/superpowers/specs/2026-09-13-email-integration-design.md`
## Global Constraints
- No dedicated git repository — every "Commit" step is replaced by "confirm the file is saved".
- SSH: `ssh -F /dev/null -o IdentitiesOnly=yes -i ~/.ssh/jarvis_core_key jarvis-core@72.61.186.98`. Backend: `Claude outputs/`. Frontend: `web/`.
- Credentials (already verified working 13.09.2026): `EMAIL_IMAP_HOST=mx2f35.netcup.net`, `EMAIL_IMAP_PORT=143` (STARTTLS), `EMAIL_SMTP_HOST=mx2f35.netcup.net`, `EMAIL_SMTP_PORT=465` (implicit SSL), `EMAIL_USER=kontakt@mbo-tech-it.de`, `EMAIL_PASSWORD=E%21gq10i` (literal string, not URL-encoded).
- `send_email` must never be called by Claude without the user first confirming recipient/subject/body in the conversation — enforced via system-prompt instruction, same pattern as `delete_calendar_event`.
- n8n already has a credential "JARVIS Postgres" (id `8b6g9X080UhxUgLv`) pointing at the `jarvis` database, and a fresh credential "JARVIS Email IMAP" (id `4rU5KYIv1sKCJkAd`) already created with the above IMAP settings (`secure: false`, since STARTTLS on port 143 is used, not implicit TLS) — reuse both, don't recreate.
- The n8n API key from the weather workflow setup is expired/inaccessible; a new one ("JARVIS Email Workflow") was created during spec verification but its value wasn't persisted anywhere durable — generate a fresh one via the n8n UI (Settings → n8n API) when Task 4 needs it, same procedure as the weather workflow.
- n8n's `n8n-nodes-base.emailReadImap` (Email Trigger IMAP) node is confirmed present in this n8n version, with a `postProcessAction: "nothing"` option that must be used so caching doesn't mark real business emails as read.
---
### Task 1: IMAP/SMTP helpers + config + unit tests
**Files:**
- Modify: `Claude outputs/main.py` (imports, config constants, `_decode_mime_words`, `_list_recent_emails_sync`/`list_recent_emails`, `_send_email_sync`/`send_email`)
- Modify: `Claude outputs/docker-compose.yml` (jarvis-api environment)
- Test: `Claude outputs/tests/test_email.py`
**Interfaces:**
- Produces: `async def list_recent_emails(limit: int = 10) -> list` returning `[{"from": str, "subject": str, "date": str, "unread": bool}, ...]`, newest first; `async def send_email(to: str, subject: str, body: str) -> dict` returning `{"to", "subject", "sent": True}`. Consumed by Task 3 (chat tools).
- [ ] **Step 1: Write the failing tests**
Create `Claude outputs/tests/test_email.py`:
```python
import base64
import os
import sys
from unittest.mock import MagicMock, patch
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import main
def _encoded_subject(text: str) -> bytes:
return b"=?utf-8?B?" + base64.b64encode(text.encode("utf-8")) + b"?="
def _header_bytes(subject_bytes: bytes) -> bytes:
return (
b"From: Sender <sender@example.com>\r\n"
b"Subject: " + subject_bytes + b"\r\n"
b"Date: Fri, 11 Sep 2026 15:47:02 +0000\r\n\r\n"
)
def test_list_recent_emails_decodes_subject_and_marks_unread():
fake_conn = MagicMock()
fake_conn.search.return_value = ("OK", [b"1"])
fake_conn.fetch.return_value = (
"OK",
[(b"1 (FLAGS () BODY[HEADER.FIELDS (FROM SUBJECT DATE)] {123}", _header_bytes(_encoded_subject("Ümlaut Betreff")))],
)
with patch.object(main.imaplib, "IMAP4", return_value=fake_conn):
emails = main._list_recent_emails_sync(limit=1)
assert len(emails) == 1
assert emails[0]["subject"] == "Ümlaut Betreff"
assert emails[0]["from"] == "Sender <sender@example.com>"
assert emails[0]["unread"] is True
fake_conn.login.assert_called_once_with(main.EMAIL_USER, main.EMAIL_PASSWORD)
def test_list_recent_emails_marks_seen_messages_as_read():
fake_conn = MagicMock()
fake_conn.search.return_value = ("OK", [b"1"])
fake_conn.fetch.return_value = (
"OK",
[(b"1 (FLAGS (\\Seen) BODY[HEADER.FIELDS (FROM SUBJECT DATE)] {123}", _header_bytes(b"Plain Subject"))],
)
with patch.object(main.imaplib, "IMAP4", return_value=fake_conn):
emails = main._list_recent_emails_sync(limit=1)
assert emails[0]["unread"] is False
assert emails[0]["subject"] == "Plain Subject"
def test_send_email_calls_smtp_with_composed_message():
fake_smtp = MagicMock()
with patch.object(main.smtplib, "SMTP_SSL", return_value=fake_smtp):
result = main._send_email_sync("empfaenger@example.com", "Betreff", "Text")
fake_smtp.login.assert_called_once_with(main.EMAIL_USER, main.EMAIL_PASSWORD)
fake_smtp.send_message.assert_called_once()
sent_msg = fake_smtp.send_message.call_args[0][0]
assert sent_msg["To"] == "empfaenger@example.com"
assert sent_msg["Subject"] == "Betreff"
assert result == {"to": "empfaenger@example.com", "subject": "Betreff", "sent": True}
```
- [ ] **Step 2: Run tests to verify they fail**
```bash
SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key"
ssh $SSHOPTS jarvis-core@72.61.186.98 "mkdir -p /tmp/jarvis-test/tests"
OUT="C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs"
scp $SSHOPTS "$OUT/main.py" "$OUT/requirements.txt" "$OUT/requirements-dev.txt" "$OUT/pytest.ini" jarvis-core@72.61.186.98:/tmp/jarvis-test/
scp $SSHOPTS "$OUT/tests/test_email.py" jarvis-core@72.61.186.98:/tmp/jarvis-test/tests/
ssh $SSHOPTS jarvis-core@72.61.186.98 "docker run --rm -v /tmp/jarvis-test:/app -w /app python:3.11-slim bash -c 'pip install -q -r requirements-dev.txt -r requirements.txt && python -m pytest tests/test_email.py -v'"
```
Expected: FAIL — `_list_recent_emails_sync`/`_send_email_sync` don't exist yet.
- [ ] **Step 3: Add imports and config constants**
In `Claude outputs/main.py`, add near the other imports (after `import caldav`):
```python
import imaplib
import smtplib
import ssl
from email import message_from_bytes
from email.header import decode_header
from email.message import EmailMessage
```
Add near the other `# ============ CONFIG ============` constants:
```python
EMAIL_IMAP_HOST = os.getenv("EMAIL_IMAP_HOST")
EMAIL_IMAP_PORT = int(os.getenv("EMAIL_IMAP_PORT", "143"))
EMAIL_SMTP_HOST = os.getenv("EMAIL_SMTP_HOST")
EMAIL_SMTP_PORT = int(os.getenv("EMAIL_SMTP_PORT", "465"))
EMAIL_USER = os.getenv("EMAIL_USER")
EMAIL_PASSWORD = os.getenv("EMAIL_PASSWORD")
```
- [ ] **Step 4: Add the email helpers**
Add after the calendar helpers (`async def delete_event(...)`, end of that block):
```python
def _decode_mime_words(value: str) -> str:
parts = decode_header(value)
decoded = []
for text, charset in parts:
if isinstance(text, bytes):
decoded.append(text.decode(charset or "utf-8", errors="replace"))
else:
decoded.append(text)
return "".join(decoded)
def _list_recent_emails_sync(limit: int = 10) -> list:
conn = imaplib.IMAP4(EMAIL_IMAP_HOST, EMAIL_IMAP_PORT)
conn.starttls(ssl.create_default_context())
conn.login(EMAIL_USER, EMAIL_PASSWORD)
conn.select("INBOX", readonly=True)
_, data = conn.search(None, "ALL")
ids = data[0].split()[-limit:]
emails = []
for msg_id in reversed(ids):
_, msg_data = conn.fetch(msg_id, "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)] FLAGS)")
flags_line = msg_data[0][0].decode("utf-8", errors="replace")
msg = message_from_bytes(msg_data[0][1])
emails.append(
{
"from": _decode_mime_words(msg.get("From", "")),
"subject": _decode_mime_words(msg.get("Subject", "")),
"date": msg.get("Date", ""),
"unread": "\\Seen" not in flags_line,
}
)
conn.logout()
return emails
async def list_recent_emails(limit: int = 10) -> list:
return await asyncio.to_thread(_list_recent_emails_sync, limit)
def _send_email_sync(to: str, subject: str, body: str) -> dict:
msg = EmailMessage()
msg["From"] = EMAIL_USER
msg["To"] = to
msg["Subject"] = subject
msg.set_content(body)
conn = smtplib.SMTP_SSL(EMAIL_SMTP_HOST, EMAIL_SMTP_PORT, context=ssl.create_default_context())
conn.login(EMAIL_USER, EMAIL_PASSWORD)
conn.send_message(msg)
conn.quit()
return {"to": to, "subject": subject, "sent": True}
async def send_email(to: str, subject: str, body: str) -> dict:
return await asyncio.to_thread(_send_email_sync, to, subject, body)
```
- [ ] **Step 5: Run tests to verify they pass**
Re-run the Step 2 command. Expected: 3 passed.
- [ ] **Step 6: Wire the email env vars into the deployed container**
In `Claude outputs/docker-compose.yml`, under `jarvis-api: environment:`, add after `NEXTCLOUD_APP_PASSWORD`:
```yaml
- EMAIL_IMAP_HOST=mx2f35.netcup.net
- EMAIL_IMAP_PORT=143
- EMAIL_SMTP_HOST=mx2f35.netcup.net
- EMAIL_SMTP_PORT=465
- EMAIL_USER=kontakt@mbo-tech-it.de
- EMAIL_PASSWORD=${EMAIL_PASSWORD:-}
```
Add the password to the deployed `.env`:
```bash
SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key"
ssh $SSHOPTS jarvis-core@72.61.186.98 "grep -q EMAIL_PASSWORD /home/jarvis-core/jarvis/.env || echo 'EMAIL_PASSWORD=E%21gq10i' >> /home/jarvis-core/jarvis/.env"
```
- [ ] **Step 7: Confirm files saved** (no git repo for this project — see Global Constraints)
---
### Task 2: `email_cache` table + `GET /api/v1/emails` endpoint
**Files:**
- Create: `Claude outputs/migrations/005_email_cache.sql`
- Modify: `Claude outputs/main.py` (add `get_latest_emails` DB helper + endpoint)
**Interfaces:**
- Produces: `GET /api/v1/emails?limit=10` returning `{"emails": [...]}`, each `{"sender", "subject", "received_at"}`. Consumed by Task 5's dashboard widget. Populated by Task 4's n8n workflow.
- [ ] **Step 1: Create and apply the migration**
Create `Claude outputs/migrations/005_email_cache.sql`:
```sql
CREATE TABLE email_cache (
id SERIAL PRIMARY KEY,
sender VARCHAR(255) NOT NULL,
subject VARCHAR(500) NOT NULL,
received_at TIMESTAMP NOT NULL,
cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_email_cache_received_at ON email_cache(received_at);
```
```bash
SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key"
scp $SSHOPTS "C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs\migrations\005_email_cache.sql" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/migrations/005_email_cache.sql
ssh $SSHOPTS jarvis-core@72.61.186.98 "docker exec -i jarvis-postgres psql -U jarvis -d jarvis < /home/jarvis-core/jarvis/migrations/005_email_cache.sql"
```
Expected output: `CREATE TABLE`, `CREATE INDEX`.
- [ ] **Step 2: Add the DB helper and endpoint**
In `Claude outputs/main.py`, add near the other DB helpers (after `get_latest_weather`):
```python
async def get_latest_emails(limit: int = 10):
return await db_query(
"SELECT sender, subject, received_at FROM email_cache ORDER BY received_at DESC LIMIT %s",
(limit,),
fetch="all",
)
```
Add the endpoint after `get_calendar_events`:
```python
@app.get("/api/v1/emails", dependencies=[Depends(require_admin_key)])
async def get_cached_emails(limit: int = 10):
"""Recently cached emails, populated by the n8n 'Neue E-Mails Cache' workflow"""
if not pg_pool:
raise HTTPException(status_code=503, detail="Database is not configured")
rows = await get_latest_emails(limit)
return {
"emails": [
{
"sender": row["sender"],
"subject": row["subject"],
"received_at": row["received_at"].isoformat(),
}
for row in rows
]
}
```
- [ ] **Step 3: Deploy and verify against the live API**
```bash
SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key"
OUT="C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs"
scp $SSHOPTS "$OUT/main.py" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/api/main.py
scp $SSHOPTS "$OUT/docker-compose.yml" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/docker-compose.yml
ssh $SSHOPTS jarvis-core@72.61.186.98 "cd /home/jarvis-core/jarvis && docker compose up -d jarvis-api && sleep 10 && docker logs --tail 20 jarvis-api"
```
Expected: log ends with `Application startup complete.`.
```bash
ADMIN_KEY=$(ssh $SSHOPTS jarvis-core@72.61.186.98 "grep API_KEY_ADMIN /home/jarvis-core/jarvis/.env" | cut -d= -f2-)
ssh $SSHOPTS jarvis-core@72.61.186.98 "curl -s -H 'X-Admin-Key: $ADMIN_KEY' http://localhost:8000/api/v1/emails; echo"
```
Expected: `{"emails":[]}` (table is empty until Task 4's workflow runs).
- [ ] **Step 4: Confirm files saved** (no git repo for this project — see Global Constraints)
---
### Task 3: Chat tool use (read + confirmed send)
**Files:**
- Modify: `Claude outputs/main.py` (tool definitions, `execute_tool`, system-prompt instructions)
- Test: `Claude outputs/tests/test_chat_tools.py` (extend)
**Interfaces:**
- Consumes: `list_recent_emails`, `send_email` (Task 1).
- Produces: two new entries in `CALENDAR_TOOLS` (kept in that list/constant name to avoid a second near-identical tools array — rename is out of scope) and matching branches in `execute_tool`.
- [ ] **Step 1: Add the tool definitions**
In `Claude outputs/main.py`, add two entries to the `CALENDAR_TOOLS` list (after `delete_calendar_event`'s closing `},`):
```python
{
"name": "list_recent_emails",
"description": "List the most recent emails in the kontakt@mbo-tech-it.de inbox.",
"input_schema": {
"type": "object",
"properties": {
"limit": {"type": "integer", "description": "How many recent emails to list, e.g. 10"}
},
"required": ["limit"],
},
},
{
"name": "send_email",
"description": (
"Send an email from kontakt@mbo-tech-it.de. Only call this after "
"the user has explicitly confirmed recipient, subject and body "
"in the conversation."
),
"input_schema": {
"type": "object",
"properties": {
"to": {"type": "string", "description": "Recipient email address"},
"subject": {"type": "string", "description": "Email subject"},
"body": {"type": "string", "description": "Plain text email body"},
},
"required": ["to", "subject", "body"],
},
},
```
- [ ] **Step 2: Extend the system-prompt instructions**
In `Claude outputs/main.py`, change the `CALENDAR_ASSISTANT_INSTRUCTIONS` string (append to the existing text, keep everything before it unchanged):
```python
CALENDAR_ASSISTANT_INSTRUCTIONS = (
"Du hast Zugriff auf den Kalender 'FFW-Onza-Alle' ueber die Tools "
"list_calendar_events, create_calendar_event, update_calendar_event und "
"delete_calendar_event. create_calendar_event und update_calendar_event "
"kannst du direkt aufrufen, sobald du die noetigen Angaben hast - keine "
"Rueckfrage noetig. Bevor du delete_calendar_event aufrufst, frage den "
"Nutzer aber immer explizit im Klartext nach Bestaetigung und rufe das "
"Tool erst auf, nachdem der Nutzer im naechsten Chat-Beitrag zugestimmt "
"hat. Fuer update_calendar_event und delete_calendar_event brauchst du "
"die uid des Termins - ruf dafuer zuerst list_calendar_events auf, falls "
"du sie noch nicht aus dem Gespraech kennst.\n\n"
"Du hast ausserdem Zugriff auf das Postfach kontakt@mbo-tech-it.de ueber "
"die Tools list_recent_emails und send_email. list_recent_emails kannst "
"du direkt aufrufen. Bevor du send_email aufrufst, zeige dem Nutzer "
"Empfaenger, Betreff und Text zur Kontrolle und rufe das Tool erst auf, "
"nachdem der Nutzer im naechsten Chat-Beitrag zugestimmt hat - "
"kontakt@mbo-tech-it.de ist die offizielle Firmenadresse, eine "
"versendete Mail laesst sich nicht zurueckholen."
)
```
- [ ] **Step 3: Wire the dispatcher**
In `Claude outputs/main.py`, add to `execute_tool` (after the `delete_calendar_event` branch, before `raise ValueError`):
```python
if name == "list_recent_emails":
emails = await list_recent_emails(tool_input.get("limit", 10))
return json.dumps(emails)
if name == "send_email":
result = await send_email(tool_input["to"], tool_input["subject"], tool_input["body"])
return json.dumps(result)
```
- [ ] **Step 4: Extend the chat-tools regression test**
In `Claude outputs/tests/test_chat_tools.py`, add at the end of the file:
```python
@pytest.mark.asyncio
async def test_run_chat_completion_lists_recent_emails():
first = MagicMock()
first.stop_reason = "tool_use"
first.content = [_tool_use_block("list_recent_emails", {"limit": 5}, "tool_1")]
first.usage = _usage(15, 6)
second = MagicMock()
second.stop_reason = "end_turn"
second.content = [_text_block("Du hast 5 neue Mails.")]
second.usage = _usage(20, 8)
main.claude_client = MagicMock()
main.claude_client.messages.create.side_effect = [first, second]
with patch.object(main, "list_recent_emails", new=AsyncMock(return_value=[])):
text, output_tokens, total_tokens = await main.run_chat_completion(
[{"role": "user", "content": "Was ist neu im Postfach?"}]
)
assert text == "Du hast 5 neue Mails."
assert output_tokens == 6 + 8
assert total_tokens == 15 + 6 + 20 + 8
```
- [ ] **Step 5: Run all backend tests**
```bash
SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key"
OUT="C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs"
scp $SSHOPTS "$OUT/main.py" jarvis-core@72.61.186.98:/tmp/jarvis-test/main.py
scp $SSHOPTS "$OUT/tests/test_chat_tools.py" "$OUT/tests/test_email.py" jarvis-core@72.61.186.98:/tmp/jarvis-test/tests/
ssh $SSHOPTS jarvis-core@72.61.186.98 "docker run --rm -v /tmp/jarvis-test:/app -w /app python:3.11-slim bash -c 'pip install -q -r requirements-dev.txt -r requirements.txt && python -m pytest tests/ -v'"
```
Expected: all tests pass (should be 21 total: 17 previous + 3 email + 1 new chat-tools).
- [ ] **Step 6: Deploy and verify against the live API**
```bash
SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key"
OUT="C:\Users\Jonny\Projekte\Claude\JARVIS\Claude outputs"
scp $SSHOPTS "$OUT/main.py" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/api/main.py
ssh $SSHOPTS jarvis-core@72.61.186.98 "cd /home/jarvis-core/jarvis && docker compose restart jarvis-api && sleep 10 && docker logs --tail 20 jarvis-api"
```
Expected: log ends with `Application startup complete.`.
```bash
ADMIN_KEY=$(ssh $SSHOPTS jarvis-core@72.61.186.98 "grep API_KEY_ADMIN /home/jarvis-core/jarvis/.env" | cut -d= -f2-)
ssh $SSHOPTS jarvis-core@72.61.186.98 "curl -s -X POST http://localhost:8000/api/v1/chat -H 'Content-Type: application/json' -H 'X-Admin-Key: $ADMIN_KEY' -d '{\"message\": \"Was ist neu im Postfach kontakt@mbo-tech-it.de? Zeig die letzten 5 Mails.\"}'; echo"
```
Expected: a natural-language response listing real subjects/senders from the live inbox (proof the tool round-trip actually hit IMAP).
Then test the confirmation gate:
```bash
ssh $SSHOPTS jarvis-core@72.61.186.98 "curl -s -X POST http://localhost:8000/api/v1/chat -H 'Content-Type: application/json' -H 'X-Admin-Key: $ADMIN_KEY' -d '{\"message\": \"Schick eine Testmail an kontakt@mbo-tech-it.de mit Betreff JARVIS Test und Text Hallo aus JARVIS.\"}'; echo"
```
Expected: JARVIS asks for confirmation, does NOT send yet (no `is_error` and no immediate "gesendet" claim). Confirm in a follow-up (`conversation_id` from the response) with `{"conversation_id": <id>, "message": "Ja, senden."}` and expect a real send confirmation - then check the inbox arrived (e.g. re-run `list_recent_emails` via chat, or check via the Task 1 IMAP test pattern).
- [ ] **Step 7: Confirm files saved** (no git repo for this project — see Global Constraints)
---
### Task 4: n8n "Neue E-Mails Cache" workflow
**Files:** none (n8n workflow lives in n8n's own database, not in this repo)
**Interfaces:**
- Consumes: n8n credentials "JARVIS Email IMAP" (id `4rU5KYIv1sKCJkAd`) and "JARVIS Postgres" (id `8b6g9X080UhxUgLv`), both already created.
- Produces: rows in `email_cache` (Task 2's table), consumed by Task 5's widget.
- [ ] **Step 1: Generate a fresh n8n API key**
Via browser: log in at `https://n8n.jarvis.mbo-tech-it.de` (owner account `jonny@mbo-tech-it.de`), Settings → n8n API → Create API key, label "JARVIS Email Workflow Runtime", scope "All". Copy the value into a page-local JS variable immediately (it's only shown once) — same procedure as the weather workflow setup: paste into a visible input (e.g. the API-key-list search box) and read `.value` via `javascript_exec`, since the creation dialog's own field is CSS-truncated.
- [ ] **Step 2: Create the workflow via the n8n REST API**
Using `fetch` in the browser page context with `X-N8N-API-KEY` set to the key from Step 1 (POST to `/api/v1/workflows`):
```json
{
"name": "Neue E-Mails Cache",
"nodes": [
{
"id": "trigger1",
"name": "Email Trigger IMAP",
"type": "n8n-nodes-base.emailReadImap",
"typeVersion": 2,
"position": [240, 300],
"parameters": {
"mailbox": "INBOX",
"postProcessAction": "nothing",
"format": "simple",
"options": {}
},
"credentials": {
"imap": { "id": "4rU5KYIv1sKCJkAd", "name": "JARVIS Email IMAP" }
}
},
{
"id": "pg1",
"name": "Insert Email",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [460, 300],
"parameters": {
"operation": "insert",
"schema": { "__rl": true, "value": "public", "mode": "list" },
"table": { "__rl": true, "value": "email_cache", "mode": "list" },
"columns": {
"mappingMode": "defineBelow",
"value": {
"sender": "={{ $json.from }}",
"subject": "={{ $json.subject }}",
"received_at": "={{ $json.date }}"
},
"matchingColumns": [],
"schema": []
}
},
"credentials": {
"postgres": { "id": "8b6g9X080UhxUgLv", "name": "JARVIS Postgres" }
}
}
],
"connections": {
"Email Trigger IMAP": { "main": [[{ "node": "Insert Email", "type": "main", "index": 0 }]] }
},
"settings": { "executionOrder": "v1" }
}
```
Note: the exact field name for the sender on the IMAP trigger's "simple" format output is unconfirmed (could be a plain string or an object like `{text, value}}` depending on the mail parser) — this is discovered and corrected in Step 4 below using a real test email, the same way the weather workflow's node parameters were verified against the live API before finalizing.
- [ ] **Step 3: Activate the workflow**
```
POST /api/v1/workflows/<id>/activate
```
with the same API key.
- [ ] **Step 4: Send a real test email and verify the cache row**
Use the already-deployed and tested `send_email` (Task 3) to send a message to the inbox itself, so the new trigger has something to catch:
```bash
SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key"
ADMIN_KEY=$(ssh $SSHOPTS jarvis-core@72.61.186.98 "grep API_KEY_ADMIN /home/jarvis-core/jarvis/.env" | cut -d= -f2-)
ssh $SSHOPTS jarvis-core@72.61.186.98 "curl -s -X POST http://localhost:8000/api/v1/chat -H 'Content-Type: application/json' -H 'X-Admin-Key: $ADMIN_KEY' -d '{\"message\": \"Schick eine Testmail an kontakt@mbo-tech-it.de mit Betreff JARVIS n8n Cache Test und Text Test.\"}'; echo"
```
Confirm the send in a follow-up chat message (see Task 3 Step 6's confirmation pattern). Wait ~30-60s for the IMAP trigger to poll, then check:
```bash
ssh $SSHOPTS jarvis-core@72.61.186.98 "docker exec jarvis-postgres psql -U jarvis -d jarvis -c 'SELECT sender, subject, received_at FROM email_cache ORDER BY id DESC LIMIT 3;'"
```
Expected: a row with subject "JARVIS n8n Cache Test". If the `sender`/`subject`/`received_at` columns are empty or malformed, inspect the actual execution data in the n8n UI (Executions tab on the workflow) to see the real output shape from the "Email Trigger IMAP" node, and adjust the Postgres node's `columns.value` expressions accordingly (e.g. `$json.from.text` instead of `$json.from`) via a `PATCH /api/v1/workflows/<id>` call, then resend a test email to reverify.
- [ ] **Step 5: Confirm the workflow is saved** (lives in n8n's own DB, not this repo — no file to save)
---
### Task 5: "Neue E-Mails" dashboard widget
**Files:**
- Create: `web/src/components/EmailWidget.tsx`
- Modify: `web/src/components/Dashboard.tsx` (render it)
**Interfaces:**
- Consumes: `apiFetch` (existing), `GET /api/v1/emails` (Task 2).
- Produces: `EmailWidget` default export, no props, rendered inside `Dashboard`.
- [ ] **Step 1: Create the widget**
Create `web/src/components/EmailWidget.tsx`:
```tsx
import { useEffect, useState } from "react";
import { apiFetch } from "../api";
type CachedEmail = { sender: string; subject: string; received_at: string };
export default function EmailWidget() {
const [emails, setEmails] = useState<CachedEmail[] | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function load() {
try {
const response = await apiFetch("/api/v1/emails?limit=5");
if (!response.ok) {
setError("Postfach-Cache nicht verfuegbar");
return;
}
const data = await response.json();
setEmails(data.emails);
} catch {
setError("Postfach-Cache nicht verfuegbar");
}
}
load();
}, []);
return (
<div className="email-widget">
<h3>Neue E-Mails</h3>
{error && <p className="error">{error}</p>}
{emails && emails.length === 0 && <p>Noch keine gecachten E-Mails.</p>}
{emails && emails.length > 0 && (
<ul>
{emails.map((e, i) => (
<li key={i}>
{`${new Date(e.received_at).toLocaleDateString("de-DE")}: ${e.sender} \u2013 ${e.subject}`}
</li>
))}
</ul>
)}
</div>
);
}
```
- [ ] **Step 2: Render it in the Dashboard**
In `web/src/components/Dashboard.tsx`, add the import next to `CalendarWidget`'s:
```tsx
import EmailWidget from "./EmailWidget";
```
Add `<EmailWidget />` right after `<CalendarWidget />`.
- [ ] **Step 3: Verify the build works**
```bash
cd "C:\Users\Jonny\Projekte\Claude\JARVIS\web"
npm run build
```
Expected: exits 0.
- [ ] **Step 4: Deploy**
```bash
SSHOPTS="-F /dev/null -o IdentitiesOnly=yes -i /c/Users/Jonny/.ssh/jarvis_core_key"
OUT="C:\Users\Jonny\Projekte\Claude\JARVIS"
scp $SSHOPTS -r "$OUT\web\src" jarvis-core@72.61.186.98:/home/jarvis-core/jarvis/web/
ssh $SSHOPTS jarvis-core@72.61.186.98 "cd /home/jarvis-core/jarvis && docker compose build jarvis-web && docker compose up -d jarvis-web"
```
- [ ] **Step 5: End-to-end browser verification**
Using the claude-in-chrome browser tools: log in at `https://jarvis.mbo-tech-it.de` (append a cache-busting query param like `?nocache=1` on first load after deploy if the page looks stale, per the known nginx/browser-cache behavior documented in `JARVIS_HANDOFF.md`), open Dashboard, confirm "Neue E-Mails" lists the test email(s) from Task 4.
- [ ] **Step 6: Confirm files saved** (no git repo for this project — see Global Constraints)