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 "json"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # ockito 渠道实现
8
+ # API: https://ockito.com/web-api
9
+ # 流程: 获取 gtoken -> 获取邮箱 -> 用 Bearer token 收信
10
+ module Ockito
11
+ CHANNEL = "ockito"
12
+ BASE_URL = "https://ockito.com/web-api"
13
+
14
+ DEFAULT_HEADERS = {
15
+ "Accept" => "application/json",
16
+ "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"
17
+ }.freeze
18
+
19
+ module_function
20
+
21
+ # 请求并解析 JSON
22
+ def fetch_json(path, method: :get, headers: nil, payload: nil)
23
+ request_headers = DEFAULT_HEADERS.dup
24
+ request_headers.merge!(headers) if headers
25
+
26
+ resp = if method == :post
27
+ Http.post("#{BASE_URL}#{path}", headers: request_headers, json: payload || {})
28
+ else
29
+ Http.get("#{BASE_URL}#{path}", headers: request_headers)
30
+ end
31
+
32
+ text = resp.body || ""
33
+ data = text.empty? ? {} : JSON.parse(text)
34
+ raise "ockito http #{resp.status_code}" unless resp.ok?
35
+
36
+ [resp.status_code, data.is_a?(Hash) ? data : {}]
37
+ end
38
+
39
+ # 打包 token
40
+ def pack_token(access_token, refresh_token)
41
+ JSON.generate({ "access_token" => access_token, "refresh_token" => refresh_token })
42
+ end
43
+
44
+ # 解包 token
45
+ def unpack_token(token)
46
+ value = token.to_s.strip
47
+ raise "ockito: 无效的会话 token" if value.empty? || !value.start_with?("{")
48
+
49
+ data = JSON.parse(value)
50
+ raise "ockito: 无效的会话 token" unless data.is_a?(Hash)
51
+
52
+ access = data["access_token"].to_s.strip
53
+ refresh = data["refresh_token"].to_s.strip
54
+ raise "ockito: 无效的会话 token" if access.empty? || refresh.empty?
55
+
56
+ [access, refresh]
57
+ end
58
+
59
+ # 刷新 access token
60
+ def refresh_access_token(refresh_token)
61
+ _, data = fetch_json("/grefresh", method: :post,
62
+ headers: { "Authorization" => "Bearer #{refresh_token}", "X-PASSTHROUGH" => "Y" })
63
+ access = data["access_token"].to_s.strip
64
+ raise "ockito: 刷新 token 失败" if access.empty?
65
+
66
+ access
67
+ end
68
+
69
+ # 带 Bearer 认证的请求(401 时自动刷新重试)
70
+ def fetch_bearer_json(path, access_token, refresh_token)
71
+ _, data = fetch_json(path, headers: { "Authorization" => "Bearer #{access_token}" })
72
+ data
73
+ rescue RuntimeError => e
74
+ raise unless e.message.include?("401")
75
+
76
+ refreshed = refresh_access_token(refresh_token)
77
+ _, data = fetch_json(path, headers: { "Authorization" => "Bearer #{refreshed}" })
78
+ data
79
+ end
80
+
81
+ # 扁平化收件箱行
82
+ def flatten_inbox_row(raw, recipient)
83
+ {
84
+ "id" => (raw["uid"] || "").to_s,
85
+ "from" => (raw["sender"] || "").to_s,
86
+ "to" => recipient,
87
+ "subject" => (raw["subject"] || "").to_s,
88
+ "text" => (raw["snippet"] || "").to_s,
89
+ "html" => (raw["html"] || "").to_s,
90
+ "date" => (raw["timestamp"] || "").to_s,
91
+ "is_read" => false,
92
+ "attachments" => []
93
+ }
94
+ end
95
+
96
+ # 扁平化邮件详情
97
+ def flatten_message(raw, recipient)
98
+ obj = raw["obj"].is_a?(Hash) ? raw["obj"] : raw
99
+ {
100
+ "id" => (raw["uid"] || "").to_s,
101
+ "from" => (obj["sender_email"] || obj["SenderEmail"] || obj["from_"] || obj["From"] || obj["from"] || obj["sender_name"] || obj["SenderName"] || "").to_s,
102
+ "to" => (obj["to"] || obj["To"] || recipient).to_s,
103
+ "subject" => (obj["subject"] || obj["Subject"] || "").to_s,
104
+ "text" => (obj["text"] || "").to_s,
105
+ "html" => (obj["html"] || "").to_s,
106
+ "date" => (obj["date"] || obj["Date"] || "").to_s,
107
+ "is_read" => false,
108
+ "attachments" => []
109
+ }
110
+ end
111
+
112
+ # 创建临时邮箱
113
+ # @return [EmailInfo]
114
+ def generate_email
115
+ _, login = fetch_json("/gtoken", method: :post, payload: {})
116
+ access_token = login["access_token"].to_s.strip
117
+ refresh_token = login["refresh_token"].to_s.strip
118
+ raise "ockito: 无效的 token 响应" if access_token.empty? || refresh_token.empty?
119
+
120
+ _, email_data = fetch_json("/email", headers: { "Authorization" => "Bearer #{access_token}" })
121
+ email_value = email_data["email"]
122
+ email = if email_value.is_a?(String)
123
+ email_value.strip
124
+ elsif email_value.is_a?(Hash)
125
+ (email_value["email"] || "").to_s.strip
126
+ else
127
+ ""
128
+ end
129
+ raise "ockito: 无效的邮箱响应" if email.empty? || !email.include?("@")
130
+
131
+ EmailInfo.new(channel: CHANNEL, email: email, token: pack_token(access_token, refresh_token))
132
+ end
133
+
134
+ # 获取邮件列表
135
+ # @param token [String] JSON 编码的 access+refresh token
136
+ # @param email [String]
137
+ # @return [Array<Email>]
138
+ def get_emails(token, email)
139
+ access_token, refresh_token = unpack_token(token)
140
+ address = email.to_s.strip
141
+ raise "ockito: 邮箱地址为空" if address.empty?
142
+
143
+ data = fetch_bearer_json("/retrieve/#{URI.encode_www_form_component(address)}/emails", access_token, refresh_token)
144
+ rows = data["inbox"]
145
+ return [] unless rows.is_a?(Array)
146
+
147
+ rows.select { |row| row.is_a?(Hash) }.map do |row|
148
+ uid = row["uid"].to_s.strip
149
+ if uid.empty?
150
+ Normalize.normalize_email(flatten_inbox_row(row, address), address)
151
+ else
152
+ begin
153
+ detail = fetch_bearer_json("/retrieve/#{URI.encode_www_form_component(address)}/#{URI.encode_www_form_component(uid)}", access_token, refresh_token)
154
+ Normalize.normalize_email(flatten_message(detail, address), address)
155
+ rescue StandardError
156
+ Normalize.normalize_email(flatten_inbox_row(row, address), address)
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # 1SecMail 渠道 — https://tmaily.com
6
+ module OneSecMail
7
+ CHANNEL = "1sec-mail"
8
+ BASE_URL = "https://tmaily.com/"
9
+ HEADERS = { "Accept" => "application/json", "User-Agent" => "Mozilla/5.0" }.freeze
10
+
11
+ module_function
12
+
13
+ # 生成临时邮箱,从 Set-Cookie 提取会话 Cookie
14
+ # @return [EmailInfo]
15
+ def generate_email
16
+ resp = Http.get("#{BASE_URL}generate", headers: HEADERS, timeout: 15)
17
+ resp.raise_for_status
18
+ cookie_val = resp.cookie_value("TMaily_sid")
19
+ raise "1sec-mail: 未获取到会话 Cookie" if cookie_val.nil? || cookie_val.empty?
20
+
21
+ cookie = "TMaily_sid=#{cookie_val}"
22
+ data = resp.json
23
+ email = data.is_a?(Hash) ? data["address"].to_s.strip : ""
24
+ raise "1sec-mail: 无效的邮箱响应" unless email.include?("@")
25
+
26
+ EmailInfo.new(channel: CHANNEL, email: email, token: cookie)
27
+ end
28
+
29
+ # @param email [String]
30
+ # @param token [String] 会话 Cookie
31
+ # @return [Array<Email>]
32
+ def get_emails(email, token)
33
+ address = email.to_s.strip
34
+ raise "1sec-mail: 邮箱地址为空" if address.empty?
35
+
36
+ resp = Http.get("#{BASE_URL}emails?address=#{address}",
37
+ headers: HEADERS.merge("Cookie" => token.to_s), timeout: 15)
38
+ resp.raise_for_status
39
+ rows = resp.json
40
+ raise "1sec-mail: 无效的邮件列表响应" unless rows.is_a?(Array)
41
+
42
+ rows.select { |r| r.is_a?(Hash) }.map { |item| Normalize.normalize_email(item, address) }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # restmail-net 渠道 — https://restmail.net
6
+ # Mozilla 开源项目,无需创建邮箱,ad-hoc 模式
7
+ module RestmailNet
8
+ CHANNEL = "restmail-net"
9
+ BASE_URL = "https://restmail.net"
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/149.0.0.0 Safari/537.36"
13
+ }.freeze
14
+
15
+ module_function
16
+
17
+ def random_username
18
+ chars = ("a".."z").to_a + ("0".."9").to_a
19
+ Array.new(rand(10..12)) { chars.sample }.join
20
+ end
21
+
22
+ # 创建邮箱:随机 username 即可使用,无需请求服务端
23
+ # @return [EmailInfo]
24
+ def generate_email
25
+ EmailInfo.new(channel: CHANNEL, email: "#{random_username}@restmail.net")
26
+ end
27
+
28
+ # @param email [String]
29
+ # @return [Array<Email>]
30
+ def get_emails(email)
31
+ address = email.to_s.strip
32
+ raise "restmail-net: 邮箱地址为空" if address.empty?
33
+
34
+ username = address.split("@").first.to_s
35
+ raise "restmail-net: 无法提取用户名" if username.empty?
36
+
37
+ resp = Http.get("#{BASE_URL}/mail/#{username}", headers: HEADERS, timeout: 15)
38
+ return [] if resp.status_code == 404
39
+
40
+ resp.raise_for_status
41
+ data = resp.json
42
+ return [] unless data.is_a?(Array)
43
+
44
+ data.select { |m| m.is_a?(Hash) }.map do |msg|
45
+ from_addr = ""
46
+ if msg["from"].is_a?(Array) && msg["from"][0].is_a?(Hash)
47
+ from_addr = msg["from"][0]["address"].to_s
48
+ end
49
+ from_addr = msg.dig("headers", "from").to_s if from_addr.empty? && msg["headers"].is_a?(Hash)
50
+ Normalize.normalize_email({
51
+ "id" => msg["id"] || "", "from" => from_addr, "to" => address,
52
+ "subject" => msg["subject"] || "", "text" => msg["text"] || "",
53
+ "html" => msg["html"] || "", "date" => msg["receivedAt"] || "",
54
+ "is_read" => false
55
+ }, address)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "uri"
5
+
6
+ module TempmailSdk
7
+ module Providers
8
+ # Rootsh(BccTo) 渠道 — https://rootsh.com
9
+ # 需要 cookie session 认证,token 存储 time 值用于增量获取邮件
10
+ module Rootsh
11
+ CHANNEL = "rootsh"
12
+ BASE = "https://rootsh.com"
13
+ DEFAULT_DOMAIN = "bccto.cc"
14
+ TOK_PREFIX = "rootsh1:"
15
+
16
+ HEADERS = {
17
+ "Accept" => "application/json, text/plain, */*",
18
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
19
+ "Cache-Control" => "no-cache",
20
+ "DNT" => "1",
21
+ "Pragma" => "no-cache",
22
+ "Referer" => "#{BASE}/",
23
+ "X-Requested-With" => "XMLHttpRequest",
24
+ "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"
25
+ }.freeze
26
+
27
+ module_function
28
+
29
+ def random_local(length = 10)
30
+ chars = ("a".."z").to_a + ("0".."9").to_a
31
+ Array.new(length) { chars.sample }.join
32
+ end
33
+
34
+ def encode_token(last_check_time, cookies)
35
+ payload = JSON.generate({ "t" => last_check_time, "c" => cookies })
36
+ "#{TOK_PREFIX}#{payload}"
37
+ end
38
+
39
+ def decode_token(token)
40
+ raise "rootsh: 无效的 session token" unless token&.start_with?(TOK_PREFIX)
41
+
42
+ data = JSON.parse(token[TOK_PREFIX.length..])
43
+ last_check_time = data["t"] || 0
44
+ cookies = data["c"] || {}
45
+ raise "rootsh: 无效的 session token" unless cookies.is_a?(Hash)
46
+
47
+ [last_check_time, cookies]
48
+ end
49
+
50
+ def build_cookie_header(cookies)
51
+ cookies.map { |k, v| "#{k}=#{v}" }.join("; ")
52
+ end
53
+
54
+ def extract_cookies(resp)
55
+ cookies = {}
56
+ resp.set_cookies.each do |line|
57
+ pair = line.split(";").first.to_s.strip
58
+ k, v = pair.split("=", 2)
59
+ cookies[k] = v if k && v
60
+ end
61
+ cookies
62
+ end
63
+
64
+ # 创建临时邮箱
65
+ # @param domain [String, nil] 可选域名
66
+ # @param channel [String] 渠道标识
67
+ # @return [EmailInfo]
68
+ def generate_email(domain = nil, channel = CHANNEL)
69
+ dom = (domain || "").strip
70
+ dom = DEFAULT_DOMAIN if dom.empty?
71
+ local = random_local
72
+ mail_addr = "#{local}@#{dom}"
73
+
74
+ # 获取 session cookie
75
+ r1 = Http.get("#{BASE}/", headers: {
76
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
77
+ "User-Agent" => HEADERS["User-Agent"]
78
+ })
79
+ r1.raise_for_status
80
+ cookies = extract_cookies(r1)
81
+
82
+ # POST /applymail 申请邮箱
83
+ r2 = Http.post("#{BASE}/applymail", headers: HEADERS.merge(
84
+ "Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8",
85
+ "Cookie" => build_cookie_header(cookies)
86
+ ), body: "mail=#{mail_addr}")
87
+ r2.raise_for_status
88
+ cookies.merge!(extract_cookies(r2))
89
+
90
+ body = r2.json
91
+ raise "rootsh: applymail 响应格式无效" unless body.is_a?(Hash)
92
+ raise "rootsh: 邮箱申请失败: #{body['tips']}" unless body["success"].to_s == "true"
93
+
94
+ actual_email = (body["user"] || "").strip
95
+ actual_email = mail_addr if actual_email.empty?
96
+ last_check_time = body["time"] || 0
97
+
98
+ token = encode_token(last_check_time, cookies)
99
+ EmailInfo.new(channel: channel, email: actual_email, token: token)
100
+ end
101
+
102
+ # 获取邮件列表
103
+ # @param token [String]
104
+ # @param email [String]
105
+ # @return [Array<Email>]
106
+ def get_emails(token, email)
107
+ raise "rootsh: token 不能为空" if token.nil? || token.empty?
108
+
109
+ addr = (email || "").strip
110
+ raise "rootsh: 邮箱地址不能为空" if addr.empty?
111
+
112
+ last_check_time, cookies = decode_token(token)
113
+ ts = (Time.now.to_f * 1000).to_i
114
+
115
+ r = Http.post("#{BASE}/getmail", headers: HEADERS.merge(
116
+ "Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8",
117
+ "Cookie" => build_cookie_header(cookies)
118
+ ), body: "mail=#{addr}&time=#{last_check_time}&_=#{ts}")
119
+ r.raise_for_status
120
+ body = r.json
121
+
122
+ return [] unless body.is_a?(Hash) && body["success"].to_s == "true"
123
+
124
+ mail_list = body["mail"]
125
+ return [] unless mail_list.is_a?(Array)
126
+
127
+ mail_list.filter_map do |item|
128
+ next unless item.is_a?(Array) && item.length >= 5
129
+
130
+ from_email = item[1].to_s
131
+ from_display = item[0].to_s
132
+ from_addr = from_email.empty? ? from_display : from_email
133
+ subject = item[2].to_s
134
+ date_str = item[3].to_s
135
+ mail_fid = item[4].to_s
136
+
137
+ # 获取邮件正文
138
+ html_content = ""
139
+ unless mail_fid.empty?
140
+ begin
141
+ rv = Http.post("#{BASE}/viewmail", headers: HEADERS.merge(
142
+ "Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8",
143
+ "Cookie" => build_cookie_header(cookies)
144
+ ), body: "mail=#{mail_fid}&to=#{addr}")
145
+ if rv.ok?
146
+ vb = rv.json
147
+ html_content = vb["mail"] || "" if vb.is_a?(Hash) && vb["success"].to_s == "true"
148
+ end
149
+ rescue StandardError
150
+ # 单封邮件获取失败不影响其他
151
+ end
152
+ end
153
+
154
+ raw = {
155
+ "id" => mail_fid,
156
+ "from" => from_addr,
157
+ "to" => addr,
158
+ "subject" => subject,
159
+ "date" => date_str,
160
+ "html" => html_content
161
+ }
162
+ Normalize.normalize_email(raw, addr)
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # shitty.email 渠道 — https://shitty.email
8
+ module ShittyEmail
9
+ CHANNEL = "shitty-email"
10
+ API_BASE = "https://shitty.email/api"
11
+ HEADERS = { "Accept" => "application/json", "User-Agent" => "Mozilla/5.0" }.freeze
12
+
13
+ module_function
14
+
15
+ def headers_with_token(token = nil)
16
+ h = HEADERS.dup
17
+ h["X-Session-Token"] = token if token && !token.empty?
18
+ h
19
+ end
20
+
21
+ # 创建临时邮箱
22
+ # @return [EmailInfo]
23
+ def generate_email
24
+ resp = Http.post("#{API_BASE}/inbox", headers: HEADERS, timeout: 15)
25
+ resp.raise_for_status
26
+ data = resp.json
27
+ raise "shitty-email: 响应格式无效" unless data.is_a?(Hash)
28
+
29
+ email = (data["email"] || "").strip
30
+ token = (data["token"] || "").strip
31
+ raise "shitty-email: 创建邮箱响应无效" if email.empty? || !email.include?("@") || token.empty?
32
+
33
+ EmailInfo.new(channel: CHANNEL, email: email, token: token, expires_at: data["expiresAt"])
34
+ end
35
+
36
+ # 获取邮件列表
37
+ # @param token [String] 会话令牌
38
+ # @param email [String] 邮箱地址
39
+ # @return [Array<Email>]
40
+ def get_emails(token, email)
41
+ session_token = (token || "").strip
42
+ address = (email || "").strip
43
+ raise "shitty-email: token 为空" if session_token.empty?
44
+ raise "shitty-email: 邮箱地址为空" if address.empty?
45
+
46
+ resp = Http.get("#{API_BASE}/inbox", headers: headers_with_token(session_token), timeout: 15)
47
+ resp.raise_for_status
48
+ data = resp.json
49
+ return [] unless data.is_a?(Hash)
50
+
51
+ rows = data["emails"]
52
+ return [] unless rows.is_a?(Array)
53
+
54
+ rows.filter_map do |item|
55
+ next unless item.is_a?(Hash)
56
+
57
+ message_id = (item["id"] || "").to_s.strip
58
+ detail = nil
59
+ unless message_id.empty?
60
+ begin
61
+ dr = Http.get("#{API_BASE}/email/#{URI.encode_www_form_component(message_id)}",
62
+ headers: headers_with_token(session_token), timeout: 15)
63
+ detail = dr.json if dr.ok?
64
+ rescue StandardError
65
+ # 单封邮件获取失败不影响其他
66
+ end
67
+ end
68
+
69
+ source = (detail.is_a?(Hash) ? detail : item)
70
+ raw = {
71
+ "id" => source["id"],
72
+ "from" => source["from"],
73
+ "to" => source["to"] || address,
74
+ "subject" => source["subject"],
75
+ "text" => source["text"],
76
+ "html" => source["html"],
77
+ "date" => source["date"]
78
+ }
79
+ Normalize.normalize_email(raw, address)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # smailpro.com 渠道实现
6
+ # 两段式流程:先获取 JWT payload,再调用 api.sonjj.com 接口
7
+ module Smailpro
8
+ CHANNEL = "smailpro"
9
+ BASE_URL = "https://smailpro.com"
10
+ API_BASE_URL = "https://api.sonjj.com/v1/temp_email"
11
+
12
+ HEADERS = {
13
+ "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",
14
+ "Accept" => "application/json, text/plain, */*",
15
+ "Referer" => "#{BASE_URL}/"
16
+ }.freeze
17
+
18
+ module_function
19
+
20
+ # 获取访问 sonjj API 所需的 JWT
21
+ def fetch_payload(target_url, extra = nil)
22
+ url = "#{BASE_URL}/app/payload?url=#{URI.encode_www_form_component(target_url)}"
23
+ if extra
24
+ extra.each { |k, v| url += "&#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(v)}" }
25
+ end
26
+ resp = Http.get(url, headers: HEADERS)
27
+ resp.raise_for_status
28
+ payload = resp.body.strip.delete('"')
29
+ raise "smailpro: payload 为空" if payload.empty?
30
+
31
+ payload
32
+ end
33
+
34
+ # 携带 JWT 调用 sonjj API
35
+ def call_api(target_url, extra = nil)
36
+ payload = fetch_payload(target_url, extra)
37
+ resp = Http.get("#{target_url}?payload=#{URI.encode_www_form_component(payload)}", headers: HEADERS)
38
+ resp.raise_for_status
39
+ resp.json
40
+ end
41
+
42
+ # 创建临时邮箱
43
+ # @return [EmailInfo]
44
+ def generate_email
45
+ data = call_api("#{API_BASE_URL}/create")
46
+ raise "smailpro: 创建响应格式异常" unless data.is_a?(Hash)
47
+
48
+ email = data["email"].to_s.strip
49
+ raise "smailpro: 创建邮箱失败" if email.empty?
50
+
51
+ EmailInfo.new(channel: CHANNEL, email: email, token: email, expires_at: data["expired_at"])
52
+ end
53
+
54
+ # 获取单封邮件正文
55
+ def fetch_message(email, mid)
56
+ return ["", ""] if mid.to_s.empty?
57
+
58
+ data = call_api("#{API_BASE_URL}/message", { "email" => email, "mid" => mid })
59
+ return ["", ""] unless data.is_a?(Hash)
60
+
61
+ [(data["body"] || "").to_s, (data["textBody"] || "").to_s]
62
+ rescue StandardError
63
+ ["", ""]
64
+ end
65
+
66
+ # 获取邮件列表
67
+ # @param email [String]
68
+ # @param token [String]
69
+ # @return [Array<Email>]
70
+ def get_emails(email, token = "")
71
+ addr = email.to_s.strip
72
+ raise "smailpro: 邮箱地址为空" if addr.empty?
73
+
74
+ data = call_api("#{API_BASE_URL}/inbox", { "email" => addr })
75
+ raise "smailpro: 邮件列表响应格式异常" unless data.is_a?(Hash)
76
+
77
+ # API 响应: {"status": true, "data": {"messages": [...]}}
78
+ inner = data["data"]
79
+ messages = if inner.is_a?(Hash)
80
+ inner["messages"]
81
+ else
82
+ data["messages"]
83
+ end
84
+ return [] unless messages.is_a?(Array) && !messages.empty?
85
+
86
+ messages.select { |m| m.is_a?(Hash) }.map do |m|
87
+ mid = m["mid"].to_s.strip
88
+ html_body, text_body = fetch_message(addr, mid)
89
+ raw = {
90
+ "id" => mid,
91
+ "from" => m["from"] || "",
92
+ "to" => addr,
93
+ "subject" => m["subject"] || "",
94
+ "date" => m["datetime"] || ""
95
+ }
96
+ raw["html"] = html_body if !html_body.empty? || !text_body.empty?
97
+ raw["text"] = text_body if !html_body.empty? || !text_body.empty?
98
+ Normalize.normalize_email(raw, addr)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # ta-easy.com 临时邮箱渠道
6
+ # API: https://api-endpoint.ta-easy.com/temp-email/
7
+ module TaEasy
8
+ CHANNEL = "ta-easy"
9
+ API_BASE = "https://api-endpoint.ta-easy.com"
10
+ ORIGIN = "https://www.ta-easy.com"
11
+
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/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
15
+ "origin" => ORIGIN,
16
+ "referer" => "#{ORIGIN}/"
17
+ }.freeze
18
+
19
+ module_function
20
+
21
+ # 创建临时邮箱
22
+ # @return [EmailInfo]
23
+ def generate_email
24
+ resp = Http.post("#{API_BASE}/temp-email/address/new",
25
+ headers: HEADERS.merge("Content-Length" => "0"), body: "")
26
+ resp.raise_for_status
27
+ data = resp.json
28
+ raise "ta-easy: #{data['message'] || 'create failed'}" unless data["status"] == "success" && data["address"] && data["token"]
29
+
30
+ expires_at = data["expiresAt"].is_a?(Numeric) ? data["expiresAt"].to_i : nil
31
+ EmailInfo.new(channel: CHANNEL, email: data["address"], token: data["token"], expires_at: expires_at)
32
+ end
33
+
34
+ # 获取邮件列表
35
+ # @param email [String]
36
+ # @param token [String]
37
+ # @return [Array<Email>]
38
+ def get_emails(email, token)
39
+ resp = Http.post("#{API_BASE}/temp-email/inbox/list",
40
+ headers: HEADERS.merge("Content-Type" => "application/json"),
41
+ json: { "token" => token, "email" => email })
42
+ resp.raise_for_status
43
+ data = resp.json
44
+ raise "ta-easy: #{data['message'] || 'inbox failed'}" unless data["status"] == "success"
45
+
46
+ raw_list = data["data"]
47
+ return [] unless raw_list.is_a?(Array)
48
+
49
+ raw_list.map { |raw| Normalize.normalize_email(raw, email) }
50
+ end
51
+ end
52
+ end
53
+ end