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>