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
- <div className="space-y-2">
180
- <label className="text-sm text-stone-700">Admin Password</label>
181
- <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} />
182
- </div>
 
 
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>