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,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module TempmailSdk
|
|
7
|
+
module Providers
|
|
8
|
+
# Nukemail 渠道实现(nukemail.app)
|
|
9
|
+
#
|
|
10
|
+
# 完整接入契约:
|
|
11
|
+
# 挑战 GET /api/pow/challenge?difficulty=4 → 200 {"id","challenge","difficulty"};
|
|
12
|
+
# 求 nonce 使 SHA-256(challenge+nonce) 十六进制前 4 位为 0
|
|
13
|
+
# (前端 solvePow 逐 nonce 自 0 递增)。
|
|
14
|
+
# 建箱 POST /api/inbox/create body {"address","domain","pow_id","pow_nonce"}
|
|
15
|
+
# → 200 {"token":"NUKE-xxxxxxxx","email":"名@域名"},并
|
|
16
|
+
# Set-Cookie: nukemail_token=<token>(Secure; HttpOnly; SameSite=lax, 72h)。
|
|
17
|
+
# 读信 GET /api/inbox(Cookie: nukemail_token=<token>)→ 200
|
|
18
|
+
# {"token","state","addresses":[...],"messages":[...],"is_premium"...};
|
|
19
|
+
# messages 元素字段为 sender、sender_name、subject、body_html、body_text、
|
|
20
|
+
# received_at、read。
|
|
21
|
+
# 会话恢复 POST /api/inbox/resume {"accessCode":<token>} 重设 Cookie,仅作兜底。
|
|
22
|
+
# 会话隔离:nukemail_token 由生成结果持久化为 token,读信时以显式 Cookie 头携带。
|
|
23
|
+
module Nukemail
|
|
24
|
+
CHANNEL = "nukemail"
|
|
25
|
+
BASE_URL = "https://nukemail.app"
|
|
26
|
+
|
|
27
|
+
HEADERS = {
|
|
28
|
+
"Accept" => "application/json",
|
|
29
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
|
|
30
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# 求解 PoW:返回使 SHA-256(challenge+nonce) 十六进制
|
|
36
|
+
# 前 difficulty 位为 0 的最小 nonce(算法与前端 solvePow 逐字对齐)。
|
|
37
|
+
# @param challenge [String] 挑战串
|
|
38
|
+
# @param difficulty [Integer] 难度(十六进制前缀 0 位数)
|
|
39
|
+
# @return [Integer] nonce
|
|
40
|
+
def solve_pow(challenge, difficulty)
|
|
41
|
+
prefix = "0" * difficulty
|
|
42
|
+
nonce = 0
|
|
43
|
+
loop do
|
|
44
|
+
digest = Digest::SHA256.hexdigest("#{challenge}#{nonce}")
|
|
45
|
+
break if digest.start_with?(prefix)
|
|
46
|
+
|
|
47
|
+
nonce += 1
|
|
48
|
+
end
|
|
49
|
+
nonce
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 生成本地随机名(与前端 generateRandomName 等价形态)
|
|
53
|
+
# @return [String]
|
|
54
|
+
def random_address
|
|
55
|
+
chars = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
56
|
+
"nuke" + Array.new(10) { chars[rand(chars.length)] }.join
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# 取第一个非 premium 的活跃域名
|
|
60
|
+
# @return [String]
|
|
61
|
+
def default_domain
|
|
62
|
+
resp = Http.get("#{BASE_URL}/api/domains", headers: HEADERS, timeout: 15)
|
|
63
|
+
raise "nukemail generate: domains http #{resp.status_code}" unless resp.ok?
|
|
64
|
+
|
|
65
|
+
data = resp.json
|
|
66
|
+
domains = data.is_a?(Hash) ? data["domains"] : nil
|
|
67
|
+
raise "nukemail generate: domains 响应非对象" unless domains.is_a?(Array)
|
|
68
|
+
|
|
69
|
+
domains.each do |d|
|
|
70
|
+
next unless d.is_a?(Hash)
|
|
71
|
+
|
|
72
|
+
dom = d["domain"].to_s.strip
|
|
73
|
+
return dom if !dom.empty? && d["is_premium_only"] != true
|
|
74
|
+
end
|
|
75
|
+
raise "nukemail generate: 无可用非 premium 域名"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 创建临时邮箱(PoW 建箱)
|
|
79
|
+
# token 为平台返回的 NUKE-<随机> 访问码,读信时转成 nukemail_token Cookie 携带。
|
|
80
|
+
# @return [EmailInfo]
|
|
81
|
+
def generate_email
|
|
82
|
+
# 1) 取 PoW 挑战
|
|
83
|
+
resp = Http.get("#{BASE_URL}/api/pow/challenge?difficulty=4",
|
|
84
|
+
headers: HEADERS, timeout: 15)
|
|
85
|
+
raise "nukemail generate: challenge http #{resp.status_code}" unless resp.ok?
|
|
86
|
+
|
|
87
|
+
ch = resp.json
|
|
88
|
+
raise "nukemail generate: challenge 响应非对象" unless ch.is_a?(Hash)
|
|
89
|
+
|
|
90
|
+
ch_id = ch["id"].to_s.strip
|
|
91
|
+
challenge = ch["challenge"].to_s.strip
|
|
92
|
+
raise "nukemail generate: challenge 响应缺少 id/challenge" if ch_id.empty? || challenge.empty?
|
|
93
|
+
|
|
94
|
+
difficulty = ch["difficulty"].to_i
|
|
95
|
+
difficulty = 4 if difficulty <= 0
|
|
96
|
+
|
|
97
|
+
# 2) 本地求 PoW 解(SHA-256 前缀 4 零)
|
|
98
|
+
nonce = solve_pow(challenge, difficulty)
|
|
99
|
+
|
|
100
|
+
# 3) 取域名并建箱
|
|
101
|
+
dom = default_domain
|
|
102
|
+
body = {
|
|
103
|
+
"address" => random_address,
|
|
104
|
+
"domain" => dom,
|
|
105
|
+
"pow_id" => ch_id,
|
|
106
|
+
"pow_nonce" => nonce.to_s
|
|
107
|
+
}
|
|
108
|
+
headers = {
|
|
109
|
+
"Content-Type" => "application/json",
|
|
110
|
+
"Accept" => "application/json",
|
|
111
|
+
"User-Agent" => HEADERS["User-Agent"]
|
|
112
|
+
}
|
|
113
|
+
resp2 = Http.post("#{BASE_URL}/api/inbox/create",
|
|
114
|
+
headers: headers, json: body, timeout: 15)
|
|
115
|
+
unless resp2.ok?
|
|
116
|
+
raise "nukemail generate: create http #{resp2.status_code}"
|
|
117
|
+
end
|
|
118
|
+
data = resp2.json
|
|
119
|
+
raise "nukemail generate: create 响应非对象" unless data.is_a?(Hash)
|
|
120
|
+
|
|
121
|
+
token = data["token"].to_s.strip
|
|
122
|
+
email = data["email"].to_s.strip
|
|
123
|
+
raise "nukemail generate: create 响应缺少 token/email" if token.empty? || email.empty?
|
|
124
|
+
|
|
125
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: token)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# 读取收件箱
|
|
129
|
+
# 主通道 GET /api/inbox 带 Cookie: nukemail_token=<token>;
|
|
130
|
+
# 会话过期时经 POST /api/inbox/resume 显式重设会话后重试一次。
|
|
131
|
+
# @param email [String] 邮箱地址
|
|
132
|
+
# @param token [String] 建箱返回的 NUKE-<随机> 访问码
|
|
133
|
+
# @return [Array<Email>]
|
|
134
|
+
def get_emails(email, token)
|
|
135
|
+
addr = email.to_s.strip
|
|
136
|
+
tk = token.to_s.strip
|
|
137
|
+
raise "nukemail: token 为空" if tk.empty?
|
|
138
|
+
|
|
139
|
+
cookie = "nukemail_token=#{tk}"
|
|
140
|
+
|
|
141
|
+
fetch = lambda do
|
|
142
|
+
resp = Http.get("#{BASE_URL}/api/inbox",
|
|
143
|
+
headers: HEADERS.merge("Cookie" => cookie), timeout: 15)
|
|
144
|
+
raise "nukemail 读信: http #{resp.status_code}" unless resp.ok?
|
|
145
|
+
|
|
146
|
+
resp.json
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
data = fetch.call
|
|
150
|
+
raise "nukemail 读信: 响应非对象" unless data.is_a?(Hash)
|
|
151
|
+
|
|
152
|
+
if data["state"].to_s.empty? || data["state"] == "expired"
|
|
153
|
+
# 会话可能已过期:经 resume 重设会话后重试
|
|
154
|
+
headers = {
|
|
155
|
+
"Content-Type" => "application/json",
|
|
156
|
+
"User-Agent" => HEADERS["User-Agent"]
|
|
157
|
+
}
|
|
158
|
+
begin
|
|
159
|
+
Http.post("#{BASE_URL}/api/inbox/resume",
|
|
160
|
+
headers: headers, json: { "accessCode" => tk }, timeout: 15)
|
|
161
|
+
rescue StandardError
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
data = fetch.call
|
|
165
|
+
raise "nukemail 读信: 响应非对象" unless data.is_a?(Hash)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
messages = data["messages"]
|
|
169
|
+
return [] unless messages.is_a?(Array)
|
|
170
|
+
|
|
171
|
+
messages.filter_map do |m|
|
|
172
|
+
next unless m.is_a?(Hash)
|
|
173
|
+
|
|
174
|
+
flat = m.dup
|
|
175
|
+
flat["to"] = addr
|
|
176
|
+
# 平台消息字段为 body_html/body_text,补齐 text/html 候选(若原字段缺失)
|
|
177
|
+
flat["text"] = flat["body_text"] if flat["text"].nil?
|
|
178
|
+
flat["html"] = flat["body_html"] if flat["html"].nil?
|
|
179
|
+
flat["date"] = m["received_at"]
|
|
180
|
+
flat["read"] = m["read"]
|
|
181
|
+
# sender 是小写发件人地址,sender_name 是展示名
|
|
182
|
+
flat["sender_email"] = m["sender"]
|
|
183
|
+
Normalize.normalize_email(flat, addr)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# Nullmail 渠道实现(nullmail.cc / maildock.store)
|
|
8
|
+
#
|
|
9
|
+
# 无认证 REST:
|
|
10
|
+
# POST /api/emails(空 JSON body)建箱,响应
|
|
11
|
+
# {"address":"...@maildock.store","expiry":"2026-09-27T02:25:28.778Z"};
|
|
12
|
+
# GET /api/emails/{address}(URL 编码)读信,响应 {"expiry":"...","emails":[...]},
|
|
13
|
+
# 列表项只有 id/sender/subject/delivered,正文须逐封二拉 GET /api/emails/{addr}/body/{id}
|
|
14
|
+
# (响应 {"body":...})。
|
|
15
|
+
module Nullmail
|
|
16
|
+
CHANNEL = "nullmail"
|
|
17
|
+
BASE_URL = "https://www.nullmail.cc"
|
|
18
|
+
DOMAIN = "maildock.store"
|
|
19
|
+
|
|
20
|
+
HEADERS = {
|
|
21
|
+
"Accept" => "application/json",
|
|
22
|
+
"Origin" => BASE_URL,
|
|
23
|
+
"Referer" => "#{BASE_URL}/",
|
|
24
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
25
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# 创建 nullmail 临时邮箱
|
|
31
|
+
# POST /api/emails(空 JSON body),token 复用完整地址以便收件箱回查
|
|
32
|
+
# @return [EmailInfo]
|
|
33
|
+
def generate_email
|
|
34
|
+
resp = Http.post("#{BASE_URL}/api/emails",
|
|
35
|
+
headers: HEADERS.merge("Content-Type" => "application/json"),
|
|
36
|
+
json: {}, timeout: 15)
|
|
37
|
+
resp.raise_for_status
|
|
38
|
+
data = resp.json
|
|
39
|
+
addr = data.is_a?(Hash) ? data["address"].to_s.strip : ""
|
|
40
|
+
raise "nullmail: 建箱响应缺少 address 字段" if addr.empty?
|
|
41
|
+
|
|
42
|
+
EmailInfo.new(channel: CHANNEL, email: addr, token: addr,
|
|
43
|
+
expires_at: data["expiry"])
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 单封正文二拉
|
|
47
|
+
# GET /api/emails/{addr}/body/{id},响应 {"body":"<完整纯文本正文>"}
|
|
48
|
+
# @param addr [String] 邮箱地址
|
|
49
|
+
# @param id [Object] 邮件 ID
|
|
50
|
+
# @return [String]
|
|
51
|
+
def fetch_body(addr, id)
|
|
52
|
+
uri = "#{BASE_URL}/api/emails/#{URI.encode_www_form_component(addr)}" \
|
|
53
|
+
"/body/#{URI.encode_www_form_component(id.to_s)}"
|
|
54
|
+
resp = Http.get(uri, headers: HEADERS, timeout: 15)
|
|
55
|
+
resp.raise_for_status
|
|
56
|
+
data = resp.json
|
|
57
|
+
data.is_a?(Hash) ? data["body"].to_s : ""
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 读取收件箱
|
|
61
|
+
# GET /api/emails/{address}(完整地址 URL 编码),列表项无正文,逐封二拉 body 端点;
|
|
62
|
+
# 正文拉取失败降级留空不阻断列表。
|
|
63
|
+
# @param email [String] 邮箱地址
|
|
64
|
+
# @param _token [String] 令牌(本渠道忽略,以邮箱为准)
|
|
65
|
+
# @return [Array<Email>]
|
|
66
|
+
def get_emails(email, _token)
|
|
67
|
+
addr = email.to_s.strip
|
|
68
|
+
raise "nullmail: 邮箱地址为空" if addr.empty?
|
|
69
|
+
|
|
70
|
+
uri = "#{BASE_URL}/api/emails/#{URI.encode_www_form_component(addr)}"
|
|
71
|
+
resp = Http.get(uri, headers: HEADERS, timeout: 15)
|
|
72
|
+
resp.raise_for_status
|
|
73
|
+
data = resp.json
|
|
74
|
+
emails = data.is_a?(Hash) ? data["emails"] : nil
|
|
75
|
+
return [] unless emails.is_a?(Array)
|
|
76
|
+
|
|
77
|
+
emails.filter_map do |m|
|
|
78
|
+
next unless m.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
row = {
|
|
81
|
+
"id" => m["id"],
|
|
82
|
+
"from" => m["sender"] || m["from"],
|
|
83
|
+
"to" => addr,
|
|
84
|
+
"subject" => m["subject"],
|
|
85
|
+
# 列表只有 id/sender/subject/delivered,delivered 显式映射为 date
|
|
86
|
+
"date" => m["delivered"] || m["date"]
|
|
87
|
+
}
|
|
88
|
+
body = fetch_body(addr, m["id"]) if m["id"]
|
|
89
|
+
row["text"] = body unless body.to_s.empty?
|
|
90
|
+
Normalize.normalize_email(row, addr)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# Shadowmail 渠道实现(shadowmail.win)
|
|
6
|
+
#
|
|
7
|
+
# 完整接入契约:
|
|
8
|
+
# 注册 POST /api/register {"email":"<随机前缀>@gmail.com","password":"Abcd1234!"}
|
|
9
|
+
# → 200 {"message":"Successfully Registered"};
|
|
10
|
+
# 登录 POST /api/login → 200 {"message":"Successfull Login"}
|
|
11
|
+
# 并 Set-Cookie: sessionId=<uuid>(HttpOnly; Secure; Max-Age 3600);
|
|
12
|
+
# 建箱 POST /api/new-address → 200 {...,"address":"<id>@shadowmail.win","id":<id>};
|
|
13
|
+
# 读信 POST /api/get-emails {"address":"<地址>"} → 200 {"message":"Emails read",
|
|
14
|
+
# "mails":[...]};mails 元素字段 id/address_id/sender/subject/body/created_at。
|
|
15
|
+
# 会话隔离:全域使用显式 Cookie 头(sessionId=<uuid>),凭据串 token 持久化
|
|
16
|
+
# 注册邮箱/密码/会话 id,便于会话过期后自动重生。
|
|
17
|
+
module Shadowmail
|
|
18
|
+
CHANNEL = "shadowmail"
|
|
19
|
+
BASE_URL = "https://shadowmail.win"
|
|
20
|
+
# 固定注册密码(平台无自选密码入口,注册即固定)
|
|
21
|
+
PW = "Abcd1234!"
|
|
22
|
+
# 平台唯一收信域
|
|
23
|
+
DOMAIN = "shadowmail.win"
|
|
24
|
+
# 本渠道凭据串前缀
|
|
25
|
+
TOKEN_PREFIX = "shadowmail|"
|
|
26
|
+
|
|
27
|
+
HEADERS = {
|
|
28
|
+
"Content-Type" => "application/json",
|
|
29
|
+
"Accept" => "application/json",
|
|
30
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
|
|
31
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
# 生成随机注册邮箱前缀(sdk+8 位小写字母)
|
|
37
|
+
# @return [String]
|
|
38
|
+
def random_account
|
|
39
|
+
chars = "abcdefghijklmnopqrstuvwxyz"
|
|
40
|
+
"sdk" + Array.new(8) { chars[rand(chars.length)] }.join
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# 携带显式 Cookie 的 JSON POST 请求,返回 [body, status, set_cookies]
|
|
44
|
+
# @param path [String] 请求路径
|
|
45
|
+
# @param body [Hash] JSON 请求体
|
|
46
|
+
# @param cookie [String] Cookie 头(可为空)
|
|
47
|
+
# @return [Array(Array, Integer, Array<String>)]
|
|
48
|
+
def do_post(path, body, cookie)
|
|
49
|
+
headers = HEADERS.dup
|
|
50
|
+
headers["Cookie"] = cookie unless cookie.to_s.empty?
|
|
51
|
+
resp = Http.post("#{BASE_URL}#{path}", headers: headers, json: body, timeout: 15)
|
|
52
|
+
[resp.json, resp.status_code, resp.set_cookies]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# 从 Set-Cookie 中提取 sessionId 值(纯 uuid,不含键名)
|
|
56
|
+
# @param cookies [Array<String>]
|
|
57
|
+
# @return [String]
|
|
58
|
+
def session_from(cookies)
|
|
59
|
+
cookies.each do |line|
|
|
60
|
+
kv = line.split(";").first.to_s.strip
|
|
61
|
+
return kv.split("=", 2)[1].to_s if kv.start_with?("sessionId=")
|
|
62
|
+
end
|
|
63
|
+
""
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# 注册或登录(POST /api/register、/api/login),返回会话 sessionId
|
|
67
|
+
# @param account [String] 注册邮箱
|
|
68
|
+
# @param password [String] 密码
|
|
69
|
+
# @param is_login [Boolean] true 走登录,false 走注册
|
|
70
|
+
# @return [String] sessionId 值
|
|
71
|
+
def register_login(account, password, is_login)
|
|
72
|
+
path = is_login ? "/api/login" : "/api/register"
|
|
73
|
+
body = { "email" => account, "password" => password }
|
|
74
|
+
data, status, cookies = do_post(path, body, "")
|
|
75
|
+
raise "shadowmail #{path}: http #{status}" if status < 200 || status >= 300
|
|
76
|
+
|
|
77
|
+
msg = data.is_a?(Hash) ? data["message"].to_s : ""
|
|
78
|
+
if is_login
|
|
79
|
+
raise "shadowmail login: #{msg}" unless msg == "Successfull Login"
|
|
80
|
+
else
|
|
81
|
+
# 重复注册(幂等):消息为 Email already in use 时视为账号已存在,继续走登录
|
|
82
|
+
unless ["Successfully Registered", "Email already in use"].include?(msg)
|
|
83
|
+
raise "shadowmail register: #{msg}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
session = session_from(cookies)
|
|
87
|
+
if is_login && session.empty?
|
|
88
|
+
raise "shadowmail login: 未下发 sessionId Cookie"
|
|
89
|
+
end
|
|
90
|
+
session
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# 注册账号并创建临时邮箱地址
|
|
94
|
+
# @return token 凭据串格式:"shadowmail|<account>|<password>|<sessionId>"
|
|
95
|
+
# @return [EmailInfo]
|
|
96
|
+
def generate_email
|
|
97
|
+
account = "#{random_account}@gmail.com"
|
|
98
|
+
|
|
99
|
+
# 1) 注册(幂等:已存在同名账号则跳过)
|
|
100
|
+
register_login(account, PW, false)
|
|
101
|
+
# 2) 登录取得 sessionId
|
|
102
|
+
session = register_login(account, PW, true)
|
|
103
|
+
# 3) 创建地址(每账号 12 槽):SDK 层显式 Cookie 头传 sessionId
|
|
104
|
+
data, status, = do_post("/api/new-address", {}, "sessionId=#{session}")
|
|
105
|
+
raise "shadowmail new-address: http #{status}" if status < 200 || status >= 300
|
|
106
|
+
|
|
107
|
+
address = data.is_a?(Hash) ? data["address"].to_s.strip : ""
|
|
108
|
+
raise "shadowmail new-address: 响应缺少有效地址" unless address.end_with?("@#{DOMAIN}")
|
|
109
|
+
|
|
110
|
+
# Token 持久化:account|password|sessionId(sessionId 为 uuid,无分隔符冲突)
|
|
111
|
+
token = TOKEN_PREFIX + [account, PW, session].join("|")
|
|
112
|
+
EmailInfo.new(channel: CHANNEL, email: address.downcase.strip, token: token)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# 解析凭据串为 account/password/sessionId 三元组
|
|
116
|
+
# @param token [String]
|
|
117
|
+
# @return [Array(String, String, String)]
|
|
118
|
+
def parse_token(token)
|
|
119
|
+
raise "shadowmail: token 格式错误" unless token.to_s.start_with?(TOKEN_PREFIX)
|
|
120
|
+
|
|
121
|
+
parts = token.to_s[TOKEN_PREFIX.length..].to_s.split("|", -1)
|
|
122
|
+
raise "shadowmail: token 字段缺失" unless parts.length == 3
|
|
123
|
+
|
|
124
|
+
account, password, session = parts
|
|
125
|
+
raise "shadowmail: token 凭据字段为空" if account.empty? || password.empty? || session.empty?
|
|
126
|
+
|
|
127
|
+
[account, password, session]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# 读取收件箱
|
|
131
|
+
# 会话失效时自动以凭据内 account/password 重新登录换新 sessionId。
|
|
132
|
+
# @param email [String] 平台新地址(<id>@shadowmail.win)
|
|
133
|
+
# @param token [String] 建箱下发的凭据串
|
|
134
|
+
# @return [Array<Email>]
|
|
135
|
+
def get_emails(email, token)
|
|
136
|
+
account, password, session = parse_token(token)
|
|
137
|
+
addr = email.to_s.strip
|
|
138
|
+
|
|
139
|
+
do_fetch = lambda do
|
|
140
|
+
do_post("/api/get-emails", { "address" => addr }, "sessionId=#{session}")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
data, status, = do_fetch.call
|
|
144
|
+
# sessionId 最长 1 小时(Max-Age 3600),过期后重登录重试一次
|
|
145
|
+
if status == 401 || status == 404
|
|
146
|
+
new_session = register_login(account, password, true)
|
|
147
|
+
unless new_session.empty?
|
|
148
|
+
session = new_session
|
|
149
|
+
data, status, = do_fetch.call
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
raise "shadowmail get-emails: http #{status}" if status < 200 || status >= 300
|
|
153
|
+
|
|
154
|
+
msg = data.is_a?(Hash) ? data["message"].to_s : ""
|
|
155
|
+
raise "shadowmail get-emails: #{msg}" unless msg == "Emails read"
|
|
156
|
+
|
|
157
|
+
mails = data.is_a?(Hash) ? data["mails"] : nil
|
|
158
|
+
return [] unless mails.is_a?(Array)
|
|
159
|
+
|
|
160
|
+
mails.filter_map do |m|
|
|
161
|
+
next unless m.is_a?(Hash)
|
|
162
|
+
|
|
163
|
+
flat = m.dup
|
|
164
|
+
flat["from"] = m["sender"]
|
|
165
|
+
flat["to"] = addr
|
|
166
|
+
flat["date"] = m["created_at"]
|
|
167
|
+
# 平台无 text/html 区分,body 为正文(默认按纯文本处理,普通化可按需互转)
|
|
168
|
+
flat["text"] = m["body"]
|
|
169
|
+
Normalize.normalize_email(flat, addr)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# shitpost.email 渠道实现(公共实例,克隆自 shamu4life/throwaway-email)
|
|
8
|
+
#
|
|
9
|
+
# 无认证 REST:
|
|
10
|
+
# POST /api/create 建箱(username/domain/ttl → email/token/type/expires)
|
|
11
|
+
# GET /api/inbox?email=&token= 读信(messages[] 含 from/fromName/subject/text/html/date)
|
|
12
|
+
# 域名池:shitpost.email / letsfuckingpiss.party
|
|
13
|
+
module ShitpostEmail
|
|
14
|
+
CHANNEL = "shitpost-email"
|
|
15
|
+
BASE_URL = "https://shitpost.email"
|
|
16
|
+
DOMAINS = %w[shitpost.email letsfuckingpiss.party].freeze
|
|
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
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# 生成 "sdk"+10 位 [a-z0-9] 随机用户名
|
|
27
|
+
CHARS = ("a".."z").to_a + ("0".."9").to_a
|
|
28
|
+
def random_username
|
|
29
|
+
"sdk" + Array.new(10) { CHARS.sample }.join
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 创建临时邮箱
|
|
33
|
+
# POST /api/create,body 为 {username, domain, ttl}
|
|
34
|
+
# @return [EmailInfo]
|
|
35
|
+
def generate_email
|
|
36
|
+
body = { "username" => random_username, "domain" => DOMAINS.sample, "ttl" => 3600 }
|
|
37
|
+
resp = Http.post("#{BASE_URL}/api/create",
|
|
38
|
+
headers: HEADERS.merge("Content-Type" => "application/json"),
|
|
39
|
+
json: body, timeout: 15)
|
|
40
|
+
resp.raise_for_status
|
|
41
|
+
data = resp.json
|
|
42
|
+
email = data.is_a?(Hash) ? data["email"].to_s.strip : ""
|
|
43
|
+
token = data.is_a?(Hash) ? data["token"].to_s.strip : ""
|
|
44
|
+
raise "shitpost-email: 建箱响应缺少 email 或 token" if email.empty? || token.empty?
|
|
45
|
+
|
|
46
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: token,
|
|
47
|
+
expires_at: data["expires"])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# 读取收件箱
|
|
51
|
+
# GET /api/inbox?email=&token=
|
|
52
|
+
# @param email [String] 邮箱地址
|
|
53
|
+
# @param token [String] 建箱返回的令牌
|
|
54
|
+
# @return [Array<Email>]
|
|
55
|
+
def get_emails(email, token)
|
|
56
|
+
addr = email.to_s.strip
|
|
57
|
+
tk = token.to_s
|
|
58
|
+
raise "shitpost-email: token 为空" if tk.strip.empty?
|
|
59
|
+
|
|
60
|
+
url = "#{BASE_URL}/api/inbox?email=#{URI.encode_www_form_component(addr)}" \
|
|
61
|
+
"&token=#{URI.encode_www_form_component(tk)}"
|
|
62
|
+
resp = Http.get(url, headers: HEADERS, timeout: 15)
|
|
63
|
+
resp.raise_for_status
|
|
64
|
+
data = resp.json
|
|
65
|
+
messages = data.is_a?(Hash) ? data["messages"] : nil
|
|
66
|
+
return [] unless messages.is_a?(Array)
|
|
67
|
+
|
|
68
|
+
messages.filter_map do |m|
|
|
69
|
+
next unless m.is_a?(Hash)
|
|
70
|
+
|
|
71
|
+
row = {
|
|
72
|
+
"id" => m["id"],
|
|
73
|
+
"from" => m["from"],
|
|
74
|
+
"to" => addr,
|
|
75
|
+
"subject" => m["subject"],
|
|
76
|
+
"text" => m["text"],
|
|
77
|
+
"html" => m["html"],
|
|
78
|
+
"date" => m["date"]
|
|
79
|
+
}
|
|
80
|
+
Normalize.normalize_email(row, addr)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -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
|