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,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
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module TempmailSdk
|
|
7
|
+
module Providers
|
|
8
|
+
# tempmail.cn 渠道实现(HTTP API 模式)
|
|
9
|
+
#
|
|
10
|
+
# 流程:WebSocket 创建邮箱(request mailbox 事件),获取 shortID
|
|
11
|
+
# GET /api/mails/{email} 获取邮件列表(HTTP API)
|
|
12
|
+
# Ruby 端使用 HTTP API 拉取邮件,不实现 WebSocket 推送
|
|
13
|
+
# token 不需要(无状态 HTTP API)
|
|
14
|
+
module TempmailCn
|
|
15
|
+
CHANNEL = "tempmail-cn"
|
|
16
|
+
DEFAULT_HOST = "tempmail.cn"
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# 规范化 host(去除协议前缀、路径、@ 符号等)
|
|
21
|
+
# @param domain [String, nil]
|
|
22
|
+
# @return [String]
|
|
23
|
+
def normalize_host(domain)
|
|
24
|
+
return DEFAULT_HOST if domain.nil? || domain.to_s.strip.empty?
|
|
25
|
+
|
|
26
|
+
host = domain.to_s.strip
|
|
27
|
+
if host.start_with?("http://", "https://")
|
|
28
|
+
begin
|
|
29
|
+
uri = URI.parse(host)
|
|
30
|
+
host = uri.host.to_s unless uri.host.to_s.empty?
|
|
31
|
+
rescue URI::InvalidURIError
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
elsif host.include?("/")
|
|
35
|
+
host = host.split("/").first.to_s
|
|
36
|
+
end
|
|
37
|
+
host = host.split("@").last.to_s if host.include?("@")
|
|
38
|
+
host = host.strip.gsub(/\A\.+|\.+\z/, "")
|
|
39
|
+
host.empty? ? DEFAULT_HOST : host
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 生成随机本地部分(小写字母 + 数字)
|
|
43
|
+
# @param length [Integer]
|
|
44
|
+
# @return [String]
|
|
45
|
+
def random_local(length = 10)
|
|
46
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
47
|
+
Array.new(length) { chars.sample }.join
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# 创建 tempmail.cn 临时邮箱
|
|
51
|
+
# @param domain [String, nil]
|
|
52
|
+
# @return [EmailInfo]
|
|
53
|
+
def generate_email(domain = nil)
|
|
54
|
+
host = normalize_host(domain)
|
|
55
|
+
# 直接生成随机邮箱(HTTP 模式,不走 WebSocket)
|
|
56
|
+
email = "#{random_local}@#{host}"
|
|
57
|
+
EmailInfo.new(channel: CHANNEL, email: email)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 获取 tempmail.cn 邮件列表
|
|
61
|
+
# @param email [String]
|
|
62
|
+
# @return [Array<Email>]
|
|
63
|
+
def get_emails(email)
|
|
64
|
+
email = email.to_s.strip
|
|
65
|
+
raise "tempmail-cn: 邮箱地址为空" if email.empty?
|
|
66
|
+
|
|
67
|
+
at_idx = email.index("@")
|
|
68
|
+
host = at_idx ? email[(at_idx + 1)..] : DEFAULT_HOST
|
|
69
|
+
host = normalize_host(host)
|
|
70
|
+
|
|
71
|
+
api_url = "https://#{host}/api/mails/#{URI.encode_uri_component(email)}"
|
|
72
|
+
hdrs = {
|
|
73
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
74
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
|
|
75
|
+
"Accept" => "application/json",
|
|
76
|
+
"Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
|
77
|
+
"Cache-Control" => "no-cache",
|
|
78
|
+
"DNT" => "1",
|
|
79
|
+
"Pragma" => "no-cache",
|
|
80
|
+
"Referer" => "https://#{host}/"
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
resp = Http.get(api_url, headers: hdrs, timeout: 15)
|
|
84
|
+
resp.raise_for_status
|
|
85
|
+
|
|
86
|
+
data = resp.json
|
|
87
|
+
mails = data.is_a?(Hash) ? Array(data["mails"]) : []
|
|
88
|
+
|
|
89
|
+
mails.filter_map do |raw|
|
|
90
|
+
next unless raw.is_a?(Hash)
|
|
91
|
+
|
|
92
|
+
id = raw["id"].to_s
|
|
93
|
+
id = raw["_id"].to_s if id.empty?
|
|
94
|
+
flat = {
|
|
95
|
+
"id" => id,
|
|
96
|
+
"from" => raw["from"].to_s,
|
|
97
|
+
"to" => email,
|
|
98
|
+
"subject" => raw["subject"].to_s,
|
|
99
|
+
"text" => raw["text"].to_s,
|
|
100
|
+
"html" => raw["html"].to_s,
|
|
101
|
+
"date" => raw["date"].to_s,
|
|
102
|
+
"isRead" => false,
|
|
103
|
+
"attachments" => raw["attachments"]
|
|
104
|
+
}
|
|
105
|
+
flat["text"] = raw["body"].to_s if flat["text"].empty?
|
|
106
|
+
Normalize.normalize_email(flat, email)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "time"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module TempmailSdk
|
|
9
|
+
module Providers
|
|
10
|
+
# Tempmail.ee 渠道实现(tempmail.ee)
|
|
11
|
+
#
|
|
12
|
+
# 平台读信端 /api/mails 的 403 Access denied 是「会话 Cookie 绑定校验」:
|
|
13
|
+
# temp_email 与 temp_mail_session 共同构成读信凭据,缺一即 403。
|
|
14
|
+
# 只要在同一事务内完成 change → 提取 Set-Cookie → 读信,即可真实读到邮件。
|
|
15
|
+
#
|
|
16
|
+
# 已验证通行配方:
|
|
17
|
+
# - POST /api/mailbox/change 若不带 sec-ch-ua 头会被平台拒绝
|
|
18
|
+
# (403 Browser request required),带齐即 200,并下发会话 Cookie;
|
|
19
|
+
# - POST /api/mails 带完整 sec-ch-ua 三件套 + 会话 Cookie 即 200。
|
|
20
|
+
# 本端无全局 Cookie 罐,会话凭据由本渠道以显式 Cookie 请求头逐请求携带。
|
|
21
|
+
module TempmailEe
|
|
22
|
+
CHANNEL = "tempmail-ee"
|
|
23
|
+
BASE_URL = "https://tempmail.ee"
|
|
24
|
+
|
|
25
|
+
# 与 sec-ch-ua 品牌版本一致的固定 UA(Chrome 154 / Linux)
|
|
26
|
+
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " \
|
|
27
|
+
"(KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36"
|
|
28
|
+
|
|
29
|
+
# Token 前缀,用于识别本渠道会话凭据串
|
|
30
|
+
TOKEN_PREFIX = "tempmail-ee|"
|
|
31
|
+
|
|
32
|
+
SEC_CH_HEADERS = {
|
|
33
|
+
"Sec-Ch-Ua" => '"Chromium";v="154", "Google Chrome";v="154", "Not.A/Brand";v="99"',
|
|
34
|
+
"Sec-Ch-Ua-Mobile" => "?0",
|
|
35
|
+
"Sec-Ch-Ua-Platform" => '"Linux"'
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
# 建箱提交的浏览器指纹(与官方前端一致)
|
|
39
|
+
BROWSER_INTEGRITY = {
|
|
40
|
+
"webdriver" => false, "languagesMissing" => false, "languageMissing" => false,
|
|
41
|
+
"pluginsMissing" => false, "pluginsUndefined" => false, "outerSizeMissing" => false,
|
|
42
|
+
"innerSizeMissing" => false, "screenMissing" => false, "screenDepthMissing" => false,
|
|
43
|
+
"timezoneMissing" => false, "timezoneOffsetMissing" => false,
|
|
44
|
+
"userAgentDataPresent" => true, "userAgentMissing" => false,
|
|
45
|
+
"platformClass" => "Linux", "mobile" => false, "collectionFailed" => false
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
module_function
|
|
49
|
+
|
|
50
|
+
# 组装浏览器特征安全头(同站 fetch 全套)
|
|
51
|
+
# @param with_sec_ch [Boolean] 是否携带 sec-ch-ua 三件套(change 必须)
|
|
52
|
+
# @return [Hash]
|
|
53
|
+
def browser_headers(with_sec_ch)
|
|
54
|
+
hdrs = {
|
|
55
|
+
"Accept" => "application/json",
|
|
56
|
+
"Content-Type" => "application/json",
|
|
57
|
+
"X-Requested-With" => "XMLHttpRequest",
|
|
58
|
+
"Origin" => BASE_URL,
|
|
59
|
+
"Referer" => "#{BASE_URL}/",
|
|
60
|
+
"Sec-Fetch-Site" => "same-origin",
|
|
61
|
+
"Sec-Fetch-Mode" => "cors",
|
|
62
|
+
"Sec-Fetch-Dest" => "empty",
|
|
63
|
+
"Accept-Language" => "en-US,en;q=0.9",
|
|
64
|
+
"User-Agent" => USER_AGENT
|
|
65
|
+
}
|
|
66
|
+
hdrs.merge!(SEC_CH_HEADERS) if with_sec_ch
|
|
67
|
+
hdrs
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 创建 tempmail.ee 临时邮箱
|
|
71
|
+
# 事务会话:GET / 面熟首访 → POST /api/mailbox/change(sec-ch-ua 全套头)
|
|
72
|
+
# → 提取 Set-Cookie 中的 temp_mail_session,随 token 透传给读信。
|
|
73
|
+
# @return [EmailInfo]
|
|
74
|
+
def generate_email
|
|
75
|
+
# 步骤 1:GET / 建立会话面熟(响应 Cookie 不参与后续,凭据以 change 为准)
|
|
76
|
+
begin
|
|
77
|
+
Http.get(BASE_URL,
|
|
78
|
+
headers: {
|
|
79
|
+
"User-Agent" => USER_AGENT,
|
|
80
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9," \
|
|
81
|
+
"image/avif,image/webp,*/*;q=0.8"
|
|
82
|
+
},
|
|
83
|
+
timeout: 15)
|
|
84
|
+
rescue StandardError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 步骤 2:POST /api/mailbox/change 提交指纹换新邮箱(必须带 sec-ch-ua)
|
|
89
|
+
body = { "turnstileToken" => nil, "browserIntegrity" => BROWSER_INTEGRITY }
|
|
90
|
+
ch = Http.post("#{BASE_URL}/api/mailbox/change",
|
|
91
|
+
headers: browser_headers(true), json: body, timeout: 15)
|
|
92
|
+
data = ch.json
|
|
93
|
+
email = data.is_a?(Hash) && data["success"] ? data["newEmail"].to_s.strip : ""
|
|
94
|
+
raise "tempmail-ee: 建箱响应缺少邮箱(status #{ch.status_code})" if email.empty?
|
|
95
|
+
|
|
96
|
+
# 步骤 3:从 Set-Cookie 接管会话凭据,供读信校验使用
|
|
97
|
+
session = ch.cookie_value("temp_mail_session").to_s.strip
|
|
98
|
+
raise "tempmail-ee: 建箱成功但未下发会话 Cookie,无法读信" if session.empty?
|
|
99
|
+
|
|
100
|
+
expires_at = data["expiresAt"].to_s
|
|
101
|
+
if expires_at.empty?
|
|
102
|
+
expires_at = ((Time.now.to_f + 60 * 60) * 1000).to_i
|
|
103
|
+
end
|
|
104
|
+
EmailInfo.new(channel: CHANNEL, email: email,
|
|
105
|
+
token: "#{TOKEN_PREFIX}temp_email=#{email}; temp_mail_session=#{session}",
|
|
106
|
+
expires_at: expires_at)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# 解析读信凭据(防御 token 与 email 不一致,以请求邮箱为准重拼 Cookie)
|
|
110
|
+
# @param token [String] Generate 时下发的渠道凭据串
|
|
111
|
+
# @param email [String] 请求读取的邮箱
|
|
112
|
+
# @return [String] 显式 Cookie 请求头(temp_email + temp_mail_session)
|
|
113
|
+
def resolve_cookie(token, email)
|
|
114
|
+
raise "tempmail-ee: 会话凭据缺失或已失效,请重新 Generate 获取新邮箱" unless token.to_s.start_with?(TOKEN_PREFIX)
|
|
115
|
+
|
|
116
|
+
cred = token.to_s[TOKEN_PREFIX.length..]
|
|
117
|
+
session = ""
|
|
118
|
+
cred.split(";").each do |part|
|
|
119
|
+
kv = part.strip
|
|
120
|
+
session = kv[("temp_mail_session=".length)..] if kv.start_with?("temp_mail_session=")
|
|
121
|
+
end
|
|
122
|
+
raise "tempmail-ee: 会话凭据缺失或已失效,请重新 Generate 获取新邮箱" if session.empty?
|
|
123
|
+
|
|
124
|
+
"temp_email=#{email}; temp_mail_session=#{session}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# 读取 tempmail.ee 收件箱
|
|
128
|
+
# POST /api/mails(显式会话 Cookie + sec-ch-ua);
|
|
129
|
+
# 列表只含元数据(id/fromAddress/toAddress/subject/createdAt/isRead),
|
|
130
|
+
# 正文逐封 GET /api/mails/{id},content 为 MIME multipart 原文(HTML 实体转义版)。
|
|
131
|
+
# @param email [String] 邮箱地址
|
|
132
|
+
# @param token [String] 渠道会话凭据串
|
|
133
|
+
# @return [Array<Email>]
|
|
134
|
+
def get_emails(email, token)
|
|
135
|
+
addr = email.to_s.strip
|
|
136
|
+
raise "tempmail-ee: 邮箱为空" if addr.empty?
|
|
137
|
+
raise "tempmail-ee: token 为空" if token.to_s.strip.empty?
|
|
138
|
+
|
|
139
|
+
cookie = resolve_cookie(token, addr)
|
|
140
|
+
hdrs = browser_headers(true).merge("Cookie" => cookie)
|
|
141
|
+
|
|
142
|
+
resp = Http.post("#{BASE_URL}/api/mails",
|
|
143
|
+
headers: hdrs, json: { "email" => addr }, timeout: 15)
|
|
144
|
+
if resp.status_code == 403
|
|
145
|
+
raise "tempmail-ee inbox: http 403(会话 Cookie 校验失败,邮箱可能已过期,请重新 Generate)"
|
|
146
|
+
end
|
|
147
|
+
resp.raise_for_status
|
|
148
|
+
data = resp.json
|
|
149
|
+
mails = data.is_a?(Hash) ? data["mails"] : nil
|
|
150
|
+
return [] unless mails.is_a?(Array)
|
|
151
|
+
|
|
152
|
+
mails.filter_map do |row|
|
|
153
|
+
next unless row.is_a?(Hash)
|
|
154
|
+
|
|
155
|
+
mail = row_skeleton(row, addr)
|
|
156
|
+
next if mail.nil?
|
|
157
|
+
|
|
158
|
+
fetch_detail(mail, cookie)
|
|
159
|
+
mail
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# 将列表行(仅元数据)转换为 Email 骨架;id 缺失视为无效行
|
|
164
|
+
# @param row [Hash] 列表元素(id/fromAddress/toAddress/subject/createdAt/isRead)
|
|
165
|
+
# @param email [String] 当前邮箱
|
|
166
|
+
# @return [Email, nil]
|
|
167
|
+
def row_skeleton(row, email)
|
|
168
|
+
id = row["id"].to_s.strip
|
|
169
|
+
return nil if id.empty?
|
|
170
|
+
|
|
171
|
+
to_addr = row["toAddress"].to_s.strip
|
|
172
|
+
to_addr = email if to_addr.empty?
|
|
173
|
+
created = row["createdAt"].to_s.strip
|
|
174
|
+
created = Time.now.utc.iso8601 if created.empty?
|
|
175
|
+
Email.new(
|
|
176
|
+
id: id,
|
|
177
|
+
from_addr: row["fromAddress"].to_s.strip,
|
|
178
|
+
to: to_addr,
|
|
179
|
+
subject: row["subject"].to_s.strip,
|
|
180
|
+
date: created,
|
|
181
|
+
is_read: row["isRead"] == true || row["isRead"] == 1 || row["isRead"].to_s == "1"
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# 拉取单封详情并填充正文;失败不阻塞列表其余邮件
|
|
186
|
+
# GET /api/mails/{id}(带显式会话 Cookie)
|
|
187
|
+
# @param mail [Email] 列表骨架
|
|
188
|
+
# @param cookie [String] 会话 Cookie 头
|
|
189
|
+
def fetch_detail(mail, cookie)
|
|
190
|
+
hdrs = browser_headers(false).merge("Cookie" => cookie)
|
|
191
|
+
resp = Http.get("#{BASE_URL}/api/mails/#{URI.encode_www_form_component(mail.id)}",
|
|
192
|
+
headers: hdrs, timeout: 15)
|
|
193
|
+
return unless resp.ok?
|
|
194
|
+
|
|
195
|
+
detail = begin
|
|
196
|
+
resp.json
|
|
197
|
+
rescue StandardError
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
return unless detail.is_a?(Hash)
|
|
201
|
+
|
|
202
|
+
content = detail["content"].to_s
|
|
203
|
+
if content.empty?
|
|
204
|
+
# content 缺失时回退兜底候选键,避免平台字段演进后正文丢失
|
|
205
|
+
content = detail["text"].to_s
|
|
206
|
+
content = detail["body"].to_s if content.empty?
|
|
207
|
+
content = detail["html"].to_s if content.empty?
|
|
208
|
+
return if content.empty?
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
text, html = parse_content(content)
|
|
212
|
+
mail.text = text if mail.text.empty?
|
|
213
|
+
mail.html = html if mail.html.empty?
|
|
214
|
+
mail.from_addr = detail["fromAddress"].to_s if mail.from_addr.empty?
|
|
215
|
+
mail.subject = detail["subject"].to_s if mail.subject.empty?
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# 解析详情 content(MIME multipart 原文,HTML 实体已转义)
|
|
219
|
+
# 1) 反转义;2) 按首个 boundary 行切块解码;3) text/data-html 互为兜底合成。
|
|
220
|
+
# @param raw [String] 原始 content 字符串
|
|
221
|
+
# @return [Array(String, String)] [纯文本正文, HTML 正文]
|
|
222
|
+
def parse_content(raw)
|
|
223
|
+
payload = CGI.unescapeHTML(raw.to_s)
|
|
224
|
+
payload = payload.gsub("=", "=").gsub("+", "+").gsub("/", "/")
|
|
225
|
+
payload = payload.gsub("\r\n", "\n")
|
|
226
|
+
|
|
227
|
+
lines = payload.split("\n", -1)
|
|
228
|
+
boundary = boundary_of(lines)
|
|
229
|
+
text = ""
|
|
230
|
+
html = ""
|
|
231
|
+
if boundary.empty?
|
|
232
|
+
text = singleton_part(payload)
|
|
233
|
+
else
|
|
234
|
+
text, html = parse_parts(lines, boundary)
|
|
235
|
+
end
|
|
236
|
+
if text.empty? && html.empty?
|
|
237
|
+
text = singleton_part(payload)
|
|
238
|
+
end
|
|
239
|
+
if text.empty? && !html.empty?
|
|
240
|
+
text = HtmlUtils.html_to_text(html)
|
|
241
|
+
end
|
|
242
|
+
if html.empty? && !text.empty?
|
|
243
|
+
html = "<html><body><pre>#{HtmlUtils.escape(text)}</pre></body></html>"
|
|
244
|
+
end
|
|
245
|
+
[text, html]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# 扫描首块寻找 multipart 边界行("--" 前缀)
|
|
249
|
+
# @param lines [Array<String>]
|
|
250
|
+
# @return [String] 边界串(无则空串)
|
|
251
|
+
def boundary_of(lines)
|
|
252
|
+
lines.first(120).each do |line|
|
|
253
|
+
return line[2..].to_s.strip if line.start_with?("--") && line.length > 2
|
|
254
|
+
end
|
|
255
|
+
""
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# 按 boundary 拆分 multipart 并解码归并 text/html 两个 part
|
|
259
|
+
# @param lines [Array<String>]
|
|
260
|
+
# @param boundary [String] 边界串
|
|
261
|
+
# @return [Array(String, String)] [纯文本正文, HTML 正文]
|
|
262
|
+
def parse_parts(lines, boundary)
|
|
263
|
+
text = ""
|
|
264
|
+
html = ""
|
|
265
|
+
i = 0
|
|
266
|
+
while i < lines.length
|
|
267
|
+
line = lines[i]
|
|
268
|
+
unless line.start_with?("--#{boundary}")
|
|
269
|
+
i += 1
|
|
270
|
+
next
|
|
271
|
+
end
|
|
272
|
+
break if line.start_with?("--#{boundary}--")
|
|
273
|
+
|
|
274
|
+
headers = {}
|
|
275
|
+
j = i + 1
|
|
276
|
+
# part 头部:直到首个空行
|
|
277
|
+
while j < lines.length && lines[j] != "" && !lines[j].start_with?("--#{boundary}")
|
|
278
|
+
ln = lines[j]
|
|
279
|
+
if (k = ln.index(":")) && k.positive?
|
|
280
|
+
headers[ln[0...k].strip.downcase] = ln[(k + 1)..].to_s.strip
|
|
281
|
+
end
|
|
282
|
+
j += 1
|
|
283
|
+
end
|
|
284
|
+
j += 1 if j < lines.length && lines[j] == ""
|
|
285
|
+
# part 正文:到下一个边界行为止
|
|
286
|
+
body = []
|
|
287
|
+
while j < lines.length && !lines[j].start_with?("--#{boundary}")
|
|
288
|
+
body << lines[j]
|
|
289
|
+
j += 1
|
|
290
|
+
end
|
|
291
|
+
text, html = merge_part(body, headers, text, html)
|
|
292
|
+
i = j
|
|
293
|
+
end
|
|
294
|
+
[text, html]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# 单个 part 的解码归并:按 content-type 归类,缺省回退 text 槽
|
|
298
|
+
# @param body [Array<String>] part 正文行
|
|
299
|
+
# @param headers [Hash] part 头部
|
|
300
|
+
# @param text [String] 已归并纯文本
|
|
301
|
+
# @param html [String] 已归并 HTML
|
|
302
|
+
# @return [Array(String, String)]
|
|
303
|
+
def merge_part(body, headers, text, html)
|
|
304
|
+
ct = headers["content-type"].to_s.downcase.split(";").first
|
|
305
|
+
cte = headers["content-transfer-encoding"].to_s.downcase
|
|
306
|
+
content = decode_part(body.join("\n"), cte)
|
|
307
|
+
if ct.include?("text/plain")
|
|
308
|
+
text = content if text.empty?
|
|
309
|
+
elsif ct.include?("text/html")
|
|
310
|
+
html = content if html.empty?
|
|
311
|
+
elsif text.empty?
|
|
312
|
+
text = content
|
|
313
|
+
end
|
|
314
|
+
[text, html]
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# 单 part(无边界或拆不出内容)降级解析:
|
|
318
|
+
# 头部区隔(首个空行之后)取 body,并识别 Content-Transfer-Encoding 解码
|
|
319
|
+
# @param payload [String]
|
|
320
|
+
# @return [String]
|
|
321
|
+
def singleton_part(payload)
|
|
322
|
+
body = payload.strip
|
|
323
|
+
return body unless body.include?("\n")
|
|
324
|
+
|
|
325
|
+
lines = payload.split("\n", -1)
|
|
326
|
+
# 头部以 RFC822 形式出现时,正文从首个空行后开始
|
|
327
|
+
lines.each_with_index do |ln, idx|
|
|
328
|
+
if ln.strip.empty?
|
|
329
|
+
body = lines[(idx + 1)..].join("\n")
|
|
330
|
+
break
|
|
331
|
+
end
|
|
332
|
+
break if idx > 40 || (idx >= 3 && !ln.include?(":"))
|
|
333
|
+
end
|
|
334
|
+
lower = payload.downcase
|
|
335
|
+
cte = "base64" if lower.include?("base64")
|
|
336
|
+
cte = "quoted-printable" if lower.include?("quoted-printable")
|
|
337
|
+
decode_part(body, cte.to_s)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# 按 Content-Transfer-Encoding 解码 part 内容
|
|
341
|
+
# @param data [String]
|
|
342
|
+
# @param cte [String]
|
|
343
|
+
# @return [String]
|
|
344
|
+
def decode_part(data, cte)
|
|
345
|
+
data = data.to_s.strip
|
|
346
|
+
case cte
|
|
347
|
+
when "base64"
|
|
348
|
+
joined = data.gsub(/[\r\n\t ]/, "")
|
|
349
|
+
begin
|
|
350
|
+
Base64.strict_decode64(joined).strip.force_encoding("UTF-8")
|
|
351
|
+
rescue ArgumentError
|
|
352
|
+
data
|
|
353
|
+
end
|
|
354
|
+
when "quoted-printable"
|
|
355
|
+
decode_quoted_printable(data)
|
|
356
|
+
else
|
|
357
|
+
data
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# 最小 quoted-printable 解码(=XX 十六进制转字节,行尾 = 软换行)
|
|
362
|
+
# @param data [String]
|
|
363
|
+
# @return [String]
|
|
364
|
+
def decode_quoted_printable(data)
|
|
365
|
+
out = data.gsub(/=\r?\n/, "")
|
|
366
|
+
out = out.gsub(/=([0-9A-Fa-f]{2})/) { [$1].pack("H2") }
|
|
367
|
+
out.force_encoding("UTF-8")
|
|
368
|
+
rescue StandardError
|
|
369
|
+
data
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
end
|