move132 commited on
Commit ·
5f9cdb7
1
Parent(s): 3ad09d8
add moemail provider
Browse files
services/register/mail_provider.py
CHANGED
|
@@ -361,6 +361,61 @@ class GptMailProvider(BaseMailProvider):
|
|
| 361 |
self.session.close()
|
| 362 |
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
def _entries(mail_config: dict) -> list[dict]:
|
| 365 |
return [{**item, "provider_ref": f"{item['type']}#{index + 1}"} for index, item in enumerate(mail_config["providers"])]
|
| 366 |
|
|
@@ -395,6 +450,8 @@ def _create_provider(mail_config: dict, provider: str = "", provider_ref: str =
|
|
| 395 |
return DuckMailProvider(entry, conf)
|
| 396 |
if entry["type"] == "gptmail":
|
| 397 |
return GptMailProvider(entry, conf)
|
|
|
|
|
|
|
| 398 |
raise RuntimeError(f"不支持的 mail.provider: {entry['type']}")
|
| 399 |
|
| 400 |
|
|
|
|
| 361 |
self.session.close()
|
| 362 |
|
| 363 |
|
| 364 |
+
class MoEmailProvider(BaseMailProvider):
|
| 365 |
+
name = "moemail"
|
| 366 |
+
|
| 367 |
+
def __init__(self, entry: dict, conf: dict):
|
| 368 |
+
super().__init__(conf, str(entry.get("provider_ref") or ""))
|
| 369 |
+
self.api_base = str(entry["api_base"]).rstrip("/")
|
| 370 |
+
self.api_key = str(entry["api_key"]).strip()
|
| 371 |
+
raw_domains = entry.get("domain") or []
|
| 372 |
+
if isinstance(raw_domains, list):
|
| 373 |
+
self.domain = [str(item).strip() for item in raw_domains if str(item).strip()]
|
| 374 |
+
else:
|
| 375 |
+
self.domain = [str(raw_domains).strip()] if str(raw_domains).strip() else []
|
| 376 |
+
self.expiry_time = int(entry.get("expiry_time") or 0)
|
| 377 |
+
self.session = curl_requests.Session(impersonate="chrome")
|
| 378 |
+
|
| 379 |
+
def _request(self, method: str, path: str, params: dict | None = None, payload: dict | None = None, expected: tuple[int, ...] = (200,)):
|
| 380 |
+
resp = self.session.request(method.upper(), f"{self.api_base}{path}", headers={"X-API-Key": self.api_key, "Content-Type": "application/json", "User-Agent": self.conf["user_agent"]}, params=params, json=payload, timeout=self.conf["request_timeout"], verify=False)
|
| 381 |
+
if resp.status_code not in expected:
|
| 382 |
+
raise RuntimeError(f"MoEmail 请求失败: {method} {path}, HTTP {resp.status_code}, body={resp.text[:300]}")
|
| 383 |
+
data = resp.json()
|
| 384 |
+
if not isinstance(data, dict):
|
| 385 |
+
raise RuntimeError(f"MoEmail {method} {path} 返回结构不是对象")
|
| 386 |
+
return data
|
| 387 |
+
|
| 388 |
+
def create_mailbox(self, username: str | None = None) -> dict[str, Any]:
|
| 389 |
+
data = self._request("POST", "/api/emails/generate", payload={"name": username or _random_mailbox_name(), "expiryTime": self.expiry_time, "domain": _next_domain(self.domain)}, expected=(200, 201))
|
| 390 |
+
address = str(data.get("email") or "").strip()
|
| 391 |
+
email_id = str(data.get("id") or data.get("email_id") or "").strip()
|
| 392 |
+
if not address or not email_id:
|
| 393 |
+
raise RuntimeError("MoEmail 缺少 email 或 id")
|
| 394 |
+
return {"provider": self.name, "provider_ref": self.provider_ref, "address": address, "email_id": email_id}
|
| 395 |
+
|
| 396 |
+
def fetch_latest_message(self, mailbox: dict[str, Any]) -> dict[str, Any] | None:
|
| 397 |
+
email_id = str(mailbox.get("email_id") or "").strip()
|
| 398 |
+
if not email_id:
|
| 399 |
+
raise RuntimeError("MoEmail 缺少 email_id")
|
| 400 |
+
data = self._request("GET", f"/api/emails/{email_id}")
|
| 401 |
+
items = data.get("messages") or []
|
| 402 |
+
messages = [item for item in items if isinstance(item, dict)] if isinstance(items, list) else []
|
| 403 |
+
if not messages:
|
| 404 |
+
return None
|
| 405 |
+
_, item = max(enumerate(messages), key=lambda pair: (((_parse_received_at(pair[1].get("createdAt") or pair[1].get("created_at") or pair[1].get("receivedAt") or pair[1].get("date") or pair[1].get("timestamp")) or datetime.fromtimestamp(0, tz=timezone.utc)).timestamp()), pair[0]))
|
| 406 |
+
message_id = str(item.get("id") or item.get("message_id") or item.get("_id") or "").strip()
|
| 407 |
+
detail = self._request("GET", f"/api/emails/{email_id}/{message_id}") if message_id else {"message": item}
|
| 408 |
+
message = detail.get("message") if isinstance(detail.get("message"), dict) else detail
|
| 409 |
+
text_content, html_content = _extract_content(message)
|
| 410 |
+
sender = message.get("from") or message.get("sender") or ""
|
| 411 |
+
if isinstance(sender, dict):
|
| 412 |
+
sender = sender.get("address") or sender.get("email") or sender.get("name") or ""
|
| 413 |
+
return {"provider": self.name, "mailbox": mailbox["address"], "message_id": message_id, "subject": str(message.get("subject") or item.get("subject") or ""), "sender": str(sender), "text_content": text_content, "html_content": html_content, "received_at": _parse_received_at(message.get("createdAt") or message.get("created_at") or message.get("receivedAt") or message.get("date") or message.get("timestamp") or item.get("createdAt") or item.get("created_at") or item.get("receivedAt") or item.get("date") or item.get("timestamp")), "raw": detail}
|
| 414 |
+
|
| 415 |
+
def close(self) -> None:
|
| 416 |
+
self.session.close()
|
| 417 |
+
|
| 418 |
+
|
| 419 |
def _entries(mail_config: dict) -> list[dict]:
|
| 420 |
return [{**item, "provider_ref": f"{item['type']}#{index + 1}"} for index, item in enumerate(mail_config["providers"])]
|
| 421 |
|
|
|
|
| 450 |
return DuckMailProvider(entry, conf)
|
| 451 |
if entry["type"] == "gptmail":
|
| 452 |
return GptMailProvider(entry, conf)
|
| 453 |
+
if entry["type"] == "moemail":
|
| 454 |
+
return MoEmailProvider(entry, conf)
|
| 455 |
raise RuntimeError(f"不支持的 mail.provider: {entry['type']}")
|
| 456 |
|
| 457 |
|
web/src/app/register/components/register-card.tsx
CHANGED
|
@@ -49,6 +49,7 @@ export function RegisterCard() {
|
|
| 49 |
enable: true,
|
| 50 |
...(type === "cloudflare_temp_email" ? { api_base: "", admin_password: "", domain: [] } : {}),
|
| 51 |
...(type === "tempmail_lol" ? { api_key: "", domain: [] } : {}),
|
|
|
|
| 52 |
...(type === "duckmail" ? { api_key: "", default_domain: "duckmail.sbs" } : {}),
|
| 53 |
...(type === "gptmail" ? { api_key: "", default_domain: "" } : {}),
|
| 54 |
});
|
|
@@ -165,24 +166,27 @@ export function RegisterCard() {
|
|
| 165 |
<SelectContent>
|
| 166 |
<SelectItem value="cloudflare_temp_email">cloudflare_temp_email</SelectItem>
|
| 167 |
<SelectItem value="tempmail_lol">tempmail_lol</SelectItem>
|
|
|
|
| 168 |
<SelectItem value="duckmail">duckmail</SelectItem>
|
| 169 |
<SelectItem value="gptmail">gptmail(未测试)</SelectItem>
|
| 170 |
</SelectContent>
|
| 171 |
</Select>
|
| 172 |
</div>
|
| 173 |
-
{type === "cloudflare_temp_email" ? (
|
| 174 |
<>
|
| 175 |
<div className="space-y-2">
|
| 176 |
<label className="text-sm text-stone-700">API Base</label>
|
| 177 |
<Input value={String(provider.api_base || "")} onChange={(event) => updateProvider(index, { api_base: event.target.value })} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
| 178 |
</div>
|
| 179 |
-
|
| 180 |
-
<
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
| 183 |
</>
|
| 184 |
) : null}
|
| 185 |
-
{type === "tempmail_lol" || type === "duckmail" || type === "gptmail" ? (
|
| 186 |
<div className="space-y-2">
|
| 187 |
<label className="text-sm text-stone-700">API Key</label>
|
| 188 |
<Input value={String(provider.api_key || "")} onChange={(event) => updateProvider(index, { api_key: event.target.value })} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
|
@@ -196,10 +200,10 @@ export function RegisterCard() {
|
|
| 196 |
) : null}
|
| 197 |
</div>
|
| 198 |
|
| 199 |
-
{type === "tempmail_lol" || type === "cloudflare_temp_email" ? (
|
| 200 |
<div className="space-y-2">
|
| 201 |
<label className="text-sm text-stone-700">Domain</label>
|
| 202 |
-
<Textarea value={domains} onChange={(event) => updateProvider(index, { domain: event.target.value.split(/[\n,]/).map((item) => item.trim()).filter(Boolean) })} placeholder="每行一个域名,留空则使用服务默认域名" className="min-h-20 rounded-xl border-stone-200 bg-white font-mono text-xs" disabled={config.enabled} />
|
| 203 |
</div>
|
| 204 |
) : null}
|
| 205 |
</div>
|
|
|
|
| 49 |
enable: true,
|
| 50 |
...(type === "cloudflare_temp_email" ? { api_base: "", admin_password: "", domain: [] } : {}),
|
| 51 |
...(type === "tempmail_lol" ? { api_key: "", domain: [] } : {}),
|
| 52 |
+
...(type === "moemail" ? { api_base: "", api_key: "", domain: [] } : {}),
|
| 53 |
...(type === "duckmail" ? { api_key: "", default_domain: "duckmail.sbs" } : {}),
|
| 54 |
...(type === "gptmail" ? { api_key: "", default_domain: "" } : {}),
|
| 55 |
});
|
|
|
|
| 166 |
<SelectContent>
|
| 167 |
<SelectItem value="cloudflare_temp_email">cloudflare_temp_email</SelectItem>
|
| 168 |
<SelectItem value="tempmail_lol">tempmail_lol</SelectItem>
|
| 169 |
+
<SelectItem value="moemail">moemail</SelectItem>
|
| 170 |
<SelectItem value="duckmail">duckmail</SelectItem>
|
| 171 |
<SelectItem value="gptmail">gptmail(未测试)</SelectItem>
|
| 172 |
</SelectContent>
|
| 173 |
</Select>
|
| 174 |
</div>
|
| 175 |
+
{type === "cloudflare_temp_email" || type === "moemail" ? (
|
| 176 |
<>
|
| 177 |
<div className="space-y-2">
|
| 178 |
<label className="text-sm text-stone-700">API Base</label>
|
| 179 |
<Input value={String(provider.api_base || "")} onChange={(event) => updateProvider(index, { api_base: event.target.value })} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
| 180 |
</div>
|
| 181 |
+
{type === "cloudflare_temp_email" ? (
|
| 182 |
+
<div className="space-y-2">
|
| 183 |
+
<label className="text-sm text-stone-700">Admin Password</label>
|
| 184 |
+
<Input value={String(provider.admin_password || "")} onChange={(event) => updateProvider(index, { admin_password: event.target.value })} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
| 185 |
+
</div>
|
| 186 |
+
) : null}
|
| 187 |
</>
|
| 188 |
) : null}
|
| 189 |
+
{type === "tempmail_lol" || type === "moemail" || type === "duckmail" || type === "gptmail" ? (
|
| 190 |
<div className="space-y-2">
|
| 191 |
<label className="text-sm text-stone-700">API Key</label>
|
| 192 |
<Input value={String(provider.api_key || "")} onChange={(event) => updateProvider(index, { api_key: event.target.value })} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
|
|
|
| 200 |
) : null}
|
| 201 |
</div>
|
| 202 |
|
| 203 |
+
{type === "tempmail_lol" || type === "cloudflare_temp_email" || type === "moemail" ? (
|
| 204 |
<div className="space-y-2">
|
| 205 |
<label className="text-sm text-stone-700">Domain</label>
|
| 206 |
+
<Textarea value={domains} onChange={(event) => updateProvider(index, { domain: event.target.value.split(/[\n,]/).map((item) => item.trim()).filter(Boolean) })} placeholder={type === "moemail" ? "每行一个域名" : "每行一个域名,留空则使用服务默认域名"} className="min-h-20 rounded-xl border-stone-200 bg-white font-mono text-xs" disabled={config.enabled} />
|
| 207 |
</div>
|
| 208 |
) : null}
|
| 209 |
</div>
|