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,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