tempmail_sdk 1.3.3 → 1.3.5
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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/tempmail_sdk/channels.rb +29 -7
- data/lib/tempmail_sdk/channels_data.rb +29 -7
- data/lib/tempmail_sdk/providers/altmails.rb +99 -0
- data/lib/tempmail_sdk/providers/apihz.rb +138 -0
- data/lib/tempmail_sdk/providers/awamail.rb +95 -0
- data/lib/tempmail_sdk/providers/best_temp_mail.rb +77 -0
- data/lib/tempmail_sdk/providers/chatgpt_org_uk.rb +129 -0
- data/lib/tempmail_sdk/providers/clawdemail.rb +122 -0
- data/lib/tempmail_sdk/providers/crazymailing.rb +114 -0
- data/lib/tempmail_sdk/providers/disposablemail.rb +125 -0
- data/lib/tempmail_sdk/providers/disposablemail_app.rb +66 -0
- data/lib/tempmail_sdk/providers/email10min.rb +152 -0
- data/lib/tempmail_sdk/providers/emailnator.rb +132 -0
- data/lib/tempmail_sdk/providers/emailtemp_org.rb +131 -0
- data/lib/tempmail_sdk/providers/expressinboxhub.rb +118 -0
- data/lib/tempmail_sdk/providers/fakemail.rb +143 -0
- data/lib/tempmail_sdk/providers/firetempmail.rb +86 -0
- data/lib/tempmail_sdk/providers/flybymail.rb +93 -0
- data/lib/tempmail_sdk/providers/generator_email.rb +146 -0
- data/lib/tempmail_sdk/providers/haribu.rb +29 -3
- data/lib/tempmail_sdk/providers/huskmail.rb +132 -0
- data/lib/tempmail_sdk/providers/internxt.rb +176 -0
- data/lib/tempmail_sdk/providers/linshi_xyz.rb +79 -0
- data/lib/tempmail_sdk/providers/linshiyouxiang_net.rb +85 -0
- data/lib/tempmail_sdk/providers/mail_sunls.rb +42 -1
- data/lib/tempmail_sdk/providers/mail_td.rb +161 -0
- data/lib/tempmail_sdk/providers/mailcat_ai.rb +52 -0
- data/lib/tempmail_sdk/providers/maildrop.rb +79 -4
- data/lib/tempmail_sdk/providers/mailgolem.rb +96 -0
- data/lib/tempmail_sdk/providers/mailinator.rb +9 -2
- data/lib/tempmail_sdk/providers/mailmomy.rb +1 -1
- data/lib/tempmail_sdk/providers/mailtemp_cc.rb +91 -0
- data/lib/tempmail_sdk/providers/mailticking.rb +139 -0
- data/lib/tempmail_sdk/providers/minuteinbox.rb +174 -0
- data/lib/tempmail_sdk/providers/moakt.rb +29 -3
- data/lib/tempmail_sdk/providers/mtempmail.rb +140 -0
- data/lib/tempmail_sdk/providers/mytempmail_cc.rb +76 -0
- data/lib/tempmail_sdk/providers/nowtempmail.rb +112 -0
- data/lib/tempmail_sdk/providers/noxen_de5_net.rb +392 -0
- data/lib/tempmail_sdk/providers/nukemail.rb +188 -0
- data/lib/tempmail_sdk/providers/nullmail.rb +95 -0
- data/lib/tempmail_sdk/providers/openinbox.rb +102 -0
- data/lib/tempmail_sdk/providers/shadowmail.rb +174 -0
- data/lib/tempmail_sdk/providers/shitpost_email.rb +85 -0
- data/lib/tempmail_sdk/providers/smail_pw.rb +156 -0
- data/lib/tempmail_sdk/providers/smails.rb +128 -0
- data/lib/tempmail_sdk/providers/socketio_mail.rb +25 -0
- data/lib/tempmail_sdk/providers/temp_mail_gg.rb +362 -0
- data/lib/tempmail_sdk/providers/tempgbox.rb +118 -0
- data/lib/tempmail_sdk/providers/tempmail100.rb +148 -0
- data/lib/tempmail_sdk/providers/tempmail_cn.rb +111 -0
- data/lib/tempmail_sdk/providers/tempmail_ee.rb +373 -0
- data/lib/tempmail_sdk/providers/tempmailportal.rb +131 -0
- data/lib/tempmail_sdk/providers/tempmails_io.rb +89 -0
- data/lib/tempmail_sdk/providers/tempmailto.rb +312 -0
- data/lib/tempmail_sdk/providers/temporarymail_com.rb +184 -0
- data/lib/tempmail_sdk/providers/tenmin_app.rb +100 -0
- data/lib/tempmail_sdk/providers/tenminutemail_net.rb +183 -0
- data/lib/tempmail_sdk/providers/thirty_min_email.rb +90 -0
- data/lib/tempmail_sdk/providers/tmpkit.rb +129 -0
- data/lib/tempmail_sdk/providers/twentyfourmail_chacuo.rb +83 -0
- data/lib/tempmail_sdk/providers/vip_215.rb +171 -0
- data/lib/tempmail_sdk/providers/zerodrop.rb +90 -0
- data/lib/tempmail_sdk/registry.rb +285 -23
- data/lib/tempmail_sdk/types.rb +13 -0
- data/lib/tempmail_sdk/version.rb +1 -1
- data/lib/tempmail_sdk.rb +1 -1
- metadata +58 -6
- data/lib/tempmail_sdk/providers/gonebox_email.rb +0 -89
- data/lib/tempmail_sdk/providers/temp_mail_now.rb +0 -103
- data/lib/tempmail_sdk/providers/webmailtemp.rb +0 -108
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# LinshiXYZ 渠道实现(linshi.xyz)
|
|
8
|
+
#
|
|
9
|
+
# 无建箱请求:本地随机 6 位 hex 前缀(官网 client 即用短 id)+ @linshi.xyz。
|
|
10
|
+
# 读信 GET https://linshi.xyz/api/mails/{前缀},无邮件时返回空数组 [],
|
|
11
|
+
# 有邮件时为邮件对象数组(元素含 headers{from,to,subject,date} 与 html 正文)。
|
|
12
|
+
# 归一化时将 headers 平铺并注入收件人地址;非数组骨架(如 {ok:false})整体失败,
|
|
13
|
+
# 交由上层 fallback 渠道处理。
|
|
14
|
+
module LinshiXyz
|
|
15
|
+
CHANNEL = "linshi-xyz"
|
|
16
|
+
BASE_URL = "https://linshi.xyz"
|
|
17
|
+
DOMAIN = "linshi.xyz"
|
|
18
|
+
HEX_DIGITS = "0123456789abcdef"
|
|
19
|
+
|
|
20
|
+
HEADERS = {
|
|
21
|
+
"Accept" => "application/json",
|
|
22
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
|
|
23
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# 生成本地随机 6 位 hex 前缀(与官网 client 相同格式)
|
|
29
|
+
# @return [String]
|
|
30
|
+
def local_name
|
|
31
|
+
Array.new(6) { HEX_DIGITS[rand(HEX_DIGITS.length)] }.join
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 创建 linshi.xyz 临时邮箱
|
|
35
|
+
# 无需建箱请求,本地随机 6 位 hex 前缀;token 复用完整地址。
|
|
36
|
+
# @return [EmailInfo]
|
|
37
|
+
def generate_email
|
|
38
|
+
addr = "#{local_name}@#{DOMAIN}"
|
|
39
|
+
EmailInfo.new(channel: CHANNEL, email: addr, token: addr)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 读取 linshi.xyz 收件箱
|
|
43
|
+
# GET /api/mails/{前缀}(URL 编码),响应为邮件对象数组。
|
|
44
|
+
# @param email [String] 完整邮箱地址(前缀@linshi.xyz)
|
|
45
|
+
# @param token [String] 复用完整地址
|
|
46
|
+
# @return [Array<Email>]
|
|
47
|
+
def get_emails(email, token)
|
|
48
|
+
addr = email.to_s.strip
|
|
49
|
+
raise "linshi-xyz: 邮箱地址无效: #{addr.inspect}" if addr.empty? || !addr.include?("@")
|
|
50
|
+
|
|
51
|
+
local = addr.split("@", 2).first.to_s
|
|
52
|
+
resp = Http.get("#{BASE_URL}/api/mails/#{URI.encode_www_form_component(local)}",
|
|
53
|
+
headers: HEADERS, timeout: 15)
|
|
54
|
+
raise "linshi-xyz: 读取收件箱失败 http #{resp.status_code}" unless resp.ok?
|
|
55
|
+
|
|
56
|
+
list = resp.json
|
|
57
|
+
raise "linshi-xyz: 收件箱响应非数组" unless list.is_a?(Array)
|
|
58
|
+
|
|
59
|
+
list.filter_map do |m|
|
|
60
|
+
next unless m.is_a?(Hash)
|
|
61
|
+
|
|
62
|
+
Normalize.normalize_email(normalize_row(m, addr), addr)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# 将 linshi.xyz 邮件对象归一化:headers 平铺为顶层字段并注入收件人。
|
|
67
|
+
# @param m [Hash] 原始邮件对象
|
|
68
|
+
# @param email [String] 收件人地址
|
|
69
|
+
# @return [Hash]
|
|
70
|
+
def normalize_row(m, email)
|
|
71
|
+
flat = m.dup
|
|
72
|
+
headers = m["headers"]
|
|
73
|
+
flat.merge!(headers) if headers.is_a?(Hash)
|
|
74
|
+
flat["to"] = email unless flat.key?("to")
|
|
75
|
+
flat
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# linshiyouxiang.net 渠道实现
|
|
8
|
+
#
|
|
9
|
+
# 流程:
|
|
10
|
+
# 1. GET / 获取 temp_mail cookie,从 HTML 正则提取 tempMailGlobal(邮箱)和 mailCodeGlobal(校验 code)
|
|
11
|
+
# 2. POST /get-messages {"email":"...","code":"..."} 获取邮件列表
|
|
12
|
+
# token 存储 mailCodeGlobal 的值(HMAC 哈希,后续请求用于校验)
|
|
13
|
+
module LinshiyouxiangNet
|
|
14
|
+
CHANNEL = "linshiyouxiang-net"
|
|
15
|
+
BASE_URL = "https://www.linshiyouxiang.net"
|
|
16
|
+
|
|
17
|
+
EMAIL_RE = /tempMailGlobal\s*=\s*'([^']+)'/.freeze
|
|
18
|
+
CODE_RE = /mailCodeGlobal\s*=\s*'([^']+)'/.freeze
|
|
19
|
+
|
|
20
|
+
BROWSER_HEADERS = {
|
|
21
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
22
|
+
"Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
|
|
23
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
24
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
AJAX_HEADERS = {
|
|
28
|
+
"Accept" => "application/json, text/javascript, */*; q=0.01",
|
|
29
|
+
"Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
|
|
30
|
+
"Content-Type" => "application/json",
|
|
31
|
+
"Origin" => BASE_URL,
|
|
32
|
+
"Referer" => "#{BASE_URL}/",
|
|
33
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
34
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
|
35
|
+
"X-Requested-With" => "XMLHttpRequest"
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# 创建临时邮箱
|
|
41
|
+
# @return [EmailInfo]
|
|
42
|
+
def generate_email
|
|
43
|
+
resp = Http.get("#{BASE_URL}/", headers: BROWSER_HEADERS)
|
|
44
|
+
resp.raise_for_status
|
|
45
|
+
html = resp.body
|
|
46
|
+
|
|
47
|
+
em = html.match(EMAIL_RE)
|
|
48
|
+
raise "linshiyouxiang-net: 未能从首页提取邮箱地址" unless em
|
|
49
|
+
|
|
50
|
+
email = em[1].strip
|
|
51
|
+
raise "linshiyouxiang-net: 提取的邮箱地址为空" if email.empty?
|
|
52
|
+
|
|
53
|
+
code = ""
|
|
54
|
+
if (cm = html.match(CODE_RE))
|
|
55
|
+
code = cm[1].strip
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: code)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 获取邮件列表
|
|
62
|
+
# POST /get-messages {"email":"...","code":"<token>"}
|
|
63
|
+
# @param email [String]
|
|
64
|
+
# @param token [String] mailCodeGlobal 值
|
|
65
|
+
# @return [Array<Email>]
|
|
66
|
+
def get_emails(email, token)
|
|
67
|
+
raise "linshiyouxiang-net: 邮箱地址为空" if email.to_s.strip.empty?
|
|
68
|
+
|
|
69
|
+
resp = Http.post("#{BASE_URL}/get-messages",
|
|
70
|
+
headers: AJAX_HEADERS,
|
|
71
|
+
json: { "email" => email, "code" => token.to_s })
|
|
72
|
+
resp.raise_for_status
|
|
73
|
+
data = resp.json
|
|
74
|
+
emails_raw = data["emails"]
|
|
75
|
+
return [] unless emails_raw.is_a?(Array) && !emails_raw.empty?
|
|
76
|
+
|
|
77
|
+
emails_raw.filter_map do |raw|
|
|
78
|
+
next unless raw.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
Normalize.normalize_email(raw, email)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -47,7 +47,41 @@ module TempmailSdk
|
|
|
47
47
|
EmailInfo.new(channel: CHANNEL, email: "#{local}@#{domain}")
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# 通过详情接口获取单封邮件完整正文
|
|
51
|
+
# GET /api/fetch/{id}
|
|
52
|
+
# 失败时返回 nil,调用方回退到列表数据
|
|
53
|
+
def fetch_detail(id)
|
|
54
|
+
mid = id.to_s.strip
|
|
55
|
+
return nil if mid.empty?
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
resp = Http.get("#{BASE}/api/fetch/#{URI.encode_www_form_component(mid)}",
|
|
59
|
+
headers: HEADERS, timeout: 15)
|
|
60
|
+
return nil if resp.status_code < 200 || resp.status_code >= 300
|
|
61
|
+
|
|
62
|
+
data = resp.json
|
|
63
|
+
data.is_a?(Hash) ? data : nil
|
|
64
|
+
rescue StandardError
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 从列表条目提取邮件 ID(支持多字段)
|
|
70
|
+
def extract_id(row)
|
|
71
|
+
%w[id _id mail_id messageId message_id].each do |key|
|
|
72
|
+
v = row[key]
|
|
73
|
+
next if v.nil?
|
|
74
|
+
|
|
75
|
+
return v.to_s.strip if v.is_a?(String) && !v.strip.empty?
|
|
76
|
+
return v.to_i.to_s if v.is_a?(Numeric)
|
|
77
|
+
end
|
|
78
|
+
""
|
|
79
|
+
end
|
|
80
|
+
|
|
50
81
|
# 获取邮件列表
|
|
82
|
+
# 1. GET /api/fetch?to={email} 拉取列表元数据
|
|
83
|
+
# 2. 对每封邮件 GET /api/fetch/{id} 拉取详情(含完整 text/html)
|
|
84
|
+
# 3. 详情失败时保留列表字段作为回退
|
|
51
85
|
# @param email [String]
|
|
52
86
|
# @return [Array<Email>]
|
|
53
87
|
def get_emails(email)
|
|
@@ -60,7 +94,14 @@ module TempmailSdk
|
|
|
60
94
|
return [] unless data.is_a?(Array)
|
|
61
95
|
|
|
62
96
|
data.select { |raw| raw.is_a?(Hash) }.map do |raw|
|
|
63
|
-
|
|
97
|
+
mail_id = extract_id(raw)
|
|
98
|
+
merged = raw.dup
|
|
99
|
+
# 无条件调用详情接口,用详情字段覆盖列表字段
|
|
100
|
+
unless mail_id.empty?
|
|
101
|
+
detail = fetch_detail(mail_id)
|
|
102
|
+
merged.merge!(detail) if detail
|
|
103
|
+
end
|
|
104
|
+
Normalize.normalize_email(merged, addr)
|
|
64
105
|
end
|
|
65
106
|
end
|
|
66
107
|
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module TempmailSdk
|
|
7
|
+
module Providers
|
|
8
|
+
# mail.td 渠道实现(基于 SHA-256 Proof-of-Work 的临时邮箱服务)
|
|
9
|
+
#
|
|
10
|
+
# 流程:
|
|
11
|
+
# 1. GET /api/domains 获取可用域名(过滤 pro_only)
|
|
12
|
+
# 2. 求解 PoW: SHA-256(address + timestamp + nonce) 需满足 difficulty 个前导零位
|
|
13
|
+
# 3. POST /api/accounts 携带 PoW 创建账户 → 返回 JWT + ID
|
|
14
|
+
# 4. GET /api/accounts/{id}/messages?page=1 携带 Bearer JWT 获取邮件
|
|
15
|
+
# token 格式: JSON {"jwt":"...","id":"..."}
|
|
16
|
+
module MailTd
|
|
17
|
+
CHANNEL = "mail-td"
|
|
18
|
+
API_BASE = "https://api.mail.td/api"
|
|
19
|
+
RAND_CHARS = ("a".."z").to_a + ("0".."9").to_a
|
|
20
|
+
|
|
21
|
+
HEADERS = {
|
|
22
|
+
"Accept" => "application/json",
|
|
23
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
24
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# 生成随机小写字母 + 数字字符串
|
|
30
|
+
def random_string(len)
|
|
31
|
+
Array.new(len) { RAND_CHARS.sample }.join
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 检查哈希是否满足 difficulty 个前导零位
|
|
35
|
+
def leading_zero_bits?(hash_bytes, difficulty)
|
|
36
|
+
full = difficulty / 8
|
|
37
|
+
remain = difficulty % 8
|
|
38
|
+
(0...full).each { |i| return false if hash_bytes.getbyte(i) != 0 }
|
|
39
|
+
if remain.positive? && full < hash_bytes.bytesize
|
|
40
|
+
mask = (0xFF << (8 - remain)) & 0xFF
|
|
41
|
+
return false if (hash_bytes.getbyte(full) & mask) != 0
|
|
42
|
+
end
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 求解 Proof-of-Work
|
|
47
|
+
# 目标:SHA-256(address + timestamp + nonce) 前导零位 >= difficulty
|
|
48
|
+
def solve_pow(address, timestamp, difficulty)
|
|
49
|
+
base = "#{address}#{timestamp}"
|
|
50
|
+
nonce = 0
|
|
51
|
+
while nonce < 100_000_000
|
|
52
|
+
hash = Digest::SHA256.digest("#{base}#{nonce}")
|
|
53
|
+
return nonce.to_s if leading_zero_bits?(hash, difficulty)
|
|
54
|
+
|
|
55
|
+
nonce += 1
|
|
56
|
+
end
|
|
57
|
+
raise "mail-td: PoW 求解超时"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 获取可用域名列表(过滤 pro_only)
|
|
61
|
+
def fetch_domains
|
|
62
|
+
resp = Http.get("#{API_BASE}/domains", headers: HEADERS)
|
|
63
|
+
raise "mail-td: 域名请求 HTTP #{resp.status_code}" unless resp.ok?
|
|
64
|
+
|
|
65
|
+
data = resp.json
|
|
66
|
+
domains = data["domains"] || []
|
|
67
|
+
free = domains.select { |d| d["pro_only"] == false && !d["domain"].to_s.empty? }
|
|
68
|
+
.map { |d| d["domain"] }
|
|
69
|
+
raise "mail-td: 无可用免费域名" if free.empty?
|
|
70
|
+
|
|
71
|
+
free
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# 创建临时邮箱
|
|
75
|
+
# @return [EmailInfo]
|
|
76
|
+
def generate_email
|
|
77
|
+
domain = fetch_domains.sample
|
|
78
|
+
username = random_string(10)
|
|
79
|
+
address = "#{username}@#{domain}"
|
|
80
|
+
password = random_string(20)
|
|
81
|
+
auth_key = Digest::SHA256.hexdigest(password)
|
|
82
|
+
|
|
83
|
+
addr_lower = address.strip.downcase
|
|
84
|
+
difficulty = 15
|
|
85
|
+
pow_token = ""
|
|
86
|
+
|
|
87
|
+
4.times do
|
|
88
|
+
timestamp = Time.now.to_i
|
|
89
|
+
nonce = solve_pow(addr_lower, timestamp, difficulty)
|
|
90
|
+
pow_obj = { "t" => timestamp, "n" => nonce, "d" => difficulty }
|
|
91
|
+
pow_obj["token"] = pow_token unless pow_token.empty?
|
|
92
|
+
|
|
93
|
+
resp = Http.post("#{API_BASE}/accounts",
|
|
94
|
+
headers: HEADERS.merge("Content-Type" => "application/json"),
|
|
95
|
+
json: { "address" => address, "auth_key" => auth_key, "pow" => pow_obj })
|
|
96
|
+
|
|
97
|
+
result = begin
|
|
98
|
+
resp.json
|
|
99
|
+
rescue StandardError
|
|
100
|
+
{}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if result["status"] == "retry"
|
|
104
|
+
rd = result["required_difficulty"]
|
|
105
|
+
difficulty = rd.is_a?(Numeric) ? rd.to_i : difficulty + 2
|
|
106
|
+
pow_token = result["token"].to_s
|
|
107
|
+
next
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
raise "mail-td: 创建账户 HTTP #{resp.status_code}: #{result['error']}" unless resp.ok?
|
|
111
|
+
|
|
112
|
+
result_addr = result["address"].to_s
|
|
113
|
+
result_token = result["token"].to_s
|
|
114
|
+
result_id = result["id"].to_s
|
|
115
|
+
raise "mail-td: 响应缺少必要字段" if result_addr.empty? || result_token.empty? || result_id.empty?
|
|
116
|
+
|
|
117
|
+
tok = JSON.generate("jwt" => result_token, "id" => result_id)
|
|
118
|
+
return EmailInfo.new(channel: CHANNEL, email: result_addr, token: tok)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
raise "mail-td: PoW 重试次数超限"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# 获取邮件列表
|
|
125
|
+
# @param token [String] JSON {"jwt":"...","id":"..."}
|
|
126
|
+
# @param email [String]
|
|
127
|
+
# @return [Array<Email>]
|
|
128
|
+
def get_emails(token, email)
|
|
129
|
+
raise "mail-td: token 为空" if token.to_s.empty?
|
|
130
|
+
|
|
131
|
+
parsed = JSON.parse(token)
|
|
132
|
+
jwt = parsed["jwt"].to_s
|
|
133
|
+
id = parsed["id"].to_s
|
|
134
|
+
raise "mail-td: token 缺少 jwt 或 id" if jwt.empty? || id.empty?
|
|
135
|
+
|
|
136
|
+
resp = Http.get("#{API_BASE}/accounts/#{id}/messages?page=1",
|
|
137
|
+
headers: HEADERS.merge("Authorization" => "Bearer #{jwt}"))
|
|
138
|
+
raise "mail-td: 邮件请求 HTTP #{resp.status_code}" unless resp.ok?
|
|
139
|
+
|
|
140
|
+
data = resp.json
|
|
141
|
+
messages = data["messages"]
|
|
142
|
+
return [] unless messages.is_a?(Array) && !messages.empty?
|
|
143
|
+
|
|
144
|
+
messages.map do |msg|
|
|
145
|
+
from_addr = msg.dig("from", "address").to_s
|
|
146
|
+
Normalize.normalize_email({
|
|
147
|
+
"id" => msg["id"].to_s,
|
|
148
|
+
"from" => from_addr,
|
|
149
|
+
"to" => email,
|
|
150
|
+
"subject" => msg["subject"].to_s,
|
|
151
|
+
"text" => msg["text"].to_s,
|
|
152
|
+
"html" => msg["html"].to_s,
|
|
153
|
+
"created_at" => msg["created_at"].to_s
|
|
154
|
+
}, email)
|
|
155
|
+
end
|
|
156
|
+
rescue JSON::ParserError
|
|
157
|
+
raise "mail-td: token 格式无效"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# mailcat.ai 渠道实现
|
|
6
|
+
#
|
|
7
|
+
# 流程:POST /mailboxes 创建邮箱(无 body),返回 email + token
|
|
8
|
+
# GET /inbox 获取邮件列表,需要 Authorization: Bearer {token}
|
|
9
|
+
module MailcatAi
|
|
10
|
+
CHANNEL = "mailcat-ai"
|
|
11
|
+
BASE_URL = "https://api.mailcat.ai"
|
|
12
|
+
|
|
13
|
+
HEADERS = {
|
|
14
|
+
"Accept" => "application/json",
|
|
15
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
16
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# 创建 mailcat.ai 临时邮箱
|
|
22
|
+
# @return [EmailInfo]
|
|
23
|
+
def generate_email
|
|
24
|
+
resp = Http.post("#{BASE_URL}/mailboxes", headers: HEADERS, timeout: 15)
|
|
25
|
+
resp.raise_for_status
|
|
26
|
+
data = resp.json
|
|
27
|
+
email = data.dig("data", "email").to_s.strip
|
|
28
|
+
token = data.dig("data", "token").to_s.strip
|
|
29
|
+
raise "mailcat-ai: 响应缺少 email 或 token" if email.empty? || token.empty?
|
|
30
|
+
|
|
31
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: token)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 获取 mailcat.ai 邮件列表
|
|
35
|
+
# @param token [String] Bearer token
|
|
36
|
+
# @param email [String] 邮箱地址
|
|
37
|
+
# @return [Array<Email>]
|
|
38
|
+
def get_emails(token, email)
|
|
39
|
+
hdrs = HEADERS.merge("Authorization" => "Bearer #{token.to_s.strip}")
|
|
40
|
+
resp = Http.get("#{BASE_URL}/inbox", headers: hdrs, timeout: 15)
|
|
41
|
+
resp.raise_for_status
|
|
42
|
+
data = resp.json
|
|
43
|
+
items = data.is_a?(Hash) ? Array(data["data"]) : []
|
|
44
|
+
items.filter_map do |raw|
|
|
45
|
+
next unless raw.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
Normalize.normalize_email(raw.merge("to" => email), email)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -62,6 +62,55 @@ module TempmailSdk
|
|
|
62
62
|
EmailInfo.new(channel: CHANNEL, email: email, token: email)
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
# 通过详情接口获取单封邮件完整内容
|
|
66
|
+
# GET /api/email_content.php?id={id}
|
|
67
|
+
# 详情响应字段(从前端代码确认):
|
|
68
|
+
# content: 完整 HTML 正文
|
|
69
|
+
# subject / from_addr / date: 邮件元数据
|
|
70
|
+
# attachment: JSON 字符串数组 [{filename, path, size}]
|
|
71
|
+
def fetch_detail(id)
|
|
72
|
+
mid = id.to_s.strip
|
|
73
|
+
return nil if mid.empty?
|
|
74
|
+
|
|
75
|
+
begin
|
|
76
|
+
resp = Http.get("#{BASE}/api/email_content.php?id=#{URI.encode_www_form_component(mid)}",
|
|
77
|
+
headers: HEADERS, timeout: 15)
|
|
78
|
+
return nil if resp.status_code < 200 || resp.status_code >= 300
|
|
79
|
+
|
|
80
|
+
data = resp.body.empty? ? nil : resp.json
|
|
81
|
+
data.is_a?(Hash) ? data : nil
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# 解析详情接口的 attachment 字段(JSON 字符串)为附件数组
|
|
88
|
+
def parse_attachments(raw)
|
|
89
|
+
return [] if raw.nil? || !raw.is_a?(String) || raw.strip.empty?
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
items = JSON.parse(raw)
|
|
93
|
+
rescue JSON::ParserError
|
|
94
|
+
return []
|
|
95
|
+
end
|
|
96
|
+
return [] unless items.is_a?(Array)
|
|
97
|
+
|
|
98
|
+
items.filter_map do |it|
|
|
99
|
+
next unless it.is_a?(Hash)
|
|
100
|
+
|
|
101
|
+
filename = (it["filename"] || "").to_s.strip
|
|
102
|
+
next if filename.empty?
|
|
103
|
+
|
|
104
|
+
entry = { "filename" => filename }
|
|
105
|
+
entry["size"] = it["size"].to_i if it["size"].is_a?(Numeric)
|
|
106
|
+
entry
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# 获取邮件列表并对每封邮件补拉详情
|
|
111
|
+
# 1. GET /api/emails.php 拉取列表(仅含 description 摘要)
|
|
112
|
+
# 2. 对每封邮件 GET /api/email_content.php?id={id} 拉取详情(含 content 完整 HTML)
|
|
113
|
+
# 3. 详情失败时保留列表 description 作为回退
|
|
65
114
|
# @param email [String]
|
|
66
115
|
# @param token [String, nil]
|
|
67
116
|
# @return [Array<Email>]
|
|
@@ -78,12 +127,38 @@ module TempmailSdk
|
|
|
78
127
|
return [] unless rows.is_a?(Array)
|
|
79
128
|
|
|
80
129
|
rows.select { |row| row.is_a?(Hash) }.map do |row|
|
|
130
|
+
mail_id = row["id"].to_s
|
|
131
|
+
desc = (row["description"] || "").strip
|
|
81
132
|
ir = row["isRead"]
|
|
133
|
+
is_read = ir == true || ir == 1
|
|
134
|
+
|
|
135
|
+
from = (row["from_addr"] || "").strip
|
|
136
|
+
subject = (row["subject"] || "").strip
|
|
137
|
+
date = cx_date_to_iso(row["date"] || "")
|
|
138
|
+
html = ""
|
|
139
|
+
attachments = []
|
|
140
|
+
|
|
141
|
+
# 拉取详情覆盖 html/from/subject/date/attachments
|
|
142
|
+
unless mail_id.empty?
|
|
143
|
+
detail = fetch_detail(mail_id)
|
|
144
|
+
if detail
|
|
145
|
+
d_content = detail["content"]
|
|
146
|
+
html = d_content if d_content.is_a?(String) && !d_content.strip.empty?
|
|
147
|
+
d_from = detail["from_addr"]
|
|
148
|
+
from = d_from.strip if d_from.is_a?(String) && !d_from.strip.empty?
|
|
149
|
+
d_subj = detail["subject"]
|
|
150
|
+
subject = d_subj.strip if d_subj.is_a?(String) && !d_subj.strip.empty?
|
|
151
|
+
d_date = detail["date"]
|
|
152
|
+
date = cx_date_to_iso(d_date) if d_date.is_a?(String) && !d_date.strip.empty?
|
|
153
|
+
attachments = parse_attachments(detail["attachment"])
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
82
157
|
Email.new(
|
|
83
|
-
id:
|
|
84
|
-
subject:
|
|
85
|
-
html:
|
|
86
|
-
is_read:
|
|
158
|
+
id: mail_id, from_addr: from, to: addr,
|
|
159
|
+
subject: subject, text: desc,
|
|
160
|
+
html: html, date: date,
|
|
161
|
+
is_read: is_read, attachments: attachments
|
|
87
162
|
)
|
|
88
163
|
end
|
|
89
164
|
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# mailgolem.com 渠道实现
|
|
8
|
+
#
|
|
9
|
+
# 流程:GET / 获取 session cookie + CSRF token → GET /random-email-address 创建邮箱
|
|
10
|
+
# GET / 重新获取 session + CSRF → POST /fetch-emails/{email} 获取邮件列表
|
|
11
|
+
# token 存储 CSRF token 值(收信时重新建立 session)
|
|
12
|
+
module Mailgolem
|
|
13
|
+
CHANNEL = "mailgolem"
|
|
14
|
+
BASE_URL = "https://mailgolem.com"
|
|
15
|
+
|
|
16
|
+
BROWSER_HEADERS = {
|
|
17
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
18
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
|
|
19
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif," \
|
|
20
|
+
"image/webp,image/apng,*/*;q=0.8",
|
|
21
|
+
"Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
CSRF_RE = /<input[^>]+name="_token"[^>]+id="token"[^>]+value="([^"]*)"/im
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# 从首页 HTML 提取 CSRF token
|
|
29
|
+
# @param html [String]
|
|
30
|
+
# @return [String]
|
|
31
|
+
def extract_csrf(html)
|
|
32
|
+
m = html.match(CSRF_RE)
|
|
33
|
+
m ? m[1].strip : ""
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 访问首页建立 session 并提取 CSRF token
|
|
37
|
+
# @return [String] csrf token
|
|
38
|
+
def fetch_csrf
|
|
39
|
+
resp = Http.get(BASE_URL + "/", headers: BROWSER_HEADERS, timeout: 15)
|
|
40
|
+
resp.raise_for_status
|
|
41
|
+
csrf = extract_csrf(resp.body)
|
|
42
|
+
raise "mailgolem: 未能从首页提取 CSRF token" if csrf.empty?
|
|
43
|
+
|
|
44
|
+
csrf
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# 创建 mailgolem.com 临时邮箱
|
|
48
|
+
# @return [EmailInfo]
|
|
49
|
+
def generate_email
|
|
50
|
+
csrf = fetch_csrf
|
|
51
|
+
resp = Http.get(
|
|
52
|
+
BASE_URL + "/random-email-address",
|
|
53
|
+
headers: BROWSER_HEADERS.merge("Referer" => BASE_URL + "/"),
|
|
54
|
+
timeout: 15
|
|
55
|
+
)
|
|
56
|
+
resp.raise_for_status
|
|
57
|
+
email = resp.body.to_s.strip
|
|
58
|
+
raise "mailgolem: 获取到的邮箱地址无效" if email.empty? || !email.include?("@")
|
|
59
|
+
|
|
60
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: csrf)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 获取 mailgolem.com 邮件列表
|
|
64
|
+
# @param email [String]
|
|
65
|
+
# @param token [String] 旧 CSRF token(收信时重新建立 session)
|
|
66
|
+
# @return [Array<Email>]
|
|
67
|
+
def get_emails(email, token)
|
|
68
|
+
_ = token
|
|
69
|
+
csrf = fetch_csrf
|
|
70
|
+
form = URI.encode_www_form("_token" => csrf)
|
|
71
|
+
fetch_url = "#{BASE_URL}/fetch-emails/#{URI.encode_uri_component(email)}"
|
|
72
|
+
resp = Http.post(
|
|
73
|
+
fetch_url,
|
|
74
|
+
headers: BROWSER_HEADERS.merge(
|
|
75
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
|
76
|
+
"X-Requested-With" => "XMLHttpRequest",
|
|
77
|
+
"Accept" => "application/json, text/plain, */*",
|
|
78
|
+
"Referer" => BASE_URL + "/",
|
|
79
|
+
"Origin" => BASE_URL
|
|
80
|
+
),
|
|
81
|
+
body: form,
|
|
82
|
+
timeout: 15
|
|
83
|
+
)
|
|
84
|
+
resp.raise_for_status
|
|
85
|
+
items = resp.json
|
|
86
|
+
return [] unless items.is_a?(Array)
|
|
87
|
+
|
|
88
|
+
items.filter_map do |item|
|
|
89
|
+
next unless item.is_a?(Hash)
|
|
90
|
+
|
|
91
|
+
Normalize.normalize_email(item.merge("to" => email), email)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -136,13 +136,20 @@ module TempmailSdk
|
|
|
136
136
|
|
|
137
137
|
# 将邮件摘要与正文/附件展平为 normalize 可识别的字段
|
|
138
138
|
def flatten_message(summary, recipient_email, text_payload, html_payload, att_payload)
|
|
139
|
+
# 优先读 "text" 键(/text 端点实际返回键),回退兜底 "text/plain"(防御性编程)
|
|
140
|
+
text_content = text_from_payload(text_payload, "text")
|
|
141
|
+
text_content = text_from_payload(text_payload, "text/plain") if text_content.empty?
|
|
142
|
+
# 优先读 "html" 键,回退兜底 "text/html"
|
|
143
|
+
html_content = text_from_payload(html_payload, "html")
|
|
144
|
+
html_content = text_from_payload(html_payload, "text/html") if html_content.empty?
|
|
145
|
+
|
|
139
146
|
{
|
|
140
147
|
"id" => (summary["id"] || summary["messageId"] || "").to_s,
|
|
141
148
|
"from" => first_non_empty(summary["from"], summary["origfrom"]),
|
|
142
149
|
"to" => summary["to"] || recipient_email,
|
|
143
150
|
"subject" => summary["subject"] || "",
|
|
144
|
-
"text" =>
|
|
145
|
-
"html" =>
|
|
151
|
+
"text" => text_content,
|
|
152
|
+
"html" => html_content,
|
|
146
153
|
"date" => to_iso_time(summary["time"] || summary["date"]),
|
|
147
154
|
"seen" => false,
|
|
148
155
|
"attachments" => build_attachments(att_payload)
|
|
@@ -12,7 +12,7 @@ module TempmailSdk
|
|
|
12
12
|
# 2. 本地随机 10 位 [a-z0-9] local part,拼接为 <local>@<域名>
|
|
13
13
|
# 3. GET /api/mail/messages?to=<email>&page=1&limit=20 -> {"emails": [...], ...}
|
|
14
14
|
#
|
|
15
|
-
#
|
|
15
|
+
# 域名变体渠道(如 17666688.shop、sbook.pics 等)复用同一后端,
|
|
16
16
|
# 仅在 generate_email(domain) 传入固定域名即可,收信逻辑完全一致。
|
|
17
17
|
module Mailmomy
|
|
18
18
|
CHANNEL = "mailmomy"
|