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,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# Temporarymail 渠道实现(temporarymail.com)
|
|
8
|
+
#
|
|
9
|
+
# 无认证 REST(key 为空即随机建箱):
|
|
10
|
+
# GET /api/?action=requestEmailAccess&key=&value=random 建箱,
|
|
11
|
+
# 响应 {"address":"...","secretKey":"..."},secretKey 用于后续 checkInbox。
|
|
12
|
+
# 读信 GET /api/?action=checkInbox&value=<secretKey>,响应有两种形态:
|
|
13
|
+
# 空收件箱为 [],有信时为 map[id]→邮件元数据对象。
|
|
14
|
+
# 详情 POST /api/?action=getEmail&value=<id> 覆盖真实主题(列表常为 "[No Subject]");
|
|
15
|
+
# 全文 GET /view/?i=<id> 返回 HTML 化网页,本地剥标签还原纯文本。
|
|
16
|
+
# 地址最长周期固定为 4 小时。
|
|
17
|
+
module TemporarymailCom
|
|
18
|
+
CHANNEL = "temporarymail-com"
|
|
19
|
+
BASE_URL = "https://temporarymail.com"
|
|
20
|
+
|
|
21
|
+
# 备用浏览器 UA(403 重试用,规避共享池随机 UA 耗尽)
|
|
22
|
+
ALT_UA = "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
|
+
|
|
25
|
+
HEADERS = {
|
|
26
|
+
"Accept" => "application/json, text/plain, */*",
|
|
27
|
+
"Accept-Language" => "en-US,en;q=0.9",
|
|
28
|
+
"Sec-Fetch-Site" => "same-origin",
|
|
29
|
+
"Sec-Fetch-Mode" => "cors",
|
|
30
|
+
"Sec-Fetch-Dest" => "empty",
|
|
31
|
+
"Referer" => "#{BASE_URL}/",
|
|
32
|
+
"Origin" => BASE_URL,
|
|
33
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
|
|
34
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# 构造 /api/ 请求头(403 时换备用 UA 重试)
|
|
40
|
+
# @param ua [String] User-Agent
|
|
41
|
+
# @return [Hash]
|
|
42
|
+
def api_headers(ua)
|
|
43
|
+
HEADERS.merge("User-Agent" => ua)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 创建 temporarymail.com 临时邮箱
|
|
47
|
+
# GET /api/?action=requestEmailAccess&key=&value=random;token 复用 secretKey。
|
|
48
|
+
# @return [EmailInfo]
|
|
49
|
+
def generate_email
|
|
50
|
+
resp = Http.get("#{BASE_URL}/api/?action=requestEmailAccess&key=&value=random",
|
|
51
|
+
headers: api_headers(HEADERS["User-Agent"]), timeout: 15)
|
|
52
|
+
if resp.status_code == 429
|
|
53
|
+
raise "temporarymail: 创建邮箱平台限流(429),请稍后重试"
|
|
54
|
+
end
|
|
55
|
+
resp.raise_for_status
|
|
56
|
+
data = resp.json
|
|
57
|
+
raise "temporarymail: 创建响应非对象" unless data.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
addr = data["address"].to_s.strip
|
|
60
|
+
key = data["secretKey"].to_s.strip
|
|
61
|
+
raise "temporarymail: 创建响应缺少 address 或 secretKey" if addr.empty? || key.empty?
|
|
62
|
+
|
|
63
|
+
EmailInfo.new(channel: CHANNEL, email: addr, token: key)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# 解析平台双形态响应([] 或 map[id]→对象)为数组
|
|
67
|
+
# @param data [Object] JSON 解析结果
|
|
68
|
+
# @return [Array<Hash>, nil]
|
|
69
|
+
def list_of(data)
|
|
70
|
+
return data if data.is_a?(Array)
|
|
71
|
+
|
|
72
|
+
data.values if data.is_a?(Hash)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 读取 temporarymail 收件箱
|
|
76
|
+
# 列表主题常为 "[No Subject]":逐封拉详情覆盖真实主题,并逐封抓 /view/ 全文。
|
|
77
|
+
# @param email [String] 邮箱地址
|
|
78
|
+
# @param token [String] 建箱返回的 secretKey
|
|
79
|
+
# @return [Array<Email>]
|
|
80
|
+
def get_emails(email, token)
|
|
81
|
+
tk = token.to_s.strip
|
|
82
|
+
raise "temporarymail: token 为空" if tk.empty?
|
|
83
|
+
|
|
84
|
+
body = check_inbox(tk)
|
|
85
|
+
list = list_of(body)
|
|
86
|
+
return [] unless list.is_a?(Array)
|
|
87
|
+
|
|
88
|
+
addr = email.to_s.strip
|
|
89
|
+
list.filter_map do |m|
|
|
90
|
+
next unless m.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
row = m.dup
|
|
93
|
+
row["to"] = addr unless row.key?("to")
|
|
94
|
+
id = row["id"].to_s.strip
|
|
95
|
+
unless id.empty?
|
|
96
|
+
# 详情覆盖真实主题/发件人(失败不致命:列表元数据兜底)
|
|
97
|
+
det = fetch_detail(id)
|
|
98
|
+
if det.is_a?(Hash)
|
|
99
|
+
row["subject"] = det["subject"] if det["subject"].to_s.strip != ""
|
|
100
|
+
row["from"] = det["from"] if det["from"].to_s.strip != ""
|
|
101
|
+
end
|
|
102
|
+
# /view/ 渲染端点全文(失败不致命:列表元数据兜底)
|
|
103
|
+
text = fetch_view(id)
|
|
104
|
+
row["text"] = text if !text.empty?
|
|
105
|
+
end
|
|
106
|
+
Normalize.normalize_email(row, addr)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# 拉取 checkInbox 响应体;403 换备用 UA 重试一次,429 报平台限流。
|
|
111
|
+
# @param token [String] secretKey
|
|
112
|
+
# @return [Object] JSON 解析结果
|
|
113
|
+
def check_inbox(token)
|
|
114
|
+
uas = [HEADERS["User-Agent"], ALT_UA]
|
|
115
|
+
last_status = 0
|
|
116
|
+
last_raw = ""
|
|
117
|
+
uas.each do |ua|
|
|
118
|
+
uri = "#{BASE_URL}/api/?action=checkInbox&value=#{URI.encode_www_form_component(token)}"
|
|
119
|
+
resp = Http.get(uri, headers: api_headers(ua), timeout: 15)
|
|
120
|
+
if resp.status_code == 429
|
|
121
|
+
raise "temporarymail: 读取收件箱平台限流(429),请拉大轮询间隔"
|
|
122
|
+
end
|
|
123
|
+
if resp.ok?
|
|
124
|
+
return resp.json
|
|
125
|
+
end
|
|
126
|
+
last_status = resp.status_code
|
|
127
|
+
last_raw = resp.body.to_s
|
|
128
|
+
# 403/404 疑似 UA 键控风控,换备用 UA 重试一次
|
|
129
|
+
next if [403, 404].include?(resp.status_code)
|
|
130
|
+
|
|
131
|
+
raise "temporarymail: 读取收件箱失败 http #{resp.status_code}: #{last_raw}"
|
|
132
|
+
end
|
|
133
|
+
raise "temporarymail: 读取收件箱失败 http #{last_status}(两次尝试均被拒)"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# 拉取单封详情(POST /api/?action=getEmail&value=<id>),风控失败返回 nil 降级
|
|
137
|
+
# @param id [String] 邮件 ID
|
|
138
|
+
# @return [Hash, nil]
|
|
139
|
+
def fetch_detail(id)
|
|
140
|
+
uri = "#{BASE_URL}/api/?action=getEmail&value=#{URI.encode_www_form_component(id)}"
|
|
141
|
+
resp = Http.post(uri, headers: api_headers(HEADERS["User-Agent"]), timeout: 15)
|
|
142
|
+
return nil if resp.status_code == 429 || !resp.ok?
|
|
143
|
+
|
|
144
|
+
data = resp.json
|
|
145
|
+
return nil unless data.is_a?(Hash)
|
|
146
|
+
|
|
147
|
+
data.values.first
|
|
148
|
+
rescue StandardError
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# 抓取 /view/ 渲染端点全文并还原纯文本
|
|
153
|
+
# @param id [String] 邮件 ID
|
|
154
|
+
# @return [String]
|
|
155
|
+
def fetch_view(id)
|
|
156
|
+
uri = "#{BASE_URL}/view/?i=#{URI.encode_www_form_component(id)}&width=800"
|
|
157
|
+
resp = Http.get(uri,
|
|
158
|
+
headers: {
|
|
159
|
+
"Accept" => "text/html, */*",
|
|
160
|
+
"User-Agent" => HEADERS["User-Agent"],
|
|
161
|
+
"Referer" => "#{BASE_URL}/"
|
|
162
|
+
},
|
|
163
|
+
timeout: 15)
|
|
164
|
+
return "" unless resp.ok?
|
|
165
|
+
|
|
166
|
+
view_to_text(resp.body.to_s)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# 将 /view/ 响应剥标签还原为纯文本(<br>/<p> 换行保留)
|
|
170
|
+
# @param src [String] HTML 化网页原文
|
|
171
|
+
# @return [String]
|
|
172
|
+
def view_to_text(src)
|
|
173
|
+
s = src.to_s
|
|
174
|
+
%w[<br\ /> <br/> <br> <p> </p>].each { |tag| s = s.gsub(tag, "\n") }
|
|
175
|
+
s = s.gsub(/<script\b[^>]*>.*?<\/script>/mi, " ")
|
|
176
|
+
s = s.gsub(/<style\b[^>]*>.*?<\/style>/mi, " ")
|
|
177
|
+
s = s.gsub(/<[^>]+>/, " ")
|
|
178
|
+
s = s.gsub(" ", "&").gsub(" ", " ").gsub(">", ">").gsub("<", "<")
|
|
179
|
+
s = s.gsub(""", '"').gsub("'", "'").gsub("&", "&")
|
|
180
|
+
s.split("\n").map { |ln| ln.strip }.join("\n").strip
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# Tenmin.app 渠道实现(真实 API 域 api.tenmin.app)
|
|
6
|
+
#
|
|
7
|
+
# 建箱+读信共用 GET /api/inbox/{localpart}(Accept: application/json)。
|
|
8
|
+
# localpart 为随机 6 位小写十六进制串(首访即建箱,平台无显式创建接口);
|
|
9
|
+
# 响应:{"inboxId":..,"address":"..@tenmin.app","ttl":600,"count":0,"messages":[]},
|
|
10
|
+
# messages[] 元素字段:id/from/subject/text/html/receivedAt(from 为 {name,address} 对象)。
|
|
11
|
+
module TenminApp
|
|
12
|
+
CHANNEL = "tenmin-app"
|
|
13
|
+
BASE_URL = "https://api.tenmin.app"
|
|
14
|
+
DEFAULT_DOMAIN = "tenmin.app"
|
|
15
|
+
|
|
16
|
+
HEADERS = {
|
|
17
|
+
"Accept" => "application/json",
|
|
18
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
19
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
HEX_CHARS = ("0".."9").to_a + ("a".."f").to_a
|
|
25
|
+
|
|
26
|
+
# 生成 6 位小写十六进制随机 localpart
|
|
27
|
+
def random_local
|
|
28
|
+
Array.new(6) { HEX_CHARS.sample }.join
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 请求 /api/inbox/{localpart}
|
|
32
|
+
# @param localpart [String] 邮箱 localpart
|
|
33
|
+
# @return [Hash] 解析后的收件箱响应(解析失败返回空 Hash)
|
|
34
|
+
def fetch_inbox(localpart)
|
|
35
|
+
resp = Http.get("#{BASE_URL}/api/inbox/#{localpart}", headers: HEADERS, timeout: 15)
|
|
36
|
+
resp.raise_for_status
|
|
37
|
+
data = resp.json
|
|
38
|
+
data.is_a?(Hash) ? data : {}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 创建 tenmin.app 临时邮箱
|
|
42
|
+
# 首次 GET 随机 localpart 即自动建箱(10 分钟 TTL),token 存储 localpart
|
|
43
|
+
# @return [EmailInfo]
|
|
44
|
+
def generate_email
|
|
45
|
+
local = random_local
|
|
46
|
+
data = fetch_inbox(local)
|
|
47
|
+
address = data["address"].to_s.strip
|
|
48
|
+
address = "#{local}@#{DEFAULT_DOMAIN}" if address.empty?
|
|
49
|
+
ttl = data["ttl"].to_i
|
|
50
|
+
expires_at = nil
|
|
51
|
+
expires_at = ((Time.now.to_f + ttl) * 1000).to_i if ttl.positive?
|
|
52
|
+
EmailInfo.new(channel: CHANNEL, email: address, token: local, expires_at: expires_at)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# 读取 tenmin.app 收件箱
|
|
56
|
+
# 复用建箱同一 localpart 轮询;from 为 {name,address} 对象,拆开格式化后归一。
|
|
57
|
+
# @param email [String] 邮箱地址
|
|
58
|
+
# @param token [String] localpart
|
|
59
|
+
# @return [Array<Email>]
|
|
60
|
+
def get_emails(email, token)
|
|
61
|
+
local = token.to_s.strip
|
|
62
|
+
raise "tenmin-app: token 为空" if local.empty?
|
|
63
|
+
|
|
64
|
+
data = fetch_inbox(local)
|
|
65
|
+
messages = data["messages"]
|
|
66
|
+
return [] unless messages.is_a?(Array)
|
|
67
|
+
|
|
68
|
+
addr = email.to_s.strip
|
|
69
|
+
messages.filter_map do |m|
|
|
70
|
+
next unless m.is_a?(Hash)
|
|
71
|
+
|
|
72
|
+
from = m["from"]
|
|
73
|
+
# from 为对象({name,address})时拆出地址字段
|
|
74
|
+
from_str = if from.is_a?(Hash)
|
|
75
|
+
fname = from["name"].to_s.strip
|
|
76
|
+
faddr = from["address"].to_s.strip
|
|
77
|
+
if !faddr.empty? && !fname.empty?
|
|
78
|
+
"#{fname} <#{faddr}>"
|
|
79
|
+
else
|
|
80
|
+
faddr.empty? ? fname : faddr
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
from.to_s
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
row = {
|
|
87
|
+
"id" => m["id"],
|
|
88
|
+
"from" => from_str,
|
|
89
|
+
"to" => addr,
|
|
90
|
+
"subject" => m["subject"],
|
|
91
|
+
"text" => m["text"],
|
|
92
|
+
"html" => m["html"],
|
|
93
|
+
"date" => m["receivedAt"] || m["created_at"] || m["date"]
|
|
94
|
+
}
|
|
95
|
+
Normalize.normalize_email(row, addr)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# 10minutemail.net 渠道实现
|
|
8
|
+
#
|
|
9
|
+
# 流程:GET / 获取 PHPSESSID session cookie + 从 HTML 输入框提取邮箱地址
|
|
10
|
+
# GET /mailbox.ajax.php?_={毫秒时间戳} 获取邮件列表(HTML 表格)
|
|
11
|
+
# GET /readmail.html?mid={id} 获取单封邮件正文
|
|
12
|
+
# token 为空字符串(session 由 cookie jar 自动维护)
|
|
13
|
+
module TenminutemailNet
|
|
14
|
+
CHANNEL = "10minutemail-net"
|
|
15
|
+
BASE_URL = "https://10minutemail.net"
|
|
16
|
+
|
|
17
|
+
EMAIL_RE = /value="([^"]+@[^"]+)"/
|
|
18
|
+
ROW_RE = /<tr[^>]*>(.*?)<\/tr>/im
|
|
19
|
+
MID_RE = /readmail\.html\?mid=([^'"&]+)/i
|
|
20
|
+
CELL_RE = /<td[^>]*>(.*?)<\/td>/im
|
|
21
|
+
TITLE_RE = /title="([^"]+)"/i
|
|
22
|
+
CF_RE = /data-cfemail="([0-9a-fA-F]+)"/i
|
|
23
|
+
TAG_RE = /<[^>]+>/
|
|
24
|
+
BODY_MARK = 'class="mailinhtml"'
|
|
25
|
+
|
|
26
|
+
BROWSER_HEADERS = {
|
|
27
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
28
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
|
|
29
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif," \
|
|
30
|
+
"image/webp,image/apng,*/*;q=0.8",
|
|
31
|
+
"Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
AJAX_HEADERS = BROWSER_HEADERS.merge(
|
|
35
|
+
"Accept" => "*/*",
|
|
36
|
+
"X-Requested-With" => "XMLHttpRequest",
|
|
37
|
+
"Referer" => BASE_URL + "/"
|
|
38
|
+
).freeze
|
|
39
|
+
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# 解码 Cloudflare 邮箱保护混淆字符串
|
|
43
|
+
# @param encoded [String] 十六进制字符串
|
|
44
|
+
# @return [String]
|
|
45
|
+
def cf_decode(encoded)
|
|
46
|
+
data = [encoded].pack("H*")
|
|
47
|
+
return "" if data.length < 2
|
|
48
|
+
|
|
49
|
+
key = data.getbyte(0)
|
|
50
|
+
out = data.bytes[1..].map { |b| b ^ key }.pack("C*")
|
|
51
|
+
out.include?("@") ? out : ""
|
|
52
|
+
rescue StandardError
|
|
53
|
+
""
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 从单元格 HTML 提取纯文本(优先解码 CF 混淆)
|
|
57
|
+
# @param cell [String]
|
|
58
|
+
# @return [String]
|
|
59
|
+
def extract_text(cell)
|
|
60
|
+
if (m = cell.match(CF_RE))
|
|
61
|
+
decoded = cf_decode(m[1])
|
|
62
|
+
return decoded unless decoded.empty?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
text = cell.gsub(TAG_RE, "")
|
|
66
|
+
text = text.gsub(" ", " ").gsub(" ", " ")
|
|
67
|
+
.gsub("&", "&").gsub("<", "<")
|
|
68
|
+
.gsub(">", ">").gsub(""", '"')
|
|
69
|
+
text.strip
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# 从整页 HTML 提取 mailinhtml div 正文
|
|
73
|
+
# @param page [String]
|
|
74
|
+
# @return [String]
|
|
75
|
+
def extract_body(page)
|
|
76
|
+
si = page.index(BODY_MARK)
|
|
77
|
+
return "" unless si
|
|
78
|
+
|
|
79
|
+
gt = page.index(">", si)
|
|
80
|
+
return "" unless gt
|
|
81
|
+
|
|
82
|
+
content_start = gt + 1
|
|
83
|
+
rest = page[content_start..]
|
|
84
|
+
|
|
85
|
+
ei = rest.index("email-decode.min.js")
|
|
86
|
+
segment = if ei
|
|
87
|
+
s = rest[0...ei]
|
|
88
|
+
s = s[0...s.rindex("<script")] if s.rindex("<script")
|
|
89
|
+
s.strip.sub(/<\/div>\s*\z/, "").strip.sub(/<\/div>\s*\z/, "").strip
|
|
90
|
+
else
|
|
91
|
+
di = rest.index("</div>")
|
|
92
|
+
di ? rest[0...di].strip : rest.strip
|
|
93
|
+
end
|
|
94
|
+
segment
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# 创建 10minutemail.net 临时邮箱
|
|
98
|
+
# @return [EmailInfo]
|
|
99
|
+
def generate_email
|
|
100
|
+
resp = Http.get(BASE_URL + "/", headers: BROWSER_HEADERS, timeout: 15)
|
|
101
|
+
resp.raise_for_status
|
|
102
|
+
m = resp.body.match(EMAIL_RE)
|
|
103
|
+
raise "10minutemail-net: 未能从首页提取邮箱地址" unless m
|
|
104
|
+
|
|
105
|
+
email = m[1].strip
|
|
106
|
+
raise "10minutemail-net: 获取到的邮箱地址无效" if email.empty? || !email.include?("@")
|
|
107
|
+
|
|
108
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: "")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# 获取 10minutemail.net 邮件列表
|
|
112
|
+
# @param email [String]
|
|
113
|
+
# @param token [String] 未使用(session 由 cookie jar 维护)
|
|
114
|
+
# @return [Array<Email>]
|
|
115
|
+
def get_emails(email, token)
|
|
116
|
+
_ = token
|
|
117
|
+
ts = (Time.now.to_f * 1000).to_i
|
|
118
|
+
list_url = "#{BASE_URL}/mailbox.ajax.php?_=#{ts}"
|
|
119
|
+
resp = Http.get(list_url, headers: AJAX_HEADERS, timeout: 15)
|
|
120
|
+
resp.raise_for_status
|
|
121
|
+
|
|
122
|
+
rows = resp.body.scan(ROW_RE)
|
|
123
|
+
emails = []
|
|
124
|
+
rows.each do |row_inner_arr|
|
|
125
|
+
row_inner = row_inner_arr[0]
|
|
126
|
+
next if row_inner.downcase.include?("<th")
|
|
127
|
+
|
|
128
|
+
mid_m = row_inner.match(MID_RE)
|
|
129
|
+
next unless mid_m
|
|
130
|
+
|
|
131
|
+
mail_id = mid_m[1].strip
|
|
132
|
+
next if mail_id.empty?
|
|
133
|
+
|
|
134
|
+
cells = row_inner.scan(CELL_RE).map { |c| c[0] }
|
|
135
|
+
from_cell = cells[0] || ""
|
|
136
|
+
subject_cell = cells[1] || ""
|
|
137
|
+
date_cell = cells[2] || ""
|
|
138
|
+
|
|
139
|
+
from = extract_text(from_cell)
|
|
140
|
+
subject = extract_text(subject_cell)
|
|
141
|
+
date = if (tm = date_cell.match(TITLE_RE))
|
|
142
|
+
tm[1].strip
|
|
143
|
+
else
|
|
144
|
+
extract_text(date_cell)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# 未读行的 <tr> 样式含 font-weight: bold
|
|
148
|
+
full_row = "<tr#{row_inner}</tr>"
|
|
149
|
+
is_read = !full_row.downcase.include?("font-weight: bold")
|
|
150
|
+
|
|
151
|
+
html_body = fetch_body(mail_id)
|
|
152
|
+
|
|
153
|
+
flat = {
|
|
154
|
+
"id" => mail_id, "from" => from, "to" => email,
|
|
155
|
+
"subject" => subject, "html" => html_body,
|
|
156
|
+
"date" => date, "isRead" => is_read
|
|
157
|
+
}
|
|
158
|
+
emails << Normalize.normalize_email(flat, email)
|
|
159
|
+
end
|
|
160
|
+
emails
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# 获取单封邮件正文
|
|
164
|
+
# @param mail_id [String]
|
|
165
|
+
# @return [String]
|
|
166
|
+
def fetch_body(mail_id)
|
|
167
|
+
return "" if mail_id.empty?
|
|
168
|
+
|
|
169
|
+
url = "#{BASE_URL}/readmail.html?mid=#{mail_id}"
|
|
170
|
+
resp = Http.get(
|
|
171
|
+
url,
|
|
172
|
+
headers: BROWSER_HEADERS.merge("Referer" => BASE_URL + "/"),
|
|
173
|
+
timeout: 15
|
|
174
|
+
)
|
|
175
|
+
return "" unless resp.status_code == 200
|
|
176
|
+
|
|
177
|
+
extract_body(resp.body)
|
|
178
|
+
rescue StandardError
|
|
179
|
+
""
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# 30minemail 渠道实现(30minemail.com)
|
|
8
|
+
#
|
|
9
|
+
# 官网邮箱由服务端 16 位 hex 本地名识别:
|
|
10
|
+
# GET /?generate 返回完整 HTML 页面,从中解析 <地址>@30minemail.com;
|
|
11
|
+
# 本地名无效邮箱访问 messages.php 时返回 ok:false/expired:true,故必须经服务端建箱。
|
|
12
|
+
# 读信 GET /messages.php?email=<完整地址>&_=<unix毫秒>,响应
|
|
13
|
+
# {"ok":true,"expired":false,"count":0,"emails":[],"expires_in":...}。
|
|
14
|
+
# emails 元素字段实测:{id,from,to,subject,date,html}(html 为完整正文)。
|
|
15
|
+
# 无认证、无 Cookie、无 CSRF。
|
|
16
|
+
module ThirtyMinEmail
|
|
17
|
+
CHANNEL = "30minemail"
|
|
18
|
+
BASE_URL = "https://30minemail.com"
|
|
19
|
+
DOMAIN = "30minemail.com"
|
|
20
|
+
|
|
21
|
+
HEADERS = {
|
|
22
|
+
"Accept" => "text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8",
|
|
23
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
|
|
24
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# 创建 30minemail.com 临时邮箱
|
|
30
|
+
# GET /?generate 返回 HTML 页面,从其中解析 16 位 hex 本地名地址;
|
|
31
|
+
# token 复用完整地址(服务端以地址定位收件箱)。
|
|
32
|
+
# @return [EmailInfo]
|
|
33
|
+
def generate_email
|
|
34
|
+
resp = Http.get("#{BASE_URL}/?generate", headers: HEADERS, timeout: 15)
|
|
35
|
+
resp.raise_for_status
|
|
36
|
+
page = resp.body.to_s
|
|
37
|
+
idx = page.index("@#{DOMAIN}")
|
|
38
|
+
raise "30minemail: 创建页面未找到邮箱地址" if idx.nil?
|
|
39
|
+
|
|
40
|
+
# 向前查找本地名起点:空白或 > 之后
|
|
41
|
+
start = idx
|
|
42
|
+
while start.positive? && ![" ", "\n", "\t", ">", '"'].include?(page[start - 1])
|
|
43
|
+
start -= 1
|
|
44
|
+
end
|
|
45
|
+
local = page[start...idx].to_s.strip
|
|
46
|
+
raise "30minemail: 创建页面解析地址异常" if local.length < 8
|
|
47
|
+
|
|
48
|
+
addr = "#{local}@#{DOMAIN}"
|
|
49
|
+
EmailInfo.new(channel: CHANNEL, email: addr, token: addr)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 读取 30minemail.com 收件箱
|
|
53
|
+
# GET /messages.php?email=<完整地址>&_=<unix毫秒>,模拟官方轮询参数。
|
|
54
|
+
# @param email [String] 完整邮箱地址
|
|
55
|
+
# @param token [String] 复用完整地址(服务端以地址定位收件箱)
|
|
56
|
+
# @return [Array<Email>]
|
|
57
|
+
def get_emails(email, token)
|
|
58
|
+
addr = email.to_s.strip
|
|
59
|
+
raise "30minemail: 邮箱地址为空" if addr.empty?
|
|
60
|
+
|
|
61
|
+
uri = "#{BASE_URL}/messages.php?email=#{URI.encode_www_form_component(addr)}&_=#{(Time.now.to_f * 1000).to_i}"
|
|
62
|
+
resp = Http.get(uri,
|
|
63
|
+
headers: {
|
|
64
|
+
"Accept" => "application/json",
|
|
65
|
+
"User-Agent" => HEADERS["User-Agent"]
|
|
66
|
+
},
|
|
67
|
+
timeout: 15)
|
|
68
|
+
raise "30minemail: 读取收件箱失败 http #{resp.status_code}" unless resp.ok?
|
|
69
|
+
|
|
70
|
+
data = resp.json
|
|
71
|
+
raise "30minemail: 收件箱响应非对象" unless data.is_a?(Hash)
|
|
72
|
+
|
|
73
|
+
unless data["ok"] == true && data["expired"] != true
|
|
74
|
+
raise "30minemail: 收件箱不可用或已过期"
|
|
75
|
+
end
|
|
76
|
+
emails = data["emails"]
|
|
77
|
+
return [] unless emails.is_a?(Array)
|
|
78
|
+
|
|
79
|
+
emails.filter_map do |m|
|
|
80
|
+
next unless m.is_a?(Hash)
|
|
81
|
+
|
|
82
|
+
row = m.dup
|
|
83
|
+
# 列表元素无 to 字段时注入收件人地址
|
|
84
|
+
row["to"] = addr unless row.key?("to")
|
|
85
|
+
Normalize.normalize_email(row, addr)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# Tmpkit 渠道实现(tmpkit.com,Next.js tRPC 前端 + Go-Guerrilla SMTP 后端)
|
|
8
|
+
#
|
|
9
|
+
# 流程:
|
|
10
|
+
# POST /api/rpc/tempmail/initSession(body 为 tRPC 包裹 {"json":{}})建箱,
|
|
11
|
+
# 响应 session.sessionId 与 session.email(sessionId 同值 Set-Cookie)
|
|
12
|
+
# POST /api/rpc/tempmail/getEmails({"json":{"offset":0,"limit":20}} +
|
|
13
|
+
# Cookie tempmail_session=<sessionId>)列信(列表键 mailId,无 id)
|
|
14
|
+
# POST /api/rpc/tempmail/getEmailDetail({"json":{"mailId":<数字>}})取正文
|
|
15
|
+
# 会话粘性:token 保存 sessionId(tempMailSession=<sid>),读信显式携带
|
|
16
|
+
# Cookie 请求头,防并行会话串箱。
|
|
17
|
+
module Tmpkit
|
|
18
|
+
CHANNEL = "tmpkit"
|
|
19
|
+
BASE_URL = "https://tmpkit.com"
|
|
20
|
+
RPC_PREFIX = "#{BASE_URL}/api/rpc/tempmail"
|
|
21
|
+
|
|
22
|
+
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " \
|
|
23
|
+
"(KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36"
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# tRPC 调用请求头(Content-Type JSON、Accept 通配、同站 Origin/Referer)
|
|
28
|
+
# @param cookie [String] sessionId,非空时携带 tempmail_session Cookie
|
|
29
|
+
# @return [Hash]
|
|
30
|
+
def rpc_headers(cookie = "")
|
|
31
|
+
hdrs = {
|
|
32
|
+
"User-Agent" => USER_AGENT,
|
|
33
|
+
"Accept" => "*/*",
|
|
34
|
+
"Content-Type" => "application/json",
|
|
35
|
+
"Origin" => BASE_URL,
|
|
36
|
+
"Referer" => "#{BASE_URL}/en"
|
|
37
|
+
}
|
|
38
|
+
hdrs["Cookie"] = "tempmail_session=#{cookie}" unless cookie.empty?
|
|
39
|
+
hdrs
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 对 tmpkit 发起 rpc 调用(tRPC 包裹 {"json":<reqBody>})
|
|
43
|
+
# 响应外层 {"json":{...}} 解析为 Hash 返回。
|
|
44
|
+
# @param procedure [String] rpc 过程名
|
|
45
|
+
# @param req_body [Hash] 过程参数
|
|
46
|
+
# @param cookie [String] sessionId
|
|
47
|
+
# @return [Hash]
|
|
48
|
+
def rpc(procedure, req_body, cookie = "")
|
|
49
|
+
resp = Http.post("#{RPC_PREFIX}/#{procedure}",
|
|
50
|
+
headers: rpc_headers(cookie),
|
|
51
|
+
body: JSON.generate({ json: req_body }),
|
|
52
|
+
timeout: 15)
|
|
53
|
+
resp.raise_for_status
|
|
54
|
+
outer = resp.json
|
|
55
|
+
payload = outer.is_a?(Hash) ? outer["json"] : nil
|
|
56
|
+
raise "tmpkit: #{procedure} 响应缺 json 载荷" unless payload.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
payload
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 创建 tmpkit.com 临时邮箱
|
|
62
|
+
# 调 initSession(tRPC 包裹 {"json":{}}),sessionId 与邮箱地址同返。
|
|
63
|
+
# @return [EmailInfo]
|
|
64
|
+
def generate_email
|
|
65
|
+
data = rpc("initSession", {})
|
|
66
|
+
sess = data["session"]
|
|
67
|
+
raise "tmpkit: 创建会话响应缺 session 字段" unless sess.is_a?(Hash)
|
|
68
|
+
|
|
69
|
+
email = sess["email"].to_s.strip
|
|
70
|
+
session_id = sess["sessionId"].to_s.strip
|
|
71
|
+
raise "tmpkit: 创建会话响应缺少必要字段(email/sessionId)" if email.empty? || session_id.empty? || !email.include?("@")
|
|
72
|
+
|
|
73
|
+
EmailInfo.new(channel: CHANNEL, email: email,
|
|
74
|
+
token: "tempMailSession=#{session_id}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# 获取 tmpkit.com 邮件列表
|
|
78
|
+
# getEmails(offset 0 / limit 20)取摘要,逐封 getEmailDetail 拉详情并
|
|
79
|
+
# 将详情键并入摘要;详情失败回退列表摘要。getEmails 返回的 session.email
|
|
80
|
+
# 与收信邮箱不符时报错,防止会话被覆盖。
|
|
81
|
+
# @param email [String] 邮箱地址
|
|
82
|
+
# @param token [String] 会话凭据串(tempMailSession=<sessionId>)
|
|
83
|
+
# @return [Array<Email>]
|
|
84
|
+
def get_emails(email, token)
|
|
85
|
+
raw = token.to_s.strip
|
|
86
|
+
mailbox = raw.start_with?("tempMailSession=") ? raw.delete_prefix("tempMailSession=") : raw
|
|
87
|
+
raise "tmpkit: 会话 token 为空" if mailbox.empty?
|
|
88
|
+
|
|
89
|
+
data = rpc("getEmails", { offset: 0, limit: 20 }, mailbox)
|
|
90
|
+
|
|
91
|
+
# 会话指向校验:getEmails 响应自带 session.email(无 Cookie 时
|
|
92
|
+
# session 为 null,同样视为会话失效)
|
|
93
|
+
sess = data["session"]
|
|
94
|
+
if sess.is_a?(Hash)
|
|
95
|
+
got = sess["email"].to_s.strip
|
|
96
|
+
raise "tmpkit: 会话邮箱不匹配(响应 #{got},请求 #{email})" if !got.empty? && got != email
|
|
97
|
+
else
|
|
98
|
+
raise "tmpkit: 会话已失效(getEmails 返回空会话)"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
list = data["emails"]
|
|
102
|
+
raise "tmpkit: 邮件列表响应缺 emails 字段" unless list.is_a?(Array)
|
|
103
|
+
|
|
104
|
+
list.map do |item|
|
|
105
|
+
next unless item.is_a?(Hash)
|
|
106
|
+
|
|
107
|
+
m = item.dup
|
|
108
|
+
# mailId 为数字:合法时逐封拉详情(详情键并入摘要,跳过会话类键)
|
|
109
|
+
mail_id = m["mailId"]
|
|
110
|
+
if mail_id.is_a?(Numeric) && mail_id.positive?
|
|
111
|
+
begin
|
|
112
|
+
detail = rpc("getEmailDetail", { mailId: mail_id.to_i }, mailbox)
|
|
113
|
+
if detail.is_a?(Hash)
|
|
114
|
+
detail.each do |k, v|
|
|
115
|
+
next if %w[session emails total error].include?(k)
|
|
116
|
+
|
|
117
|
+
m[k] = v
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
rescue StandardError
|
|
121
|
+
# 详情失败回退列表摘要
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
Normalize.normalize_email(m, email)
|
|
125
|
+
end.compact
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|