feat: add test connection button in config UI, restyle add handler button

linuztx committed Mar 16, 2026 at 11:24 UTC 96cc2cd1982b89aacbb68e6de255e693fe5c11bb
3 files changed +120 -112
plugins/_email_integration/api/test_connection.py new
+78
@@ -0,0 +1,78 @@
1 +"""Test IMAP/SMTP connection for an email handler config."""
2 +
3 +from helpers.api import ApiHandler, Request
4 +from helpers.errors import format_error
5 +
6 +from plugins._email_integration.helpers.imap_client import (
7 + connect_imap,
8 + disconnect_imap,
9 + get_highest_uid,
10 +)
11 +from plugins._email_integration.helpers.smtp_client import SmtpConfig, send_reply
12 +
13 +
14 +class TestConnection(ApiHandler):
15 +
16 + async def process(self, input: dict, request: Request) -> dict:
17 + handler = input.get("handler", {})
18 + results: list[dict] = []
19 + account_type = handler.get("account_type", "imap")
20 +
21 + if account_type == "imap":
22 + await self._test_imap(handler, results)
23 + await self._test_smtp(handler, results)
24 +
25 + ok = all(r["ok"] for r in results)
26 + return {"success": ok, "results": results}
27 +
28 + async def _test_imap(self, handler: dict, results: list[dict]):
29 + try:
30 + client = await connect_imap(
31 + server=handler.get("imap_server", ""),
32 + port=int(handler.get("imap_port", 993)),
33 + username=handler.get("username", ""),
34 + password=handler.get("password", ""),
35 + )
36 + uid = await get_highest_uid(client)
37 + await disconnect_imap(client)
38 + results.append({
39 + "test": "IMAP",
40 + "ok": True,
41 + "message": f"Connected, highest UID: {uid}",
42 + })
43 + except Exception as e:
44 + results.append({
45 + "test": "IMAP",
46 + "ok": False,
47 + "message": format_error(e),
48 + })
49 +
50 + async def _test_smtp(self, handler: dict, results: list[dict]):
51 + try:
52 + smtp_server = handler.get("smtp_server") or handler.get("imap_server", "")
53 + cfg = SmtpConfig(
54 + server=smtp_server,
55 + port=int(handler.get("smtp_port", 587)),
56 + username=handler.get("username", ""),
57 + password=handler.get("password", ""),
58 + )
59 + error = await send_reply(
60 + config=cfg,
61 + to=handler.get("username", ""),
62 + subject="Agent Zero - Connection Test",
63 + body="SMTP connection test successful.",
64 + )
65 + if error:
66 + results.append({"test": "SMTP", "ok": False, "message": error})
67 + else:
68 + results.append({
69 + "test": "SMTP",
70 + "ok": True,
71 + "message": "Connected, test email sent to self",
72 + })
73 + except Exception as e:
74 + results.append({
75 + "test": "SMTP",
76 + "ok": False,
77 + "message": format_error(e),
78 + })
plugins/_email_integration/test_connection.py deleted
-111
@@ -1,111 +0,0 @@
1 -"""
2 -Email integration connection test.
3 -Usage: EMAIL_USER=you@gmail.com EMAIL_PASS=xxxx python plugins/_email_integration/test_connection.py
4 -
5 -Tests:
6 - 1. IMAP connect + get highest UID (baseline)
7 - 2. Fetch new emails since that UID (should be 0 on first run)
8 - 3. SMTP send test email to self
9 - 4. Fetch again (should find the test email)
10 -"""
11 -
12 -import asyncio
13 -import os
14 -import sys
15 -
16 -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)) + "/../..")
17 -
18 -from plugins._email_integration.helpers.imap_client import (
19 - connect_imap,
20 - disconnect_imap,
21 - fetch_new,
22 - get_highest_uid,
23 -)
24 -from plugins._email_integration.helpers.smtp_client import SmtpConfig, send_reply
25 -
26 -
27 -async def test_full_flow(user: str, password: str):
28 - print(f"\n--- Full Flow Test ({user}) ---\n")
29 -
30 - # Step 1: Connect and get baseline UID
31 - client = await connect_imap(
32 - server="imap.gmail.com", port=993,
33 - username=user, password=password,
34 - )
35 - print("[OK] IMAP login")
36 -
37 - baseline_uid = await get_highest_uid(client)
38 - print(f"[OK] Baseline UID: {baseline_uid}")
39 -
40 - # Step 2: Fetch new since baseline — should be 0
41 - messages, new_uid = await fetch_new(
42 - client, "/tmp/email_test_attachments",
43 - last_uid=baseline_uid, max_messages=5,
44 - )
45 - print(f"[OK] Fetch new since UID {baseline_uid}: {len(messages)} messages (expected 0)")
46 -
47 - await disconnect_imap(client)
48 -
49 - # Step 3: Send test email to self
50 - print("\n--- Sending test email to self ---")
51 - cfg = SmtpConfig(
52 - server="smtp.gmail.com", port=587,
53 - username=user, password=password,
54 - )
55 - ok = await send_reply(
56 - config=cfg, to=user,
57 - subject="A0 Email Test [a0-test123]",
58 - body="Test email from Agent Zero. If you see this, SMTP works.",
59 - )
60 - print(f"[{'OK' if ok else 'FAIL'}] SMTP send")
61 -
62 - if not ok:
63 - return
64 -
65 - # Step 4: Wait for email to arrive, then fetch
66 - print("\n--- Waiting 5s for email delivery ---")
67 - await asyncio.sleep(5)
68 -
69 - client2 = await connect_imap(
70 - server="imap.gmail.com", port=993,
71 - username=user, password=password,
72 - )
73 -
74 - messages2, new_uid2 = await fetch_new(
75 - client2, "/tmp/email_test_attachments",
76 - last_uid=baseline_uid, max_messages=5,
77 - )
78 - print(f"[OK] Fetch new since UID {baseline_uid}: {len(messages2)} messages")
79 - for msg in messages2:
80 - print(f" From: {msg.sender}")
81 - print(f" Subject: {msg.subject}")
82 - print(f" Body: {msg.body[:80]}...")
83 - print()
84 -
85 - print(f"[OK] New last_uid: {new_uid2}")
86 -
87 - # Step 5: Fetch again with updated UID — should be 0
88 - messages3, _ = await fetch_new(
89 - client2, "/tmp/email_test_attachments",
90 - last_uid=new_uid2, max_messages=5,
91 - )
92 - print(f"[OK] Re-fetch since UID {new_uid2}: {len(messages3)} messages (expected 0)")
93 -
94 - await disconnect_imap(client2)
95 - print("\n--- ALL TESTS PASSED ---")
96 -
97 -
98 -async def main():
99 - user = os.environ.get("EMAIL_USER", "")
100 - password = os.environ.get("EMAIL_PASS", "")
101 -
102 - if not user or not password:
103 - print("Set EMAIL_USER and EMAIL_PASS environment variables")
104 - print("Example: EMAIL_USER=you@gmail.com EMAIL_PASS=xxxx python ...")
105 - sys.exit(1)
106 -
107 - await test_full_flow(user, password)
108 -
109 -
110 -if __name__ == "__main__":
111 - asyncio.run(main())
plugins/_email_integration/webui/config.html
+42 -1
@@ -7,6 +7,8 @@
7 <div x-data="{
8 get handlers() { return config?.handlers || [] },
9 editing: null,
10 + testing: null,
11 + test_results: null,
12 add_handler() {
13 if (!config.handlers) config.handlers = [];
14 config.handlers.push({
@@ -38,6 +40,20 @@
40 },
41 set_whitelist(handler, val) {
42 handler.sender_whitelist = val.split(',').map(s => s.trim()).filter(s => s);
43 + },
44 + async test_connection(idx) {
45 + this.testing = idx;
46 + this.test_results = null;
47 + try {
48 + const { callJsonApi } = await import('/js/api.js');
49 + const res = await callJsonApi('/plugins/_email_integration/test_connection', {
50 + handler: this.handlers[idx]
51 + });
52 + this.test_results = res;
53 + } catch (e) {
54 + this.test_results = { success: false, results: [{ test: 'Connection', ok: false, message: String(e) }] };
55 + }
56 + this.testing = null;
57 }
58 }">
59 <template x-if="config">
@@ -218,12 +234,37 @@
234 </div>
235 </div>
236
237 + <!-- Test connection -->
238 + <div style="margin-top: 12px; display: flex; align-items: center; gap: 12px;">
239 + <button class="btn-cancel" @click.stop="test_connection(idx)"
240 + :disabled="testing === idx"
241 + style="padding: 4px 16px; font-size: 0.85rem;">
242 + <span x-show="testing !== idx">Test Connection</span>
243 + <span x-show="testing === idx">Testing...</span>
244 + </button>
245 + </div>
246 +
247 + <!-- Test results -->
248 + <template x-if="test_results && editing === idx">
249 + <div style="margin-top: 8px; padding: 8px 12px; border-radius: 6px; font-size: 0.85rem;
250 + border: 1px solid var(--border-color, #333);">
251 + <template x-for="r in test_results.results" :key="r.test">
252 + <div style="display: flex; align-items: center; gap: 8px; padding: 4px 0;">
253 + <span x-text="r.ok ? '✓' : '✗'"
254 + :style="'font-weight: bold; color:' + (r.ok ? '#4caf50' : '#f44336')"></span>
255 + <span style="font-weight: 500; min-width: 50px;" x-text="r.test"></span>
256 + <span style="opacity: 0.8;" x-text="r.message"></span>
257 + </div>
258 + </template>
259 + </div>
260 + </template>
261 +
262 </div>
263 </template>
264 </div>
265 </template>
266
226 - <button class="btn-ok" @click="add_handler()" style="margin-top: 8px;">
267 + <button class="btn-cancel" @click="add_handler()" style="margin-top: 8px; padding: 4px 16px; font-size: 0.85rem;">
268 Add Handler
269 </button>
270 </div>