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,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# smails.dev 渠道实现
|
|
8
|
+
#
|
|
9
|
+
# 流程:
|
|
10
|
+
# POST /api/mailbox 建箱(空 JSON body,响应 address/token)
|
|
11
|
+
# GET /api/mailbox/messages 读信(Header Authorization: Bearer <token>,数组响应)
|
|
12
|
+
# GET /api/mailbox/messages/{id} 取单封详情(Bearer)
|
|
13
|
+
# 列表元素按 from/subject/text/html/date 多候选归一;详情拉取失败时以列表摘要兜底。
|
|
14
|
+
module Smails
|
|
15
|
+
CHANNEL = "smails"
|
|
16
|
+
BASE_URL = "https://smails.dev"
|
|
17
|
+
|
|
18
|
+
HEADERS = {
|
|
19
|
+
"Accept" => "application/json",
|
|
20
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
21
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# 可行 ID 候选键,按序提取(结果转为字符串)
|
|
25
|
+
ID_KEYS = %w[id Id slug messageId message_id].freeze
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# 设置 smails.dev 请求的通用请求头
|
|
30
|
+
# @param token [String] 认证令牌,非空时携带 Authorization: Bearer
|
|
31
|
+
# @return [Hash]
|
|
32
|
+
def auth_headers(token = "")
|
|
33
|
+
hdrs = HEADERS.dup
|
|
34
|
+
hdrs["Authorization"] = "Bearer #{token}" unless token.to_s.empty?
|
|
35
|
+
hdrs
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 创建 smails.dev 临时邮箱
|
|
39
|
+
# POST /api/mailbox(空 JSON body)返回 address 与 token
|
|
40
|
+
# @return [EmailInfo]
|
|
41
|
+
def generate_email
|
|
42
|
+
resp = Http.post("#{BASE_URL}/api/mailbox",
|
|
43
|
+
headers: HEADERS.merge("Content-Type" => "application/json"),
|
|
44
|
+
json: {}, timeout: 15)
|
|
45
|
+
resp.raise_for_status
|
|
46
|
+
data = resp.json
|
|
47
|
+
address = data.is_a?(Hash) ? data["address"].to_s.strip : ""
|
|
48
|
+
token = data.is_a?(Hash) ? data["token"].to_s.strip : ""
|
|
49
|
+
raise "smails: 创建邮箱响应缺少必要字段" if address.empty? || token.empty?
|
|
50
|
+
|
|
51
|
+
EmailInfo.new(channel: CHANNEL, email: address, token: token)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# 获取 smails.dev 邮件列表
|
|
55
|
+
# GET /api/mailbox/messages;对每个元素按 id 逐封请求详情合并,失败时以摘要归一。
|
|
56
|
+
# @param token [String] Bearer 令牌
|
|
57
|
+
# @param email [String] 邮箱地址
|
|
58
|
+
# @return [Array<Email>]
|
|
59
|
+
def get_emails(token, email)
|
|
60
|
+
tk = token.to_s.strip
|
|
61
|
+
raise "smails: token 为空" if tk.empty?
|
|
62
|
+
|
|
63
|
+
resp = Http.get("#{BASE_URL}/api/mailbox/messages", headers: auth_headers(tk), timeout: 15)
|
|
64
|
+
resp.raise_for_status
|
|
65
|
+
list = resp.json
|
|
66
|
+
return [] unless list.is_a?(Array)
|
|
67
|
+
|
|
68
|
+
addr = email.to_s.strip
|
|
69
|
+
list.filter_map do |m|
|
|
70
|
+
next unless m.is_a?(Hash)
|
|
71
|
+
|
|
72
|
+
Normalize.normalize_email(detail_row(tk, m, addr), addr)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 从列表元素中提取消息 ID(候选键 id/Id/slug/messageId/message_id)
|
|
77
|
+
# @param row [Hash]
|
|
78
|
+
# @return [String]
|
|
79
|
+
def message_id_of(row)
|
|
80
|
+
ID_KEYS.each do |key|
|
|
81
|
+
val = row[key]
|
|
82
|
+
return val.to_s.strip unless val.nil? || val.to_s.strip.empty?
|
|
83
|
+
end
|
|
84
|
+
""
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# 逐封拉取详情并合并到列表摘要;无 ID 或详情失败时以摘要归一
|
|
88
|
+
# @param token [String] Bearer 令牌
|
|
89
|
+
# @param summary [Hash] 列表元素摘要
|
|
90
|
+
# @param email [String] 收件人邮箱
|
|
91
|
+
# @return [Hash] 归一化输入行
|
|
92
|
+
def detail_row(token, summary, email)
|
|
93
|
+
id = message_id_of(summary)
|
|
94
|
+
uri = "#{BASE_URL}/api/mailbox/messages/#{URI.encode_www_form_component(id)}"
|
|
95
|
+
resp = Http.get(uri, headers: auth_headers(token), timeout: 15)
|
|
96
|
+
merged = summary
|
|
97
|
+
if resp.ok?
|
|
98
|
+
detail = begin
|
|
99
|
+
resp.json
|
|
100
|
+
rescue StandardError
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
# 详情缺失字段不覆盖列表摘要(包含 null 的键跳过)
|
|
104
|
+
merged = summary.merge(detail.reject { |_k, v| v.nil? }) if detail.is_a?(Hash)
|
|
105
|
+
end
|
|
106
|
+
to_normalized_row(merged, email)
|
|
107
|
+
rescue StandardError
|
|
108
|
+
to_normalized_row(summary, email)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# 将原始消息 Hash 转换为归一化输入行
|
|
112
|
+
# @param raw [Hash]
|
|
113
|
+
# @param email [String]
|
|
114
|
+
# @return [Hash]
|
|
115
|
+
def to_normalized_row(raw, email)
|
|
116
|
+
{
|
|
117
|
+
"id" => raw["id"] || raw["messageId"] || raw["message_id"],
|
|
118
|
+
"from" => raw["from"],
|
|
119
|
+
"to" => email,
|
|
120
|
+
"subject" => raw["subject"],
|
|
121
|
+
"text" => raw["text"],
|
|
122
|
+
"html" => raw["html"],
|
|
123
|
+
"date" => raw["date"]
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# socketio-mail 渠道桩实现
|
|
6
|
+
#
|
|
7
|
+
# 该渠道依赖 WebSocket/Socket.IO 协议,Ruby 标准 HTTP 客户端无法实现。
|
|
8
|
+
# 当前注册为桩,调用时抛出 NotImplementedProviderError。
|
|
9
|
+
module SocketioMail
|
|
10
|
+
CHANNEL = "socketio-mail"
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @raise [NotImplementedError]
|
|
15
|
+
def generate_email
|
|
16
|
+
raise NotImplementedProviderError, "channel not implemented yet: #{CHANNEL}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @raise [NotImplementedError]
|
|
20
|
+
def get_emails(_email, _token)
|
|
21
|
+
raise NotImplementedProviderError, "channel not implemented yet: #{CHANNEL}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module TempmailSdk
|
|
8
|
+
module Providers
|
|
9
|
+
# TempMailGG 渠道实现(temp-mail.gg,Laravel + Livewire v3)
|
|
10
|
+
#
|
|
11
|
+
# 全站无验证码,纯 HTTP 可实现:
|
|
12
|
+
# - GET https://temp-mail.gg/ 返回 data-csrf 属性令牌与初始
|
|
13
|
+
# wire:snapshot(JSON;data.email 初始为空,邮箱必须由 Livewire
|
|
14
|
+
# generateEmail 显式生成)。
|
|
15
|
+
# - POST https://temp-mail.gg/livewire/update 是唯一边界(同站
|
|
16
|
+
# fetch 协议):JSON body 顶层 _token(=data-csrf)+
|
|
17
|
+
# components[0]:{snapshot, updates:{}, calls:[]}。
|
|
18
|
+
# generateEmail 生成邮箱并回传新 snapshot。
|
|
19
|
+
# - 轮询 update(calls 为空 = 平台 20 秒刷信形态)响应
|
|
20
|
+
# effects.html 含收件箱 UI;逐封 selectEmail 拉详情正文。
|
|
21
|
+
# - Laravel 每次 livewire/update 轮换会话 Cookie,同值重放会 419,
|
|
22
|
+
# 必须用响应 Set-Cookie 覆写后续请求。
|
|
23
|
+
#
|
|
24
|
+
# 会话粘性:Generate 时把 {email, csrf, snapshot} 打成模块级凭据
|
|
25
|
+
# 状态;token 保存 JSON 凭据串。GetEmails 复用快照轮询/点开详情,
|
|
26
|
+
# 并以响应快照 data.email 断言会话仍指向本邮箱(防串箱)。
|
|
27
|
+
# 本端无全局 Cookie 罐,会话 Cookie 由本渠道模块级维护并逐请求
|
|
28
|
+
# 以显式 Cookie 头回填。邮箱约 30 分钟无活动过期。
|
|
29
|
+
module TempMailGg
|
|
30
|
+
CHANNEL = "temp-mail-gg"
|
|
31
|
+
BASE_URL = "https://temp-mail.gg"
|
|
32
|
+
|
|
33
|
+
# 渠道凭据串前缀,用于识别会话接管
|
|
34
|
+
TOKEN_PREFIX = "temp-mail-gg|"
|
|
35
|
+
|
|
36
|
+
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " \
|
|
37
|
+
"(KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36"
|
|
38
|
+
|
|
39
|
+
HOME_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9," \
|
|
40
|
+
"image/avif,image/webp,*/*;q=0.8"
|
|
41
|
+
|
|
42
|
+
# 模块级会话状态:站点 Cookie 键值 + Generate 时打包的凭据串
|
|
43
|
+
# (JSON:{email, csrf, snapshot},snapshot 为解析后的 Hash)
|
|
44
|
+
@cookies = {}
|
|
45
|
+
@session = nil
|
|
46
|
+
|
|
47
|
+
DATA_CSRF_RE = /data-csrf="([^"]*)"/
|
|
48
|
+
SNAPSHOT_RE = /wire:snapshot="([^"]*)"/
|
|
49
|
+
RELATIVE_RE = /^\s*(\d+)\s+(second|minute|hour|day)s?\s+ago\s*$/i
|
|
50
|
+
|
|
51
|
+
module_function
|
|
52
|
+
|
|
53
|
+
# 解析 Set-Cookie 头,覆写合并进模块级会话 Cookie 状态
|
|
54
|
+
# @param resp [Http::Response]
|
|
55
|
+
def store_cookies(resp)
|
|
56
|
+
resp.set_cookies.each do |line|
|
|
57
|
+
kv = line.split(";").first.to_s.strip
|
|
58
|
+
next if kv.empty?
|
|
59
|
+
|
|
60
|
+
k, v = kv.split("=", 2)
|
|
61
|
+
@cookies[k.strip] = v.to_s if k && !k.strip.empty?
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 由模块级 Cookie 状态拼出显式 Cookie 请求头
|
|
66
|
+
# @return [String, nil] 无 Cookie 时为 nil
|
|
67
|
+
def cookie_header
|
|
68
|
+
return nil if @cookies.empty?
|
|
69
|
+
|
|
70
|
+
@cookies.map { |k, v| "#{k}=#{v}" }.join("; ")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# 组装 livewire/update 同步请求头(同站 fetch 全套)
|
|
74
|
+
# @return [Hash]
|
|
75
|
+
def post_headers
|
|
76
|
+
hdrs = {
|
|
77
|
+
"User-Agent" => USER_AGENT,
|
|
78
|
+
"Accept" => "text/html, application/xhtml+xml",
|
|
79
|
+
"Accept-Language" => "en-US,en;q=0.9",
|
|
80
|
+
"Content-Type" => "application/json",
|
|
81
|
+
"X-Livewire" => "",
|
|
82
|
+
"X-Requested-With" => "XMLHttpRequest",
|
|
83
|
+
"Origin" => BASE_URL,
|
|
84
|
+
"Referer" => "#{BASE_URL}/"
|
|
85
|
+
}
|
|
86
|
+
hdrs["Cookie"] = cookie_header if cookie_header
|
|
87
|
+
hdrs
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# 创建 temp-mail.gg 临时邮箱
|
|
91
|
+
# GET 首页取 CSRF + 初始快照 -> update calls=generateEmail ->
|
|
92
|
+
# 从响应快照取 data.email,并打包 {email, csrf, snapshot} 凭据串。
|
|
93
|
+
# 邮箱约 30 分钟无活动过期。
|
|
94
|
+
# @return [EmailInfo]
|
|
95
|
+
def generate_email
|
|
96
|
+
resp = Http.get(BASE_URL,
|
|
97
|
+
headers: {
|
|
98
|
+
"User-Agent" => USER_AGENT,
|
|
99
|
+
"Accept" => HOME_ACCEPT,
|
|
100
|
+
"Accept-Language" => "en-US,en;q=0.9"
|
|
101
|
+
},
|
|
102
|
+
timeout: 15)
|
|
103
|
+
raise "temp-mail-gg: 首页 http #{resp.status_code}" unless resp.ok?
|
|
104
|
+
|
|
105
|
+
store_cookies(resp)
|
|
106
|
+
page = resp.body
|
|
107
|
+
m_csrf = page.match(DATA_CSRF_RE)
|
|
108
|
+
m_snap = page.match(SNAPSHOT_RE)
|
|
109
|
+
if m_csrf.nil? || m_snap.nil?
|
|
110
|
+
raise "temp-mail-gg: 首页缺少 data-csrf 或 wire:snapshot,无法建箱"
|
|
111
|
+
end
|
|
112
|
+
csrf = m_csrf[1]
|
|
113
|
+
# 属性内快照 JSON 被 HTML 实体转义,一层反转义后还原为原始 JSON
|
|
114
|
+
snap_raw = CGI.unescapeHTML(m_snap[1])
|
|
115
|
+
|
|
116
|
+
# generateEmail: 平台免费额度为免登录每时段 5 个,耗尽时响应无 email
|
|
117
|
+
update = livewire_update(
|
|
118
|
+
{ "snapshot" => snap_raw, "updates" => {}, "calls" => [{ "path" => "", "method" => "generateEmail", "params" => [] }] },
|
|
119
|
+
csrf
|
|
120
|
+
)
|
|
121
|
+
comp = update["components"][0]
|
|
122
|
+
raise "temp-mail-gg: generateEmail 响应异常(components 缺失)" if comp.nil?
|
|
123
|
+
|
|
124
|
+
snapshot_text = comp["snapshot"].to_s
|
|
125
|
+
if snapshot_text.strip.empty?
|
|
126
|
+
raise "temp-mail-gg: generateEmail 响应异常(components 缺失)"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
begin
|
|
130
|
+
snapshot = JSON.parse(snapshot_text)
|
|
131
|
+
rescue JSON::ParserError
|
|
132
|
+
raise "temp-mail-gg: 解析 generateEmail 响应快照失败"
|
|
133
|
+
end
|
|
134
|
+
data = snapshot.is_a?(Hash) ? snapshot["data"] : nil
|
|
135
|
+
email = data.is_a?(Hash) ? data["email"].to_s.strip : ""
|
|
136
|
+
if email.empty?
|
|
137
|
+
raise "temp-mail-gg: 建箱失败(响应快照无 email),可能已耗尽免登录配额(每时段 5 个)"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
token = TOKEN_PREFIX + JSON.generate("email" => email, "csrf" => csrf, "snapshot" => snapshot)
|
|
141
|
+
|
|
142
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: token,
|
|
143
|
+
expires_at: ((Time.now.to_f + 30 * 60) * 1000).to_i)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# 读取 temp-mail.gg 收件箱
|
|
147
|
+
# 流程:校验凭据串 -> 轮询 update(无 calls)取 Inbox 列表 ->
|
|
148
|
+
# 对每封 selectEmail 提取详情正文。
|
|
149
|
+
# @param email [String] 邮箱地址(与 token 内会话邮箱一致才继续)
|
|
150
|
+
# @param token [String] 凭据串(prefix | {email,csrf,snapshot})
|
|
151
|
+
# @return [Array<Email>]
|
|
152
|
+
def get_emails(email, token)
|
|
153
|
+
addr = email.to_s.strip
|
|
154
|
+
raise "temp-mail-gg: 邮箱为空,请重新 Generate" if addr.empty?
|
|
155
|
+
|
|
156
|
+
session = decode_session(token)
|
|
157
|
+
if session["email"].to_s.casecmp(addr) != 0
|
|
158
|
+
raise "temp-mail-gg: 邮箱与凭据不匹配"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# 轮询刷新(calls 为空 = 平台 20s 自动刷新形态)
|
|
162
|
+
poll = livewire_update(
|
|
163
|
+
{ "snapshot" => JSON.generate(session["snapshot"]), "updates" => {}, "calls" => [] },
|
|
164
|
+
session["csrf"]
|
|
165
|
+
)
|
|
166
|
+
comp = poll["components"][0]
|
|
167
|
+
raise "temp-mail-gg: 轮询响应异常(components 缺失)" if comp.nil?
|
|
168
|
+
|
|
169
|
+
if comp["snapshot"].to_s.strip != ""
|
|
170
|
+
begin
|
|
171
|
+
poll_snap = JSON.parse(comp["snapshot"])
|
|
172
|
+
if poll_snap.is_a?(Hash)
|
|
173
|
+
current = poll_snap["data"].is_a?(Hash) ? poll_snap["data"]["email"].to_s.strip : ""
|
|
174
|
+
if !current.empty? && current.casecmp(addr) != 0
|
|
175
|
+
raise "temp-mail-gg: 会话已被切换"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
rescue JSON::ParserError
|
|
179
|
+
# 快照解析失败不阻断本轮,透传原快照继续
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
html_block = comp.dig("effects", "html").to_s.strip
|
|
183
|
+
if html_block.empty?
|
|
184
|
+
raise "temp-mail-gg: 轮询响应无 effects.html,会话可能已失效"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
rows = parse_list(html_block)
|
|
188
|
+
return [] if rows.empty?
|
|
189
|
+
|
|
190
|
+
latest_snap = comp["snapshot"].to_s.strip
|
|
191
|
+
latest_snap = JSON.generate(session["snapshot"]) if latest_snap.empty?
|
|
192
|
+
|
|
193
|
+
rows.filter_map do |row|
|
|
194
|
+
next if row[:id].empty?
|
|
195
|
+
|
|
196
|
+
build_email(row, addr, session["csrf"], latest_snap)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# 解析凭据串 -> 会话 Hash(email + csrf + snapshot)
|
|
201
|
+
# @param token [String]
|
|
202
|
+
# @return [Hash]
|
|
203
|
+
def decode_session(token)
|
|
204
|
+
t = token.to_s
|
|
205
|
+
raise "temp-mail-gg: 凭据串前缀不符,请重新 Generate" unless t.start_with?(TOKEN_PREFIX)
|
|
206
|
+
|
|
207
|
+
begin
|
|
208
|
+
session = JSON.parse(t[TOKEN_PREFIX.length..])
|
|
209
|
+
rescue JSON::ParserError
|
|
210
|
+
raise "temp-mail-gg: 解析凭据串失败"
|
|
211
|
+
end
|
|
212
|
+
unless session.is_a?(Hash) && !session["email"].to_s.empty? && !session["snapshot"].to_s.empty?
|
|
213
|
+
raise "temp-mail-gg: 凭据串缺失邮箱或快照,请重新 Generate"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
session
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# 调用 livewire/update(响应 Set-Cookie 覆写模块级 Cookie 状态)
|
|
220
|
+
# @param component [Hash] components[0]:{snapshot, updates, calls}
|
|
221
|
+
# @param csrf [String] 顶层 _token
|
|
222
|
+
# @return [Hash] 解析后的响应 JSON
|
|
223
|
+
def livewire_update(component, csrf)
|
|
224
|
+
payload = JSON.generate("_token" => csrf, "components" => [component])
|
|
225
|
+
resp = Http.post("#{BASE_URL}/livewire/update",
|
|
226
|
+
headers: post_headers, body: payload, timeout: 15)
|
|
227
|
+
if resp.status_code == 419
|
|
228
|
+
raise "temp-mail-gg: livewire 会话过期(419),请重新 Generate"
|
|
229
|
+
end
|
|
230
|
+
raise "temp-mail-gg: livewire/update http #{resp.status_code}" unless resp.ok?
|
|
231
|
+
|
|
232
|
+
store_cookies(resp)
|
|
233
|
+
data = resp.json
|
|
234
|
+
raise "temp-mail-gg: livewire/update 响应非对象" unless data.is_a?(Hash)
|
|
235
|
+
|
|
236
|
+
data
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# 解析轮询 effects.html 的 Inbox 条目
|
|
240
|
+
# 条目容器 <div wire:click="selectEmail(<数字id>)">,其内
|
|
241
|
+
# h3=发件人、p=主题、p=正文预览、span=相对时间。
|
|
242
|
+
# @param html_block [String]
|
|
243
|
+
# @return [Array<Hash>]
|
|
244
|
+
def parse_list(html_block)
|
|
245
|
+
rows = []
|
|
246
|
+
html_block.scan(/<div[^>]*wire:click="selectEmail\((\d+)\)"[^>]*>(.*?)<\/div>/mi).each do |cap|
|
|
247
|
+
id, chunk = cap
|
|
248
|
+
row = { id: id, from: "", subject: "", preview: "", when: "" }
|
|
249
|
+
|
|
250
|
+
# 向内收窄提取:h3(类含 font-semibold)、p(类含 text-zinc-300 / line-clamp-2)、span(类含 text-xs)
|
|
251
|
+
row[:from] = pick_class_text(chunk, "<h3", /font-semibold/)
|
|
252
|
+
row[:subject] = pick_class_text(chunk, "<p", /text-zinc-300/)
|
|
253
|
+
row[:preview] = pick_class_text(chunk, "<p", /line-clamp-2/)
|
|
254
|
+
row[:when] = pick_class_text(chunk, "<span", /text-xs/)
|
|
255
|
+
|
|
256
|
+
rows << row
|
|
257
|
+
end
|
|
258
|
+
rows
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# 提取首个 class 含指定模式的标签内容并去 HTML 标签
|
|
262
|
+
# @param html [String] 范围 HTML
|
|
263
|
+
# @param tag [String] 起始标签(如 "<p")
|
|
264
|
+
# @param class_re [Regexp] class 匹配模式
|
|
265
|
+
# @return [String]
|
|
266
|
+
def pick_class_text(html, tag, class_re)
|
|
267
|
+
m = html.match(/#{Regexp.escape(tag)}\s+class="[^"]*#{class_re.source}[^"]*"[^>]*>(.*?)<\//mi)
|
|
268
|
+
return "" if m.nil?
|
|
269
|
+
|
|
270
|
+
HtmlUtils.html_to_text(m[1])
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# 逐封构建 Email:selectEmail 拉详情,失败回退列表字段
|
|
274
|
+
# @param row [Hash] 列表行
|
|
275
|
+
# @param email [String] 收件人
|
|
276
|
+
# @param csrf [String] 凭据 CSRF
|
|
277
|
+
# @param snapshot [String] 当前最新快照 JSON 串(每封详情后滚动更新)
|
|
278
|
+
# @return [Email, nil]
|
|
279
|
+
def build_email(row, email, csrf, snapshot)
|
|
280
|
+
text = ""
|
|
281
|
+
detail = livewire_update(
|
|
282
|
+
{ "snapshot" => snapshot, "updates" => {}, "calls" => [{ "path" => "", "method" => "selectEmail", "params" => [row[:id].to_i] }] },
|
|
283
|
+
csrf
|
|
284
|
+
)
|
|
285
|
+
dcomp = detail["components"][0]
|
|
286
|
+
unless dcomp.nil?
|
|
287
|
+
dhtml = dcomp.dig("effects", "html").to_s
|
|
288
|
+
parsed = parse_detail(dhtml)
|
|
289
|
+
row[:from] = parsed[:from] unless parsed[:from].empty?
|
|
290
|
+
row[:subject] = parsed[:subject] unless parsed[:subject].empty?
|
|
291
|
+
text = parsed[:text] if !parsed[:text].empty? || text.empty?
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
subject = row[:subject]
|
|
295
|
+
text = subject if text.empty?
|
|
296
|
+
html = "<html><body><pre>#{HtmlUtils.escape(text)}</pre></body></html>"
|
|
297
|
+
date = parse_relative(row[:when])
|
|
298
|
+
|
|
299
|
+
Normalize.normalize_email(
|
|
300
|
+
{
|
|
301
|
+
"id" => row[:id],
|
|
302
|
+
"from_email" => row[:from],
|
|
303
|
+
"to" => email,
|
|
304
|
+
"subject" => subject,
|
|
305
|
+
"text" => text,
|
|
306
|
+
"html" => html,
|
|
307
|
+
"date" => date
|
|
308
|
+
},
|
|
309
|
+
email
|
|
310
|
+
)
|
|
311
|
+
rescue StandardError
|
|
312
|
+
nil
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# 解析 selectEmail 详情视图(模态框):h3=主题、From: 前缀=发件人、
|
|
316
|
+
# x-show 含 activeTab === 'text' 的 div=正文(含标签则去标签)。
|
|
317
|
+
# @param html_block [String]
|
|
318
|
+
# @return [Hash] {from:, subject:, text:}
|
|
319
|
+
def parse_detail(html_block)
|
|
320
|
+
out = { from: "", subject: "", text: "" }
|
|
321
|
+
html_block.scan(/<h3[^>]*>(.*?)<\/h3>/mi).each do |m|
|
|
322
|
+
cls = html_block[/<h3[^>]*class="([^"]*)"[^>]*>/i, 1].to_s
|
|
323
|
+
next if cls.empty?
|
|
324
|
+
|
|
325
|
+
next unless cls.split.include?("text-xl")
|
|
326
|
+
|
|
327
|
+
out[:subject] = HtmlUtils.html_to_text(m[0])
|
|
328
|
+
break
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
html_block.scan(/<span[^>]*>(.*?)<\/span>/mi).each do |m|
|
|
332
|
+
inner = HtmlUtils.html_to_text(m[0])
|
|
333
|
+
next unless inner.start_with?("From:")
|
|
334
|
+
|
|
335
|
+
out[:from] = inner.sub(/\AFrom:\s*/i, "").strip
|
|
336
|
+
break
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
if (m = html_block.match(%r{<div[^>]*x-show="[^"]*activeTab === 'text'[^"]*"[^>]*>(.*?)</div>}mi))
|
|
340
|
+
inner = m[1]
|
|
341
|
+
inner = HtmlUtils.html_to_text(inner) if inner =~ /<[^>]+>/
|
|
342
|
+
out[:text] = inner.strip
|
|
343
|
+
end
|
|
344
|
+
out
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# 解析相对时间("N seconds/minutes/hours/days ago")为 UTC ISO8601
|
|
348
|
+
# @param s [String]
|
|
349
|
+
# @return [String]
|
|
350
|
+
def parse_relative(s)
|
|
351
|
+
m = s.to_s.strip.match(RELATIVE_RE)
|
|
352
|
+
return Time.now.utc.iso8601 if m.nil?
|
|
353
|
+
|
|
354
|
+
n = m[1].to_i
|
|
355
|
+
unit = { "second" => 1, "minute" => 60, "hour" => 3600, "day" => 86_400 }[m[2].downcase]
|
|
356
|
+
(Time.now.utc - n * unit).iso8601
|
|
357
|
+
rescue StandardError
|
|
358
|
+
Time.now.utc.iso8601
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# tempgbox.net 渠道实现
|
|
8
|
+
#
|
|
9
|
+
# 流程:POST /api/proxy?route=generate 创建邮箱(JSON body,X-Device-ID 头)
|
|
10
|
+
# POST /api/proxy?route=inbox 获取邮件列表
|
|
11
|
+
# 响应体为 HTML 页面,实际数据编码在 data-x 属性(base64 JSON)
|
|
12
|
+
# token 存储 device_id
|
|
13
|
+
module Tempgbox
|
|
14
|
+
CHANNEL = "tempgbox"
|
|
15
|
+
API_URL = "https://tempgbox.net/api/proxy"
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# 生成随机 64 位十六进制 device id
|
|
20
|
+
# @return [String]
|
|
21
|
+
def random_device_id
|
|
22
|
+
require "securerandom"
|
|
23
|
+
SecureRandom.hex(32)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# 生成随机 IP 地址
|
|
27
|
+
# @return [String]
|
|
28
|
+
def random_ip
|
|
29
|
+
"#{rand(1..254)}.#{rand(1..254)}.#{rand(1..254)}.#{rand(1..254)}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 构造请求头
|
|
33
|
+
# @param device_id [String]
|
|
34
|
+
# @return [Hash]
|
|
35
|
+
def build_headers(device_id)
|
|
36
|
+
ip = random_ip
|
|
37
|
+
{
|
|
38
|
+
"Accept" => "text/html,application/json",
|
|
39
|
+
"Content-Type" => "application/json",
|
|
40
|
+
"Origin" => "https://tempgbox.net",
|
|
41
|
+
"Referer" => "https://tempgbox.net/",
|
|
42
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
43
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
|
|
44
|
+
"X-Device-ID" => device_id,
|
|
45
|
+
"X-Forwarded-For" => ip,
|
|
46
|
+
"X-Real-IP" => ip,
|
|
47
|
+
"X-Originating-IP" => ip
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 从响应 HTML 中解码 data-x 属性(base64 JSON)
|
|
52
|
+
# @param body [String]
|
|
53
|
+
# @return [Hash]
|
|
54
|
+
def decode_payload(body)
|
|
55
|
+
require "base64"
|
|
56
|
+
m = body.match(/data-x=["']([^"']+)["']/)
|
|
57
|
+
raise "tempgbox: 响应中缺少 data-x 属性" unless m
|
|
58
|
+
|
|
59
|
+
raw = Base64.strict_decode64(m[1])
|
|
60
|
+
JSON.parse(raw)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 发起 POST 请求并解码响应
|
|
64
|
+
# @param route [String]
|
|
65
|
+
# @param device_id [String]
|
|
66
|
+
# @param body [Hash]
|
|
67
|
+
# @return [Hash]
|
|
68
|
+
def post_route(route, device_id, body)
|
|
69
|
+
resp = Http.post(
|
|
70
|
+
"#{API_URL}?route=#{route}",
|
|
71
|
+
headers: build_headers(device_id),
|
|
72
|
+
body: JSON.generate(body),
|
|
73
|
+
timeout: 15
|
|
74
|
+
)
|
|
75
|
+
resp.raise_for_status
|
|
76
|
+
decode_payload(resp.body)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# 创建 tempgbox.net 临时邮箱
|
|
80
|
+
# @return [EmailInfo]
|
|
81
|
+
def generate_email
|
|
82
|
+
device_id = random_device_id
|
|
83
|
+
payload = post_route("generate", device_id, "variant" => "googlemail")
|
|
84
|
+
alias_data = payload["alias"]
|
|
85
|
+
raise "tempgbox: 响应缺少 alias" unless alias_data.is_a?(Hash)
|
|
86
|
+
|
|
87
|
+
email = alias_data["email"].to_s.strip
|
|
88
|
+
email = alias_data["alias"].to_s.strip if email.empty?
|
|
89
|
+
raise "tempgbox: 响应缺少 email" if email.empty?
|
|
90
|
+
|
|
91
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: device_id)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# 获取 tempgbox.net 邮件列表
|
|
95
|
+
# @param device_id [String] token
|
|
96
|
+
# @param email [String]
|
|
97
|
+
# @return [Array<Email>]
|
|
98
|
+
def get_emails(device_id, email)
|
|
99
|
+
raise "tempgbox: missing device id" if device_id.to_s.strip.empty?
|
|
100
|
+
|
|
101
|
+
payload = post_route("inbox", device_id, "email" => email)
|
|
102
|
+
messages = Array(payload["messages"])
|
|
103
|
+
|
|
104
|
+
messages.filter_map do |raw|
|
|
105
|
+
next unless raw.is_a?(Hash)
|
|
106
|
+
|
|
107
|
+
raw["from"] ||= raw["sender"]
|
|
108
|
+
raw["text"] ||= raw["body_text"]
|
|
109
|
+
raw["html"] ||= raw["body_html"]
|
|
110
|
+
raw["date"] ||= raw["received_at"]
|
|
111
|
+
raw["messageId"] ||= raw["message_id"]
|
|
112
|
+
raw["attachments"] ||= raw["attachments_info"]
|
|
113
|
+
Normalize.normalize_email(raw.merge("to" => email), email)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|