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,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "cgi"
5
+ require "json"
6
+ require "uri"
7
+
8
+ module TempmailSdk
9
+ module Providers
10
+ # moakt.com 渠道实现(HTML 抓取型)
11
+ #
12
+ # 流程:
13
+ # 1. GET 语言首页,收集初始 Cookie 与可用域名 <option> 列表;
14
+ # 2. POST /{locale}/inbox 创建邮箱(随机域名或指定域名),从 302 响应捕获 tm_session Cookie;
15
+ # 3. GET /{locale}/inbox 解析 #email-address 得到邮箱地址;
16
+ # 4. 会话凭证(locale + 序列化 Cookie 头)经 base64 打包进 token;
17
+ # 5. 收信时列表解析 /{locale}/email/{uuid},正文 GET .../html 解析 .email-body。
18
+ #
19
+ # 多个渠道别名(moakt.cc/moakt.co/moakt.ws/tmail.ws 等)共用此实现,仅固定域名不同。
20
+ module Moakt
21
+ CHANNEL = "moakt"
22
+ ORIGIN = "https://www.moakt.com"
23
+ TOK_PREFIX = "mok1:"
24
+
25
+ # 默认 User-Agent(config.headers 未指定时使用)
26
+ DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
27
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
28
+
29
+ # 页面请求的固定基础头
30
+ DEFAULT_HEADERS = {
31
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
32
+ "Cache-Control" => "no-cache",
33
+ "DNT" => "1",
34
+ "Pragma" => "no-cache",
35
+ "Upgrade-Insecure-Requests" => "1"
36
+ }.freeze
37
+
38
+ EMAIL_DIV_RE = /<div\s+id="email-address"\s*>([^<]+)<\/div>/im
39
+ DOMAIN_OPTION_RE = /<option\s+value="([^"]+)">\s*@[^<]+<\/option>/im
40
+ MAIL_DOMAIN_RE = /\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+\z/i
41
+ HREF_EMAIL_RE = /href="(\/[^"]+\/email\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"/
42
+ TITLE_RE = /<li\s+class="title"\s*>([^<]*)<\/li>/im
43
+ DATE_RE = /<li\s+class="date"[^>]*>[\s\S]*?<span[^>]*>([^<]+)<\/span>/im
44
+ SENDER_RE = /<li\s+class="sender"[^>]*>[\s\S]*?<span[^>]*>([\s\S]*?)<\/span>\s*<\/li>/im
45
+ BODY_RE = /<div\s+class="email-body"\s*>([\s\S]*?)<\/div>/im
46
+ FROM_ADDR_RE = /<([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>/
47
+ TAG_RE = /<[^>]+>/
48
+
49
+ module_function
50
+
51
+ # 解析域名参数,返回 [locale, mail_domain]
52
+ # - 空或含非法字符:语言页取默认 "zh",随机域名
53
+ # - 形如域名:locale="zh",使用指定域名
54
+ # - 其它字符串:视为 locale,随机域名
55
+ def request_parts(domain)
56
+ s = domain.to_s.strip
57
+ return ["zh", ""] if s.empty? || s.match?(/[\/?#\\]/)
58
+ return ["zh", s.sub(/\A@/, "").downcase] if s.match?(MAIL_DOMAIN_RE)
59
+
60
+ [s, ""]
61
+ end
62
+
63
+ # 从首页 HTML 收集服务端提供的可用域名集合
64
+ def parse_server_domains(page)
65
+ page.scan(DOMAIN_OPTION_RE)
66
+ .map { |m| m[0].strip.sub(/\A@/, "").downcase }
67
+ .reject(&:empty?)
68
+ end
69
+
70
+ # 生成随机本地部分(小写字母 + 数字)
71
+ def random_local(length = 12)
72
+ chars = ("a".."z").to_a + ("0".."9").to_a
73
+ Array.new(length) { chars.sample }.join
74
+ end
75
+
76
+ # 提取邮箱地址的域名部分(小写)
77
+ def email_domain(email)
78
+ _head, sep, domain = email.to_s.rpartition("@")
79
+ sep.empty? ? "" : domain.strip.downcase
80
+ end
81
+
82
+ # 将 Cookie 头字符串解析为 name => value 映射
83
+ def parse_cookie_map(hdr)
84
+ map = {}
85
+ hdr.to_s.split(";").each do |part|
86
+ part = part.strip
87
+ next unless part.include?("=")
88
+
89
+ k, v = part.split("=", 2)
90
+ k = k.strip
91
+ map[k] = v.to_s.strip unless k.empty?
92
+ end
93
+ map
94
+ end
95
+
96
+ # 从响应的 Set-Cookie 头提取本次新设置的 Cookie
97
+ def cookies_from_response(resp)
98
+ map = {}
99
+ resp.set_cookies.each do |line|
100
+ pair = line.split(";", 2).first.to_s.strip
101
+ next unless pair.include?("=")
102
+
103
+ k, v = pair.split("=", 2)
104
+ k = k.strip
105
+ map[k] = v.to_s.strip unless k.empty?
106
+ end
107
+ map
108
+ end
109
+
110
+ # 合并旧 Cookie 头与响应新设置的 Cookie,按 key 排序拼接(保证稳定)
111
+ def merge_cookie_hdr(prev, resp)
112
+ d = parse_cookie_map(prev)
113
+ d.merge!(cookies_from_response(resp))
114
+ d.keys.sort.map { |k| "#{k}=#{d[k]}" }.join("; ")
115
+ end
116
+
117
+ # 构造页面请求头
118
+ def page_headers(referer, ua)
119
+ DEFAULT_HEADERS.merge(
120
+ "User-Agent" => ua,
121
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9," \
122
+ "image/avif,image/webp,image/apng,*/*;q=0.8",
123
+ "Referer" => referer
124
+ )
125
+ end
126
+
127
+ # 从 config 取 UA,缺省用内置默认值
128
+ def user_agent
129
+ (Config.get_config.headers || {})["User-Agent"] || DEFAULT_UA
130
+ end
131
+
132
+ # 将会话(locale + Cookie 头)编码为不透明 token
133
+ def encode_sess(locale, cookie_hdr)
134
+ raw = JSON.generate("l" => locale, "c" => cookie_hdr)
135
+ TOK_PREFIX + Base64.strict_encode64(raw)
136
+ end
137
+
138
+ # 解码 token,返回 [locale, cookie_hdr]
139
+ def decode_sess(tok)
140
+ raise "moakt: invalid session token" unless tok.to_s.start_with?(TOK_PREFIX)
141
+
142
+ data = Base64.strict_decode64(tok[TOK_PREFIX.length..])
143
+ o = JSON.parse(data)
144
+ loc = o["l"].to_s.strip
145
+ c = o["c"].to_s.strip
146
+ raise "moakt: invalid session token" if loc.empty? || c.empty?
147
+
148
+ [loc, c]
149
+ rescue ArgumentError, JSON::ParserError
150
+ raise "moakt: invalid session token"
151
+ end
152
+
153
+ # 从收件箱页解析邮箱地址
154
+ def parse_inbox_email(html_s)
155
+ m = html_s.match(EMAIL_DIV_RE)
156
+ raise "moakt: email-address not found" unless m
157
+
158
+ addr = CGI.unescapeHTML(m[1].strip)
159
+ raise "moakt: empty email-address" if addr.empty?
160
+
161
+ addr
162
+ end
163
+
164
+ # 去除 HTML 标签,压缩为文本
165
+ def strip_tags(s)
166
+ s.gsub(TAG_RE, " ").strip
167
+ end
168
+
169
+ # 从收件箱页提取邮件 UUID 列表(去重、排除删除链接)
170
+ def list_mail_ids(html_s)
171
+ seen = {}
172
+ out = []
173
+ html_s.scan(HREF_EMAIL_RE).each do |cap|
174
+ path = cap[0]
175
+ next if path.include?("/delete")
176
+
177
+ mid = path.split("/").last
178
+ if mid.length == 36 && !seen[mid]
179
+ seen[mid] = true
180
+ out << mid
181
+ end
182
+ end
183
+ out
184
+ end
185
+
186
+ # 解析邮件详情页,返回标准化前的原始 Hash
187
+ def parse_detail(page, mid, recipient)
188
+ from_s = ""
189
+ if (sm = page.match(SENDER_RE))
190
+ inner = CGI.unescapeHTML(sm[1])
191
+ from_s = strip_tags(inner)
192
+ if (em = inner.match(FROM_ADDR_RE))
193
+ from_s = em[1].strip
194
+ end
195
+ end
196
+
197
+ subj = ""
198
+ subj = CGI.unescapeHTML(page.match(TITLE_RE)[1].strip) if page.match?(TITLE_RE)
199
+
200
+ date_s = ""
201
+ date_s = CGI.unescapeHTML(page.match(DATE_RE)[1].strip) if page.match?(DATE_RE)
202
+
203
+ body = ""
204
+ body = page.match(BODY_RE)[1].strip if page.match?(BODY_RE)
205
+
206
+ {
207
+ "id" => mid,
208
+ "to" => recipient,
209
+ "from" => from_s,
210
+ "subject" => subj,
211
+ "date" => date_s,
212
+ "html" => body
213
+ }
214
+ end
215
+
216
+ # 创建临时邮箱
217
+ # @param domain [String, nil] 指定接入域名或语言标识
218
+ # @return [EmailInfo]
219
+ def generate_email(domain = nil)
220
+ loc, mail_domain = request_parts(domain)
221
+ base = "#{ORIGIN}/#{loc}"
222
+ inbox = "#{base}/inbox"
223
+ ua = user_agent
224
+
225
+ r1 = Http.get(base, headers: page_headers(base, ua))
226
+ r1.raise_for_status
227
+ cookie_hdr = merge_cookie_hdr("", r1)
228
+
229
+ if mail_domain.empty?
230
+ post_data = { "random" => "1" }
231
+ else
232
+ unless parse_server_domains(r1.body).include?(mail_domain)
233
+ raise "moakt: unsupported domain #{mail_domain}"
234
+ end
235
+
236
+ post_data = {
237
+ "setemail" => "",
238
+ "username" => random_local,
239
+ "domain" => mail_domain,
240
+ "preferred_domain" => ""
241
+ }
242
+ end
243
+
244
+ # POST /inbox 创建邮箱,从 302 响应捕获 tm_session Cookie(Net::HTTP 默认不跟随重定向)
245
+ r2 = Http.post(
246
+ inbox,
247
+ headers: page_headers(base, ua).merge(
248
+ "Content-Type" => "application/x-www-form-urlencoded",
249
+ "Cookie" => cookie_hdr
250
+ ),
251
+ body: URI.encode_www_form(post_data)
252
+ )
253
+ cookie_hdr = merge_cookie_hdr(cookie_hdr, r2)
254
+
255
+ unless parse_cookie_map(cookie_hdr).key?("tm_session")
256
+ raise "moakt: missing tm_session cookie"
257
+ end
258
+
259
+ # GET /inbox 获取邮箱地址
260
+ r3 = Http.get(inbox, headers: page_headers(base, ua).merge("Cookie" => cookie_hdr))
261
+ r3.raise_for_status
262
+ cookie_hdr = merge_cookie_hdr(cookie_hdr, r3)
263
+
264
+ email = parse_inbox_email(r3.body)
265
+ if !mail_domain.empty? && email_domain(email) != mail_domain
266
+ raise "moakt: domain mismatch expected=#{mail_domain} actual=#{email_domain(email)}"
267
+ end
268
+
269
+ EmailInfo.new(channel: CHANNEL, email: email, token: encode_sess(loc, cookie_hdr))
270
+ end
271
+
272
+ # 收取邮件列表
273
+ # @param email [String]
274
+ # @param token [String]
275
+ # @return [Array<Email>]
276
+ def get_emails(email, token)
277
+ loc, cookie_hdr = decode_sess(token)
278
+ inbox = "#{ORIGIN}/#{loc}/inbox"
279
+ base_ref = "#{ORIGIN}/#{loc}"
280
+ ua = user_agent
281
+
282
+ r = Http.get(inbox, headers: page_headers(base_ref, ua).merge("Cookie" => cookie_hdr))
283
+ r.raise_for_status
284
+
285
+ list_mail_ids(r.body).filter_map do |mid|
286
+ detail = "#{ORIGIN}/#{loc}/email/#{mid}/html"
287
+ refer = "#{ORIGIN}/#{loc}/email/#{mid}"
288
+ rd = Http.get(detail, headers: page_headers(refer, ua).merge("Cookie" => cookie_hdr))
289
+ next unless rd.status_code == 200
290
+
291
+ Normalize.normalize_email(parse_detail(rd.body, mid, email), email)
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # Neighbours 渠道实现
6
+ # API: https://neighbours.sh/api/v1
7
+ # 支持多域名选择
8
+ module Neighbours
9
+ CHANNEL = "neighbours"
10
+ API_BASE = "https://neighbours.sh/api/v1"
11
+ HEADERS = {
12
+ "Accept" => "application/json",
13
+ "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"
14
+ }.freeze
15
+
16
+ module_function
17
+
18
+ # 生成随机用户名
19
+ def random_local
20
+ chars = ("a".."z").to_a + ("0".."9").to_a
21
+ "sdk" + Array.new(16) { chars.sample }.join
22
+ end
23
+
24
+ # 从 from/to 结构中提取首个可用地址
25
+ def first_address(value)
26
+ return "" if value.nil?
27
+ return value.strip if value.is_a?(String)
28
+
29
+ if value.is_a?(Array)
30
+ value.each do |item|
31
+ hit = first_address(item)
32
+ return hit unless hit.empty?
33
+ end
34
+ return ""
35
+ end
36
+
37
+ if value.is_a?(Hash)
38
+ addr = value["address"].to_s.strip
39
+ return addr unless addr.empty?
40
+
41
+ text = value["text"].to_s.strip
42
+ return text if text.include?("@")
43
+
44
+ return first_address(value["value"])
45
+ end
46
+
47
+ value.to_s.strip
48
+ end
49
+
50
+ # 请求 JSON,allow_404 时 404 返回 nil
51
+ def request_json(path, allow_404: false)
52
+ resp = Http.get("#{API_BASE}#{path}", headers: HEADERS, timeout: 15)
53
+ return nil if allow_404 && resp.status_code == 404
54
+
55
+ resp.raise_for_status
56
+ data = resp.json
57
+ data.is_a?(Hash) ? data : nil
58
+ end
59
+
60
+ # 将邮件映射为标准化中间格式
61
+ def flatten_message(raw, recipient)
62
+ {
63
+ "id" => (raw["uid"] || "").to_s,
64
+ "from" => first_address(raw["from"]),
65
+ "to" => first_address(raw["to"]).then { |v| v.empty? ? recipient : v },
66
+ "subject" => (raw["subject"] || "").to_s,
67
+ "text" => (raw["text"] || raw["snippet"] || "").to_s,
68
+ "html" => (raw["html"] || "").to_s,
69
+ "date" => (raw["date"] || raw["received_at"] || "").to_s,
70
+ "is_read" => false,
71
+ "attachments" => raw["attachments"] || []
72
+ }
73
+ end
74
+
75
+ # 获取可用域名列表
76
+ def list_domains
77
+ data = request_json("/config/domains")
78
+ domains = []
79
+ if data.is_a?(Hash)
80
+ nested = data["data"]
81
+ if nested.is_a?(Hash)
82
+ (nested["domains"] || []).each do |item|
83
+ s = item.to_s.strip.downcase
84
+ domains << s unless s.empty?
85
+ end
86
+ end
87
+ end
88
+ raise "neighbours: 域名列表为空" if domains.empty?
89
+
90
+ domains
91
+ end
92
+
93
+ # 获取单封邮件详情
94
+ def fetch_detail(address, uid)
95
+ data = request_json("/inbox/#{URI.encode_www_form_component(address)}/#{URI.encode_www_form_component(uid)}", allow_404: true)
96
+ return nil unless data.is_a?(Hash)
97
+
98
+ detail = data["data"]
99
+ detail.is_a?(Hash) ? detail : nil
100
+ end
101
+
102
+ # 创建临时邮箱
103
+ # @param domain [String, nil]
104
+ # @return [EmailInfo]
105
+ def generate_email(domain = nil)
106
+ domains = list_domains
107
+ if domain && !domain.to_s.strip.empty?
108
+ wanted = domain.strip.downcase
109
+ raise "neighbours: 不支持的域名 #{domain}" unless domains.include?(wanted)
110
+
111
+ selected = wanted
112
+ else
113
+ selected = domains.sample
114
+ end
115
+ email = "#{random_local}@#{selected}"
116
+ EmailInfo.new(channel: CHANNEL, email: email, token: email)
117
+ end
118
+
119
+ # 获取邮件列表
120
+ # @param email [String]
121
+ # @return [Array<Email>]
122
+ def get_emails(email)
123
+ address = email.to_s.strip
124
+ raise "neighbours: 邮箱地址为空" if address.empty?
125
+
126
+ data = request_json("/inbox/#{URI.encode_www_form_component(address)}", allow_404: true)
127
+ return [] unless data.is_a?(Hash)
128
+
129
+ rows = data["data"]
130
+ return [] unless rows.is_a?(Array)
131
+
132
+ rows.select { |item| item.is_a?(Hash) }.map do |item|
133
+ uid = item["uid"].to_s.strip
134
+ detail = uid.empty? ? nil : fetch_detail(address, uid)
135
+ Normalize.normalize_email(flatten_message(detail || item, address), address)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # neighbours-sh 渠道实现
6
+ # 公共收件箱模式,任意用户名即可收信,无需注册
7
+ # API: https://neighbours.sh/api/v1
8
+ module NeighboursSh
9
+ CHANNEL = "neighbours-sh"
10
+ API_BASE = "https://neighbours.sh/api/v1"
11
+ DOMAIN = "neighbours.sh"
12
+ HEADERS = {
13
+ "Accept" => "application/json",
14
+ "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"
15
+ }.freeze
16
+
17
+ module_function
18
+
19
+ # 生成随机用户名:前缀 sdk + 10 位小写字母数字
20
+ def random_local
21
+ chars = ("a".."z").to_a + ("0".."9").to_a
22
+ "sdk" + Array.new(10) { chars.sample }.join
23
+ end
24
+
25
+ # 从 from/to 结构中提取首个可用地址
26
+ def first_address(value)
27
+ return "" if value.nil?
28
+ return value.strip if value.is_a?(String)
29
+
30
+ if value.is_a?(Array)
31
+ value.each do |item|
32
+ hit = first_address(item)
33
+ return hit unless hit.empty?
34
+ end
35
+ return ""
36
+ end
37
+
38
+ if value.is_a?(Hash)
39
+ addr = value["address"].to_s.strip
40
+ return addr unless addr.empty?
41
+
42
+ text = value["text"].to_s.strip
43
+ return text if text.include?("@")
44
+
45
+ return first_address(value["value"])
46
+ end
47
+
48
+ value.to_s.strip
49
+ end
50
+
51
+ # 请求 JSON,allow_404 时 404 返回 nil
52
+ def request_json(path, allow_404: false)
53
+ resp = Http.get("#{API_BASE}#{path}", headers: HEADERS, timeout: 15)
54
+ return nil if allow_404 && resp.status_code == 404
55
+
56
+ resp.raise_for_status
57
+ data = resp.json
58
+ data.is_a?(Hash) ? data : nil
59
+ end
60
+
61
+ # 将邮件详情映射为标准化中间格式
62
+ def flatten_message(detail, recipient)
63
+ {
64
+ "id" => (detail["uid"] || "").to_s,
65
+ "from" => first_address(detail["from"]),
66
+ "to" => first_address(detail["to"]).then { |v| v.empty? ? recipient : v },
67
+ "subject" => (detail["subject"] || "").to_s,
68
+ "text" => (detail["text"] || "").to_s,
69
+ "html" => (detail["html"] || "").to_s,
70
+ "date" => (detail["date"] || "").to_s,
71
+ "is_read" => false,
72
+ "attachments" => detail["attachments"] || []
73
+ }
74
+ end
75
+
76
+ # 获取单封邮件详情
77
+ def fetch_detail(address, uid)
78
+ data = request_json("/inbox/#{URI.encode_www_form_component(address)}/#{URI.encode_www_form_component(uid)}", allow_404: true)
79
+ return nil unless data.is_a?(Hash)
80
+
81
+ detail = data["data"]
82
+ detail.is_a?(Hash) ? detail : nil
83
+ end
84
+
85
+ # 创建临时邮箱(公共收件箱,本地生成地址)
86
+ # @return [EmailInfo]
87
+ def generate_email
88
+ email = "#{random_local}@#{DOMAIN}"
89
+ EmailInfo.new(channel: CHANNEL, email: email, token: email)
90
+ end
91
+
92
+ # 获取邮件列表
93
+ # @param email [String]
94
+ # @return [Array<Email>]
95
+ def get_emails(email)
96
+ address = email.to_s.strip
97
+ raise "neighbours-sh: 缺少邮箱地址" if address.empty?
98
+
99
+ data = request_json("/inbox/#{URI.encode_www_form_component(address)}", allow_404: true)
100
+ return [] unless data
101
+
102
+ rows = data["data"]
103
+ return [] unless rows.is_a?(Array)
104
+
105
+ rows.select { |item| item.is_a?(Hash) }.filter_map do |item|
106
+ uid = item["uid"].to_s.strip
107
+ next if uid.empty?
108
+
109
+ detail = fetch_detail(address, uid)
110
+ next unless detail
111
+
112
+ Normalize.normalize_email(flatten_message(detail, address), address)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # nimail 渠道实现
6
+ # API: https://www.nimail.cn
7
+ # POST 表单 API,无需认证,token 即邮箱地址本身
8
+ module Nimail
9
+ CHANNEL = "nimail"
10
+ BASE_URL = "https://www.nimail.cn"
11
+
12
+ module_function
13
+
14
+ # 生成随机邮箱前缀
15
+ def random_local(n = 10)
16
+ chars = ("a".."z").to_a + ("0".."9").to_a
17
+ Array.new(n) { chars.sample }.join
18
+ end
19
+
20
+ # 获取 User-Agent
21
+ def user_agent
22
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
23
+ end
24
+
25
+ # 按优先级从消息字典中提取首个非空值
26
+ def first(msg, *keys)
27
+ keys.each do |key|
28
+ val = msg[key]
29
+ return val.to_s if !val.nil? && val.to_s != ""
30
+ end
31
+ ""
32
+ end
33
+
34
+ # 创建 nimail.cn 临时邮箱
35
+ # @return [EmailInfo]
36
+ def generate_email
37
+ email = "#{random_local(10)}@nimail.cn"
38
+
39
+ resp = Http.post(
40
+ "#{BASE_URL}/api/applymail",
41
+ headers: {
42
+ "User-Agent" => user_agent,
43
+ "Content-Type" => "application/x-www-form-urlencoded",
44
+ "Origin" => BASE_URL,
45
+ "Referer" => "#{BASE_URL}/"
46
+ },
47
+ body: "mail=#{URI.encode_www_form_component(email)}"
48
+ )
49
+ resp.raise_for_status
50
+
51
+ data = resp.json
52
+ unless data.is_a?(Hash) && data["success"] == "true" && data["user"]
53
+ raise "nimail: 创建邮箱失败 #{data.inspect}"
54
+ end
55
+
56
+ user = data["user"].to_s
57
+ EmailInfo.new(channel: CHANNEL, email: user, token: user)
58
+ end
59
+
60
+ # 获取邮件列表
61
+ # @param email [String]
62
+ # @return [Array<Email>]
63
+ def get_emails(email)
64
+ addr = email.to_s.strip
65
+ raise "nimail: 缺少邮箱地址" if addr.empty?
66
+
67
+ resp = Http.post(
68
+ "#{BASE_URL}/api/getmails",
69
+ headers: {
70
+ "User-Agent" => user_agent,
71
+ "Content-Type" => "application/x-www-form-urlencoded",
72
+ "Origin" => BASE_URL,
73
+ "Referer" => "#{BASE_URL}/"
74
+ },
75
+ body: "mail=#{URI.encode_www_form_component(addr)}&time=0"
76
+ )
77
+ resp.raise_for_status
78
+
79
+ data = resp.json
80
+ return [] unless data.is_a?(Hash) && data["success"] == "true"
81
+
82
+ mail = data["mail"]
83
+ return [] unless mail.is_a?(Array)
84
+
85
+ mail.select { |msg| msg.is_a?(Hash) }.map do |msg|
86
+ row = {
87
+ "id" => first(msg, "id", "time"),
88
+ "from" => first(msg, "from", "sender"),
89
+ "to" => addr,
90
+ "subject" => first(msg, "subject", "title"),
91
+ "text" => first(msg, "text", "content"),
92
+ "html" => first(msg, "html", "content"),
93
+ "date" => first(msg, "date", "time")
94
+ }
95
+ Normalize.normalize_email(row, addr)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end