tempmail_sdk 1.3.3
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 +7 -0
- data/README.md +116 -0
- data/lib/tempmail_sdk/channels.rb +289 -0
- data/lib/tempmail_sdk/channels_data.rb +289 -0
- data/lib/tempmail_sdk/client.rb +193 -0
- data/lib/tempmail_sdk/config.rb +104 -0
- data/lib/tempmail_sdk/html_utils.rb +42 -0
- data/lib/tempmail_sdk/http.rb +225 -0
- data/lib/tempmail_sdk/logger.rb +38 -0
- data/lib/tempmail_sdk/normalize.rb +193 -0
- data/lib/tempmail_sdk/providers/anonbox.rb +248 -0
- data/lib/tempmail_sdk/providers/anonymmail.rb +92 -0
- data/lib/tempmail_sdk/providers/byom.rb +48 -0
- data/lib/tempmail_sdk/providers/catchmail.rb +110 -0
- data/lib/tempmail_sdk/providers/cleantempmail.rb +49 -0
- data/lib/tempmail_sdk/providers/devmail_uk.rb +58 -0
- data/lib/tempmail_sdk/providers/dropmail.rb +133 -0
- data/lib/tempmail_sdk/providers/dropmail_click.rb +81 -0
- data/lib/tempmail_sdk/providers/dropmail_me.rb +163 -0
- data/lib/tempmail_sdk/providers/duckmail.rb +63 -0
- data/lib/tempmail_sdk/providers/eyepaste.rb +119 -0
- data/lib/tempmail_sdk/providers/fake_email_site.rb +48 -0
- data/lib/tempmail_sdk/providers/fake_legal.rb +109 -0
- data/lib/tempmail_sdk/providers/fmail.rb +87 -0
- data/lib/tempmail_sdk/providers/freecustom.rb +135 -0
- data/lib/tempmail_sdk/providers/getnada.rb +146 -0
- data/lib/tempmail_sdk/providers/gonebox_email.rb +89 -0
- data/lib/tempmail_sdk/providers/guerrillamail.rb +62 -0
- data/lib/tempmail_sdk/providers/guerrillamail_mirrors.rb +81 -0
- data/lib/tempmail_sdk/providers/harakirimail.rb +84 -0
- data/lib/tempmail_sdk/providers/haribu.rb +176 -0
- data/lib/tempmail_sdk/providers/inboxes.rb +110 -0
- data/lib/tempmail_sdk/providers/inboxkitten.rb +90 -0
- data/lib/tempmail_sdk/providers/linshiyou.rb +157 -0
- data/lib/tempmail_sdk/providers/lroid.rb +170 -0
- data/lib/tempmail_sdk/providers/m2u.rb +126 -0
- data/lib/tempmail_sdk/providers/mail10s.rb +59 -0
- data/lib/tempmail_sdk/providers/mail123.rb +82 -0
- data/lib/tempmail_sdk/providers/mail_cx.rb +133 -0
- data/lib/tempmail_sdk/providers/mail_sunls.rb +68 -0
- data/lib/tempmail_sdk/providers/mail_tm.rb +99 -0
- data/lib/tempmail_sdk/providers/mailcatch.rb +91 -0
- data/lib/tempmail_sdk/providers/maildrop.rb +92 -0
- data/lib/tempmail_sdk/providers/maildrop_cc.rb +110 -0
- data/lib/tempmail_sdk/providers/mailforspam.rb +96 -0
- data/lib/tempmail_sdk/providers/mailhole_de.rb +70 -0
- data/lib/tempmail_sdk/providers/mailinator.rb +182 -0
- data/lib/tempmail_sdk/providers/mailmomy.rb +108 -0
- data/lib/tempmail_sdk/providers/mailnesia.rb +141 -0
- data/lib/tempmail_sdk/providers/mffac.rb +101 -0
- data/lib/tempmail_sdk/providers/moakt.rb +296 -0
- data/lib/tempmail_sdk/providers/neighbours.rb +140 -0
- data/lib/tempmail_sdk/providers/neighbours_sh.rb +117 -0
- data/lib/tempmail_sdk/providers/nimail.rb +100 -0
- data/lib/tempmail_sdk/providers/ockito.rb +163 -0
- data/lib/tempmail_sdk/providers/one_sec_mail.rb +46 -0
- data/lib/tempmail_sdk/providers/restmail_net.rb +60 -0
- data/lib/tempmail_sdk/providers/rootsh.rb +167 -0
- data/lib/tempmail_sdk/providers/shitty_email.rb +84 -0
- data/lib/tempmail_sdk/providers/smailpro.rb +103 -0
- data/lib/tempmail_sdk/providers/ta_easy.rb +53 -0
- data/lib/tempmail_sdk/providers/temp_mail_io.rb +67 -0
- data/lib/tempmail_sdk/providers/temp_mail_now.rb +103 -0
- data/lib/tempmail_sdk/providers/temp_mail_org.rb +89 -0
- data/lib/tempmail_sdk/providers/tempemail_co.rb +142 -0
- data/lib/tempmail_sdk/providers/tempemail_info.rb +121 -0
- data/lib/tempmail_sdk/providers/tempemails_net.rb +159 -0
- data/lib/tempmail_sdk/providers/tempfastmail.rb +76 -0
- data/lib/tempmail_sdk/providers/tempgmailer.rb +126 -0
- data/lib/tempmail_sdk/providers/tempgo_email.rb +74 -0
- data/lib/tempmail_sdk/providers/tempinbox.rb +70 -0
- data/lib/tempmail_sdk/providers/tempmail.rb +66 -0
- data/lib/tempmail_sdk/providers/tempmail365.rb +119 -0
- data/lib/tempmail_sdk/providers/tempmail_fish.rb +89 -0
- data/lib/tempmail_sdk/providers/tempmail_fyi.rb +124 -0
- data/lib/tempmail_sdk/providers/tempmail_lol.rb +42 -0
- data/lib/tempmail_sdk/providers/tempmail_lol_v2.rb +57 -0
- data/lib/tempmail_sdk/providers/tempmail_plus.rb +80 -0
- data/lib/tempmail_sdk/providers/tempmailc.rb +80 -0
- data/lib/tempmail_sdk/providers/tempmailpro.rb +89 -0
- data/lib/tempmail_sdk/providers/tempmailten.rb +150 -0
- data/lib/tempmail_sdk/providers/temporam.rb +95 -0
- data/lib/tempmail_sdk/providers/tempp_mails.rb +150 -0
- data/lib/tempmail_sdk/providers/tempy_email.rb +73 -0
- data/lib/tempmail_sdk/providers/ten_minute_mail_net.rb +125 -0
- data/lib/tempmail_sdk/providers/tenminute_one.rb +255 -0
- data/lib/tempmail_sdk/providers/throwawaymail.rb +80 -0
- data/lib/tempmail_sdk/providers/tmail_link.rb +98 -0
- data/lib/tempmail_sdk/providers/uncorreotemporal.rb +84 -0
- data/lib/tempmail_sdk/providers/webmailtemp.rb +108 -0
- data/lib/tempmail_sdk/providers/xkx_me.rb +125 -0
- data/lib/tempmail_sdk/providers/zhujump.rb +256 -0
- data/lib/tempmail_sdk/registry.rb +1097 -0
- data/lib/tempmail_sdk/retry.rb +91 -0
- data/lib/tempmail_sdk/telemetry.rb +134 -0
- data/lib/tempmail_sdk/types.rb +164 -0
- data/lib/tempmail_sdk/version.rb +6 -0
- data/lib/tempmail_sdk/webui.rb +360 -0
- data/lib/tempmail_sdk.rb +76 -0
- metadata +142 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# ockito 渠道实现
|
|
8
|
+
# API: https://ockito.com/web-api
|
|
9
|
+
# 流程: 获取 gtoken -> 获取邮箱 -> 用 Bearer token 收信
|
|
10
|
+
module Ockito
|
|
11
|
+
CHANNEL = "ockito"
|
|
12
|
+
BASE_URL = "https://ockito.com/web-api"
|
|
13
|
+
|
|
14
|
+
DEFAULT_HEADERS = {
|
|
15
|
+
"Accept" => "application/json",
|
|
16
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# 请求并解析 JSON
|
|
22
|
+
def fetch_json(path, method: :get, headers: nil, payload: nil)
|
|
23
|
+
request_headers = DEFAULT_HEADERS.dup
|
|
24
|
+
request_headers.merge!(headers) if headers
|
|
25
|
+
|
|
26
|
+
resp = if method == :post
|
|
27
|
+
Http.post("#{BASE_URL}#{path}", headers: request_headers, json: payload || {})
|
|
28
|
+
else
|
|
29
|
+
Http.get("#{BASE_URL}#{path}", headers: request_headers)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
text = resp.body || ""
|
|
33
|
+
data = text.empty? ? {} : JSON.parse(text)
|
|
34
|
+
raise "ockito http #{resp.status_code}" unless resp.ok?
|
|
35
|
+
|
|
36
|
+
[resp.status_code, data.is_a?(Hash) ? data : {}]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 打包 token
|
|
40
|
+
def pack_token(access_token, refresh_token)
|
|
41
|
+
JSON.generate({ "access_token" => access_token, "refresh_token" => refresh_token })
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 解包 token
|
|
45
|
+
def unpack_token(token)
|
|
46
|
+
value = token.to_s.strip
|
|
47
|
+
raise "ockito: 无效的会话 token" if value.empty? || !value.start_with?("{")
|
|
48
|
+
|
|
49
|
+
data = JSON.parse(value)
|
|
50
|
+
raise "ockito: 无效的会话 token" unless data.is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
access = data["access_token"].to_s.strip
|
|
53
|
+
refresh = data["refresh_token"].to_s.strip
|
|
54
|
+
raise "ockito: 无效的会话 token" if access.empty? || refresh.empty?
|
|
55
|
+
|
|
56
|
+
[access, refresh]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# 刷新 access token
|
|
60
|
+
def refresh_access_token(refresh_token)
|
|
61
|
+
_, data = fetch_json("/grefresh", method: :post,
|
|
62
|
+
headers: { "Authorization" => "Bearer #{refresh_token}", "X-PASSTHROUGH" => "Y" })
|
|
63
|
+
access = data["access_token"].to_s.strip
|
|
64
|
+
raise "ockito: 刷新 token 失败" if access.empty?
|
|
65
|
+
|
|
66
|
+
access
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 带 Bearer 认证的请求(401 时自动刷新重试)
|
|
70
|
+
def fetch_bearer_json(path, access_token, refresh_token)
|
|
71
|
+
_, data = fetch_json(path, headers: { "Authorization" => "Bearer #{access_token}" })
|
|
72
|
+
data
|
|
73
|
+
rescue RuntimeError => e
|
|
74
|
+
raise unless e.message.include?("401")
|
|
75
|
+
|
|
76
|
+
refreshed = refresh_access_token(refresh_token)
|
|
77
|
+
_, data = fetch_json(path, headers: { "Authorization" => "Bearer #{refreshed}" })
|
|
78
|
+
data
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# 扁平化收件箱行
|
|
82
|
+
def flatten_inbox_row(raw, recipient)
|
|
83
|
+
{
|
|
84
|
+
"id" => (raw["uid"] || "").to_s,
|
|
85
|
+
"from" => (raw["sender"] || "").to_s,
|
|
86
|
+
"to" => recipient,
|
|
87
|
+
"subject" => (raw["subject"] || "").to_s,
|
|
88
|
+
"text" => (raw["snippet"] || "").to_s,
|
|
89
|
+
"html" => (raw["html"] || "").to_s,
|
|
90
|
+
"date" => (raw["timestamp"] || "").to_s,
|
|
91
|
+
"is_read" => false,
|
|
92
|
+
"attachments" => []
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# 扁平化邮件详情
|
|
97
|
+
def flatten_message(raw, recipient)
|
|
98
|
+
obj = raw["obj"].is_a?(Hash) ? raw["obj"] : raw
|
|
99
|
+
{
|
|
100
|
+
"id" => (raw["uid"] || "").to_s,
|
|
101
|
+
"from" => (obj["sender_email"] || obj["SenderEmail"] || obj["from_"] || obj["From"] || obj["from"] || obj["sender_name"] || obj["SenderName"] || "").to_s,
|
|
102
|
+
"to" => (obj["to"] || obj["To"] || recipient).to_s,
|
|
103
|
+
"subject" => (obj["subject"] || obj["Subject"] || "").to_s,
|
|
104
|
+
"text" => (obj["text"] || "").to_s,
|
|
105
|
+
"html" => (obj["html"] || "").to_s,
|
|
106
|
+
"date" => (obj["date"] || obj["Date"] || "").to_s,
|
|
107
|
+
"is_read" => false,
|
|
108
|
+
"attachments" => []
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# 创建临时邮箱
|
|
113
|
+
# @return [EmailInfo]
|
|
114
|
+
def generate_email
|
|
115
|
+
_, login = fetch_json("/gtoken", method: :post, payload: {})
|
|
116
|
+
access_token = login["access_token"].to_s.strip
|
|
117
|
+
refresh_token = login["refresh_token"].to_s.strip
|
|
118
|
+
raise "ockito: 无效的 token 响应" if access_token.empty? || refresh_token.empty?
|
|
119
|
+
|
|
120
|
+
_, email_data = fetch_json("/email", headers: { "Authorization" => "Bearer #{access_token}" })
|
|
121
|
+
email_value = email_data["email"]
|
|
122
|
+
email = if email_value.is_a?(String)
|
|
123
|
+
email_value.strip
|
|
124
|
+
elsif email_value.is_a?(Hash)
|
|
125
|
+
(email_value["email"] || "").to_s.strip
|
|
126
|
+
else
|
|
127
|
+
""
|
|
128
|
+
end
|
|
129
|
+
raise "ockito: 无效的邮箱响应" if email.empty? || !email.include?("@")
|
|
130
|
+
|
|
131
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: pack_token(access_token, refresh_token))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# 获取邮件列表
|
|
135
|
+
# @param token [String] JSON 编码的 access+refresh token
|
|
136
|
+
# @param email [String]
|
|
137
|
+
# @return [Array<Email>]
|
|
138
|
+
def get_emails(token, email)
|
|
139
|
+
access_token, refresh_token = unpack_token(token)
|
|
140
|
+
address = email.to_s.strip
|
|
141
|
+
raise "ockito: 邮箱地址为空" if address.empty?
|
|
142
|
+
|
|
143
|
+
data = fetch_bearer_json("/retrieve/#{URI.encode_www_form_component(address)}/emails", access_token, refresh_token)
|
|
144
|
+
rows = data["inbox"]
|
|
145
|
+
return [] unless rows.is_a?(Array)
|
|
146
|
+
|
|
147
|
+
rows.select { |row| row.is_a?(Hash) }.map do |row|
|
|
148
|
+
uid = row["uid"].to_s.strip
|
|
149
|
+
if uid.empty?
|
|
150
|
+
Normalize.normalize_email(flatten_inbox_row(row, address), address)
|
|
151
|
+
else
|
|
152
|
+
begin
|
|
153
|
+
detail = fetch_bearer_json("/retrieve/#{URI.encode_www_form_component(address)}/#{URI.encode_www_form_component(uid)}", access_token, refresh_token)
|
|
154
|
+
Normalize.normalize_email(flatten_message(detail, address), address)
|
|
155
|
+
rescue StandardError
|
|
156
|
+
Normalize.normalize_email(flatten_inbox_row(row, address), address)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# 1SecMail 渠道 — https://tmaily.com
|
|
6
|
+
module OneSecMail
|
|
7
|
+
CHANNEL = "1sec-mail"
|
|
8
|
+
BASE_URL = "https://tmaily.com/"
|
|
9
|
+
HEADERS = { "Accept" => "application/json", "User-Agent" => "Mozilla/5.0" }.freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# 生成临时邮箱,从 Set-Cookie 提取会话 Cookie
|
|
14
|
+
# @return [EmailInfo]
|
|
15
|
+
def generate_email
|
|
16
|
+
resp = Http.get("#{BASE_URL}generate", headers: HEADERS, timeout: 15)
|
|
17
|
+
resp.raise_for_status
|
|
18
|
+
cookie_val = resp.cookie_value("TMaily_sid")
|
|
19
|
+
raise "1sec-mail: 未获取到会话 Cookie" if cookie_val.nil? || cookie_val.empty?
|
|
20
|
+
|
|
21
|
+
cookie = "TMaily_sid=#{cookie_val}"
|
|
22
|
+
data = resp.json
|
|
23
|
+
email = data.is_a?(Hash) ? data["address"].to_s.strip : ""
|
|
24
|
+
raise "1sec-mail: 无效的邮箱响应" unless email.include?("@")
|
|
25
|
+
|
|
26
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: cookie)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @param email [String]
|
|
30
|
+
# @param token [String] 会话 Cookie
|
|
31
|
+
# @return [Array<Email>]
|
|
32
|
+
def get_emails(email, token)
|
|
33
|
+
address = email.to_s.strip
|
|
34
|
+
raise "1sec-mail: 邮箱地址为空" if address.empty?
|
|
35
|
+
|
|
36
|
+
resp = Http.get("#{BASE_URL}emails?address=#{address}",
|
|
37
|
+
headers: HEADERS.merge("Cookie" => token.to_s), timeout: 15)
|
|
38
|
+
resp.raise_for_status
|
|
39
|
+
rows = resp.json
|
|
40
|
+
raise "1sec-mail: 无效的邮件列表响应" unless rows.is_a?(Array)
|
|
41
|
+
|
|
42
|
+
rows.select { |r| r.is_a?(Hash) }.map { |item| Normalize.normalize_email(item, address) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# restmail-net 渠道 — https://restmail.net
|
|
6
|
+
# Mozilla 开源项目,无需创建邮箱,ad-hoc 模式
|
|
7
|
+
module RestmailNet
|
|
8
|
+
CHANNEL = "restmail-net"
|
|
9
|
+
BASE_URL = "https://restmail.net"
|
|
10
|
+
HEADERS = {
|
|
11
|
+
"Accept" => "application/json",
|
|
12
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def random_username
|
|
18
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
19
|
+
Array.new(rand(10..12)) { chars.sample }.join
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# 创建邮箱:随机 username 即可使用,无需请求服务端
|
|
23
|
+
# @return [EmailInfo]
|
|
24
|
+
def generate_email
|
|
25
|
+
EmailInfo.new(channel: CHANNEL, email: "#{random_username}@restmail.net")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param email [String]
|
|
29
|
+
# @return [Array<Email>]
|
|
30
|
+
def get_emails(email)
|
|
31
|
+
address = email.to_s.strip
|
|
32
|
+
raise "restmail-net: 邮箱地址为空" if address.empty?
|
|
33
|
+
|
|
34
|
+
username = address.split("@").first.to_s
|
|
35
|
+
raise "restmail-net: 无法提取用户名" if username.empty?
|
|
36
|
+
|
|
37
|
+
resp = Http.get("#{BASE_URL}/mail/#{username}", headers: HEADERS, timeout: 15)
|
|
38
|
+
return [] if resp.status_code == 404
|
|
39
|
+
|
|
40
|
+
resp.raise_for_status
|
|
41
|
+
data = resp.json
|
|
42
|
+
return [] unless data.is_a?(Array)
|
|
43
|
+
|
|
44
|
+
data.select { |m| m.is_a?(Hash) }.map do |msg|
|
|
45
|
+
from_addr = ""
|
|
46
|
+
if msg["from"].is_a?(Array) && msg["from"][0].is_a?(Hash)
|
|
47
|
+
from_addr = msg["from"][0]["address"].to_s
|
|
48
|
+
end
|
|
49
|
+
from_addr = msg.dig("headers", "from").to_s if from_addr.empty? && msg["headers"].is_a?(Hash)
|
|
50
|
+
Normalize.normalize_email({
|
|
51
|
+
"id" => msg["id"] || "", "from" => from_addr, "to" => address,
|
|
52
|
+
"subject" => msg["subject"] || "", "text" => msg["text"] || "",
|
|
53
|
+
"html" => msg["html"] || "", "date" => msg["receivedAt"] || "",
|
|
54
|
+
"is_read" => false
|
|
55
|
+
}, address)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module TempmailSdk
|
|
7
|
+
module Providers
|
|
8
|
+
# Rootsh(BccTo) 渠道 — https://rootsh.com
|
|
9
|
+
# 需要 cookie session 认证,token 存储 time 值用于增量获取邮件
|
|
10
|
+
module Rootsh
|
|
11
|
+
CHANNEL = "rootsh"
|
|
12
|
+
BASE = "https://rootsh.com"
|
|
13
|
+
DEFAULT_DOMAIN = "bccto.cc"
|
|
14
|
+
TOK_PREFIX = "rootsh1:"
|
|
15
|
+
|
|
16
|
+
HEADERS = {
|
|
17
|
+
"Accept" => "application/json, text/plain, */*",
|
|
18
|
+
"Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
|
|
19
|
+
"Cache-Control" => "no-cache",
|
|
20
|
+
"DNT" => "1",
|
|
21
|
+
"Pragma" => "no-cache",
|
|
22
|
+
"Referer" => "#{BASE}/",
|
|
23
|
+
"X-Requested-With" => "XMLHttpRequest",
|
|
24
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
def random_local(length = 10)
|
|
30
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
31
|
+
Array.new(length) { chars.sample }.join
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def encode_token(last_check_time, cookies)
|
|
35
|
+
payload = JSON.generate({ "t" => last_check_time, "c" => cookies })
|
|
36
|
+
"#{TOK_PREFIX}#{payload}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def decode_token(token)
|
|
40
|
+
raise "rootsh: 无效的 session token" unless token&.start_with?(TOK_PREFIX)
|
|
41
|
+
|
|
42
|
+
data = JSON.parse(token[TOK_PREFIX.length..])
|
|
43
|
+
last_check_time = data["t"] || 0
|
|
44
|
+
cookies = data["c"] || {}
|
|
45
|
+
raise "rootsh: 无效的 session token" unless cookies.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
[last_check_time, cookies]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_cookie_header(cookies)
|
|
51
|
+
cookies.map { |k, v| "#{k}=#{v}" }.join("; ")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def extract_cookies(resp)
|
|
55
|
+
cookies = {}
|
|
56
|
+
resp.set_cookies.each do |line|
|
|
57
|
+
pair = line.split(";").first.to_s.strip
|
|
58
|
+
k, v = pair.split("=", 2)
|
|
59
|
+
cookies[k] = v if k && v
|
|
60
|
+
end
|
|
61
|
+
cookies
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# 创建临时邮箱
|
|
65
|
+
# @param domain [String, nil] 可选域名
|
|
66
|
+
# @param channel [String] 渠道标识
|
|
67
|
+
# @return [EmailInfo]
|
|
68
|
+
def generate_email(domain = nil, channel = CHANNEL)
|
|
69
|
+
dom = (domain || "").strip
|
|
70
|
+
dom = DEFAULT_DOMAIN if dom.empty?
|
|
71
|
+
local = random_local
|
|
72
|
+
mail_addr = "#{local}@#{dom}"
|
|
73
|
+
|
|
74
|
+
# 获取 session cookie
|
|
75
|
+
r1 = Http.get("#{BASE}/", headers: {
|
|
76
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
77
|
+
"User-Agent" => HEADERS["User-Agent"]
|
|
78
|
+
})
|
|
79
|
+
r1.raise_for_status
|
|
80
|
+
cookies = extract_cookies(r1)
|
|
81
|
+
|
|
82
|
+
# POST /applymail 申请邮箱
|
|
83
|
+
r2 = Http.post("#{BASE}/applymail", headers: HEADERS.merge(
|
|
84
|
+
"Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8",
|
|
85
|
+
"Cookie" => build_cookie_header(cookies)
|
|
86
|
+
), body: "mail=#{mail_addr}")
|
|
87
|
+
r2.raise_for_status
|
|
88
|
+
cookies.merge!(extract_cookies(r2))
|
|
89
|
+
|
|
90
|
+
body = r2.json
|
|
91
|
+
raise "rootsh: applymail 响应格式无效" unless body.is_a?(Hash)
|
|
92
|
+
raise "rootsh: 邮箱申请失败: #{body['tips']}" unless body["success"].to_s == "true"
|
|
93
|
+
|
|
94
|
+
actual_email = (body["user"] || "").strip
|
|
95
|
+
actual_email = mail_addr if actual_email.empty?
|
|
96
|
+
last_check_time = body["time"] || 0
|
|
97
|
+
|
|
98
|
+
token = encode_token(last_check_time, cookies)
|
|
99
|
+
EmailInfo.new(channel: channel, email: actual_email, token: token)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# 获取邮件列表
|
|
103
|
+
# @param token [String]
|
|
104
|
+
# @param email [String]
|
|
105
|
+
# @return [Array<Email>]
|
|
106
|
+
def get_emails(token, email)
|
|
107
|
+
raise "rootsh: token 不能为空" if token.nil? || token.empty?
|
|
108
|
+
|
|
109
|
+
addr = (email || "").strip
|
|
110
|
+
raise "rootsh: 邮箱地址不能为空" if addr.empty?
|
|
111
|
+
|
|
112
|
+
last_check_time, cookies = decode_token(token)
|
|
113
|
+
ts = (Time.now.to_f * 1000).to_i
|
|
114
|
+
|
|
115
|
+
r = Http.post("#{BASE}/getmail", headers: HEADERS.merge(
|
|
116
|
+
"Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8",
|
|
117
|
+
"Cookie" => build_cookie_header(cookies)
|
|
118
|
+
), body: "mail=#{addr}&time=#{last_check_time}&_=#{ts}")
|
|
119
|
+
r.raise_for_status
|
|
120
|
+
body = r.json
|
|
121
|
+
|
|
122
|
+
return [] unless body.is_a?(Hash) && body["success"].to_s == "true"
|
|
123
|
+
|
|
124
|
+
mail_list = body["mail"]
|
|
125
|
+
return [] unless mail_list.is_a?(Array)
|
|
126
|
+
|
|
127
|
+
mail_list.filter_map do |item|
|
|
128
|
+
next unless item.is_a?(Array) && item.length >= 5
|
|
129
|
+
|
|
130
|
+
from_email = item[1].to_s
|
|
131
|
+
from_display = item[0].to_s
|
|
132
|
+
from_addr = from_email.empty? ? from_display : from_email
|
|
133
|
+
subject = item[2].to_s
|
|
134
|
+
date_str = item[3].to_s
|
|
135
|
+
mail_fid = item[4].to_s
|
|
136
|
+
|
|
137
|
+
# 获取邮件正文
|
|
138
|
+
html_content = ""
|
|
139
|
+
unless mail_fid.empty?
|
|
140
|
+
begin
|
|
141
|
+
rv = Http.post("#{BASE}/viewmail", headers: HEADERS.merge(
|
|
142
|
+
"Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8",
|
|
143
|
+
"Cookie" => build_cookie_header(cookies)
|
|
144
|
+
), body: "mail=#{mail_fid}&to=#{addr}")
|
|
145
|
+
if rv.ok?
|
|
146
|
+
vb = rv.json
|
|
147
|
+
html_content = vb["mail"] || "" if vb.is_a?(Hash) && vb["success"].to_s == "true"
|
|
148
|
+
end
|
|
149
|
+
rescue StandardError
|
|
150
|
+
# 单封邮件获取失败不影响其他
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
raw = {
|
|
155
|
+
"id" => mail_fid,
|
|
156
|
+
"from" => from_addr,
|
|
157
|
+
"to" => addr,
|
|
158
|
+
"subject" => subject,
|
|
159
|
+
"date" => date_str,
|
|
160
|
+
"html" => html_content
|
|
161
|
+
}
|
|
162
|
+
Normalize.normalize_email(raw, addr)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# shitty.email 渠道 — https://shitty.email
|
|
8
|
+
module ShittyEmail
|
|
9
|
+
CHANNEL = "shitty-email"
|
|
10
|
+
API_BASE = "https://shitty.email/api"
|
|
11
|
+
HEADERS = { "Accept" => "application/json", "User-Agent" => "Mozilla/5.0" }.freeze
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def headers_with_token(token = nil)
|
|
16
|
+
h = HEADERS.dup
|
|
17
|
+
h["X-Session-Token"] = token if token && !token.empty?
|
|
18
|
+
h
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# 创建临时邮箱
|
|
22
|
+
# @return [EmailInfo]
|
|
23
|
+
def generate_email
|
|
24
|
+
resp = Http.post("#{API_BASE}/inbox", headers: HEADERS, timeout: 15)
|
|
25
|
+
resp.raise_for_status
|
|
26
|
+
data = resp.json
|
|
27
|
+
raise "shitty-email: 响应格式无效" unless data.is_a?(Hash)
|
|
28
|
+
|
|
29
|
+
email = (data["email"] || "").strip
|
|
30
|
+
token = (data["token"] || "").strip
|
|
31
|
+
raise "shitty-email: 创建邮箱响应无效" if email.empty? || !email.include?("@") || token.empty?
|
|
32
|
+
|
|
33
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: token, expires_at: data["expiresAt"])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 获取邮件列表
|
|
37
|
+
# @param token [String] 会话令牌
|
|
38
|
+
# @param email [String] 邮箱地址
|
|
39
|
+
# @return [Array<Email>]
|
|
40
|
+
def get_emails(token, email)
|
|
41
|
+
session_token = (token || "").strip
|
|
42
|
+
address = (email || "").strip
|
|
43
|
+
raise "shitty-email: token 为空" if session_token.empty?
|
|
44
|
+
raise "shitty-email: 邮箱地址为空" if address.empty?
|
|
45
|
+
|
|
46
|
+
resp = Http.get("#{API_BASE}/inbox", headers: headers_with_token(session_token), timeout: 15)
|
|
47
|
+
resp.raise_for_status
|
|
48
|
+
data = resp.json
|
|
49
|
+
return [] unless data.is_a?(Hash)
|
|
50
|
+
|
|
51
|
+
rows = data["emails"]
|
|
52
|
+
return [] unless rows.is_a?(Array)
|
|
53
|
+
|
|
54
|
+
rows.filter_map do |item|
|
|
55
|
+
next unless item.is_a?(Hash)
|
|
56
|
+
|
|
57
|
+
message_id = (item["id"] || "").to_s.strip
|
|
58
|
+
detail = nil
|
|
59
|
+
unless message_id.empty?
|
|
60
|
+
begin
|
|
61
|
+
dr = Http.get("#{API_BASE}/email/#{URI.encode_www_form_component(message_id)}",
|
|
62
|
+
headers: headers_with_token(session_token), timeout: 15)
|
|
63
|
+
detail = dr.json if dr.ok?
|
|
64
|
+
rescue StandardError
|
|
65
|
+
# 单封邮件获取失败不影响其他
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
source = (detail.is_a?(Hash) ? detail : item)
|
|
70
|
+
raw = {
|
|
71
|
+
"id" => source["id"],
|
|
72
|
+
"from" => source["from"],
|
|
73
|
+
"to" => source["to"] || address,
|
|
74
|
+
"subject" => source["subject"],
|
|
75
|
+
"text" => source["text"],
|
|
76
|
+
"html" => source["html"],
|
|
77
|
+
"date" => source["date"]
|
|
78
|
+
}
|
|
79
|
+
Normalize.normalize_email(raw, address)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# smailpro.com 渠道实现
|
|
6
|
+
# 两段式流程:先获取 JWT payload,再调用 api.sonjj.com 接口
|
|
7
|
+
module Smailpro
|
|
8
|
+
CHANNEL = "smailpro"
|
|
9
|
+
BASE_URL = "https://smailpro.com"
|
|
10
|
+
API_BASE_URL = "https://api.sonjj.com/v1/temp_email"
|
|
11
|
+
|
|
12
|
+
HEADERS = {
|
|
13
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
|
|
14
|
+
"Accept" => "application/json, text/plain, */*",
|
|
15
|
+
"Referer" => "#{BASE_URL}/"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# 获取访问 sonjj API 所需的 JWT
|
|
21
|
+
def fetch_payload(target_url, extra = nil)
|
|
22
|
+
url = "#{BASE_URL}/app/payload?url=#{URI.encode_www_form_component(target_url)}"
|
|
23
|
+
if extra
|
|
24
|
+
extra.each { |k, v| url += "&#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(v)}" }
|
|
25
|
+
end
|
|
26
|
+
resp = Http.get(url, headers: HEADERS)
|
|
27
|
+
resp.raise_for_status
|
|
28
|
+
payload = resp.body.strip.delete('"')
|
|
29
|
+
raise "smailpro: payload 为空" if payload.empty?
|
|
30
|
+
|
|
31
|
+
payload
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 携带 JWT 调用 sonjj API
|
|
35
|
+
def call_api(target_url, extra = nil)
|
|
36
|
+
payload = fetch_payload(target_url, extra)
|
|
37
|
+
resp = Http.get("#{target_url}?payload=#{URI.encode_www_form_component(payload)}", headers: HEADERS)
|
|
38
|
+
resp.raise_for_status
|
|
39
|
+
resp.json
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 创建临时邮箱
|
|
43
|
+
# @return [EmailInfo]
|
|
44
|
+
def generate_email
|
|
45
|
+
data = call_api("#{API_BASE_URL}/create")
|
|
46
|
+
raise "smailpro: 创建响应格式异常" unless data.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
email = data["email"].to_s.strip
|
|
49
|
+
raise "smailpro: 创建邮箱失败" if email.empty?
|
|
50
|
+
|
|
51
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: email, expires_at: data["expired_at"])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# 获取单封邮件正文
|
|
55
|
+
def fetch_message(email, mid)
|
|
56
|
+
return ["", ""] if mid.to_s.empty?
|
|
57
|
+
|
|
58
|
+
data = call_api("#{API_BASE_URL}/message", { "email" => email, "mid" => mid })
|
|
59
|
+
return ["", ""] unless data.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
[(data["body"] || "").to_s, (data["textBody"] || "").to_s]
|
|
62
|
+
rescue StandardError
|
|
63
|
+
["", ""]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# 获取邮件列表
|
|
67
|
+
# @param email [String]
|
|
68
|
+
# @param token [String]
|
|
69
|
+
# @return [Array<Email>]
|
|
70
|
+
def get_emails(email, token = "")
|
|
71
|
+
addr = email.to_s.strip
|
|
72
|
+
raise "smailpro: 邮箱地址为空" if addr.empty?
|
|
73
|
+
|
|
74
|
+
data = call_api("#{API_BASE_URL}/inbox", { "email" => addr })
|
|
75
|
+
raise "smailpro: 邮件列表响应格式异常" unless data.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
# API 响应: {"status": true, "data": {"messages": [...]}}
|
|
78
|
+
inner = data["data"]
|
|
79
|
+
messages = if inner.is_a?(Hash)
|
|
80
|
+
inner["messages"]
|
|
81
|
+
else
|
|
82
|
+
data["messages"]
|
|
83
|
+
end
|
|
84
|
+
return [] unless messages.is_a?(Array) && !messages.empty?
|
|
85
|
+
|
|
86
|
+
messages.select { |m| m.is_a?(Hash) }.map do |m|
|
|
87
|
+
mid = m["mid"].to_s.strip
|
|
88
|
+
html_body, text_body = fetch_message(addr, mid)
|
|
89
|
+
raw = {
|
|
90
|
+
"id" => mid,
|
|
91
|
+
"from" => m["from"] || "",
|
|
92
|
+
"to" => addr,
|
|
93
|
+
"subject" => m["subject"] || "",
|
|
94
|
+
"date" => m["datetime"] || ""
|
|
95
|
+
}
|
|
96
|
+
raw["html"] = html_body if !html_body.empty? || !text_body.empty?
|
|
97
|
+
raw["text"] = text_body if !html_body.empty? || !text_body.empty?
|
|
98
|
+
Normalize.normalize_email(raw, addr)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# ta-easy.com 临时邮箱渠道
|
|
6
|
+
# API: https://api-endpoint.ta-easy.com/temp-email/
|
|
7
|
+
module TaEasy
|
|
8
|
+
CHANNEL = "ta-easy"
|
|
9
|
+
API_BASE = "https://api-endpoint.ta-easy.com"
|
|
10
|
+
ORIGIN = "https://www.ta-easy.com"
|
|
11
|
+
|
|
12
|
+
HEADERS = {
|
|
13
|
+
"Accept" => "application/json",
|
|
14
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
|
|
15
|
+
"origin" => ORIGIN,
|
|
16
|
+
"referer" => "#{ORIGIN}/"
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# 创建临时邮箱
|
|
22
|
+
# @return [EmailInfo]
|
|
23
|
+
def generate_email
|
|
24
|
+
resp = Http.post("#{API_BASE}/temp-email/address/new",
|
|
25
|
+
headers: HEADERS.merge("Content-Length" => "0"), body: "")
|
|
26
|
+
resp.raise_for_status
|
|
27
|
+
data = resp.json
|
|
28
|
+
raise "ta-easy: #{data['message'] || 'create failed'}" unless data["status"] == "success" && data["address"] && data["token"]
|
|
29
|
+
|
|
30
|
+
expires_at = data["expiresAt"].is_a?(Numeric) ? data["expiresAt"].to_i : nil
|
|
31
|
+
EmailInfo.new(channel: CHANNEL, email: data["address"], token: data["token"], expires_at: expires_at)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 获取邮件列表
|
|
35
|
+
# @param email [String]
|
|
36
|
+
# @param token [String]
|
|
37
|
+
# @return [Array<Email>]
|
|
38
|
+
def get_emails(email, token)
|
|
39
|
+
resp = Http.post("#{API_BASE}/temp-email/inbox/list",
|
|
40
|
+
headers: HEADERS.merge("Content-Type" => "application/json"),
|
|
41
|
+
json: { "token" => token, "email" => email })
|
|
42
|
+
resp.raise_for_status
|
|
43
|
+
data = resp.json
|
|
44
|
+
raise "ta-easy: #{data['message'] || 'inbox failed'}" unless data["status"] == "success"
|
|
45
|
+
|
|
46
|
+
raw_list = data["data"]
|
|
47
|
+
return [] unless raw_list.is_a?(Array)
|
|
48
|
+
|
|
49
|
+
raw_list.map { |raw| Normalize.normalize_email(raw, email) }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|