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,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # Lroid 渠道 — https://lroid.com
8
+ # HTML 解析模式,session cookies 维持邮箱身份
9
+ module Lroid
10
+ CHANNEL = "lroid"
11
+ BASE_URL = "https://lroid.com"
12
+
13
+ EMAIL_RE = /<input[^>]+id=["']eposta_adres["'][^>]+value=["']([^"']+@[^"']+)["']/i
14
+ EMAIL_RE_ALT = /<input[^>]+value=["']([^"']+@[^"']+)["'][^>]+id=["']eposta_adres["']/i
15
+
16
+ HEADERS = {
17
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
18
+ "Accept-Language" => "en-US,en;q=0.9",
19
+ "Referer" => "#{BASE_URL}/",
20
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
21
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
22
+ }.freeze
23
+
24
+ module_function
25
+
26
+ # 从响应中收集并序列化 cookies
27
+ def cookie_header_from_resp(resp)
28
+ map = {}
29
+ resp.set_cookies.each do |line|
30
+ pair = line.split(";", 2).first.to_s.strip
31
+ next unless pair.include?("=")
32
+
33
+ k, v = pair.split("=", 2)
34
+ map[k.strip] = v.to_s.strip unless k.strip.empty?
35
+ end
36
+ map.keys.sort.map { |k| "#{k}=#{map[k]}" }.join("; ")
37
+ end
38
+
39
+ # 从 HTML 中提取邮箱地址
40
+ def extract_email(html_str)
41
+ m = html_str.match(EMAIL_RE) || html_str.match(EMAIL_RE_ALT)
42
+ raise "lroid: 无法从 HTML 响应中解析邮箱地址" unless m
43
+
44
+ addr = m[1].strip
45
+ raise "lroid: 解析到的邮箱地址无效" unless addr.include?("@")
46
+
47
+ addr
48
+ end
49
+
50
+ # 解码 token
51
+ def decode_token(token)
52
+ data = JSON.parse(token)
53
+ cookie = (data["cookie"] || "").strip
54
+ raise "lroid: session token 中缺少 cookie" if cookie.empty?
55
+
56
+ cookie
57
+ rescue JSON::ParserError
58
+ raise "lroid: 无效的 session token"
59
+ end
60
+
61
+ # 创建临时邮箱
62
+ # @return [EmailInfo]
63
+ def generate_email
64
+ resp = Http.get(BASE_URL, headers: HEADERS, timeout: 15)
65
+ resp.raise_for_status
66
+ email = extract_email(resp.body)
67
+ cookie_hdr = cookie_header_from_resp(resp)
68
+ token = JSON.generate("cookie" => cookie_hdr)
69
+ EmailInfo.new(channel: CHANNEL, email: email, token: token)
70
+ end
71
+
72
+ # 获取邮件列表
73
+ # @param token [String]
74
+ # @param email [String]
75
+ # @return [Array<Email>]
76
+ def get_emails(token, email = "")
77
+ cookie = decode_token(token)
78
+ address = (email || "").strip
79
+ raise "lroid: 邮箱地址不能为空" if address.empty?
80
+
81
+ # 尝试 kontrol API
82
+ begin
83
+ resp = Http.get(
84
+ "#{BASE_URL}/en/api-kontrol/",
85
+ headers: {
86
+ "Accept" => "application/json, text/html, */*;q=0.8",
87
+ "Referer" => "#{BASE_URL}/",
88
+ "Cookie" => cookie,
89
+ "User-Agent" => HEADERS["User-Agent"]
90
+ },
91
+ timeout: 15
92
+ )
93
+ resp.raise_for_status
94
+ data = resp.json
95
+ if data.is_a?(Array)
96
+ return parse_json_emails(data, address)
97
+ elsif data.is_a?(Hash)
98
+ %w[mails emails messages data inbox].each do |key|
99
+ items = data[key]
100
+ return parse_json_emails(items, address) if items.is_a?(Array)
101
+ end
102
+ if data["id"] || data["subject"] || data["from"]
103
+ return parse_json_emails([data], address)
104
+ end
105
+ end
106
+ rescue StandardError
107
+ # API 不可用,回退到 HTML 解析
108
+ end
109
+
110
+ parse_html_emails(cookie, address)
111
+ end
112
+
113
+ def parse_json_emails(items, recipient)
114
+ items.filter_map do |raw|
115
+ next unless raw.is_a?(Hash)
116
+
117
+ normalized = raw.dup
118
+ normalized["html"] = raw["body"] || raw["content"] if !raw.key?("html")
119
+ Normalize.normalize_email(normalized, recipient)
120
+ end
121
+ end
122
+
123
+ def parse_html_emails(cookie, recipient)
124
+ resp = Http.get(
125
+ BASE_URL,
126
+ headers: {
127
+ "Accept" => "text/html,application/xhtml+xml,*/*;q=0.8",
128
+ "Referer" => "#{BASE_URL}/",
129
+ "Cookie" => cookie,
130
+ "User-Agent" => HEADERS["User-Agent"]
131
+ },
132
+ timeout: 15
133
+ )
134
+ resp.raise_for_status
135
+ html_str = resp.body
136
+
137
+ # 解析 <li class="mail"> 元素
138
+ mail_items = html_str.scan(/<li[^>]*class=["'][^"']*\bmail\b[^"']*["'][^>]*>(.*?)<\/li>/mi)
139
+ return [] if mail_items.empty?
140
+
141
+ mail_items.each_with_index.filter_map do |(item_html), idx|
142
+ subject = ""
143
+ from_addr = ""
144
+ date = ""
145
+ mail_id = (idx + 1).to_s
146
+
147
+ # 提取主题
148
+ sm = item_html.match(/class=["'][^"']*\bsubject\b[^"']*["'][^>]*>(.*?)<\//mi)
149
+ subject = sm[1].gsub(/<[^>]+>/, "").strip if sm
150
+
151
+ # 提取发件人
152
+ fm = item_html.match(/class=["'][^"']*\b(?:from|sender)\b[^"']*["'][^>]*>(.*?)<\//mi)
153
+ from_addr = fm[1].gsub(/<[^>]+>/, "").strip if fm
154
+
155
+ # 提取日期
156
+ dm = item_html.match(/class=["'][^"']*\b(?:date|time)\b[^"']*["'][^>]*>(.*?)<\//mi)
157
+ date = dm[1].gsub(/<[^>]+>/, "").strip if dm
158
+
159
+ Normalize.normalize_email({
160
+ "id" => mail_id,
161
+ "from" => from_addr,
162
+ "to" => recipient,
163
+ "subject" => subject,
164
+ "date" => date
165
+ }, recipient)
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # m2u.io 渠道实现
8
+ module M2u
9
+ CHANNEL = "m2u"
10
+ API_BASE = "https://api.m2u.io"
11
+ HEADERS = {
12
+ "Accept" => "application/json",
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"
15
+ }.freeze
16
+
17
+ module_function
18
+
19
+ def pack_token(token, view_token)
20
+ JSON.generate({ "token" => token, "viewToken" => view_token })
21
+ end
22
+
23
+ def unpack_token(token)
24
+ value = token.to_s.strip
25
+ return ["", ""] if value.empty?
26
+
27
+ if value.start_with?("{")
28
+ begin
29
+ data = JSON.parse(value)
30
+ if data.is_a?(Hash)
31
+ return [data["token"].to_s.strip, data["viewToken"].to_s.strip]
32
+ end
33
+ rescue StandardError
34
+ return ["", ""]
35
+ end
36
+ end
37
+ [value, ""]
38
+ end
39
+
40
+ def flatten_message(raw, recipient)
41
+ {
42
+ "id" => (raw["id"] || raw["message_id"]).to_s,
43
+ "from" => (raw["from_addr"] || raw["from_address"] || raw["from"]).to_s,
44
+ "to" => recipient,
45
+ "subject" => (raw["subject"] || "").to_s,
46
+ "text" => (raw["text_body"] || raw["body_text"] || raw["text"]).to_s,
47
+ "html" => (raw["html_body"] || raw["body_html"] || raw["html"]).to_s,
48
+ "date" => (raw["received_at"] || raw["created_at"]).to_s,
49
+ "is_read" => raw["is_read"] || raw["isRead"] || raw["seen"] || false,
50
+ "attachments" => raw["attachments"].is_a?(Array) ? raw["attachments"] : []
51
+ }
52
+ end
53
+
54
+ # @return [EmailInfo]
55
+ def generate_email
56
+ resp = Http.post(
57
+ "#{API_BASE}/v1/mailboxes/auto",
58
+ headers: HEADERS.merge("Content-Type" => "application/json"),
59
+ json: {},
60
+ timeout: 15
61
+ )
62
+ resp.raise_for_status
63
+ data = resp.json
64
+ mailbox = data.is_a?(Hash) ? data["mailbox"] : nil
65
+ raise "m2u: invalid mailbox response" unless mailbox.is_a?(Hash)
66
+
67
+ local_part = mailbox["local_part"].to_s.strip
68
+ domain = mailbox["domain"].to_s.strip
69
+ token = mailbox["token"].to_s.strip
70
+ view_token = mailbox["view_token"].to_s.strip
71
+ email = !local_part.empty? && !domain.empty? ? "#{local_part}@#{domain}" : ""
72
+ raise "m2u: invalid mailbox response" if email.empty? || token.empty? || view_token.empty?
73
+
74
+ EmailInfo.new(
75
+ channel: CHANNEL,
76
+ email: email,
77
+ token: pack_token(token, view_token),
78
+ expires_at: mailbox["expires_at"],
79
+ created_at: mailbox["created_at"]
80
+ )
81
+ end
82
+
83
+ def fetch_detail(token, view_token, message_id)
84
+ resp = Http.get(
85
+ "#{API_BASE}/v1/mailboxes/#{URI.encode_www_form_component(token)}/messages/#{URI.encode_www_form_component(message_id)}?view=#{URI.encode_www_form_component(view_token)}",
86
+ headers: HEADERS, timeout: 15
87
+ )
88
+ return nil unless resp.ok?
89
+
90
+ data = resp.json
91
+ return nil unless data.is_a?(Hash)
92
+
93
+ detail = data["message"]
94
+ detail.is_a?(Hash) ? detail : nil
95
+ rescue StandardError
96
+ nil
97
+ end
98
+
99
+ # @param token [String] JSON 打包的 token
100
+ # @param email [String]
101
+ # @return [Array<Email>]
102
+ def get_emails(token, email)
103
+ mailbox_token, view_token = unpack_token(token)
104
+ address = email.to_s.strip
105
+ raise "m2u: missing token" if mailbox_token.empty?
106
+ raise "m2u: missing view token" if view_token.empty?
107
+ raise "m2u: empty email" if address.empty?
108
+
109
+ resp = Http.get(
110
+ "#{API_BASE}/v1/mailboxes/#{URI.encode_www_form_component(mailbox_token)}/messages?view=#{URI.encode_www_form_component(view_token)}",
111
+ headers: HEADERS, timeout: 15
112
+ )
113
+ resp.raise_for_status
114
+ data = resp.json
115
+ rows = data.is_a?(Hash) ? (data["messages"] || []) : []
116
+ return [] unless rows.is_a?(Array)
117
+
118
+ rows.select { |r| r.is_a?(Hash) }.map do |item|
119
+ message_id = (item["id"] || item["message_id"]).to_s.strip
120
+ detail = message_id.empty? ? nil : fetch_detail(mailbox_token, view_token, message_id)
121
+ Normalize.normalize_email(flatten_message(detail || item, address), address)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # mail10s.com 渠道实现
6
+ module Mail10s
7
+ CHANNEL = "mail10s"
8
+ BASE_URL = "https://mail10s.com"
9
+ DOMAIN = "mail10s.com"
10
+ HEADERS = { "Accept" => "application/json", "User-Agent" => "Mozilla/5.0" }.freeze
11
+
12
+ module_function
13
+
14
+ def random_local
15
+ chars = ("a".."z").to_a + ("0".."9").to_a
16
+ "sdk" + Array.new(16) { chars.sample }.join
17
+ end
18
+
19
+ def flatten(raw, recipient)
20
+ {
21
+ "id" => raw["id"].to_s,
22
+ "from" => (raw["sender"] || "").to_s,
23
+ "to" => recipient,
24
+ "subject" => (raw["subject"] || "").to_s,
25
+ "text" => (raw["body_text"] || "").to_s,
26
+ "html" => (raw["body_html"] || "").to_s,
27
+ "date" => (raw["received_at"] || "").to_s,
28
+ "attachments" => raw["attachments"].is_a?(Array) ? raw["attachments"] : []
29
+ }
30
+ end
31
+
32
+ # @return [EmailInfo]
33
+ def generate_email
34
+ email = "#{random_local}@#{DOMAIN}"
35
+ EmailInfo.new(channel: CHANNEL, email: email, token: email)
36
+ end
37
+
38
+ # @param email [String]
39
+ # @return [Array<Email>]
40
+ def get_emails(email)
41
+ address = email.to_s.strip
42
+ raise "mail10s: empty email" if address.empty?
43
+
44
+ resp = Http.get(
45
+ "#{BASE_URL}/api/emails/#{URI.encode_www_form_component(address)}/inbox",
46
+ headers: HEADERS, timeout: 15
47
+ )
48
+ resp.raise_for_status
49
+ data = resp.json
50
+ messages = if data.is_a?(Hash) && data["data"].is_a?(Hash)
51
+ data["data"]["messages"]
52
+ end
53
+ return [] unless messages.is_a?(Array)
54
+
55
+ messages.select { |r| r.is_a?(Hash) }.map { |row| Normalize.normalize_email(flatten(row, address), address) }
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # mail123.fr 渠道实现
6
+ module Mail123
7
+ CHANNEL = "mail123"
8
+ API_BASE = "https://mail123.fr/api/v1"
9
+ HEADERS = { "Accept" => "application/json", "User-Agent" => "Mozilla/5.0" }.freeze
10
+
11
+ module_function
12
+
13
+ def flatten(raw, recipient)
14
+ out = raw.dup
15
+ out["id"] = raw["id"].to_s
16
+ out["to"] = (raw["to"] || recipient).to_s
17
+ out["text"] = (raw["text"] || raw["preview"] || "").to_s
18
+ out["html"] = (raw["html"] || "").to_s
19
+ if raw.key?("is_unread") && (raw["is_unread"] == true || raw["is_unread"] == false)
20
+ out["isRead"] = !raw["is_unread"]
21
+ end
22
+ out
23
+ end
24
+
25
+ # @return [EmailInfo]
26
+ def generate_email
27
+ resp = Http.get("#{API_BASE}/mailbox/new", headers: HEADERS, timeout: 15)
28
+ resp.raise_for_status
29
+ data = resp.json
30
+ raise "mail123: invalid mailbox response" unless data.is_a?(Hash)
31
+
32
+ email = data["address"].to_s.strip
33
+ raise "mail123: invalid mailbox response" if email.empty? || !email.include?("@")
34
+
35
+ expires_at = nil
36
+ if data["expires_in_days"].is_a?(Numeric) && data["expires_in_days"] > 0
37
+ expires_at = ((Time.now.to_f + data["expires_in_days"].to_f * 86_400) * 1000).to_i
38
+ end
39
+
40
+ EmailInfo.new(channel: CHANNEL, email: email, token: email, expires_at: expires_at)
41
+ end
42
+
43
+ def fetch_detail(address, message_id)
44
+ resp = Http.get(
45
+ "#{API_BASE}/mailbox/#{URI.encode_www_form_component(address)}/messages/#{URI.encode_www_form_component(message_id)}",
46
+ headers: HEADERS, timeout: 15
47
+ )
48
+ return nil unless resp.ok?
49
+
50
+ data = resp.json
51
+ if data.is_a?(Hash) && data["message"].is_a?(Hash)
52
+ return data["message"]
53
+ end
54
+ nil
55
+ rescue StandardError
56
+ nil
57
+ end
58
+
59
+ # @param email [String]
60
+ # @return [Array<Email>]
61
+ def get_emails(email)
62
+ address = email.to_s.strip
63
+ raise "mail123: empty email" if address.empty?
64
+
65
+ resp = Http.get(
66
+ "#{API_BASE}/mailbox/#{URI.encode_www_form_component(address)}/messages?limit=50",
67
+ headers: HEADERS, timeout: 15
68
+ )
69
+ resp.raise_for_status
70
+ data = resp.json
71
+ rows = data.is_a?(Hash) ? (data["messages"] || []) : []
72
+ return [] unless rows.is_a?(Array)
73
+
74
+ rows.select { |r| r.is_a?(Hash) }.map do |item|
75
+ message_id = (item["id"] || "").to_s.strip
76
+ detail = message_id.empty? ? nil : fetch_detail(address, message_id)
77
+ Normalize.normalize_email(flatten(detail || item, address), address)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+
6
+ module TempmailSdk
7
+ module Providers
8
+ # mail.cx 渠道实现
9
+ # 匿名 Web API: https://mail.cx/v1
10
+ module MailCx
11
+ CHANNEL = "mail-cx"
12
+ BASE_URL = "https://mail.cx"
13
+ UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
14
+
15
+ module_function
16
+
17
+ def random_string(length)
18
+ chars = ("a".."z").to_a + ("0".."9").to_a
19
+ Array.new(length) { chars.sample }.join
20
+ end
21
+
22
+ def client_id
23
+ "tempmail-sdk-#{random_string(16)}"
24
+ end
25
+
26
+ def headers(cid)
27
+ {
28
+ "Accept" => "application/json", "Origin" => BASE_URL, "Referer" => "#{BASE_URL}/",
29
+ "User-Agent" => UA, "X-Client-ID" => cid
30
+ }
31
+ end
32
+
33
+ def fetch_config(cid)
34
+ resp = Http.get("#{BASE_URL}/v1/config", headers: headers(cid), timeout: 15)
35
+ resp.raise_for_status
36
+ data = resp.json
37
+ data.is_a?(Hash) ? data : {}
38
+ end
39
+
40
+ def pick_domain(config, preferred)
41
+ items = config["system_domains"].is_a?(Array) ? config["system_domains"] : []
42
+ domains = items.select { |i| i.is_a?(Hash) && !i["domain"].to_s.strip.empty? }
43
+ .map { |i| i["domain"].to_s.strip }
44
+ raise "mail-cx: no system domains" if domains.empty?
45
+
46
+ pref = preferred.to_s.strip.sub(/\A@/, "").downcase
47
+ unless pref.empty?
48
+ found = domains.find { |d| d.downcase == pref }
49
+ return found if found
50
+ end
51
+ default = items.find { |i| i.is_a?(Hash) && i["default"] && !i["domain"].to_s.strip.empty? }
52
+ default ? default["domain"].to_s.strip : domains.first
53
+ end
54
+
55
+ def first_non_empty(*values)
56
+ values.each do |v|
57
+ next if v.nil?
58
+
59
+ t = v.to_s.strip
60
+ return t unless t.empty?
61
+ end
62
+ ""
63
+ end
64
+
65
+ # @param preferred_domain [String, nil]
66
+ # @return [EmailInfo]
67
+ def generate_email(preferred_domain = nil)
68
+ cid = client_id
69
+ config = fetch_config(cid)
70
+ domain = pick_domain(config, preferred_domain)
71
+ created = Time.now.utc
72
+ expires_at = nil
73
+ ttl = config["ttl_seconds"]
74
+ expires_at = ((created + ttl).to_f * 1000).to_i if ttl.is_a?(Numeric) && ttl.positive?
75
+ EmailInfo.new(channel: CHANNEL, email: "#{random_string(12)}@#{domain}",
76
+ token: cid, expires_at: expires_at, created_at: created.iso8601)
77
+ end
78
+
79
+ def fetch_detail(cid, message_id)
80
+ resp = Http.get("#{BASE_URL}/v1/email/#{URI.encode_www_form_component(message_id)}",
81
+ headers: headers(cid), timeout: 15)
82
+ return nil unless resp.ok?
83
+
84
+ data = resp.json
85
+ data.is_a?(Hash) ? data : nil
86
+ end
87
+
88
+ # @param email [String]
89
+ # @param client_id [String]
90
+ # @return [Array<Email>]
91
+ def get_emails(email, client_id)
92
+ resp = Http.get("#{BASE_URL}/v1/inbox/#{URI.encode_www_form_component(email)}",
93
+ headers: headers(client_id), timeout: 30)
94
+ return [] if resp.status_code == 204
95
+
96
+ resp.raise_for_status
97
+ data = resp.json
98
+ rows = data.is_a?(Hash) ? data["emails"] : nil
99
+ return [] unless rows.is_a?(Array)
100
+
101
+ rows.map do |row|
102
+ unless row.is_a?(Hash)
103
+ next Normalize.normalize_email({}, email)
104
+ end
105
+
106
+ raw = {
107
+ "id" => row["id"], "from" => first_non_empty(row["from_email"], row["from_name"]),
108
+ "to" => email, "subject" => row["subject"] || "", "text" => row["preview_text"] || "",
109
+ "created_at" => row["created_at"], "attachments" => row["attachments"]
110
+ }
111
+ mid = row["id"].to_s.strip
112
+ unless mid.empty?
113
+ begin
114
+ detail = fetch_detail(client_id, mid)
115
+ if detail
116
+ raw = {
117
+ "id" => detail["id"], "from" => first_non_empty(detail["from_email"], detail["from_name"]),
118
+ "to" => email, "subject" => detail["subject"] || "",
119
+ "text" => first_non_empty(detail["text_body"], detail["preview_text"]),
120
+ "html" => detail["html_body"] || "", "created_at" => detail["created_at"],
121
+ "attachments" => detail["attachments"]
122
+ }
123
+ end
124
+ rescue StandardError
125
+ # 忽略详情失败
126
+ end
127
+ end
128
+ Normalize.normalize_email(raw, email)
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # Mail Sunls 渠道实现
6
+ # API: https://mail.sunls.de
7
+ # 无需 token,无需 session
8
+ module MailSunls
9
+ CHANNEL = "mail-sunls"
10
+ BASE = "https://mail.sunls.de"
11
+
12
+ HEADERS = {
13
+ "Accept" => "application/json, text/plain, */*",
14
+ "Cache-Control" => "no-cache",
15
+ "DNT" => "1",
16
+ "Referer" => "https://mail.sunls.de/",
17
+ "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"
18
+ }.freeze
19
+
20
+ module_function
21
+
22
+ # 生成随机本地部分
23
+ def random_local(length = 10)
24
+ chars = ("a".."z").to_a + ("0".."9").to_a
25
+ Array.new(length) { chars.sample }.join
26
+ end
27
+
28
+ # 从 API 获取可用域名列表
29
+ def fetch_domains
30
+ resp = Http.get("#{BASE}/api/domain", headers: HEADERS, timeout: 15)
31
+ resp.raise_for_status
32
+ data = resp.json
33
+ raise "mail-sunls: 域名列表响应格式无效" unless data.is_a?(Array)
34
+
35
+ out = data.select { |d| d.is_a?(String) && !d.strip.empty? }.map(&:strip)
36
+ raise "mail-sunls: 无可用域名" if out.empty?
37
+
38
+ out
39
+ end
40
+
41
+ # 创建临时邮箱:获取域名列表后随机生成地址
42
+ # @return [EmailInfo]
43
+ def generate_email
44
+ domains = fetch_domains
45
+ domain = domains.sample
46
+ local = random_local(10)
47
+ EmailInfo.new(channel: CHANNEL, email: "#{local}@#{domain}")
48
+ end
49
+
50
+ # 获取邮件列表
51
+ # @param email [String]
52
+ # @return [Array<Email>]
53
+ def get_emails(email)
54
+ addr = email.to_s.strip
55
+ raise "mail-sunls: 邮箱地址不能为空" if addr.empty?
56
+
57
+ resp = Http.get("#{BASE}/api/fetch?to=#{URI.encode_www_form_component(addr)}", headers: HEADERS, timeout: 15)
58
+ resp.raise_for_status
59
+ data = resp.json
60
+ return [] unless data.is_a?(Array)
61
+
62
+ data.select { |raw| raw.is_a?(Hash) }.map do |raw|
63
+ Normalize.normalize_email(raw, addr)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end