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,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# anonymmail.net 渠道实现
|
|
6
|
+
# POST JSON API,需要 cookie session
|
|
7
|
+
module Anonymmail
|
|
8
|
+
CHANNEL = "anonymmail"
|
|
9
|
+
BASE = "https://anonymmail.net"
|
|
10
|
+
HEADERS = {
|
|
11
|
+
"Accept" => "*/*",
|
|
12
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
|
13
|
+
"Referer" => "https://anonymmail.net/",
|
|
14
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
15
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def random_username(length = 10)
|
|
21
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
22
|
+
Array.new(length) { chars.sample }.join
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 获取可用域名列表
|
|
26
|
+
def fetch_domains
|
|
27
|
+
resp = Http.post("#{BASE}/api/getDomains", headers: HEADERS, timeout: 15)
|
|
28
|
+
resp.raise_for_status
|
|
29
|
+
data = resp.json
|
|
30
|
+
return [] unless data.is_a?(Array)
|
|
31
|
+
|
|
32
|
+
data.select { |item| item.is_a?(Hash) && item["domain"].to_s.strip != "" }
|
|
33
|
+
.map { |item| item["domain"].to_s.strip }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [EmailInfo]
|
|
37
|
+
def generate_email
|
|
38
|
+
domains = fetch_domains
|
|
39
|
+
raise "anonymmail: no domains available" if domains.empty?
|
|
40
|
+
|
|
41
|
+
domain = domains.sample
|
|
42
|
+
username = random_username
|
|
43
|
+
email = "#{username}@#{domain}"
|
|
44
|
+
|
|
45
|
+
# HEAD 请求获取 cookie session
|
|
46
|
+
Http.get("#{BASE}/", headers: HEADERS, timeout: 15)
|
|
47
|
+
|
|
48
|
+
# POST 创建邮箱
|
|
49
|
+
resp = Http.post("#{BASE}/api/create", headers: HEADERS, body: "email=#{email}", timeout: 15)
|
|
50
|
+
resp.raise_for_status
|
|
51
|
+
data = resp.json
|
|
52
|
+
raise "anonymmail: create inbox failed" unless data.is_a?(Hash) && data["success"]
|
|
53
|
+
|
|
54
|
+
addr = data["email"].to_s.strip
|
|
55
|
+
raise "anonymmail: missing email in response" if addr.empty?
|
|
56
|
+
|
|
57
|
+
EmailInfo.new(channel: CHANNEL, email: addr)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @param email [String]
|
|
61
|
+
# @return [Array<Email>]
|
|
62
|
+
def get_emails(email)
|
|
63
|
+
e = email.to_s.strip
|
|
64
|
+
raise "anonymmail: empty email" if e.empty?
|
|
65
|
+
|
|
66
|
+
resp = Http.post("#{BASE}/api/get", headers: HEADERS, body: "email=#{e}", timeout: 15)
|
|
67
|
+
resp.raise_for_status
|
|
68
|
+
data = resp.json
|
|
69
|
+
return [] unless data.is_a?(Hash)
|
|
70
|
+
|
|
71
|
+
inbox = data[e]
|
|
72
|
+
return [] unless inbox.is_a?(Hash)
|
|
73
|
+
|
|
74
|
+
rows = inbox["emails"]
|
|
75
|
+
return [] unless rows.is_a?(Array)
|
|
76
|
+
|
|
77
|
+
rows.select { |r| r.is_a?(Hash) }.map do |raw|
|
|
78
|
+
normalized = raw.dup
|
|
79
|
+
# 将 token 字段映射为 id
|
|
80
|
+
if normalized.key?("token") && !normalized.key?("id")
|
|
81
|
+
normalized["id"] = normalized.delete("token")
|
|
82
|
+
end
|
|
83
|
+
# 将 body 字段映射为 html
|
|
84
|
+
if normalized.key?("body") && !normalized.key?("html")
|
|
85
|
+
normalized["html"] = normalized.delete("body")
|
|
86
|
+
end
|
|
87
|
+
Normalize.normalize_email(normalized, e)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# byom.de 渠道实现
|
|
6
|
+
# 纯 GET 无认证,直接生成随机用户名
|
|
7
|
+
module Byom
|
|
8
|
+
CHANNEL = "byom"
|
|
9
|
+
DOMAIN = "byom.de"
|
|
10
|
+
BASE = "https://api.byom.de"
|
|
11
|
+
HEADERS = {
|
|
12
|
+
"Accept" => "application/json, text/plain, */*",
|
|
13
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
14
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def random_username(length = 10)
|
|
20
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
21
|
+
Array.new(length) { chars.sample }.join
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [EmailInfo]
|
|
25
|
+
def generate_email
|
|
26
|
+
username = random_username
|
|
27
|
+
EmailInfo.new(channel: CHANNEL, email: "#{username}@#{DOMAIN}")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param email [String]
|
|
31
|
+
# @return [Array<Email>]
|
|
32
|
+
def get_emails(email)
|
|
33
|
+
e = email.to_s.strip
|
|
34
|
+
raise "byom: empty email" if e.empty?
|
|
35
|
+
|
|
36
|
+
username = e.split("@").first.to_s
|
|
37
|
+
raise "byom: invalid email format" if username.empty?
|
|
38
|
+
|
|
39
|
+
resp = Http.get("#{BASE}/mails/#{username}", headers: HEADERS, timeout: 15)
|
|
40
|
+
resp.raise_for_status
|
|
41
|
+
data = resp.json
|
|
42
|
+
return [] unless data.is_a?(Array)
|
|
43
|
+
|
|
44
|
+
data.select { |r| r.is_a?(Hash) }.map { |raw| Normalize.normalize_email(raw, e) }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# Catchmail 渠道 — https://catchmail.io
|
|
8
|
+
# 匿名 REST API:创建邮箱即列表接口,逐封拉取详情
|
|
9
|
+
module Catchmail
|
|
10
|
+
CHANNEL = "catchmail"
|
|
11
|
+
API_BASE = "https://api.catchmail.io/api/v1"
|
|
12
|
+
DEFAULT_DOMAIN = "catchmail.io"
|
|
13
|
+
DOMAINS = %w[catchmail.io mailistry.com zeppost.com].freeze
|
|
14
|
+
HEADERS = {
|
|
15
|
+
"Accept" => "application/json",
|
|
16
|
+
"Referer" => "https://catchmail.io/",
|
|
17
|
+
"Origin" => "https://catchmail.io",
|
|
18
|
+
"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"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
def random_local
|
|
24
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
25
|
+
"sdk#{Array.new(16) { chars.sample }.join}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# 选择域名:命中别名域名则用之,否则默认域名
|
|
29
|
+
def pick_domain(preferred)
|
|
30
|
+
p = preferred.to_s.strip.sub(/\A@/, "").downcase
|
|
31
|
+
unless p.empty?
|
|
32
|
+
found = DOMAINS.find { |d| d.downcase == p }
|
|
33
|
+
return found if found
|
|
34
|
+
end
|
|
35
|
+
DEFAULT_DOMAIN
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 从可能带显示名的地址字段提取纯地址
|
|
39
|
+
def clean_address(value)
|
|
40
|
+
value = value.first if value.is_a?(Array)
|
|
41
|
+
text = value.to_s.strip
|
|
42
|
+
m = text.match(/<([^>]+)>/)
|
|
43
|
+
m ? m[1].strip : text
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def flatten_detail(raw, recipient)
|
|
47
|
+
body = raw["body"].is_a?(Hash) ? raw["body"] : {}
|
|
48
|
+
{
|
|
49
|
+
"id" => raw["id"] || "", "from" => clean_address(raw["from"]),
|
|
50
|
+
"to" => (a = clean_address(raw["to"])).empty? ? recipient : a,
|
|
51
|
+
"subject" => raw["subject"] || "", "text" => body["text"] || "",
|
|
52
|
+
"html" => body["html"] || "", "date" => raw["date"] || "",
|
|
53
|
+
"attachments" => raw["attachments"]
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def fetch_message(message_id, email)
|
|
58
|
+
resp = Http.get(
|
|
59
|
+
"#{API_BASE}/message/#{URI.encode_www_form_component(message_id)}?mailbox=#{URI.encode_www_form_component(email)}",
|
|
60
|
+
headers: HEADERS, timeout: 15
|
|
61
|
+
)
|
|
62
|
+
return nil unless resp.ok?
|
|
63
|
+
|
|
64
|
+
data = resp.json
|
|
65
|
+
data.is_a?(Hash) ? data : nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @param domain [String, nil]
|
|
69
|
+
# @return [EmailInfo]
|
|
70
|
+
def generate_email(domain = nil)
|
|
71
|
+
email = "#{random_local}@#{pick_domain(domain)}"
|
|
72
|
+
Http.get("#{API_BASE}/mailbox?address=#{URI.encode_www_form_component(email)}",
|
|
73
|
+
headers: HEADERS, timeout: 15).raise_for_status
|
|
74
|
+
EmailInfo.new(channel: CHANNEL, email: email)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @param email [String]
|
|
78
|
+
# @return [Array<Email>]
|
|
79
|
+
def get_emails(email)
|
|
80
|
+
address = email.to_s.strip
|
|
81
|
+
raise "catchmail: empty email" if address.empty?
|
|
82
|
+
|
|
83
|
+
resp = Http.get("#{API_BASE}/mailbox?address=#{URI.encode_www_form_component(address)}",
|
|
84
|
+
headers: HEADERS, timeout: 15)
|
|
85
|
+
resp.raise_for_status
|
|
86
|
+
data = resp.json
|
|
87
|
+
rows = data.is_a?(Hash) ? data["messages"] : nil
|
|
88
|
+
return [] unless rows.is_a?(Array)
|
|
89
|
+
|
|
90
|
+
rows.filter_map do |item|
|
|
91
|
+
next unless item.is_a?(Hash)
|
|
92
|
+
|
|
93
|
+
message_id = item["id"].to_s.strip
|
|
94
|
+
next if message_id.empty?
|
|
95
|
+
|
|
96
|
+
detail = fetch_message(message_id, address)
|
|
97
|
+
if detail
|
|
98
|
+
Normalize.normalize_email(flatten_detail(detail, address), address)
|
|
99
|
+
else
|
|
100
|
+
Normalize.normalize_email({
|
|
101
|
+
"id" => message_id, "from" => clean_address(item["from"]),
|
|
102
|
+
"to" => item["mailbox"] || address,
|
|
103
|
+
"subject" => item["subject"] || "", "date" => item["date"] || ""
|
|
104
|
+
}, address)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# cleantempmail.com 渠道实现
|
|
6
|
+
module Cleantempmail
|
|
7
|
+
CHANNEL = "cleantempmail"
|
|
8
|
+
API_BASE = "https://cleantempmail.com/api"
|
|
9
|
+
HEADERS = {
|
|
10
|
+
"Accept" => "application/json",
|
|
11
|
+
"User-Agent" => "Mozilla/5.0",
|
|
12
|
+
"X-API-Key" => (ENV["CLEANTEMPMAIL_API_KEY"].to_s.strip.empty? ? "ct-test" : ENV["CLEANTEMPMAIL_API_KEY"].to_s.strip)
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def fetch_json(path)
|
|
18
|
+
resp = Http.get("#{API_BASE}#{path}", headers: HEADERS, timeout: 15)
|
|
19
|
+
resp.raise_for_status
|
|
20
|
+
data = resp.json
|
|
21
|
+
data.is_a?(Hash) ? data : {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [EmailInfo]
|
|
25
|
+
def generate_email
|
|
26
|
+
data = fetch_json("/generate-email")
|
|
27
|
+
payload = data["data"].is_a?(Hash) ? data["data"] : {}
|
|
28
|
+
email = (payload["email"] || payload["mailbox"]).to_s.strip
|
|
29
|
+
raise "cleantempmail: invalid generate-email response" if email.empty? || !email.include?("@")
|
|
30
|
+
|
|
31
|
+
EmailInfo.new(channel: CHANNEL, email: email)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param email [String]
|
|
35
|
+
# @return [Array<Email>]
|
|
36
|
+
def get_emails(email)
|
|
37
|
+
address = email.to_s.strip
|
|
38
|
+
raise "cleantempmail: empty email" if address.empty?
|
|
39
|
+
|
|
40
|
+
data = fetch_json("/emails?email=#{URI.encode_www_form_component(address)}")
|
|
41
|
+
payload = data["data"].is_a?(Hash) ? data["data"] : {}
|
|
42
|
+
rows = payload["emails"]
|
|
43
|
+
return [] unless rows.is_a?(Array)
|
|
44
|
+
|
|
45
|
+
rows.select { |r| r.is_a?(Hash) }.map { |item| Normalize.normalize_email(item, address) }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# devmail.uk 渠道实现
|
|
6
|
+
module DevmailUk
|
|
7
|
+
CHANNEL = "devmail-uk"
|
|
8
|
+
API_BASE = "https://www.devmail.uk/api"
|
|
9
|
+
HEADERS = { "Accept" => "application/json" }.freeze
|
|
10
|
+
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def mailbox_from_email(email)
|
|
14
|
+
address = email.to_s.strip
|
|
15
|
+
return "" if address.empty?
|
|
16
|
+
return address.split("@", 2).first.to_s.strip if address.include?("@")
|
|
17
|
+
|
|
18
|
+
address
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @return [EmailInfo]
|
|
22
|
+
def generate_email
|
|
23
|
+
resp = Http.get("#{API_BASE}/new", headers: HEADERS, timeout: 15)
|
|
24
|
+
resp.raise_for_status
|
|
25
|
+
data = resp.json
|
|
26
|
+
raise "devmail-uk: invalid new email response" unless data.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
email = data["email"].to_s.strip
|
|
29
|
+
if email.empty? && !data["mailbox"].to_s.strip.empty?
|
|
30
|
+
email = "#{data['mailbox'].to_s.strip}@devmail.uk"
|
|
31
|
+
end
|
|
32
|
+
raise "devmail-uk: invalid new email response" if email.empty? || !email.include?("@")
|
|
33
|
+
|
|
34
|
+
EmailInfo.new(channel: CHANNEL, email: email)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param email [String]
|
|
38
|
+
# @return [Array<Email>]
|
|
39
|
+
def get_emails(email)
|
|
40
|
+
mailbox = mailbox_from_email(email)
|
|
41
|
+
raise "devmail-uk: empty email" if mailbox.empty?
|
|
42
|
+
|
|
43
|
+
resp = Http.get(
|
|
44
|
+
"#{API_BASE}/inbox/#{URI.encode_www_form_component(mailbox)}?detail=true",
|
|
45
|
+
headers: HEADERS, timeout: 15
|
|
46
|
+
)
|
|
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.select { |r| r.is_a?(Hash) }.map { |item| Normalize.normalize_email(item, email) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module TempmailSdk
|
|
7
|
+
module Providers
|
|
8
|
+
# dropmail.me GraphQL 渠道 — https://dropmail.me
|
|
9
|
+
# 使用 af_ 令牌访问 GraphQL API 创建 session 和查询邮件
|
|
10
|
+
module Dropmail
|
|
11
|
+
CHANNEL = "dropmail"
|
|
12
|
+
TOKEN_GENERATE_URL = "https://dropmail.me/api/token/generate"
|
|
13
|
+
TOKEN_RENEW_URL = "https://dropmail.me/api/token/renew"
|
|
14
|
+
|
|
15
|
+
CREATE_SESSION_QUERY = "mutation {introduceSession {id, expiresAt, addresses{id, address}}}"
|
|
16
|
+
GET_MAILS_QUERY = 'query ($id: ID!) { session(id:$id) { mails { id, rawSize, fromAddr, toAddr, receivedAt, text, headerFrom, headerSubject, html } } }'
|
|
17
|
+
|
|
18
|
+
TOKEN_HEADERS = {
|
|
19
|
+
"Accept" => "application/json",
|
|
20
|
+
"Content-Type" => "application/json",
|
|
21
|
+
"Origin" => "https://dropmail.me",
|
|
22
|
+
"Referer" => "https://dropmail.me/api/"
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
AUTO_CACHE_SEC = 50 * 60
|
|
26
|
+
RENEW_BEFORE_SEC = 10 * 60
|
|
27
|
+
|
|
28
|
+
@lock = Mutex.new
|
|
29
|
+
@cached_af = nil # [token_string, expiry_monotonic]
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# 从配置或环境变量中获取显式 token
|
|
34
|
+
def explicit_af_token
|
|
35
|
+
ENV["DROPMAIL_AUTH_TOKEN"].to_s.strip.then do |t|
|
|
36
|
+
return t unless t.empty?
|
|
37
|
+
end
|
|
38
|
+
ENV["DROPMAIL_API_TOKEN"].to_s.strip
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# 从 API 获取新 af_ token
|
|
42
|
+
def fetch_af_token
|
|
43
|
+
resp = Http.post(TOKEN_GENERATE_URL, headers: TOKEN_HEADERS, json: { "type" => "af", "lifetime" => "1h" })
|
|
44
|
+
resp.raise_for_status
|
|
45
|
+
body = resp.json
|
|
46
|
+
tok = (body["token"] || "").to_s.strip
|
|
47
|
+
raise "DropMail token/generate 未返回有效 af_ 令牌" if tok.empty? || !tok.start_with?("af_")
|
|
48
|
+
|
|
49
|
+
tok
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# 解析 af_ token(优先显式配置,其次自动获取并缓存)
|
|
53
|
+
def resolve_af_token
|
|
54
|
+
ex = explicit_af_token
|
|
55
|
+
return ex unless ex.empty?
|
|
56
|
+
|
|
57
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
58
|
+
@lock.synchronize do
|
|
59
|
+
if @cached_af
|
|
60
|
+
tok, exp = @cached_af
|
|
61
|
+
return tok if now < exp - RENEW_BEFORE_SEC
|
|
62
|
+
end
|
|
63
|
+
@cached_af = nil
|
|
64
|
+
new_tok = fetch_af_token
|
|
65
|
+
@cached_af = [new_tok, Process.clock_gettime(Process::CLOCK_MONOTONIC) + AUTO_CACHE_SEC]
|
|
66
|
+
new_tok
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 构造 GraphQL URL
|
|
71
|
+
def graphql_url
|
|
72
|
+
af = resolve_af_token
|
|
73
|
+
"https://dropmail.me/api/graphql/#{URI.encode_www_form_component(af)}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 发送 GraphQL 请求
|
|
77
|
+
def graphql_request(query, variables = nil)
|
|
78
|
+
data = { "query" => query }
|
|
79
|
+
data["variables"] = JSON.generate(variables) if variables
|
|
80
|
+
|
|
81
|
+
resp = Http.post(
|
|
82
|
+
graphql_url,
|
|
83
|
+
headers: { "Content-Type" => "application/x-www-form-urlencoded" },
|
|
84
|
+
body: URI.encode_www_form(data)
|
|
85
|
+
)
|
|
86
|
+
resp.raise_for_status
|
|
87
|
+
result = resp.json
|
|
88
|
+
if result["errors"]&.any?
|
|
89
|
+
raise "GraphQL error: #{result['errors'].first['message'] || 'Unknown'}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
result["data"] || {}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# 创建临时邮箱
|
|
96
|
+
# @return [EmailInfo]
|
|
97
|
+
def generate_email
|
|
98
|
+
data = graphql_request(CREATE_SESSION_QUERY)
|
|
99
|
+
session = data["introduceSession"]
|
|
100
|
+
raise "Failed to generate email" unless session && session["addresses"]&.any?
|
|
101
|
+
|
|
102
|
+
EmailInfo.new(
|
|
103
|
+
channel: CHANNEL,
|
|
104
|
+
email: session["addresses"][0]["address"],
|
|
105
|
+
token: session["id"],
|
|
106
|
+
expires_at: session["expiresAt"]
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# 获取邮件列表
|
|
111
|
+
# @param token [String] session ID
|
|
112
|
+
# @param email [String]
|
|
113
|
+
# @return [Array<Email>]
|
|
114
|
+
def get_emails(token, email = "")
|
|
115
|
+
data = graphql_request(GET_MAILS_QUERY, { "id" => token })
|
|
116
|
+
mails = ((data["session"] || {})["mails"]) || []
|
|
117
|
+
mails.map do |m|
|
|
118
|
+
raw = {
|
|
119
|
+
"id" => m["id"] || "",
|
|
120
|
+
"from" => m["fromAddr"] || "",
|
|
121
|
+
"to" => m["toAddr"] || email,
|
|
122
|
+
"subject" => m["headerSubject"] || "",
|
|
123
|
+
"text" => m["text"] || "",
|
|
124
|
+
"html" => m["html"] || "",
|
|
125
|
+
"received_at" => m["receivedAt"] || "",
|
|
126
|
+
"attachments" => []
|
|
127
|
+
}
|
|
128
|
+
Normalize.normalize_email(raw, email)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# dropmail.click 渠道实现
|
|
6
|
+
# 独立后端临时邮箱,免注册、无鉴权。Token 即邮箱地址本身。
|
|
7
|
+
module DropmailClick
|
|
8
|
+
CHANNEL = "dropmail-click"
|
|
9
|
+
BASE_URL = "https://dropmail.click"
|
|
10
|
+
DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
11
|
+
"(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def get_ua
|
|
16
|
+
cfg = Config.get_config
|
|
17
|
+
(cfg.headers || {})["User-Agent"] || DEFAULT_UA
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def first_val(msg, *keys)
|
|
21
|
+
keys.each do |key|
|
|
22
|
+
val = msg[key]
|
|
23
|
+
return val.to_s if !val.nil? && val != ""
|
|
24
|
+
end
|
|
25
|
+
""
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [EmailInfo]
|
|
29
|
+
def generate_email
|
|
30
|
+
resp = Http.post(
|
|
31
|
+
"#{BASE_URL}/api/v1/public/mailbox",
|
|
32
|
+
headers: { "User-Agent" => get_ua, "Accept" => "application/json" }
|
|
33
|
+
)
|
|
34
|
+
resp.raise_for_status
|
|
35
|
+
data = resp.json
|
|
36
|
+
raise "dropmail-click: invalid response" unless data.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
address = data["address"].to_s
|
|
39
|
+
raise "dropmail-click: missing address" if address.empty?
|
|
40
|
+
|
|
41
|
+
EmailInfo.new(
|
|
42
|
+
channel: CHANNEL,
|
|
43
|
+
email: address,
|
|
44
|
+
token: address,
|
|
45
|
+
expires_at: data["expires_at"]
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @param email [String]
|
|
50
|
+
# @return [Array<Email>]
|
|
51
|
+
def get_emails(email)
|
|
52
|
+
addr = email.to_s.strip
|
|
53
|
+
raise "dropmail-click: missing email" if addr.empty?
|
|
54
|
+
|
|
55
|
+
resp = Http.get(
|
|
56
|
+
"#{BASE_URL}/api/v1/public/mailbox/#{URI.encode_www_form_component(addr)}",
|
|
57
|
+
headers: { "User-Agent" => get_ua, "Accept" => "application/json" }
|
|
58
|
+
)
|
|
59
|
+
resp.raise_for_status
|
|
60
|
+
data = resp.json
|
|
61
|
+
return [] unless data.is_a?(Hash)
|
|
62
|
+
|
|
63
|
+
messages = data["messages"]
|
|
64
|
+
return [] unless messages.is_a?(Array)
|
|
65
|
+
|
|
66
|
+
messages.select { |m| m.is_a?(Hash) }.map do |msg|
|
|
67
|
+
row = {
|
|
68
|
+
"id" => first_val(msg, "id"),
|
|
69
|
+
"from" => first_val(msg, "from"),
|
|
70
|
+
"to" => first_val(msg, "address").empty? ? addr : first_val(msg, "address"),
|
|
71
|
+
"subject" => first_val(msg, "subject"),
|
|
72
|
+
"text" => first_val(msg, "text"),
|
|
73
|
+
"html" => first_val(msg, "html"),
|
|
74
|
+
"date" => first_val(msg, "received_at", "date")
|
|
75
|
+
}
|
|
76
|
+
Normalize.normalize_email(row, addr)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|