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.
- agent_code/__init__.py +2 -0
- agent_code/__main__.py +3 -0
- agent_code/agent.py +1542 -0
- agent_code/cli.py +69 -0
- agent_code/data/skills/filesystem/skill.md +0 -0
- agent_code/data/skills/filesystem/skill.py +133 -0
- agent_code/data/skills/public_api/skill.md +42 -0
- agent_code/data/skills/public_api/skill.py +105 -0
- agent_code/data/skills/temp_mail/skill.md +51 -0
- agent_code/data/skills/temp_mail/skill.py +716 -0
- agent_code/loader.py +217 -0
- agent_code/paths.py +54 -0
- agent_code/skill_manager.py +1171 -0
- agent_code-1.0.0.dist-info/METADATA +434 -0
- agent_code-1.0.0.dist-info/RECORD +20 -0
- agent_code-1.0.0.dist-info/WHEEL +5 -0
- agent_code-1.0.0.dist-info/entry_points.txt +2 -0
- agent_code-1.0.0.dist-info/licenses/LICENSE +21 -0
- agent_code-1.0.0.dist-info/licenses/LICENSE-GPL +674 -0
- agent_code-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
}
|