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.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +116 -0
  3. data/lib/tempmail_sdk/channels.rb +289 -0
  4. data/lib/tempmail_sdk/channels_data.rb +289 -0
  5. data/lib/tempmail_sdk/client.rb +193 -0
  6. data/lib/tempmail_sdk/config.rb +104 -0
  7. data/lib/tempmail_sdk/html_utils.rb +42 -0
  8. data/lib/tempmail_sdk/http.rb +225 -0
  9. data/lib/tempmail_sdk/logger.rb +38 -0
  10. data/lib/tempmail_sdk/normalize.rb +193 -0
  11. data/lib/tempmail_sdk/providers/anonbox.rb +248 -0
  12. data/lib/tempmail_sdk/providers/anonymmail.rb +92 -0
  13. data/lib/tempmail_sdk/providers/byom.rb +48 -0
  14. data/lib/tempmail_sdk/providers/catchmail.rb +110 -0
  15. data/lib/tempmail_sdk/providers/cleantempmail.rb +49 -0
  16. data/lib/tempmail_sdk/providers/devmail_uk.rb +58 -0
  17. data/lib/tempmail_sdk/providers/dropmail.rb +133 -0
  18. data/lib/tempmail_sdk/providers/dropmail_click.rb +81 -0
  19. data/lib/tempmail_sdk/providers/dropmail_me.rb +163 -0
  20. data/lib/tempmail_sdk/providers/duckmail.rb +63 -0
  21. data/lib/tempmail_sdk/providers/eyepaste.rb +119 -0
  22. data/lib/tempmail_sdk/providers/fake_email_site.rb +48 -0
  23. data/lib/tempmail_sdk/providers/fake_legal.rb +109 -0
  24. data/lib/tempmail_sdk/providers/fmail.rb +87 -0
  25. data/lib/tempmail_sdk/providers/freecustom.rb +135 -0
  26. data/lib/tempmail_sdk/providers/getnada.rb +146 -0
  27. data/lib/tempmail_sdk/providers/gonebox_email.rb +89 -0
  28. data/lib/tempmail_sdk/providers/guerrillamail.rb +62 -0
  29. data/lib/tempmail_sdk/providers/guerrillamail_mirrors.rb +81 -0
  30. data/lib/tempmail_sdk/providers/harakirimail.rb +84 -0
  31. data/lib/tempmail_sdk/providers/haribu.rb +176 -0
  32. data/lib/tempmail_sdk/providers/inboxes.rb +110 -0
  33. data/lib/tempmail_sdk/providers/inboxkitten.rb +90 -0
  34. data/lib/tempmail_sdk/providers/linshiyou.rb +157 -0
  35. data/lib/tempmail_sdk/providers/lroid.rb +170 -0
  36. data/lib/tempmail_sdk/providers/m2u.rb +126 -0
  37. data/lib/tempmail_sdk/providers/mail10s.rb +59 -0
  38. data/lib/tempmail_sdk/providers/mail123.rb +82 -0
  39. data/lib/tempmail_sdk/providers/mail_cx.rb +133 -0
  40. data/lib/tempmail_sdk/providers/mail_sunls.rb +68 -0
  41. data/lib/tempmail_sdk/providers/mail_tm.rb +99 -0
  42. data/lib/tempmail_sdk/providers/mailcatch.rb +91 -0
  43. data/lib/tempmail_sdk/providers/maildrop.rb +92 -0
  44. data/lib/tempmail_sdk/providers/maildrop_cc.rb +110 -0
  45. data/lib/tempmail_sdk/providers/mailforspam.rb +96 -0
  46. data/lib/tempmail_sdk/providers/mailhole_de.rb +70 -0
  47. data/lib/tempmail_sdk/providers/mailinator.rb +182 -0
  48. data/lib/tempmail_sdk/providers/mailmomy.rb +108 -0
  49. data/lib/tempmail_sdk/providers/mailnesia.rb +141 -0
  50. data/lib/tempmail_sdk/providers/mffac.rb +101 -0
  51. data/lib/tempmail_sdk/providers/moakt.rb +296 -0
  52. data/lib/tempmail_sdk/providers/neighbours.rb +140 -0
  53. data/lib/tempmail_sdk/providers/neighbours_sh.rb +117 -0
  54. data/lib/tempmail_sdk/providers/nimail.rb +100 -0
  55. data/lib/tempmail_sdk/providers/ockito.rb +163 -0
  56. data/lib/tempmail_sdk/providers/one_sec_mail.rb +46 -0
  57. data/lib/tempmail_sdk/providers/restmail_net.rb +60 -0
  58. data/lib/tempmail_sdk/providers/rootsh.rb +167 -0
  59. data/lib/tempmail_sdk/providers/shitty_email.rb +84 -0
  60. data/lib/tempmail_sdk/providers/smailpro.rb +103 -0
  61. data/lib/tempmail_sdk/providers/ta_easy.rb +53 -0
  62. data/lib/tempmail_sdk/providers/temp_mail_io.rb +67 -0
  63. data/lib/tempmail_sdk/providers/temp_mail_now.rb +103 -0
  64. data/lib/tempmail_sdk/providers/temp_mail_org.rb +89 -0
  65. data/lib/tempmail_sdk/providers/tempemail_co.rb +142 -0
  66. data/lib/tempmail_sdk/providers/tempemail_info.rb +121 -0
  67. data/lib/tempmail_sdk/providers/tempemails_net.rb +159 -0
  68. data/lib/tempmail_sdk/providers/tempfastmail.rb +76 -0
  69. data/lib/tempmail_sdk/providers/tempgmailer.rb +126 -0
  70. data/lib/tempmail_sdk/providers/tempgo_email.rb +74 -0
  71. data/lib/tempmail_sdk/providers/tempinbox.rb +70 -0
  72. data/lib/tempmail_sdk/providers/tempmail.rb +66 -0
  73. data/lib/tempmail_sdk/providers/tempmail365.rb +119 -0
  74. data/lib/tempmail_sdk/providers/tempmail_fish.rb +89 -0
  75. data/lib/tempmail_sdk/providers/tempmail_fyi.rb +124 -0
  76. data/lib/tempmail_sdk/providers/tempmail_lol.rb +42 -0
  77. data/lib/tempmail_sdk/providers/tempmail_lol_v2.rb +57 -0
  78. data/lib/tempmail_sdk/providers/tempmail_plus.rb +80 -0
  79. data/lib/tempmail_sdk/providers/tempmailc.rb +80 -0
  80. data/lib/tempmail_sdk/providers/tempmailpro.rb +89 -0
  81. data/lib/tempmail_sdk/providers/tempmailten.rb +150 -0
  82. data/lib/tempmail_sdk/providers/temporam.rb +95 -0
  83. data/lib/tempmail_sdk/providers/tempp_mails.rb +150 -0
  84. data/lib/tempmail_sdk/providers/tempy_email.rb +73 -0
  85. data/lib/tempmail_sdk/providers/ten_minute_mail_net.rb +125 -0
  86. data/lib/tempmail_sdk/providers/tenminute_one.rb +255 -0
  87. data/lib/tempmail_sdk/providers/throwawaymail.rb +80 -0
  88. data/lib/tempmail_sdk/providers/tmail_link.rb +98 -0
  89. data/lib/tempmail_sdk/providers/uncorreotemporal.rb +84 -0
  90. data/lib/tempmail_sdk/providers/webmailtemp.rb +108 -0
  91. data/lib/tempmail_sdk/providers/xkx_me.rb +125 -0
  92. data/lib/tempmail_sdk/providers/zhujump.rb +256 -0
  93. data/lib/tempmail_sdk/registry.rb +1097 -0
  94. data/lib/tempmail_sdk/retry.rb +91 -0
  95. data/lib/tempmail_sdk/telemetry.rb +134 -0
  96. data/lib/tempmail_sdk/types.rb +164 -0
  97. data/lib/tempmail_sdk/version.rb +6 -0
  98. data/lib/tempmail_sdk/webui.rb +360 -0
  99. data/lib/tempmail_sdk.rb +76 -0
  100. 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