tempmail_sdk 1.3.4 → 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/clawdemail.rb +122 -0
- data/lib/tempmail_sdk/providers/crazymailing.rb +114 -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/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/mailmomy.rb +1 -1
- data/lib/tempmail_sdk/providers/mailticking.rb +139 -0
- data/lib/tempmail_sdk/providers/mtempmail.rb +140 -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/shadowmail.rb +174 -0
- data/lib/tempmail_sdk/providers/shitpost_email.rb +85 -0
- data/lib/tempmail_sdk/providers/smails.rb +128 -0
- data/lib/tempmail_sdk/providers/temp_mail_gg.rb +362 -0
- data/lib/tempmail_sdk/providers/tempmail100.rb +148 -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/thirty_min_email.rb +90 -0
- data/lib/tempmail_sdk/providers/tmpkit.rb +129 -0
- data/lib/tempmail_sdk/providers/zerodrop.rb +90 -0
- data/lib/tempmail_sdk/registry.rb +129 -29
- data/lib/tempmail_sdk/types.rb +13 -0
- data/lib/tempmail_sdk/version.rb +1 -1
- data/lib/tempmail_sdk.rb +1 -1
- metadata +31 -7
- data/lib/tempmail_sdk/providers/gonebox_email.rb +0 -89
- data/lib/tempmail_sdk/providers/mohmal.rb +0 -208
- data/lib/tempmail_sdk/providers/temp_mail_now.rb +0 -103
- data/lib/tempmail_sdk/providers/webmailtemp.rb +0 -108
|
@@ -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,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# TempMail100 渠道实现(tempmail100.com)
|
|
6
|
+
#
|
|
7
|
+
# POST /init 建箱(空 body,响应 code/data.token(JWT),无 Cookie),
|
|
8
|
+
# POST /web/generate 建随机地址(Header Authorization: <token>,响应 data.address),
|
|
9
|
+
# GET /web/emails 读信列表(Header Authorization: <token>,响应 data.list[]/data.total)。
|
|
10
|
+
#
|
|
11
|
+
# 平台限制(实测确认,非 SDK 缺陷):
|
|
12
|
+
# 列表元素 content 恒为空字符串,详情端点对真实 token 返回 HTTP 200 + 空 body,
|
|
13
|
+
# 因此正文永久不可得,本渠道客观为「列表-only」:subject/fromAddress/fromName/
|
|
14
|
+
# timestamp/read 可正确输出,正文恒为空属平台限制。
|
|
15
|
+
module Tempmail100
|
|
16
|
+
CHANNEL = "tempmail100"
|
|
17
|
+
BASE_URL = "https://tempmail100.com"
|
|
18
|
+
|
|
19
|
+
HEADERS = {
|
|
20
|
+
"Accept" => "application/json",
|
|
21
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
|
|
22
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# 设置 tempmail100 请求的通用请求头
|
|
28
|
+
# 前端使用 Authorization: <token> 不带 Bearer。
|
|
29
|
+
# @param token [String] JWT
|
|
30
|
+
# @return [Hash]
|
|
31
|
+
def auth_headers(token)
|
|
32
|
+
hdrs = HEADERS.dup
|
|
33
|
+
hdrs["Authorization"] = token unless token.to_s.empty?
|
|
34
|
+
hdrs
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 创建 tempmail100.com 临时邮箱
|
|
38
|
+
# 流程:POST /init 取得 JWT token,再 POST /web/generate 创建地址。
|
|
39
|
+
# @return [EmailInfo]
|
|
40
|
+
def generate_email
|
|
41
|
+
# 第一步:初始化取得 token
|
|
42
|
+
resp = Http.post("#{BASE_URL}/init", headers: auth_headers(""), timeout: 15)
|
|
43
|
+
raise "tempmail100: 初始化失败 http #{resp.status_code}" unless resp.ok?
|
|
44
|
+
|
|
45
|
+
init = resp.json
|
|
46
|
+
raise "tempmail100: 初始化响应非对象" unless init.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
token = init.dig("data", "token").to_s.strip
|
|
49
|
+
raise "tempmail100: 初始化响应异常" if init["code"] != 0 || token.empty?
|
|
50
|
+
|
|
51
|
+
# 第二步:创建随机地址(Authorization: <token> 不带 Bearer)
|
|
52
|
+
resp2 = Http.post("#{BASE_URL}/web/generate",
|
|
53
|
+
headers: auth_headers(token), timeout: 15)
|
|
54
|
+
raise "tempmail100: 创建地址失败 http #{resp2.status_code}" unless resp2.ok?
|
|
55
|
+
|
|
56
|
+
gen = resp2.json
|
|
57
|
+
raise "tempmail100: 创建地址响应非对象" unless gen.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
address = gen.dig("data", "address").to_s.strip
|
|
60
|
+
unless gen["code"] == 0 && !address.empty? && address.include?("@")
|
|
61
|
+
raise "tempmail100: 创建地址响应异常"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
EmailInfo.new(channel: CHANNEL, email: address, token: token)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# 获取 tempmail100.com 邮件列表
|
|
68
|
+
# 流程:GET /web/emails(Authorization: <token> 不带 Bearer)返回 data.list/data.total。
|
|
69
|
+
# @param email [String] 邮箱地址
|
|
70
|
+
# @param token [String] 初始化返回的 JWT 认证令牌
|
|
71
|
+
# @return [Array<Email>]
|
|
72
|
+
def get_emails(email, token)
|
|
73
|
+
addr = email.to_s.strip
|
|
74
|
+
resp = Http.get("#{BASE_URL}/web/emails",
|
|
75
|
+
headers: auth_headers(token), timeout: 15)
|
|
76
|
+
raise "tempmail100: 获取邮件列表失败 http #{resp.status_code}" unless resp.ok?
|
|
77
|
+
|
|
78
|
+
data = resp.json
|
|
79
|
+
raise "tempmail100: 获取邮件列表响应非对象" unless data.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
unless data["code"] == 0
|
|
82
|
+
raise "tempmail100: 获取邮件列表响应异常: #{data["message"]}"
|
|
83
|
+
end
|
|
84
|
+
inner = data["data"]
|
|
85
|
+
list = inner.is_a?(Hash) ? inner["list"] : nil
|
|
86
|
+
return [] unless list.is_a?(Array)
|
|
87
|
+
|
|
88
|
+
list.filter_map do |m|
|
|
89
|
+
next unless m.is_a?(Hash)
|
|
90
|
+
|
|
91
|
+
Normalize.normalize_email(normalize_item(m, addr), addr)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# 将 /web/emails 列表元素归一为统一邮件结构。
|
|
96
|
+
# fromName+fromAddress 组合为 "Name <address>" 填入 from;
|
|
97
|
+
# timestamp 为毫秒值(>1e12 时按 UnixMilli 解析);read 为布尔已读标记。
|
|
98
|
+
# @param item [Hash] 列表元素
|
|
99
|
+
# @param email [String] 收件人地址
|
|
100
|
+
# @return [Hash]
|
|
101
|
+
def normalize_item(item, email)
|
|
102
|
+
from_name = str_of(item["fromName"]).strip
|
|
103
|
+
from_address = str_of(item["fromAddress"]).strip
|
|
104
|
+
unless from_name.empty? || from_name.casecmp(from_address).zero? || !from_address.include?("@")
|
|
105
|
+
from_address = "#{from_name} <#{from_address}>"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
"id" => str_of(item["uuid"]),
|
|
110
|
+
"from" => from_address,
|
|
111
|
+
"to" => str_of(item["toAddress"]).strip,
|
|
112
|
+
"subject" => str_of(item["subject"]),
|
|
113
|
+
"content" => str_of(item["content"]),
|
|
114
|
+
"timestamp" => item["timestamp"],
|
|
115
|
+
"isRead" => read_of(item["read"])
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 将接口字段值安全转换为字符串,nil 或非标量返回空串
|
|
120
|
+
# @param v [Object]
|
|
121
|
+
# @return [String]
|
|
122
|
+
def str_of(v)
|
|
123
|
+
case v
|
|
124
|
+
when String then v
|
|
125
|
+
when Numeric then v.to_i.to_s
|
|
126
|
+
when true then "true"
|
|
127
|
+
when false then "false"
|
|
128
|
+
else ""
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# 将 read 字段归一为布尔已读标记,兼容 bool / 数字(0|1) / string("true"|"1")
|
|
133
|
+
# @param v [Object]
|
|
134
|
+
# @return [Boolean]
|
|
135
|
+
def read_of(v)
|
|
136
|
+
case v
|
|
137
|
+
when true then true
|
|
138
|
+
when false then false
|
|
139
|
+
when Numeric then v != 0
|
|
140
|
+
when String
|
|
141
|
+
s = v.strip
|
|
142
|
+
s.casecmp("true").zero? || s == "1"
|
|
143
|
+
else false
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|