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 "base64"
|
|
4
|
+
require "json"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
module TempmailSdk
|
|
9
|
+
module Providers
|
|
10
|
+
# dropmail-me 渠道 — https://dropmail.me
|
|
11
|
+
# GraphQL 临时邮箱,需从页面提取 data-k 生成认证 token
|
|
12
|
+
module DropmailMe
|
|
13
|
+
CHANNEL = "dropmail-me"
|
|
14
|
+
BASE_URL = "https://dropmail.me"
|
|
15
|
+
|
|
16
|
+
HEADERS = {
|
|
17
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
18
|
+
"(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36",
|
|
19
|
+
"Accept" => "application/json",
|
|
20
|
+
"Content-Type" => "application/json"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# FNV-1a 变体哈希
|
|
26
|
+
def fnv_hash(s)
|
|
27
|
+
h = 2_166_136_261
|
|
28
|
+
s.each_char do |c|
|
|
29
|
+
h ^= c.ord
|
|
30
|
+
h += (h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)
|
|
31
|
+
h &= 0xFFFFFFFF
|
|
32
|
+
end
|
|
33
|
+
h.to_s(16)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 生成 dropmail.me API 认证 token
|
|
37
|
+
def generate_auth_token
|
|
38
|
+
resp = Http.get("#{BASE_URL}/en/", headers: {
|
|
39
|
+
"User-Agent" => HEADERS["User-Agent"],
|
|
40
|
+
"Accept" => "text/html"
|
|
41
|
+
}, timeout: 15)
|
|
42
|
+
resp.raise_for_status
|
|
43
|
+
|
|
44
|
+
match = resp.body.match(/<meta\s+name="app-config"\s+data-k="([^"]+)"/)
|
|
45
|
+
raise "dropmail-me: 无法从页面提取 data-k" unless match
|
|
46
|
+
|
|
47
|
+
data_k = match[1]
|
|
48
|
+
# 反转 + base64 解码得到 secret
|
|
49
|
+
_secret = Base64.decode64(data_k.reverse)
|
|
50
|
+
|
|
51
|
+
# 生成随机部分
|
|
52
|
+
date_str = Time.now.utc.strftime("%Y%m%d")
|
|
53
|
+
random_part = date_str + SecureRandom.alphanumeric(16)
|
|
54
|
+
|
|
55
|
+
# 计算哈希
|
|
56
|
+
hash_input = random_part + _secret
|
|
57
|
+
h = fnv_hash(hash_input)
|
|
58
|
+
|
|
59
|
+
"website_#{random_part}_#{h}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# 创建 dropmail.me 临时邮箱
|
|
63
|
+
# @return [EmailInfo]
|
|
64
|
+
def generate_email
|
|
65
|
+
auth_token = generate_auth_token
|
|
66
|
+
|
|
67
|
+
query = "mutation { introduceSession { id expiresAt addresses { address } } }"
|
|
68
|
+
resp = Http.post(
|
|
69
|
+
"#{BASE_URL}/api/graphql/#{auth_token}",
|
|
70
|
+
headers: HEADERS,
|
|
71
|
+
json: { "query" => query },
|
|
72
|
+
timeout: 15
|
|
73
|
+
)
|
|
74
|
+
resp.raise_for_status
|
|
75
|
+
data = resp.json
|
|
76
|
+
|
|
77
|
+
session_data = (data["data"] || {})[" introduceSession"] || (data["data"] || {})["introduceSession"]
|
|
78
|
+
raise "dropmail-me: 创建 session 失败" unless session_data
|
|
79
|
+
|
|
80
|
+
session_id = session_data["id"] || ""
|
|
81
|
+
addresses = session_data["addresses"] || []
|
|
82
|
+
raise "dropmail-me: 响应中缺少 session ID 或地址" if session_id.empty? || addresses.empty?
|
|
83
|
+
|
|
84
|
+
address = addresses[0]["address"] || ""
|
|
85
|
+
raise "dropmail-me: 地址为空" if address.empty?
|
|
86
|
+
|
|
87
|
+
# Token 序列化为 JSON
|
|
88
|
+
composite_token = JSON.generate({
|
|
89
|
+
"session_id" => session_id,
|
|
90
|
+
"auth_token" => auth_token
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
# 解析过期时间
|
|
94
|
+
expires_at = nil
|
|
95
|
+
expires_str = session_data["expiresAt"]
|
|
96
|
+
if expires_str
|
|
97
|
+
begin
|
|
98
|
+
dt = Time.parse(expires_str.sub("Z", "+00:00"))
|
|
99
|
+
expires_at = (dt.to_f * 1000).to_i
|
|
100
|
+
rescue ArgumentError, TypeError
|
|
101
|
+
# 忽略解析失败
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
EmailInfo.new(channel: CHANNEL, email: address, token: composite_token, expires_at: expires_at)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# 获取邮件列表
|
|
109
|
+
# @param token [String] JSON {"session_id":"...","auth_token":"..."}
|
|
110
|
+
# @param email [String]
|
|
111
|
+
# @return [Array<Email>]
|
|
112
|
+
def get_emails(token, email)
|
|
113
|
+
raise "dropmail-me: token 不能为空" if token.nil? || token.to_s.empty?
|
|
114
|
+
|
|
115
|
+
addr = (email || "").strip
|
|
116
|
+
raise "dropmail-me: 邮箱地址不能为空" if addr.empty?
|
|
117
|
+
|
|
118
|
+
# 解析复合 token
|
|
119
|
+
begin
|
|
120
|
+
token_data = JSON.parse(token)
|
|
121
|
+
rescue JSON::ParserError
|
|
122
|
+
raise "dropmail-me: token 格式无效"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
session_id = token_data["session_id"] || ""
|
|
126
|
+
auth_token = token_data["auth_token"] || ""
|
|
127
|
+
raise "dropmail-me: token 中缺少 session_id 或 auth_token" if session_id.empty? || auth_token.empty?
|
|
128
|
+
|
|
129
|
+
# 查询邮件
|
|
130
|
+
query = "{ session(id:\"#{session_id}\") { mails { id headerFrom headerSubject text html receivedAt } } }"
|
|
131
|
+
resp = Http.post(
|
|
132
|
+
"#{BASE_URL}/api/graphql/#{auth_token}",
|
|
133
|
+
headers: HEADERS,
|
|
134
|
+
json: { "query" => query },
|
|
135
|
+
timeout: 15
|
|
136
|
+
)
|
|
137
|
+
resp.raise_for_status
|
|
138
|
+
data = resp.json
|
|
139
|
+
|
|
140
|
+
session_resp = (data["data"] || {})["session"]
|
|
141
|
+
return [] unless session_resp
|
|
142
|
+
|
|
143
|
+
mails = session_resp["mails"]
|
|
144
|
+
return [] unless mails.is_a?(Array) && !mails.empty?
|
|
145
|
+
|
|
146
|
+
mails.filter_map do |msg|
|
|
147
|
+
next unless msg.is_a?(Hash)
|
|
148
|
+
|
|
149
|
+
raw = {
|
|
150
|
+
"id" => msg["id"] || "",
|
|
151
|
+
"from" => msg["headerFrom"] || "",
|
|
152
|
+
"to" => addr,
|
|
153
|
+
"subject" => msg["headerSubject"] || "",
|
|
154
|
+
"text" => msg["text"] || "",
|
|
155
|
+
"html" => msg["html"] || "",
|
|
156
|
+
"date" => msg["receivedAt"] || ""
|
|
157
|
+
}
|
|
158
|
+
Normalize.normalize_email(raw, addr)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# DuckMail 渠道实现(与 mail.tm 同构 API)
|
|
6
|
+
# API: https://api.duckmail.sbs
|
|
7
|
+
module Duckmail
|
|
8
|
+
CHANNEL = "duckmail"
|
|
9
|
+
BASE_URL = "https://api.duckmail.sbs"
|
|
10
|
+
DEFAULT_HEADERS = {
|
|
11
|
+
"Content-Type" => "application/json",
|
|
12
|
+
"Accept" => "application/json",
|
|
13
|
+
"Origin" => "https://duckmail.sbs",
|
|
14
|
+
"Referer" => "https://duckmail.sbs/"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def random_string(length)
|
|
20
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
21
|
+
Array.new(length) { chars.sample }.join
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fetch_domains
|
|
25
|
+
resp = Http.get("#{BASE_URL}/domains?page=1", headers: DEFAULT_HEADERS)
|
|
26
|
+
resp.raise_for_status
|
|
27
|
+
data = resp.json
|
|
28
|
+
members = data.is_a?(Array) ? data : (data["hydra:member"] || [])
|
|
29
|
+
members.select { |d| d["isActive"] }.map { |d| d["domain"] }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [EmailInfo]
|
|
33
|
+
def generate_email
|
|
34
|
+
domains = fetch_domains
|
|
35
|
+
raise "No available domains" if domains.empty?
|
|
36
|
+
|
|
37
|
+
address = "#{random_string(12)}@#{domains.sample}"
|
|
38
|
+
password = random_string(16)
|
|
39
|
+
acc = Http.post("#{BASE_URL}/accounts",
|
|
40
|
+
headers: DEFAULT_HEADERS.merge("Content-Type" => "application/ld+json"),
|
|
41
|
+
json: { "address" => address, "password" => password })
|
|
42
|
+
acc.raise_for_status
|
|
43
|
+
tok = Http.post("#{BASE_URL}/token", headers: DEFAULT_HEADERS,
|
|
44
|
+
json: { "address" => address, "password" => password })
|
|
45
|
+
tok.raise_for_status
|
|
46
|
+
EmailInfo.new(channel: CHANNEL, email: address, token: tok.json["token"],
|
|
47
|
+
created_at: acc.json["createdAt"])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param email [String]
|
|
51
|
+
# @param token [String]
|
|
52
|
+
# @return [Array<Email>]
|
|
53
|
+
def get_emails(email, token)
|
|
54
|
+
resp = Http.get("#{BASE_URL}/messages?page=1",
|
|
55
|
+
headers: DEFAULT_HEADERS.merge("Authorization" => "Bearer #{token}"))
|
|
56
|
+
resp.raise_for_status
|
|
57
|
+
data = resp.json
|
|
58
|
+
messages = data.is_a?(Array) ? data : (data["hydra:member"] || [])
|
|
59
|
+
messages.map { |msg| Normalize.normalize_email(MailTm.flatten(msg, email), email) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "rexml/document"
|
|
5
|
+
|
|
6
|
+
module TempmailSdk
|
|
7
|
+
module Providers
|
|
8
|
+
# Eyepaste 渠道 — https://eyepaste.com
|
|
9
|
+
# RSS XML API,无认证,直接生成随机用户名
|
|
10
|
+
module Eyepaste
|
|
11
|
+
CHANNEL = "eyepaste"
|
|
12
|
+
DOMAIN = "eyepaste.com"
|
|
13
|
+
BASE = "https://www.eyepaste.com"
|
|
14
|
+
|
|
15
|
+
HEADERS = {
|
|
16
|
+
"Accept" => "application/rss+xml, application/xml, text/xml, */*",
|
|
17
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
18
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
TAG_RE = /<[^>]+>/
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# 生成随机用户名
|
|
26
|
+
def random_username(length = 10)
|
|
27
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
28
|
+
Array.new(length) { chars.sample }.join
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# 解析 RSS description 中的邮件元数据
|
|
32
|
+
def parse_description(desc)
|
|
33
|
+
result = { "from" => "", "to" => "", "subject" => "", "date" => "", "body" => "" }
|
|
34
|
+
return result if desc.nil? || desc.empty?
|
|
35
|
+
|
|
36
|
+
p_match = desc.match(/<p[^>]*>(.*?)<\/p>/mi)
|
|
37
|
+
if p_match
|
|
38
|
+
meta = p_match[1].strip
|
|
39
|
+
from_m = meta.match(/From:\s*(.*?)(?:\s+To:|$)/m)
|
|
40
|
+
result["from"] = from_m[1].strip if from_m
|
|
41
|
+
to_m = meta.match(/To:\s*(.*?)(?:\s+Subject:|$)/m)
|
|
42
|
+
result["to"] = to_m[1].strip if to_m
|
|
43
|
+
subject_m = meta.match(/Subject:\s*(.*?)(?:\s+Date:|$)/m)
|
|
44
|
+
result["subject"] = subject_m[1].strip if subject_m
|
|
45
|
+
date_m = meta.match(/Date:\s*(.*?)$/m)
|
|
46
|
+
result["date"] = date_m[1].strip if date_m
|
|
47
|
+
|
|
48
|
+
p_end = desc.index("</p>")
|
|
49
|
+
if p_end
|
|
50
|
+
body = desc[(p_end + 4)..].strip
|
|
51
|
+
result["body"] = body unless body.empty?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
result
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# 生成临时邮箱(无需 API 调用)
|
|
58
|
+
# @return [EmailInfo]
|
|
59
|
+
def generate_email
|
|
60
|
+
username = random_username
|
|
61
|
+
email = "#{username}@#{DOMAIN}"
|
|
62
|
+
EmailInfo.new(channel: CHANNEL, email: email)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# 获取邮件列表,通过 RSS XML 解析
|
|
66
|
+
# @param email [String]
|
|
67
|
+
# @return [Array<Email>]
|
|
68
|
+
def get_emails(email)
|
|
69
|
+
e = (email || "").strip
|
|
70
|
+
raise "eyepaste: empty email" if e.empty?
|
|
71
|
+
|
|
72
|
+
resp = Http.get("#{BASE}/inbox/#{e}.rss", headers: HEADERS, timeout: 15)
|
|
73
|
+
resp.raise_for_status
|
|
74
|
+
content = resp.body
|
|
75
|
+
return [] if content.nil? || content.strip.empty?
|
|
76
|
+
|
|
77
|
+
begin
|
|
78
|
+
doc = REXML::Document.new(content)
|
|
79
|
+
rescue REXML::ParseException
|
|
80
|
+
return []
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
channel_elem = doc.root&.elements&.to_a("channel")&.first
|
|
84
|
+
return [] unless channel_elem
|
|
85
|
+
|
|
86
|
+
emails = []
|
|
87
|
+
channel_elem.elements.each("item") do |item|
|
|
88
|
+
idx = emails.length
|
|
89
|
+
title = item.elements["title"]&.text.to_s.strip
|
|
90
|
+
desc = item.elements["description"]&.text.to_s.strip
|
|
91
|
+
|
|
92
|
+
parsed = parse_description(desc)
|
|
93
|
+
subject = parsed["subject"].empty? ? title : parsed["subject"]
|
|
94
|
+
from_addr = parsed["from"]
|
|
95
|
+
to_addr = parsed["to"].empty? ? e : parsed["to"]
|
|
96
|
+
date = parsed["date"]
|
|
97
|
+
body_html = parsed["body"]
|
|
98
|
+
|
|
99
|
+
text = ""
|
|
100
|
+
text = body_html.gsub(TAG_RE, "").strip unless body_html.empty?
|
|
101
|
+
|
|
102
|
+
emails << Email.new(
|
|
103
|
+
id: (idx + 1).to_s,
|
|
104
|
+
from_addr: from_addr,
|
|
105
|
+
to: to_addr,
|
|
106
|
+
subject: subject,
|
|
107
|
+
text: text,
|
|
108
|
+
html: body_html,
|
|
109
|
+
date: date,
|
|
110
|
+
is_read: false,
|
|
111
|
+
attachments: []
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
emails
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# fake-email.site 渠道实现
|
|
6
|
+
# JSON REST API:创建临时邮箱 + 轮询收件箱
|
|
7
|
+
module FakeEmailSite
|
|
8
|
+
CHANNEL = "fake-email-site"
|
|
9
|
+
BASE = "https://api.fake-email.site"
|
|
10
|
+
HEADERS = {
|
|
11
|
+
"Accept" => "application/json",
|
|
12
|
+
"Content-Type" => "application/json",
|
|
13
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# @return [EmailInfo]
|
|
19
|
+
def generate_email
|
|
20
|
+
resp = Http.post("#{BASE}/api/temporary-address", headers: HEADERS, json: {})
|
|
21
|
+
resp.raise_for_status
|
|
22
|
+
data = resp.json
|
|
23
|
+
raise "fake-email-site: invalid response" unless data.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
email = (data["temp_email_addr"] || "").to_s.strip
|
|
26
|
+
raise "fake-email-site: missing temp_email_addr" if email.empty?
|
|
27
|
+
|
|
28
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: email)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param email [String]
|
|
32
|
+
# @return [Array<Email>]
|
|
33
|
+
def get_emails(email)
|
|
34
|
+
resp = Http.get("#{BASE}/api/inbox/poll?address=#{email}", headers: HEADERS)
|
|
35
|
+
return [] if resp.status_code == 404
|
|
36
|
+
|
|
37
|
+
resp.raise_for_status
|
|
38
|
+
data = resp.json
|
|
39
|
+
return [] unless data.is_a?(Hash)
|
|
40
|
+
|
|
41
|
+
rows = data["messages"]
|
|
42
|
+
return [] unless rows.is_a?(Array)
|
|
43
|
+
|
|
44
|
+
rows.select { |r| r.is_a?(Hash) }.map { |row| Normalize.normalize_email(row, email) }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# Fake Legal 渠道 — https://imgui.de
|
|
8
|
+
# imgui.de / pulsewebmenu.de 用 POST 保根域,其余域用 GET 创建
|
|
9
|
+
module FakeLegal
|
|
10
|
+
CHANNEL = "fake-legal"
|
|
11
|
+
BASE = "https://imgui.de"
|
|
12
|
+
HEADERS = {
|
|
13
|
+
"Accept" => "application/json, text/plain, */*",
|
|
14
|
+
"Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
|
|
15
|
+
"Cache-Control" => "no-cache",
|
|
16
|
+
"DNT" => "1",
|
|
17
|
+
"Pragma" => "no-cache",
|
|
18
|
+
"Referer" => "https://imgui.de/",
|
|
19
|
+
"sec-ch-ua" => '"Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146"',
|
|
20
|
+
"sec-ch-ua-mobile" => "?0",
|
|
21
|
+
"sec-ch-ua-platform" => '"Windows"',
|
|
22
|
+
"sec-fetch-dest" => "empty",
|
|
23
|
+
"sec-fetch-mode" => "cors",
|
|
24
|
+
"sec-fetch-site" => "same-origin",
|
|
25
|
+
"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"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
def fetch_domains
|
|
31
|
+
resp = Http.get("#{BASE}/api/domains", headers: HEADERS, timeout: 15)
|
|
32
|
+
resp.raise_for_status
|
|
33
|
+
data = resp.json
|
|
34
|
+
return [] unless data.is_a?(Hash)
|
|
35
|
+
|
|
36
|
+
raw = data["domains"]
|
|
37
|
+
return [] unless raw.is_a?(Array)
|
|
38
|
+
|
|
39
|
+
raw.filter_map { |d| d.strip if d.is_a?(String) && !d.strip.empty? }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def pick_domain(domains, preferred)
|
|
43
|
+
if preferred && !preferred.to_s.strip.empty?
|
|
44
|
+
p = preferred.to_s.strip.downcase
|
|
45
|
+
found = domains.find { |d| d.downcase == p }
|
|
46
|
+
return found if found
|
|
47
|
+
|
|
48
|
+
raise "fake-legal: domain not available: #{p}"
|
|
49
|
+
end
|
|
50
|
+
domains.sample
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def random_username(length)
|
|
54
|
+
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
|
55
|
+
Array.new(length) { chars.sample }.join
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param domain [String, nil]
|
|
59
|
+
# @param channel [String]
|
|
60
|
+
# @return [EmailInfo]
|
|
61
|
+
def generate_email(domain = nil, channel = CHANNEL)
|
|
62
|
+
domains = fetch_domains
|
|
63
|
+
raise "fake-legal: no domains" if domains.empty?
|
|
64
|
+
|
|
65
|
+
d = pick_domain(domains, domain)
|
|
66
|
+
resp = if d == "imgui.de" || d == "pulsewebmenu.de"
|
|
67
|
+
# imgui-de / pulsewebmenu-de 用 POST 保根域
|
|
68
|
+
Http.post("#{BASE}/api/inbox/custom",
|
|
69
|
+
json: { "username" => random_username(12), "domain" => d },
|
|
70
|
+
headers: HEADERS, timeout: 15)
|
|
71
|
+
else
|
|
72
|
+
# fake-legal 用 GET 创建
|
|
73
|
+
Http.get("#{BASE}/api/inbox/new?domain=#{URI.encode_www_form_component(d)}",
|
|
74
|
+
headers: HEADERS, timeout: 15)
|
|
75
|
+
end
|
|
76
|
+
resp.raise_for_status
|
|
77
|
+
data = resp.json
|
|
78
|
+
raise "fake-legal: create inbox failed" unless data.is_a?(Hash) && data["success"]
|
|
79
|
+
|
|
80
|
+
addr = data["address"]
|
|
81
|
+
raise "fake-legal: missing address" if addr.nil? || addr.to_s.strip.empty?
|
|
82
|
+
|
|
83
|
+
EmailInfo.new(channel: channel, email: addr.to_s.strip)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @param email [String]
|
|
87
|
+
# @return [Array<Email>]
|
|
88
|
+
def get_emails(email)
|
|
89
|
+
e = email.to_s.strip
|
|
90
|
+
raise "fake-legal: empty email" if e.empty?
|
|
91
|
+
|
|
92
|
+
resp = Http.get("#{BASE}/api/inbox/#{URI.encode_www_form_component(e)}",
|
|
93
|
+
headers: HEADERS, timeout: 15)
|
|
94
|
+
resp.raise_for_status
|
|
95
|
+
data = resp.json
|
|
96
|
+
return [] unless data.is_a?(Hash) && data["success"]
|
|
97
|
+
|
|
98
|
+
rows = data["emails"]
|
|
99
|
+
return [] unless rows.is_a?(Array)
|
|
100
|
+
|
|
101
|
+
rows.filter_map do |raw|
|
|
102
|
+
next unless raw.is_a?(Hash)
|
|
103
|
+
|
|
104
|
+
Normalize.normalize_email(raw, e)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# fmail.men 渠道实现
|
|
6
|
+
module Fmail
|
|
7
|
+
CHANNEL = "fmail"
|
|
8
|
+
BASE_URL = "https://fmail.men"
|
|
9
|
+
HEADERS = {
|
|
10
|
+
"Accept" => "application/json",
|
|
11
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
12
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def fetch_json(path)
|
|
18
|
+
resp = Http.get("#{BASE_URL}#{path}", headers: HEADERS, timeout: 15)
|
|
19
|
+
resp.raise_for_status
|
|
20
|
+
data = resp.json
|
|
21
|
+
data.is_a?(Hash) ? data : {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def flatten_message(raw, recipient)
|
|
25
|
+
{
|
|
26
|
+
"id" => (raw["id"] || raw["uid"] || raw["token"]).to_s,
|
|
27
|
+
"from" => (raw["sender"] || raw["from"]).to_s,
|
|
28
|
+
"to" => (raw["recipient"] || raw["to"] || recipient).to_s,
|
|
29
|
+
"subject" => (raw["subject"] || "").to_s,
|
|
30
|
+
"text" => (raw["body_text"] || raw["text"] || raw["snippet"]).to_s,
|
|
31
|
+
"html" => (raw["body_html"] || raw["html"]).to_s,
|
|
32
|
+
"date" => (raw["received_at"] || raw["timestamp"] || raw["date"]).to_s,
|
|
33
|
+
"is_read" => raw["is_read"] || false,
|
|
34
|
+
"attachments" => raw["attachments"].is_a?(Array) ? raw["attachments"] : []
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @param domain [String, nil] 可选域名
|
|
39
|
+
# @return [EmailInfo]
|
|
40
|
+
def generate_email(domain = nil)
|
|
41
|
+
selected = domain.to_s.strip.delete_prefix("@")
|
|
42
|
+
path = "/v1/random"
|
|
43
|
+
path = "#{path}?domain=#{URI.encode_www_form_component(selected)}" unless selected.empty?
|
|
44
|
+
|
|
45
|
+
data = fetch_json(path)
|
|
46
|
+
email = data["address"].to_s.strip
|
|
47
|
+
if email.empty?
|
|
48
|
+
username = data["username"].to_s.strip
|
|
49
|
+
dom = data["domain"].to_s.strip
|
|
50
|
+
email = "#{username}@#{dom}" if !username.empty? && !dom.empty?
|
|
51
|
+
end
|
|
52
|
+
raise "fmail: invalid random response" if email.empty? || !email.include?("@")
|
|
53
|
+
|
|
54
|
+
EmailInfo.new(channel: CHANNEL, email: email)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @param email [String]
|
|
58
|
+
# @return [Array<Email>]
|
|
59
|
+
def get_emails(email)
|
|
60
|
+
value = email.to_s.strip
|
|
61
|
+
local, _, domain = value.partition("@")
|
|
62
|
+
raise "fmail: invalid email" if local.empty? || domain.empty?
|
|
63
|
+
|
|
64
|
+
data = fetch_json(
|
|
65
|
+
"/v1/inbox/#{URI.encode_www_form_component(local)}?domain=#{URI.encode_www_form_component(domain)}&limit=50"
|
|
66
|
+
)
|
|
67
|
+
rows = data["emails"]
|
|
68
|
+
return [] unless rows.is_a?(Array)
|
|
69
|
+
|
|
70
|
+
rows.select { |r| r.is_a?(Hash) }.map do |row|
|
|
71
|
+
token = (row["token"] || row["id"]).to_s.strip
|
|
72
|
+
if token.empty?
|
|
73
|
+
Normalize.normalize_email(flatten_message(row, value), value)
|
|
74
|
+
else
|
|
75
|
+
begin
|
|
76
|
+
detail = fetch_json("/v1/email/#{URI.encode_www_form_component(token)}")
|
|
77
|
+
nested = detail["email"].is_a?(Hash) ? detail["email"] : detail
|
|
78
|
+
Normalize.normalize_email(flatten_message(nested.is_a?(Hash) ? nested : row, value), value)
|
|
79
|
+
rescue StandardError
|
|
80
|
+
Normalize.normalize_email(flatten_message(row, value), value)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|