agent-code 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,716 @@
1
+ """temp_mail skill — multi-provider temp email: send AND receive.
2
+
3
+ RECEIVE (auto-fallback chain):
4
+ mail.tm — REST/JSON, no signup, no captcha (primary)
5
+ Guerrilla — AJAX, receive-only
6
+ PhoboMail — MCP-based, send+receive
7
+
8
+ SEND (configure one via /email):
9
+ Resend 100/day https://resend.com
10
+ Brevo 300/day https://brevo.com
11
+ Mailjet 200/day https://mailjet.com
12
+
13
+ The temp inbox is attached as Reply-To on every send.
14
+ """
15
+
16
+ import os
17
+ import re
18
+ import sys
19
+ import json
20
+ import time
21
+ import random
22
+ import string
23
+ import threading
24
+ import subprocess
25
+ from pathlib import Path
26
+
27
+
28
+ SKILL = {
29
+ "name": "temp_mail",
30
+ "description": (
31
+ "Temporary email with multi-provider send and receive. "
32
+ "get_temp_email() creates an inbox, check_temp_inbox() lists messages, "
33
+ "read_temp_email(id) reads one, send_email(to,subject,body) sends, "
34
+ "reset_temp_email() rotates, list_providers() shows providers."
35
+ ),
36
+ }
37
+
38
+
39
+ # ===========================================================================
40
+ # Paths
41
+ # ===========================================================================
42
+
43
+ WORKSPACE = Path(os.environ.get("AGENT_WORKSPACE", "workspace")).resolve()
44
+ STATE_FILE = WORKSPACE / ".temp_mail.json"
45
+ ROOT = WORKSPACE.parent
46
+ AGENT_CFG = ROOT / "agent_config.json"
47
+
48
+
49
+ def _load_state() -> dict:
50
+ if STATE_FILE.exists():
51
+ try:
52
+ return json.loads(STATE_FILE.read_text(encoding="utf-8"))
53
+ except Exception:
54
+ pass
55
+ return {}
56
+
57
+
58
+ def _save_state(state: dict):
59
+ STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
60
+ STATE_FILE.write_text(json.dumps(state, indent=2), encoding="utf-8")
61
+
62
+
63
+ # ===========================================================================
64
+ # Lazy pip install
65
+ # ===========================================================================
66
+
67
+ _import_lock = threading.Lock()
68
+ _import_cache = {}
69
+
70
+
71
+ def _try_import(name):
72
+ try:
73
+ __import__(name)
74
+ return True
75
+ except ImportError:
76
+ return False
77
+
78
+
79
+ def _pip_install(pip_name, timeout=120):
80
+ try:
81
+ r = subprocess.run(
82
+ [sys.executable, "-m", "pip", "install", "--quiet", pip_name],
83
+ capture_output=True, text=True, encoding="utf-8",
84
+ errors="replace", timeout=timeout,
85
+ )
86
+ return r.returncode == 0, ((r.stdout or "") + (r.stderr or "")).strip()
87
+ except Exception as e:
88
+ return False, str(e)
89
+
90
+
91
+ def _ensure_import(name):
92
+ with _import_lock:
93
+ if name in _import_cache:
94
+ return _import_cache[name]
95
+ if _try_import(name):
96
+ _import_cache[name] = (True, "")
97
+ return True, ""
98
+ ok, out = _pip_install(name)
99
+ if not ok:
100
+ msg = f"pip install {name} failed: {out[:300]}"
101
+ _import_cache[name] = (False, msg)
102
+ return False, msg
103
+ try:
104
+ import importlib
105
+ importlib.invalidate_caches()
106
+ except Exception:
107
+ pass
108
+ if _try_import(name):
109
+ _import_cache[name] = (True, "")
110
+ return True, ""
111
+ msg = f"{name} installed but import fails"
112
+ _import_cache[name] = (False, msg)
113
+ return False, msg
114
+
115
+
116
+ # ===========================================================================
117
+ # Email API config
118
+ # ===========================================================================
119
+
120
+ def _load_email_api():
121
+ if not AGENT_CFG.exists():
122
+ return None
123
+ try:
124
+ cfg = json.loads(AGENT_CFG.read_text(encoding="utf-8"))
125
+ except Exception:
126
+ return None
127
+ api = cfg.get("email_api")
128
+ if not api or not api.get("api_key"):
129
+ return None
130
+ return api
131
+
132
+
133
+ # ===========================================================================
134
+ # Receive: mail.tm
135
+ # ===========================================================================
136
+
137
+ MAILTM = "https://api.mail.tm"
138
+
139
+
140
+ def _mailtm_create():
141
+ ok, err = _ensure_import("requests")
142
+ if not ok:
143
+ raise RuntimeError(err)
144
+ import requests
145
+
146
+ r = requests.get(f"{MAILTM}/domains", timeout=20)
147
+ r.raise_for_status()
148
+ members = r.json().get("hydra:member", [])
149
+ domains = [d["domain"] for d in members if d.get("isActive")]
150
+ if not domains:
151
+ raise RuntimeError("mail.tm has no active domains")
152
+
153
+ local = "".join(random.choices(string.ascii_lowercase + string.digits, k=12))
154
+ email = f"{local}@{domains[0]}"
155
+ pw = "".join(random.choices(string.ascii_letters + string.digits, k=18))
156
+
157
+ r = requests.post(f"{MAILTM}/accounts",
158
+ json={"address": email, "password": pw}, timeout=20)
159
+ if r.status_code not in (200, 201):
160
+ raise RuntimeError(f"mail.tm create: {r.status_code} {r.text[:200]}")
161
+
162
+ r = requests.post(f"{MAILTM}/token",
163
+ json={"address": email, "password": pw}, timeout=20)
164
+ r.raise_for_status()
165
+ token = r.json()["token"]
166
+
167
+ return {"provider": "mail.tm", "email": email, "password": pw,
168
+ "token": token, "created": time.time()}
169
+
170
+
171
+ def _mailtm_list(state):
172
+ ok, err = _ensure_import("requests")
173
+ if not ok:
174
+ raise RuntimeError(err)
175
+ import requests
176
+ r = requests.get(f"{MAILTM}/messages",
177
+ headers={"Authorization": f"Bearer {state['token']}"},
178
+ timeout=20)
179
+ if r.status_code == 401:
180
+ raise RuntimeError("mail.tm token expired")
181
+ r.raise_for_status()
182
+ return r.json().get("hydra:member", [])
183
+
184
+
185
+ def _mailtm_read(state, msg_id):
186
+ ok, err = _ensure_import("requests")
187
+ if not ok:
188
+ raise RuntimeError(err)
189
+ import requests
190
+ r = requests.get(f"{MAILTM}/messages/{msg_id}",
191
+ headers={"Authorization": f"Bearer {state['token']}"},
192
+ timeout=20)
193
+ r.raise_for_status()
194
+ return r.json()
195
+
196
+
197
+ # ===========================================================================
198
+ # Receive: Guerrilla
199
+ # ===========================================================================
200
+
201
+ GUERRILLA = "https://api.guerrillamail.com/ajax.php"
202
+ UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
203
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
204
+ "Chrome/125.0.0.0 Safari/537.36")
205
+
206
+
207
+ def _g_create():
208
+ ok, err = _ensure_import("requests")
209
+ if not ok:
210
+ raise RuntimeError(err)
211
+ import requests
212
+ s = requests.Session()
213
+ s.headers["User-Agent"] = UA
214
+ r = s.get(GUERRILLA, params={"f": "get_email_address", "ip": "127.0.0.1",
215
+ "agent": UA}, timeout=20)
216
+ r.raise_for_status()
217
+ data = r.json()
218
+ email = data.get("email_addr")
219
+ sid = data.get("sid_token")
220
+ if not email or not sid:
221
+ raise RuntimeError(f"guerrilla: unexpected response {data}")
222
+ return {"provider": "guerrilla", "email": email, "sid_token": sid,
223
+ "cookie": s.cookies.get_dict(), "created": time.time()}
224
+
225
+
226
+ def _g_session(state):
227
+ ok, err = _ensure_import("requests")
228
+ if not ok:
229
+ raise RuntimeError(err)
230
+ import requests
231
+ s = requests.Session()
232
+ s.headers["User-Agent"] = UA
233
+ for k, v in (state.get("cookie") or {}).items():
234
+ s.cookies.set(k, v)
235
+ return s
236
+
237
+
238
+ def _g_list(state):
239
+ s = _g_session(state)
240
+ r = s.get(GUERRILLA, params={"f": "check_email", "seq": 0,
241
+ "sid_token": state["sid_token"]}, timeout=20)
242
+ r.raise_for_status()
243
+ return r.json().get("list", [])
244
+
245
+
246
+ def _g_read(state, msg_id):
247
+ s = _g_session(state)
248
+ r = s.get(GUERRILLA, params={"f": "fetch_email", "email_id": msg_id,
249
+ "sid_token": state["sid_token"]}, timeout=20)
250
+ r.raise_for_status()
251
+ return r.json()
252
+
253
+
254
+ # ===========================================================================
255
+ # Receive: PhoboMail
256
+ # ===========================================================================
257
+
258
+ PHOBO_API = "https://phobomail.com/api"
259
+
260
+
261
+ def _phobo_create():
262
+ ok, err = _ensure_import("requests")
263
+ if not ok:
264
+ raise RuntimeError(err)
265
+ import requests
266
+ r = requests.post(f"{PHOBO_API}/register", json={}, timeout=20)
267
+ r.raise_for_status()
268
+ data = r.json()
269
+ email = data.get("email") or data.get("address")
270
+ api_key = data.get("api_key") or data.get("token")
271
+ if not email:
272
+ raise RuntimeError(f"phobo: unexpected response {data}")
273
+ return {"provider": "phobomail", "email": email,
274
+ "api_key": api_key, "created": time.time()}
275
+
276
+
277
+ def _phobo_list(state):
278
+ ok, err = _ensure_import("requests")
279
+ if not ok:
280
+ raise RuntimeError(err)
281
+ import requests
282
+ r = requests.get(f"{PHOBO_API}/inbox",
283
+ headers={"Authorization": f"Bearer {state['api_key']}"},
284
+ timeout=20)
285
+ r.raise_for_status()
286
+ return r.json().get("messages", [])
287
+
288
+
289
+ def _phobo_read(state, msg_id):
290
+ ok, err = _ensure_import("requests")
291
+ if not ok:
292
+ raise RuntimeError(err)
293
+ import requests
294
+ r = requests.get(f"{PHOBO_API}/inbox/{msg_id}",
295
+ headers={"Authorization": f"Bearer {state['api_key']}"},
296
+ timeout=20)
297
+ r.raise_for_status()
298
+ return r.json()
299
+
300
+
301
+ # ===========================================================================
302
+ # Provider registry
303
+ # ===========================================================================
304
+
305
+ RECEIVE_PROVIDERS = ["mail.tm", "guerrilla", "phobomail"]
306
+
307
+ RECEIVE_HANDLERS = {
308
+ "mail.tm": {"create": _mailtm_create, "list": _mailtm_list, "read": _mailtm_read},
309
+ "guerrilla": {"create": _g_create, "list": _g_list, "read": _g_read},
310
+ "phobomail": {"create": _phobo_create, "list": _phobo_list, "read": _phobo_read},
311
+ }
312
+
313
+ SEND_PROVIDERS = {
314
+ "resend": {"name": "Resend", "free_limit": "100/day, 3000/mo", "signup": "https://resend.com"},
315
+ "brevo": {"name": "Brevo", "free_limit": "300/day, 9000/mo", "signup": "https://brevo.com"},
316
+ "mailjet": {"name": "Mailjet", "free_limit": "200/day, 6000/mo", "signup": "https://mailjet.com"},
317
+ }
318
+
319
+
320
+ def _create_inbox():
321
+ errors = []
322
+ for name in RECEIVE_PROVIDERS:
323
+ try:
324
+ state = RECEIVE_HANDLERS[name]["create"]()
325
+ print(f"[temp_mail] inbox via {name}: {state['email']}")
326
+ return state
327
+ except Exception as e:
328
+ errors.append(f"{name}: {e}")
329
+ print(f"[temp_mail] {name} failed: {e}")
330
+ raise RuntimeError("all receive providers failed:\n " + "\n ".join(errors))
331
+
332
+
333
+ def _ensure_inbox():
334
+ state = _load_state()
335
+ if state.get("email") and state.get("provider"):
336
+ return state, None
337
+ try:
338
+ state = _create_inbox()
339
+ _save_state(state)
340
+ return state, None
341
+ except Exception as e:
342
+ return None, f"ERROR creating temp inbox: {e}"
343
+
344
+
345
+ # ===========================================================================
346
+ # Send handlers
347
+ # ===========================================================================
348
+
349
+ def _send_resend(api, to, subject, body, reply_to, html=""):
350
+ ok, err = _ensure_import("requests")
351
+ if not ok:
352
+ return f"ERROR: {err}"
353
+ import requests
354
+ from_name = api.get("from_name", "Agent")
355
+ from_addr = api.get("from_addr", "onboarding@resend.dev")
356
+ payload = {
357
+ "from": f"{from_name} <{from_addr}>",
358
+ "to": [to],
359
+ "subject": subject,
360
+ "text": body,
361
+ }
362
+ if html:
363
+ payload["html"] = html
364
+ if reply_to:
365
+ payload["reply_to"] = [reply_to]
366
+ r = requests.post("https://api.resend.com/emails",
367
+ headers={"Authorization": f"Bearer {api['api_key']}",
368
+ "Content-Type": "application/json"},
369
+ json=payload, timeout=30)
370
+ if r.status_code in (200, 201, 202):
371
+ return f"OK sent via Resend from {from_addr} → {to}"
372
+ return f"ERROR: Resend {r.status_code}: {r.text[:300]}"
373
+
374
+
375
+ def _send_brevo(api, to, subject, body, reply_to, html=""):
376
+ ok, err = _ensure_import("requests")
377
+ if not ok:
378
+ return f"ERROR: {err}"
379
+ import requests
380
+ payload = {
381
+ "sender": {"email": api.get("from_addr", ""),
382
+ "name": api.get("from_name", "Agent")},
383
+ "to": [{"email": to}],
384
+ "subject": subject,
385
+ "textContent": body,
386
+ }
387
+ if html:
388
+ payload["htmlContent"] = html
389
+ if reply_to:
390
+ payload["replyTo"] = {"email": reply_to}
391
+ r = requests.post("https://api.brevo.com/v3/smtp/email",
392
+ headers={"api-key": api["api_key"],
393
+ "Content-Type": "application/json"},
394
+ json=payload, timeout=30)
395
+ if r.status_code in (200, 201, 202):
396
+ return f"OK sent via Brevo from {api.get('from_addr','?')} → {to}"
397
+ return f"ERROR: Brevo {r.status_code}: {r.text[:300]}"
398
+
399
+
400
+ def _send_mailjet(api, to, subject, body, reply_to, html=""):
401
+ ok, err = _ensure_import("requests")
402
+ if not ok:
403
+ return f"ERROR: {err}"
404
+ import requests
405
+ payload = {
406
+ "From": {"Email": api.get("from_addr", ""),
407
+ "Name": api.get("from_name", "Agent")},
408
+ "To": [{"email": to}],
409
+ "Subject": subject,
410
+ "TextPart": body,
411
+ }
412
+ if html:
413
+ payload["HTMLPart"] = html
414
+ if reply_to:
415
+ payload["ReplyTo"] = {"email": reply_to}
416
+ r = requests.post("https://api.mailjet.com/v3.1/send",
417
+ auth=(api.get("username", ""), api["api_key"]),
418
+ json=payload, timeout=30)
419
+ if r.status_code in (200, 201):
420
+ return f"OK sent via Mailjet from {api.get('from_addr','?')} → {to}"
421
+ return f"ERROR: Mailjet {r.status_code}: {r.text[:300]}"
422
+
423
+
424
+ SEND_HANDLERS = {
425
+ "resend": _send_resend,
426
+ "brevo": _send_brevo,
427
+ "mailjet": _send_mailjet,
428
+ }
429
+
430
+
431
+ # ===========================================================================
432
+ # Tools
433
+ # ===========================================================================
434
+
435
+ def get_temp_email():
436
+ state, err = _ensure_inbox()
437
+ if err:
438
+ return err
439
+ api = _load_email_api()
440
+ send_info = (f"{api.get('provider')} (from {api.get('from_addr')})"
441
+ if api else "NOT configured — run /email")
442
+ return (f"Your temporary email: {state['email']}\n"
443
+ f"receive provider: {state['provider']}\n"
444
+ f"send provider: {send_info}\n"
445
+ f"Replies land in this inbox.")
446
+
447
+
448
+ def reset_temp_email():
449
+ _save_state({})
450
+ return get_temp_email()
451
+
452
+
453
+ def list_providers():
454
+ lines = ["RECEIVE (auto chain):"]
455
+ for name in RECEIVE_PROVIDERS:
456
+ lines.append(f" {name}")
457
+ lines.append("")
458
+ lines.append("SEND (configure via /email):")
459
+ for key, p in SEND_PROVIDERS.items():
460
+ lines.append(f" {key:<10} {p['name']:<10} {p['free_limit']:<20} {p['signup']}")
461
+ api = _load_email_api()
462
+ lines.append("")
463
+ lines.append(f"Active send provider: {api.get('provider') if api else '(none)'}")
464
+ return "\n".join(lines)
465
+
466
+
467
+ COMMON_TYPOS = {
468
+ "gmaill.com": "gmail.com", "gmial.com": "gmail.com",
469
+ "gamil.com": "gmail.com", "gmail.co": "gmail.com", "gmail.cm": "gmail.com",
470
+ "hotmial.com": "hotmail.com", "hotmai.com": "hotmail.com",
471
+ "yaho.com": "yahoo.com", "yahooo.com": "yahoo.com",
472
+ "outlok.com": "outlook.com", "outllook.com": "outlook.com",
473
+ }
474
+
475
+ EMAIL_RE = re.compile(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$")
476
+
477
+
478
+ def send_email(to: str, subject: str, body: str, html: str = ""):
479
+ to = (to or "").strip().lower()
480
+ if not EMAIL_RE.match(to):
481
+ return f"ERROR: invalid email address: {to!r}"
482
+ domain = to.split("@", 1)[1]
483
+ if domain in COMMON_TYPOS:
484
+ return (f"ERROR: '{domain}' looks like a typo. "
485
+ f"Did you mean '{COMMON_TYPOS[domain]}'? "
486
+ f"Fix and retry — do NOT resend to the typo.")
487
+
488
+ api = _load_email_api()
489
+ if not api:
490
+ return (
491
+ "ERROR: email sending not configured.\n\n"
492
+ "Pick one free provider (no captcha):\n"
493
+ " Resend — https://resend.com (100/day)\n"
494
+ " Brevo — https://brevo.com (300/day)\n"
495
+ " Mailjet — https://mailjet.com (200/day)\n\n"
496
+ "Then run one of:\n"
497
+ " /email resend re_YOUR_KEY\n"
498
+ " /email brevo xkeysib-YOUR_KEY you@yourdomain.com\n"
499
+ " /email mailjet USERNAME SECRET you@yourdomain.com\n"
500
+ "Do NOT retry send_email until /email is configured."
501
+ )
502
+
503
+ provider = (api.get("provider") or "resend").lower()
504
+ fn = SEND_HANDLERS.get(provider)
505
+ if not fn:
506
+ return f"ERROR: unknown send provider: {provider}"
507
+
508
+ state = _load_state()
509
+ reply_to = state.get("email") if state else None
510
+ return fn(api, to, subject, body, reply_to, html)
511
+
512
+
513
+ def check_temp_inbox():
514
+ state, err = _ensure_inbox()
515
+ if err:
516
+ return err
517
+ provider = state.get("provider")
518
+ handler = RECEIVE_HANDLERS.get(provider)
519
+ if not handler:
520
+ return f"ERROR: unknown provider {provider}"
521
+ try:
522
+ msgs = handler["list"](state)
523
+ if not msgs:
524
+ return f"Inbox {state['email']} is empty."
525
+ lines = [f"Inbox {state['email']} ({provider}) — {len(msgs)} message(s):"]
526
+ for m in msgs[:15]:
527
+ if provider == "mail.tm":
528
+ mid = m.get("id", "?")
529
+ frm = (m.get("from") or {}).get("address", "?")
530
+ subj = m.get("subject", "?")
531
+ elif provider == "guerrilla":
532
+ mid = m.get("mail_id", "?")
533
+ frm = m.get("mail_from", "?")
534
+ subj = m.get("mail_subject", "?")
535
+ else:
536
+ mid = m.get("id", "?")
537
+ frm = m.get("from", "?")
538
+ subj = m.get("subject", "?")
539
+ lines.append(f" id={mid} | from={frm} | subject={subj}")
540
+ return "\n".join(lines)
541
+ except Exception as e:
542
+ _save_state({})
543
+ return f"ERROR: {provider} failed ({e}). State cleared — call get_temp_email."
544
+
545
+
546
+ def read_temp_email(email_id: str):
547
+ state = _load_state()
548
+ provider = state.get("provider")
549
+ handler = RECEIVE_HANDLERS.get(provider)
550
+ if not handler:
551
+ return "ERROR: no inbox — call get_temp_email first"
552
+ try:
553
+ m = handler["read"](state, email_id)
554
+ if provider == "mail.tm":
555
+ text = m.get("text") or ""
556
+ if not text and m.get("html"):
557
+ text = re.sub(r"<[^>]+>", " ", m["html"])
558
+ text = re.sub(r"\s+", " ", text).strip()
559
+ frm = (m.get("from") or {}).get("address", "?")
560
+ return f"From: {frm}\nSubject: {m.get('subject','?')}\n---\n{text[:3000]}"
561
+ if provider == "guerrilla":
562
+ text = m.get("mail_body") or ""
563
+ text = re.sub(r"<[^>]+>", " ", text)
564
+ text = re.sub(r"\s+", " ", text).strip()
565
+ return (f"From: {m.get('mail_from','?')}\n"
566
+ f"Subject: {m.get('mail_subject','?')}\n---\n{text[:3000]}")
567
+ text = m.get("text") or m.get("body") or ""
568
+ if not text and m.get("html"):
569
+ text = re.sub(r"<[^>]+>", " ", m["html"])
570
+ text = re.sub(r"\s+", " ", text).strip()
571
+ return (f"From: {m.get('from','?')}\n"
572
+ f"Subject: {m.get('subject','?')}\n---\n{text[:3000]}")
573
+ except Exception as e:
574
+ return f"ERROR reading email: {e}"
575
+
576
+
577
+ # ===========================================================================
578
+ # OpenAI schemas
579
+ # ===========================================================================
580
+
581
+ TOOL_SCHEMAS = [
582
+ {"type": "function", "function": {
583
+ "name": "get_temp_email",
584
+ "description": "Get (or create) a temporary email address for receiving.",
585
+ "parameters": {"type": "object", "properties": {}}}},
586
+ {"type": "function", "function": {
587
+ "name": "send_email",
588
+ "description": (
589
+ "Send an email via the configured provider (Resend/Brevo/Mailjet). "
590
+ "The temp inbox is set as Reply-To. If not configured, returns "
591
+ "setup instructions — do NOT retry."
592
+ ),
593
+ "parameters": {"type": "object",
594
+ "properties": {"to": {"type": "string"},
595
+ "subject": {"type": "string"},
596
+ "body": {"type": "string"},
597
+ "html": {"type": "string"}},
598
+ "required": ["to", "subject", "body"]}}},
599
+ {"type": "function", "function": {
600
+ "name": "check_temp_inbox",
601
+ "description": "List messages in the temp inbox.",
602
+ "parameters": {"type": "object", "properties": {}}}},
603
+ {"type": "function", "function": {
604
+ "name": "read_temp_email",
605
+ "description": "Read a specific email by ID.",
606
+ "parameters": {"type": "object",
607
+ "properties": {"email_id": {"type": "string"}},
608
+ "required": ["email_id"]}}},
609
+ {"type": "function", "function": {
610
+ "name": "reset_temp_email",
611
+ "description": "Delete the current inbox and create a fresh one.",
612
+ "parameters": {"type": "object", "properties": {}}}},
613
+ {"type": "function", "function": {
614
+ "name": "list_providers",
615
+ "description": "Show available send and receive email providers.",
616
+ "parameters": {"type": "object", "properties": {}}}},
617
+ ]
618
+
619
+ TOOL_CALLABLES = {
620
+ "get_temp_email": get_temp_email,
621
+ "send_email": send_email,
622
+ "check_temp_inbox": check_temp_inbox,
623
+ "read_temp_email": read_temp_email,
624
+ "reset_temp_email": reset_temp_email,
625
+ "list_providers": list_providers,
626
+ }
627
+
628
+
629
+ # ===========================================================================
630
+ # Chat commands
631
+ # ===========================================================================
632
+
633
+ def _handle_email_command(args: list, log) -> None:
634
+ """Handle /email. args = tokens after '/email'. log(text, tag=None)."""
635
+ def load_cfg() -> dict:
636
+ if AGENT_CFG.exists():
637
+ try:
638
+ return json.loads(AGENT_CFG.read_text(encoding="utf-8"))
639
+ except Exception:
640
+ pass
641
+ return {}
642
+
643
+ def save_cfg(c: dict):
644
+ AGENT_CFG.write_text(json.dumps(c, indent=2), encoding="utf-8")
645
+
646
+ if not args:
647
+ cfg = load_cfg()
648
+ api = cfg.get("email_api")
649
+ if api:
650
+ log(f"send provider: {api.get('provider')} from {api.get('from_addr')}", "dim")
651
+ else:
652
+ log("send provider: NOT configured", "dim")
653
+ log("setup examples:", "dim")
654
+ log(" /email resend re_YOUR_KEY", "dim")
655
+ log(" /email brevo xkeysib-KEY you@domain.com", "dim")
656
+ log(" /email mailjet USER SECRET you@domain.com", "dim")
657
+ log("signup links:", "dim")
658
+ log(" Resend 100/day https://resend.com", "dim")
659
+ log(" Brevo 300/day https://brevo.com", "dim")
660
+ log(" Mailjet 200/day https://mailjet.com", "dim")
661
+ return
662
+
663
+ provider = args[0].lower()
664
+ cfg = load_cfg()
665
+
666
+ if provider == "resend" and len(args) >= 2:
667
+ cfg["email_api"] = {
668
+ "provider": "resend",
669
+ "api_key": args[1],
670
+ "from_addr": args[2] if len(args) >= 3 else "onboarding@resend.dev",
671
+ "from_name": "Agent",
672
+ }
673
+ elif provider == "brevo" and len(args) >= 3:
674
+ cfg["email_api"] = {
675
+ "provider": "brevo",
676
+ "api_key": args[1],
677
+ "from_addr": args[2],
678
+ "from_name": "Agent",
679
+ }
680
+ elif provider == "mailjet" and len(args) >= 4:
681
+ cfg["email_api"] = {
682
+ "provider": "mailjet",
683
+ "api_key": args[3],
684
+ "username": args[1],
685
+ "from_addr": args[3],
686
+ "from_name": "Agent",
687
+ }
688
+ else:
689
+ log("usage:", "error")
690
+ log(" /email resend <key> [from@addr]", "error")
691
+ log(" /email brevo <key> <from@addr>", "error")
692
+ log(" /email mailjet <username> <secret> <from@addr>", "error")
693
+ return
694
+
695
+ try:
696
+ save_cfg(cfg)
697
+ log(f"[email] saved — {provider} / {cfg['email_api']['from_addr']}", "dim")
698
+ except Exception as e:
699
+ log(f"[email] error: {e}", "error")
700
+
701
+
702
+ def _handle_providers_command(args: list, log) -> None:
703
+ for line in list_providers().splitlines():
704
+ log(line, "dim")
705
+
706
+
707
+ COMMANDS = {
708
+ "email": {
709
+ "handler": _handle_email_command,
710
+ "description": "Configure email sending (Resend/Brevo/Mailjet)",
711
+ },
712
+ "providers": {
713
+ "handler": _handle_providers_command,
714
+ "description": "List email send + receive providers",
715
+ },
716
+ }