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,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# Temporam 渠道实现
|
|
8
|
+
# API: https://temporam.com/api
|
|
9
|
+
module Temporam
|
|
10
|
+
CHANNEL = "temporam"
|
|
11
|
+
ORIGIN = "https://temporam.com"
|
|
12
|
+
|
|
13
|
+
HEADERS = {
|
|
14
|
+
"Accept" => "application/json",
|
|
15
|
+
"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"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# 生成随机用户名
|
|
21
|
+
def random_local(length = 18)
|
|
22
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
23
|
+
"sdk" + Array.new(length) { chars.sample }.join
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# GET 请求并解析 JSON
|
|
27
|
+
def get_json(path)
|
|
28
|
+
resp = Http.get("#{ORIGIN}#{path}", headers: HEADERS)
|
|
29
|
+
resp.raise_for_status
|
|
30
|
+
resp.json
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# 获取可用域名列表
|
|
34
|
+
def fetch_domains
|
|
35
|
+
data = get_json("/api/domains")
|
|
36
|
+
domains = (data["data"] || []).select { |item| item.is_a?(Hash) }.map { |item| (item["domain"] || "").strip }.reject(&:empty?)
|
|
37
|
+
raise "temporam: 域名列表为空" if domains.empty?
|
|
38
|
+
|
|
39
|
+
domains
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 将原始邮件映射为标准化格式
|
|
43
|
+
def normalize_raw(raw, email)
|
|
44
|
+
flat = raw.merge(
|
|
45
|
+
"id" => raw["id"] || raw["uuid"] || "",
|
|
46
|
+
"from" => raw["fromEmail"] || raw["from"] || "",
|
|
47
|
+
"to" => raw["toEmail"] || raw["to"] || email,
|
|
48
|
+
"date" => raw["createdAt"] || raw["created_at"] || raw["date"] || ""
|
|
49
|
+
)
|
|
50
|
+
Normalize.normalize_email(flat, email)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# 创建临时邮箱
|
|
54
|
+
# @param domain [String, nil]
|
|
55
|
+
# @return [EmailInfo]
|
|
56
|
+
def generate_email(domain = nil)
|
|
57
|
+
domains = fetch_domains
|
|
58
|
+
if domain && !domain.empty?
|
|
59
|
+
raise "temporam: 不支持的域名 #{domain}" unless domains.include?(domain)
|
|
60
|
+
|
|
61
|
+
selected = domain
|
|
62
|
+
else
|
|
63
|
+
selected = domains.sample
|
|
64
|
+
end
|
|
65
|
+
email = "#{random_local}@#{selected}"
|
|
66
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: email)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 获取邮件列表
|
|
70
|
+
# @param token [String]
|
|
71
|
+
# @param email [String]
|
|
72
|
+
# @return [Array<Email>]
|
|
73
|
+
def get_emails(token, email)
|
|
74
|
+
recipient = token.to_s.empty? ? email : token
|
|
75
|
+
raise "temporam: 缺少邮箱地址" if recipient.to_s.strip.empty?
|
|
76
|
+
|
|
77
|
+
data = get_json("/api/emails?email=#{URI.encode_www_form_component(recipient)}&limit=50")
|
|
78
|
+
rows = data["data"] || []
|
|
79
|
+
rows.select { |row| row.is_a?(Hash) }.map do |row|
|
|
80
|
+
raw = row
|
|
81
|
+
msg_id = (row["id"] || row["uuid"] || "").to_s
|
|
82
|
+
unless msg_id.empty?
|
|
83
|
+
begin
|
|
84
|
+
detail = get_json("/api/emails/#{URI.encode_www_form_component(msg_id)}")
|
|
85
|
+
raw = detail["data"] if detail["data"].is_a?(Hash)
|
|
86
|
+
rescue StandardError
|
|
87
|
+
# 详情获取失败时使用列表数据
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
normalize_raw(raw, recipient)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module TempmailSdk
|
|
8
|
+
module Providers
|
|
9
|
+
# tempp-mails.com 临时邮箱渠道(Laravel + CSRF,与 tempmailten 共享模板)
|
|
10
|
+
# GET /en 获取 session cookie + CSRF token
|
|
11
|
+
# POST /messages 获取邮箱地址和邮件列表
|
|
12
|
+
# GET /view/{id} 获取单封邮件 HTML 正文
|
|
13
|
+
module TempMails
|
|
14
|
+
CHANNEL = "tempp-mails"
|
|
15
|
+
BASE_URL = "https://tempp-mails.com"
|
|
16
|
+
TOK_PREFIX = "tpm1:"
|
|
17
|
+
|
|
18
|
+
CSRF_RE = /<meta\s+name="csrf-token"\s+content="([^"]+)"/
|
|
19
|
+
|
|
20
|
+
UA = "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"
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
def browser_headers
|
|
25
|
+
{
|
|
26
|
+
"User-Agent" => UA,
|
|
27
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
28
|
+
"Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def ajax_headers
|
|
33
|
+
{
|
|
34
|
+
"User-Agent" => UA,
|
|
35
|
+
"Accept" => "application/json, text/javascript, */*; q=0.01",
|
|
36
|
+
"Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
|
|
37
|
+
"X-Requested-With" => "XMLHttpRequest",
|
|
38
|
+
"Referer" => "#{BASE_URL}/en"
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def extract_cookie_header(resp)
|
|
43
|
+
cookies = {}
|
|
44
|
+
resp.set_cookies.each do |line|
|
|
45
|
+
pair = line.split(";").first.to_s.strip
|
|
46
|
+
k, v = pair.split("=", 2)
|
|
47
|
+
cookies[k] = v if k && v
|
|
48
|
+
end
|
|
49
|
+
cookies.sort.map { |k, v| "#{k}=#{v}" }.join("; ")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def encode_token(csrf, cookie_hdr)
|
|
53
|
+
raw = JSON.generate({ "t" => csrf, "c" => cookie_hdr })
|
|
54
|
+
"#{TOK_PREFIX}#{Base64.strict_encode64(raw)}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def decode_token(token)
|
|
58
|
+
raise "tempp-mails: 无效的 token" unless token&.start_with?(TOK_PREFIX)
|
|
59
|
+
|
|
60
|
+
data = JSON.parse(Base64.decode64(token[TOK_PREFIX.length..]))
|
|
61
|
+
csrf = (data["t"] || "").strip
|
|
62
|
+
cookie_hdr = (data["c"] || "").strip
|
|
63
|
+
raise "tempp-mails: token 中缺少 CSRF" if csrf.empty?
|
|
64
|
+
|
|
65
|
+
[csrf, cookie_hdr]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def post_messages(csrf, cookie_hdr)
|
|
69
|
+
headers = ajax_headers.merge(
|
|
70
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
|
71
|
+
"X-CSRF-TOKEN" => csrf
|
|
72
|
+
)
|
|
73
|
+
headers["Cookie"] = cookie_hdr unless cookie_hdr.empty?
|
|
74
|
+
|
|
75
|
+
resp = Http.post("#{BASE_URL}/messages", headers: headers,
|
|
76
|
+
body: "_token=#{URI.encode_www_form_component(csrf)}&captcha=")
|
|
77
|
+
resp.raise_for_status
|
|
78
|
+
data = resp.json
|
|
79
|
+
raise "tempp-mails: 解析响应 JSON 失败" unless data.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
data
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @return [EmailInfo]
|
|
85
|
+
def generate_email
|
|
86
|
+
resp = Http.get("#{BASE_URL}/en", headers: browser_headers)
|
|
87
|
+
resp.raise_for_status
|
|
88
|
+
|
|
89
|
+
cookie_hdr = extract_cookie_header(resp)
|
|
90
|
+
m = resp.body.match(CSRF_RE)
|
|
91
|
+
raise "tempp-mails: 未能从首页提取 CSRF token" unless m
|
|
92
|
+
|
|
93
|
+
csrf = m[1]
|
|
94
|
+
data = post_messages(csrf, cookie_hdr)
|
|
95
|
+
mailbox = (data["mailbox"] || "").strip
|
|
96
|
+
raise "tempp-mails: 邮箱地址无效" if mailbox.empty? || !mailbox.include?("@")
|
|
97
|
+
|
|
98
|
+
EmailInfo.new(channel: CHANNEL, email: mailbox, token: encode_token(csrf, cookie_hdr))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# @param email [String]
|
|
102
|
+
# @param token [String]
|
|
103
|
+
# @return [Array<Email>]
|
|
104
|
+
def get_emails(email, token)
|
|
105
|
+
addr = (email || "").strip
|
|
106
|
+
raise "tempp-mails: 邮箱地址为空" if addr.empty?
|
|
107
|
+
|
|
108
|
+
csrf, cookie_hdr = decode_token((token || "").strip)
|
|
109
|
+
data = post_messages(csrf, cookie_hdr)
|
|
110
|
+
|
|
111
|
+
msgs = data["messages"]
|
|
112
|
+
return [] unless msgs.is_a?(Array) && !msgs.empty?
|
|
113
|
+
|
|
114
|
+
msgs.filter_map do |msg|
|
|
115
|
+
next unless msg.is_a?(Hash)
|
|
116
|
+
|
|
117
|
+
mid = (msg["id"] || "").to_s.strip
|
|
118
|
+
next if mid.empty?
|
|
119
|
+
|
|
120
|
+
from_addr = (msg["from_email"] || "").to_s
|
|
121
|
+
from_name = (msg["from"] || "").to_s
|
|
122
|
+
from_addr = "#{from_name} <#{from_addr}>" if !from_name.empty? && from_name != from_addr
|
|
123
|
+
|
|
124
|
+
raw = {
|
|
125
|
+
"id" => mid,
|
|
126
|
+
"from" => from_addr,
|
|
127
|
+
"to" => addr,
|
|
128
|
+
"subject" => msg["subject"] || "",
|
|
129
|
+
"html" => fetch_view(cookie_hdr, mid),
|
|
130
|
+
"isRead" => !!msg["is_seen"]
|
|
131
|
+
}
|
|
132
|
+
Normalize.normalize_email(raw, addr)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def fetch_view(cookie_hdr, mid)
|
|
137
|
+
return "" if mid.nil? || mid.empty?
|
|
138
|
+
|
|
139
|
+
headers = browser_headers.merge("Referer" => "#{BASE_URL}/en")
|
|
140
|
+
headers["Cookie"] = cookie_hdr unless cookie_hdr.empty?
|
|
141
|
+
resp = Http.get("#{BASE_URL}/view/#{URI.encode_www_form_component(mid)}", headers: headers)
|
|
142
|
+
return "" unless resp.ok?
|
|
143
|
+
|
|
144
|
+
resp.body
|
|
145
|
+
rescue StandardError
|
|
146
|
+
""
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# Tempy Email 渠道实现
|
|
6
|
+
# API: https://tempy.email/api/v1
|
|
7
|
+
module TempyEmail
|
|
8
|
+
CHANNEL = "tempy-email"
|
|
9
|
+
API_BASE = "https://tempy.email/api/v1"
|
|
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/146.0.0.0 Safari/537.36"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# 扁平化邮件字段
|
|
18
|
+
def flatten_message(raw, recipient)
|
|
19
|
+
{
|
|
20
|
+
"id" => raw["id"] || raw["messageId"] || raw["message_id"] || raw["mail_id"] || "",
|
|
21
|
+
"from" => raw["from"] || raw["sender"] || raw["from_addr"] || raw["from_address"] || "",
|
|
22
|
+
"to" => raw["to"] || recipient,
|
|
23
|
+
"subject" => raw["subject"] || raw["mail_title"] || "",
|
|
24
|
+
"text" => raw["text"] || raw["body_text"] || raw["text_body"] || raw["body"] || "",
|
|
25
|
+
"html" => raw["html"] || raw["body_html"] || raw["html_body"] || "",
|
|
26
|
+
"date" => raw["date"] || raw["received_at"] || raw["created_at"] || "",
|
|
27
|
+
"is_read" => raw["is_read"] || raw["isRead"] || raw["seen"] || false,
|
|
28
|
+
"attachments" => raw["attachments"] || []
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# 创建临时邮箱
|
|
33
|
+
# @param domain [String, nil]
|
|
34
|
+
# @return [EmailInfo]
|
|
35
|
+
def generate_email(domain = nil)
|
|
36
|
+
body = {}
|
|
37
|
+
wanted = domain.to_s.strip
|
|
38
|
+
body["domain"] = wanted unless wanted.empty?
|
|
39
|
+
|
|
40
|
+
resp = Http.post("#{API_BASE}/mailbox",
|
|
41
|
+
headers: HEADERS.merge("Content-Type" => "application/json"),
|
|
42
|
+
json: body, timeout: 15)
|
|
43
|
+
resp.raise_for_status
|
|
44
|
+
data = resp.json
|
|
45
|
+
raise "tempy-email: 无效的创建响应" unless data.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
email = data["email"].to_s.strip
|
|
48
|
+
raise "tempy-email: 无效的创建响应" if email.empty?
|
|
49
|
+
|
|
50
|
+
EmailInfo.new(channel: CHANNEL, email: email, expires_at: data["expires_at"])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# 获取邮件列表
|
|
54
|
+
# @param email [String]
|
|
55
|
+
# @return [Array<Email>]
|
|
56
|
+
def get_emails(email)
|
|
57
|
+
address = email.to_s.strip
|
|
58
|
+
raise "tempy-email: 邮箱地址为空" if address.empty?
|
|
59
|
+
|
|
60
|
+
resp = Http.get("#{API_BASE}/mailbox/#{URI.encode_www_form_component(address)}/messages",
|
|
61
|
+
headers: HEADERS, timeout: 15)
|
|
62
|
+
resp.raise_for_status
|
|
63
|
+
data = resp.json
|
|
64
|
+
rows = data.is_a?(Hash) ? data["messages"] : data
|
|
65
|
+
return [] unless rows.is_a?(Array)
|
|
66
|
+
|
|
67
|
+
rows.select { |item| item.is_a?(Hash) }.map do |item|
|
|
68
|
+
Normalize.normalize_email(flatten_message(item, address), address)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module TempmailSdk
|
|
7
|
+
module Providers
|
|
8
|
+
# 10minutemail.net 临时邮箱渠道
|
|
9
|
+
# GET /address.api.php 创建邮箱(获取 PHPSESSID)
|
|
10
|
+
# GET /address.api.php 获取邮件列表(带 session cookie)
|
|
11
|
+
# GET /mail.api.php?mailid={id} 获取单封邮件详情
|
|
12
|
+
module TenMinuteMailNet
|
|
13
|
+
CHANNEL = "10minutemail-net"
|
|
14
|
+
BASE_URL = "https://10minutemail.net"
|
|
15
|
+
|
|
16
|
+
HEADERS = {
|
|
17
|
+
"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",
|
|
18
|
+
"Accept" => "application/json"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# @return [EmailInfo]
|
|
24
|
+
def generate_email
|
|
25
|
+
resp = Http.get("#{BASE_URL}/address.api.php", headers: HEADERS, timeout: 15)
|
|
26
|
+
resp.raise_for_status
|
|
27
|
+
data = resp.json
|
|
28
|
+
raise "10minutemail-net: 响应格式无效" unless data.is_a?(Hash)
|
|
29
|
+
|
|
30
|
+
address = (data["mail_get_mail"] || "").strip
|
|
31
|
+
raise "10minutemail-net: 创建邮箱失败" if address.empty?
|
|
32
|
+
|
|
33
|
+
phpsessid = resp.cookie_value("PHPSESSID")
|
|
34
|
+
raise "10minutemail-net: 未获取到 PHPSESSID cookie" if phpsessid.nil? || phpsessid.empty?
|
|
35
|
+
|
|
36
|
+
token = JSON.generate({ "cookie" => "PHPSESSID=#{phpsessid}" })
|
|
37
|
+
|
|
38
|
+
expires_at = nil
|
|
39
|
+
duetime = data["mail_get_duetime"]
|
|
40
|
+
if duetime
|
|
41
|
+
begin
|
|
42
|
+
expires_at = duetime.to_i * 1000
|
|
43
|
+
rescue StandardError
|
|
44
|
+
# 忽略
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
EmailInfo.new(channel: CHANNEL, email: address, token: token, expires_at: expires_at)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @param token [String]
|
|
52
|
+
# @param email [String]
|
|
53
|
+
# @return [Array<Email>]
|
|
54
|
+
def get_emails(token, email)
|
|
55
|
+
address = (email || "").strip
|
|
56
|
+
raise "10minutemail-net: 邮箱地址为空" if address.empty?
|
|
57
|
+
|
|
58
|
+
token_data = JSON.parse(token)
|
|
59
|
+
cookie_str = token_data["cookie"] || ""
|
|
60
|
+
|
|
61
|
+
headers = HEADERS.merge("Cookie" => cookie_str)
|
|
62
|
+
|
|
63
|
+
resp = Http.get("#{BASE_URL}/address.api.php", headers: headers, timeout: 15)
|
|
64
|
+
resp.raise_for_status
|
|
65
|
+
data = resp.json
|
|
66
|
+
|
|
67
|
+
mail_list = data["mail_list"]
|
|
68
|
+
return [] unless mail_list.is_a?(Array)
|
|
69
|
+
|
|
70
|
+
mail_list.filter_map do |item|
|
|
71
|
+
next unless item.is_a?(Hash)
|
|
72
|
+
|
|
73
|
+
mail_id = (item["mail_id"] || "").to_s.strip
|
|
74
|
+
next if mail_id.empty? || mail_id == "welcome"
|
|
75
|
+
|
|
76
|
+
detail = fetch_detail(headers, mail_id)
|
|
77
|
+
next unless detail.is_a?(Hash)
|
|
78
|
+
|
|
79
|
+
html_body = ""
|
|
80
|
+
text_body = ""
|
|
81
|
+
body_list = detail["body"]
|
|
82
|
+
if body_list.is_a?(Array)
|
|
83
|
+
body_list.each do |part|
|
|
84
|
+
next unless part.is_a?(Hash)
|
|
85
|
+
|
|
86
|
+
content_type = part["content"] || ""
|
|
87
|
+
if content_type.include?("text/html")
|
|
88
|
+
html_body = part["body"] || ""
|
|
89
|
+
elsif content_type.include?("text/plain")
|
|
90
|
+
text_body = part["body"] || ""
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
if text_body.empty?
|
|
96
|
+
plain_list = detail["plain"]
|
|
97
|
+
if plain_list.is_a?(Array) && !plain_list.empty? && plain_list[0].is_a?(String)
|
|
98
|
+
text_body = plain_list[0]
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
raw = {
|
|
103
|
+
"id" => mail_id,
|
|
104
|
+
"from" => (detail["from"] || item["from"] || "").to_s,
|
|
105
|
+
"to" => (detail["to"] || address).to_s,
|
|
106
|
+
"subject" => (detail["subject"] || item["subject"] || "").to_s,
|
|
107
|
+
"text" => text_body,
|
|
108
|
+
"html" => html_body,
|
|
109
|
+
"date" => (detail["datetime"] || item["datetime"] || "").to_s
|
|
110
|
+
}
|
|
111
|
+
Normalize.normalize_email(raw, address)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def fetch_detail(headers, mail_id)
|
|
116
|
+
resp = Http.get("#{BASE_URL}/mail.api.php?mailid=#{URI.encode_www_form_component(mail_id)}",
|
|
117
|
+
headers: headers, timeout: 15)
|
|
118
|
+
resp.raise_for_status
|
|
119
|
+
resp.json
|
|
120
|
+
rescue StandardError
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require "json"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "base64"
|
|
7
|
+
require "time"
|
|
8
|
+
|
|
9
|
+
module TempmailSdk
|
|
10
|
+
module Providers
|
|
11
|
+
# 10minutemail.one 渠道
|
|
12
|
+
# SSR __NUXT_DATA__ 中的 mailServiceToken(JWT)+ 页面 emailDomains;
|
|
13
|
+
# 收信 GET web.10minutemail.one/api/v1/mailbox/{email}
|
|
14
|
+
module TenminuteOne
|
|
15
|
+
CHANNEL = "10minute-one"
|
|
16
|
+
SITE_ORIGIN = "https://10minutemail.one"
|
|
17
|
+
API_BASE = "https://web.10minutemail.one/api/v1"
|
|
18
|
+
KNOWN_EMAIL_DOMAINS = %w[xghff.com oqqaj.com psovv.com dbwot.com ygwpr.com imxwe.com].freeze
|
|
19
|
+
DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
|
20
|
+
|
|
21
|
+
NUXT_DATA_RE = /<script[^>]*\bid="__NUXT_DATA__"[^>]*>([\s\S]*?)<\/script>/i
|
|
22
|
+
JWT_RE = /\A[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\z/
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# URL 编码邮箱(@ 编码为 %40)
|
|
27
|
+
def enc_mailbox_email(email)
|
|
28
|
+
if email.include?("@")
|
|
29
|
+
local, _, dom = email.partition("@")
|
|
30
|
+
"#{URI.encode_www_form_component(local)}%40#{URI.encode_www_form_component(dom)}"
|
|
31
|
+
else
|
|
32
|
+
URI.encode_www_form_component(email)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 解析 __NUXT_DATA__ JSON 数组
|
|
37
|
+
def parse_nuxt_array(html)
|
|
38
|
+
m = html.match(NUXT_DATA_RE)
|
|
39
|
+
raise "10minute-one: __NUXT_DATA__ not found" unless m
|
|
40
|
+
|
|
41
|
+
JSON.parse(m[1].strip)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 解引用 NUXT 数组中的值(递归追踪索引引用)
|
|
45
|
+
def resolve_ref(arr, value, depth = 0)
|
|
46
|
+
return value if depth > 64
|
|
47
|
+
return value if value == true || value == false
|
|
48
|
+
return value unless value.is_a?(Integer) && value >= 0 && value < arr.length
|
|
49
|
+
|
|
50
|
+
resolve_ref(arr, arr[value], depth + 1)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# 从 NUXT 数组中提取 mailServiceToken(JWT)
|
|
54
|
+
def parse_mail_service_token(arr)
|
|
55
|
+
# 先扫描前 48 项
|
|
56
|
+
n = [arr.length, 48].min
|
|
57
|
+
n.times do |i|
|
|
58
|
+
el = arr[i]
|
|
59
|
+
next unless el.is_a?(Hash) && el.key?("mailServiceToken")
|
|
60
|
+
|
|
61
|
+
t = resolve_ref(arr, el["mailServiceToken"])
|
|
62
|
+
return t if t.is_a?(String) && t.match?(JWT_RE)
|
|
63
|
+
end
|
|
64
|
+
# 全量扫描
|
|
65
|
+
arr.each do |el|
|
|
66
|
+
next unless el.is_a?(Hash) && el.key?("mailServiceToken")
|
|
67
|
+
|
|
68
|
+
t = resolve_ref(arr, el["mailServiceToken"])
|
|
69
|
+
return t if t.is_a?(String) && t.match?(JWT_RE)
|
|
70
|
+
end
|
|
71
|
+
# 最后尝试找裸 JWT 字符串
|
|
72
|
+
arr.each do |el|
|
|
73
|
+
return el if el.is_a?(String) && el.match?(JWT_RE)
|
|
74
|
+
end
|
|
75
|
+
raise "10minute-one: mailServiceToken not found"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# 从 HTML 页面中提取字段对应的 JSON 数组(如 emailDomains、blockedUsernames)
|
|
79
|
+
def parse_quoted_json_array(html, field)
|
|
80
|
+
key = "#{field}:\""
|
|
81
|
+
start = html.index(key)
|
|
82
|
+
return [] unless start
|
|
83
|
+
|
|
84
|
+
slice_start = start + key.length
|
|
85
|
+
return [] if slice_start >= html.length || html[slice_start] != "["
|
|
86
|
+
|
|
87
|
+
depth = 0
|
|
88
|
+
j = slice_start
|
|
89
|
+
while j < html.length
|
|
90
|
+
c = html[j]
|
|
91
|
+
if c == "["
|
|
92
|
+
depth += 1
|
|
93
|
+
elsif c == "]"
|
|
94
|
+
depth -= 1
|
|
95
|
+
if depth.zero?
|
|
96
|
+
j += 1
|
|
97
|
+
break
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
j += 1
|
|
101
|
+
end
|
|
102
|
+
raw = html[slice_start...j]
|
|
103
|
+
unesc = raw.gsub('\\"', '"')
|
|
104
|
+
v = JSON.parse(unesc)
|
|
105
|
+
v.is_a?(Array) ? v : []
|
|
106
|
+
rescue JSON::ParserError
|
|
107
|
+
[]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def pick_locale(domain)
|
|
111
|
+
s = domain.to_s.strip
|
|
112
|
+
return "zh" if s.empty?
|
|
113
|
+
return "zh" if s.include?(".") && !s.include?("/")
|
|
114
|
+
|
|
115
|
+
s
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def pick_mailbox_domain(hint, available)
|
|
119
|
+
raise "10minute-one: no email domains" if available.empty?
|
|
120
|
+
|
|
121
|
+
if hint && !hint.empty?
|
|
122
|
+
h = hint.strip.downcase
|
|
123
|
+
if h.include?(".")
|
|
124
|
+
found = available.find { |d| d.downcase == h }
|
|
125
|
+
return found if found
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
available.sample
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def random_local(n)
|
|
132
|
+
chars = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
133
|
+
Array.new(n) { chars[rand(chars.length)] }.join
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# 从 JWT 中提取过期时间戳(毫秒)
|
|
137
|
+
def jwt_exp_ms(token)
|
|
138
|
+
parts = token.split(".")
|
|
139
|
+
return nil if parts.length < 2
|
|
140
|
+
|
|
141
|
+
b = parts[1]
|
|
142
|
+
# 补齐 Base64 填充
|
|
143
|
+
b += "=" * ((-b.length) % 4)
|
|
144
|
+
b = b.tr("-_", "+/")
|
|
145
|
+
payload = JSON.parse(Base64.decode64(b))
|
|
146
|
+
exp = payload["exp"]
|
|
147
|
+
return (exp * 1000).to_i if exp.is_a?(Numeric)
|
|
148
|
+
|
|
149
|
+
nil
|
|
150
|
+
rescue StandardError
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def api_headers(tok)
|
|
155
|
+
{
|
|
156
|
+
"Accept" => "*/*",
|
|
157
|
+
"Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
|
|
158
|
+
"Authorization" => "Bearer #{tok}",
|
|
159
|
+
"Cache-Control" => "no-cache",
|
|
160
|
+
"Content-Type" => "application/json",
|
|
161
|
+
"DNT" => "1",
|
|
162
|
+
"Origin" => SITE_ORIGIN,
|
|
163
|
+
"Pragma" => "no-cache",
|
|
164
|
+
"Referer" => "#{SITE_ORIGIN}/",
|
|
165
|
+
"User-Agent" => DEFAULT_UA,
|
|
166
|
+
"X-Request-ID" => SecureRandom.hex(16),
|
|
167
|
+
"X-Timestamp" => Time.now.to_i.to_s
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def item_needs_detail(m)
|
|
172
|
+
return false if m["id"].to_s.strip.empty?
|
|
173
|
+
|
|
174
|
+
body = (m["text"] || m["body"] || m["html"] || m["mail_text"] || "").to_s.strip
|
|
175
|
+
body.empty?
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# @param domain [String, nil] 域名或 locale 提示
|
|
179
|
+
# @return [EmailInfo]
|
|
180
|
+
def generate_email(domain = nil)
|
|
181
|
+
loc = pick_locale(domain)
|
|
182
|
+
page_url = "#{SITE_ORIGIN}/#{loc}"
|
|
183
|
+
r = Http.get(page_url, headers: {
|
|
184
|
+
"User-Agent" => DEFAULT_UA,
|
|
185
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
186
|
+
"Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
|
|
187
|
+
"Cache-Control" => "no-cache",
|
|
188
|
+
"Pragma" => "no-cache",
|
|
189
|
+
"DNT" => "1",
|
|
190
|
+
"Referer" => page_url
|
|
191
|
+
})
|
|
192
|
+
r.raise_for_status
|
|
193
|
+
html = r.body
|
|
194
|
+
|
|
195
|
+
arr = parse_nuxt_array(html)
|
|
196
|
+
token = parse_mail_service_token(arr)
|
|
197
|
+
|
|
198
|
+
domains = (parse_quoted_json_array(html, "emailDomains") + KNOWN_EMAIL_DOMAINS).uniq
|
|
199
|
+
domains = KNOWN_EMAIL_DOMAINS.dup if domains.empty?
|
|
200
|
+
|
|
201
|
+
blocked = parse_quoted_json_array(html, "blockedUsernames").map(&:downcase).to_set
|
|
202
|
+
|
|
203
|
+
dom_hint = domain && domain.to_s.strip.include?(".") ? domain.to_s.strip : nil
|
|
204
|
+
mail_domain = pick_mailbox_domain(dom_hint, domains)
|
|
205
|
+
|
|
206
|
+
local = nil
|
|
207
|
+
32.times do
|
|
208
|
+
cand = random_local(10)
|
|
209
|
+
unless blocked.include?(cand.downcase)
|
|
210
|
+
local = cand
|
|
211
|
+
break
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
raise "10minute-one: could not pick username" unless local
|
|
215
|
+
|
|
216
|
+
address = "#{local}@#{mail_domain}"
|
|
217
|
+
exp_ms = jwt_exp_ms(token)
|
|
218
|
+
EmailInfo.new(channel: CHANNEL, email: address, token: token, expires_at: exp_ms)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# @param email [String]
|
|
222
|
+
# @param token [String]
|
|
223
|
+
# @return [Array<Email>]
|
|
224
|
+
def get_emails(email, token)
|
|
225
|
+
url = "#{API_BASE}/mailbox/#{enc_mailbox_email(email)}"
|
|
226
|
+
r = Http.get(url, headers: api_headers(token))
|
|
227
|
+
r.raise_for_status
|
|
228
|
+
data = r.json
|
|
229
|
+
raise "10minute-one: invalid inbox JSON" unless data.is_a?(Array)
|
|
230
|
+
|
|
231
|
+
data.filter_map do |raw|
|
|
232
|
+
next unless raw.is_a?(Hash)
|
|
233
|
+
|
|
234
|
+
row = raw.dup
|
|
235
|
+
if item_needs_detail(row)
|
|
236
|
+
mid = row["id"].to_s
|
|
237
|
+
detail_url = "#{API_BASE}/mailbox/#{enc_mailbox_email(email)}/#{URI.encode_www_form_component(mid)}"
|
|
238
|
+
begin
|
|
239
|
+
d = Http.get(detail_url, headers: api_headers(token))
|
|
240
|
+
if d.ok?
|
|
241
|
+
detail = d.json
|
|
242
|
+
if detail.is_a?(Hash)
|
|
243
|
+
detail.each { |k, v| row[k] = v unless row.key?(k) }
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
rescue StandardError
|
|
247
|
+
# 忽略详情失败
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
Normalize.normalize_email(row, email)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|