tempmail_sdk 1.3.3 → 1.3.5

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/tempmail_sdk/channels.rb +29 -7
  4. data/lib/tempmail_sdk/channels_data.rb +29 -7
  5. data/lib/tempmail_sdk/providers/altmails.rb +99 -0
  6. data/lib/tempmail_sdk/providers/apihz.rb +138 -0
  7. data/lib/tempmail_sdk/providers/awamail.rb +95 -0
  8. data/lib/tempmail_sdk/providers/best_temp_mail.rb +77 -0
  9. data/lib/tempmail_sdk/providers/chatgpt_org_uk.rb +129 -0
  10. data/lib/tempmail_sdk/providers/clawdemail.rb +122 -0
  11. data/lib/tempmail_sdk/providers/crazymailing.rb +114 -0
  12. data/lib/tempmail_sdk/providers/disposablemail.rb +125 -0
  13. data/lib/tempmail_sdk/providers/disposablemail_app.rb +66 -0
  14. data/lib/tempmail_sdk/providers/email10min.rb +152 -0
  15. data/lib/tempmail_sdk/providers/emailnator.rb +132 -0
  16. data/lib/tempmail_sdk/providers/emailtemp_org.rb +131 -0
  17. data/lib/tempmail_sdk/providers/expressinboxhub.rb +118 -0
  18. data/lib/tempmail_sdk/providers/fakemail.rb +143 -0
  19. data/lib/tempmail_sdk/providers/firetempmail.rb +86 -0
  20. data/lib/tempmail_sdk/providers/flybymail.rb +93 -0
  21. data/lib/tempmail_sdk/providers/generator_email.rb +146 -0
  22. data/lib/tempmail_sdk/providers/haribu.rb +29 -3
  23. data/lib/tempmail_sdk/providers/huskmail.rb +132 -0
  24. data/lib/tempmail_sdk/providers/internxt.rb +176 -0
  25. data/lib/tempmail_sdk/providers/linshi_xyz.rb +79 -0
  26. data/lib/tempmail_sdk/providers/linshiyouxiang_net.rb +85 -0
  27. data/lib/tempmail_sdk/providers/mail_sunls.rb +42 -1
  28. data/lib/tempmail_sdk/providers/mail_td.rb +161 -0
  29. data/lib/tempmail_sdk/providers/mailcat_ai.rb +52 -0
  30. data/lib/tempmail_sdk/providers/maildrop.rb +79 -4
  31. data/lib/tempmail_sdk/providers/mailgolem.rb +96 -0
  32. data/lib/tempmail_sdk/providers/mailinator.rb +9 -2
  33. data/lib/tempmail_sdk/providers/mailmomy.rb +1 -1
  34. data/lib/tempmail_sdk/providers/mailtemp_cc.rb +91 -0
  35. data/lib/tempmail_sdk/providers/mailticking.rb +139 -0
  36. data/lib/tempmail_sdk/providers/minuteinbox.rb +174 -0
  37. data/lib/tempmail_sdk/providers/moakt.rb +29 -3
  38. data/lib/tempmail_sdk/providers/mtempmail.rb +140 -0
  39. data/lib/tempmail_sdk/providers/mytempmail_cc.rb +76 -0
  40. data/lib/tempmail_sdk/providers/nowtempmail.rb +112 -0
  41. data/lib/tempmail_sdk/providers/noxen_de5_net.rb +392 -0
  42. data/lib/tempmail_sdk/providers/nukemail.rb +188 -0
  43. data/lib/tempmail_sdk/providers/nullmail.rb +95 -0
  44. data/lib/tempmail_sdk/providers/openinbox.rb +102 -0
  45. data/lib/tempmail_sdk/providers/shadowmail.rb +174 -0
  46. data/lib/tempmail_sdk/providers/shitpost_email.rb +85 -0
  47. data/lib/tempmail_sdk/providers/smail_pw.rb +156 -0
  48. data/lib/tempmail_sdk/providers/smails.rb +128 -0
  49. data/lib/tempmail_sdk/providers/socketio_mail.rb +25 -0
  50. data/lib/tempmail_sdk/providers/temp_mail_gg.rb +362 -0
  51. data/lib/tempmail_sdk/providers/tempgbox.rb +118 -0
  52. data/lib/tempmail_sdk/providers/tempmail100.rb +148 -0
  53. data/lib/tempmail_sdk/providers/tempmail_cn.rb +111 -0
  54. data/lib/tempmail_sdk/providers/tempmail_ee.rb +373 -0
  55. data/lib/tempmail_sdk/providers/tempmailportal.rb +131 -0
  56. data/lib/tempmail_sdk/providers/tempmails_io.rb +89 -0
  57. data/lib/tempmail_sdk/providers/tempmailto.rb +312 -0
  58. data/lib/tempmail_sdk/providers/temporarymail_com.rb +184 -0
  59. data/lib/tempmail_sdk/providers/tenmin_app.rb +100 -0
  60. data/lib/tempmail_sdk/providers/tenminutemail_net.rb +183 -0
  61. data/lib/tempmail_sdk/providers/thirty_min_email.rb +90 -0
  62. data/lib/tempmail_sdk/providers/tmpkit.rb +129 -0
  63. data/lib/tempmail_sdk/providers/twentyfourmail_chacuo.rb +83 -0
  64. data/lib/tempmail_sdk/providers/vip_215.rb +171 -0
  65. data/lib/tempmail_sdk/providers/zerodrop.rb +90 -0
  66. data/lib/tempmail_sdk/registry.rb +285 -23
  67. data/lib/tempmail_sdk/types.rb +13 -0
  68. data/lib/tempmail_sdk/version.rb +1 -1
  69. data/lib/tempmail_sdk.rb +1 -1
  70. metadata +58 -6
  71. data/lib/tempmail_sdk/providers/gonebox_email.rb +0 -89
  72. data/lib/tempmail_sdk/providers/temp_mail_now.rb +0 -103
  73. data/lib/tempmail_sdk/providers/webmailtemp.rb +0 -108
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # LinshiXYZ 渠道实现(linshi.xyz)
8
+ #
9
+ # 无建箱请求:本地随机 6 位 hex 前缀(官网 client 即用短 id)+ @linshi.xyz。
10
+ # 读信 GET https://linshi.xyz/api/mails/{前缀},无邮件时返回空数组 [],
11
+ # 有邮件时为邮件对象数组(元素含 headers{from,to,subject,date} 与 html 正文)。
12
+ # 归一化时将 headers 平铺并注入收件人地址;非数组骨架(如 {ok:false})整体失败,
13
+ # 交由上层 fallback 渠道处理。
14
+ module LinshiXyz
15
+ CHANNEL = "linshi-xyz"
16
+ BASE_URL = "https://linshi.xyz"
17
+ DOMAIN = "linshi.xyz"
18
+ HEX_DIGITS = "0123456789abcdef"
19
+
20
+ HEADERS = {
21
+ "Accept" => "application/json",
22
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
23
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
24
+ }.freeze
25
+
26
+ module_function
27
+
28
+ # 生成本地随机 6 位 hex 前缀(与官网 client 相同格式)
29
+ # @return [String]
30
+ def local_name
31
+ Array.new(6) { HEX_DIGITS[rand(HEX_DIGITS.length)] }.join
32
+ end
33
+
34
+ # 创建 linshi.xyz 临时邮箱
35
+ # 无需建箱请求,本地随机 6 位 hex 前缀;token 复用完整地址。
36
+ # @return [EmailInfo]
37
+ def generate_email
38
+ addr = "#{local_name}@#{DOMAIN}"
39
+ EmailInfo.new(channel: CHANNEL, email: addr, token: addr)
40
+ end
41
+
42
+ # 读取 linshi.xyz 收件箱
43
+ # GET /api/mails/{前缀}(URL 编码),响应为邮件对象数组。
44
+ # @param email [String] 完整邮箱地址(前缀@linshi.xyz)
45
+ # @param token [String] 复用完整地址
46
+ # @return [Array<Email>]
47
+ def get_emails(email, token)
48
+ addr = email.to_s.strip
49
+ raise "linshi-xyz: 邮箱地址无效: #{addr.inspect}" if addr.empty? || !addr.include?("@")
50
+
51
+ local = addr.split("@", 2).first.to_s
52
+ resp = Http.get("#{BASE_URL}/api/mails/#{URI.encode_www_form_component(local)}",
53
+ headers: HEADERS, timeout: 15)
54
+ raise "linshi-xyz: 读取收件箱失败 http #{resp.status_code}" unless resp.ok?
55
+
56
+ list = resp.json
57
+ raise "linshi-xyz: 收件箱响应非数组" unless list.is_a?(Array)
58
+
59
+ list.filter_map do |m|
60
+ next unless m.is_a?(Hash)
61
+
62
+ Normalize.normalize_email(normalize_row(m, addr), addr)
63
+ end
64
+ end
65
+
66
+ # 将 linshi.xyz 邮件对象归一化:headers 平铺为顶层字段并注入收件人。
67
+ # @param m [Hash] 原始邮件对象
68
+ # @param email [String] 收件人地址
69
+ # @return [Hash]
70
+ def normalize_row(m, email)
71
+ flat = m.dup
72
+ headers = m["headers"]
73
+ flat.merge!(headers) if headers.is_a?(Hash)
74
+ flat["to"] = email unless flat.key?("to")
75
+ flat
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # linshiyouxiang.net 渠道实现
8
+ #
9
+ # 流程:
10
+ # 1. GET / 获取 temp_mail cookie,从 HTML 正则提取 tempMailGlobal(邮箱)和 mailCodeGlobal(校验 code)
11
+ # 2. POST /get-messages {"email":"...","code":"..."} 获取邮件列表
12
+ # token 存储 mailCodeGlobal 的值(HMAC 哈希,后续请求用于校验)
13
+ module LinshiyouxiangNet
14
+ CHANNEL = "linshiyouxiang-net"
15
+ BASE_URL = "https://www.linshiyouxiang.net"
16
+
17
+ EMAIL_RE = /tempMailGlobal\s*=\s*'([^']+)'/.freeze
18
+ CODE_RE = /mailCodeGlobal\s*=\s*'([^']+)'/.freeze
19
+
20
+ BROWSER_HEADERS = {
21
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
22
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
23
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
24
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
25
+ }.freeze
26
+
27
+ AJAX_HEADERS = {
28
+ "Accept" => "application/json, text/javascript, */*; q=0.01",
29
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
30
+ "Content-Type" => "application/json",
31
+ "Origin" => BASE_URL,
32
+ "Referer" => "#{BASE_URL}/",
33
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
34
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
35
+ "X-Requested-With" => "XMLHttpRequest"
36
+ }.freeze
37
+
38
+ module_function
39
+
40
+ # 创建临时邮箱
41
+ # @return [EmailInfo]
42
+ def generate_email
43
+ resp = Http.get("#{BASE_URL}/", headers: BROWSER_HEADERS)
44
+ resp.raise_for_status
45
+ html = resp.body
46
+
47
+ em = html.match(EMAIL_RE)
48
+ raise "linshiyouxiang-net: 未能从首页提取邮箱地址" unless em
49
+
50
+ email = em[1].strip
51
+ raise "linshiyouxiang-net: 提取的邮箱地址为空" if email.empty?
52
+
53
+ code = ""
54
+ if (cm = html.match(CODE_RE))
55
+ code = cm[1].strip
56
+ end
57
+
58
+ EmailInfo.new(channel: CHANNEL, email: email, token: code)
59
+ end
60
+
61
+ # 获取邮件列表
62
+ # POST /get-messages {"email":"...","code":"<token>"}
63
+ # @param email [String]
64
+ # @param token [String] mailCodeGlobal 值
65
+ # @return [Array<Email>]
66
+ def get_emails(email, token)
67
+ raise "linshiyouxiang-net: 邮箱地址为空" if email.to_s.strip.empty?
68
+
69
+ resp = Http.post("#{BASE_URL}/get-messages",
70
+ headers: AJAX_HEADERS,
71
+ json: { "email" => email, "code" => token.to_s })
72
+ resp.raise_for_status
73
+ data = resp.json
74
+ emails_raw = data["emails"]
75
+ return [] unless emails_raw.is_a?(Array) && !emails_raw.empty?
76
+
77
+ emails_raw.filter_map do |raw|
78
+ next unless raw.is_a?(Hash)
79
+
80
+ Normalize.normalize_email(raw, email)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -47,7 +47,41 @@ module TempmailSdk
47
47
  EmailInfo.new(channel: CHANNEL, email: "#{local}@#{domain}")
48
48
  end
49
49
 
50
+ # 通过详情接口获取单封邮件完整正文
51
+ # GET /api/fetch/{id}
52
+ # 失败时返回 nil,调用方回退到列表数据
53
+ def fetch_detail(id)
54
+ mid = id.to_s.strip
55
+ return nil if mid.empty?
56
+
57
+ begin
58
+ resp = Http.get("#{BASE}/api/fetch/#{URI.encode_www_form_component(mid)}",
59
+ headers: HEADERS, timeout: 15)
60
+ return nil if resp.status_code < 200 || resp.status_code >= 300
61
+
62
+ data = resp.json
63
+ data.is_a?(Hash) ? data : nil
64
+ rescue StandardError
65
+ nil
66
+ end
67
+ end
68
+
69
+ # 从列表条目提取邮件 ID(支持多字段)
70
+ def extract_id(row)
71
+ %w[id _id mail_id messageId message_id].each do |key|
72
+ v = row[key]
73
+ next if v.nil?
74
+
75
+ return v.to_s.strip if v.is_a?(String) && !v.strip.empty?
76
+ return v.to_i.to_s if v.is_a?(Numeric)
77
+ end
78
+ ""
79
+ end
80
+
50
81
  # 获取邮件列表
82
+ # 1. GET /api/fetch?to={email} 拉取列表元数据
83
+ # 2. 对每封邮件 GET /api/fetch/{id} 拉取详情(含完整 text/html)
84
+ # 3. 详情失败时保留列表字段作为回退
51
85
  # @param email [String]
52
86
  # @return [Array<Email>]
53
87
  def get_emails(email)
@@ -60,7 +94,14 @@ module TempmailSdk
60
94
  return [] unless data.is_a?(Array)
61
95
 
62
96
  data.select { |raw| raw.is_a?(Hash) }.map do |raw|
63
- Normalize.normalize_email(raw, addr)
97
+ mail_id = extract_id(raw)
98
+ merged = raw.dup
99
+ # 无条件调用详情接口,用详情字段覆盖列表字段
100
+ unless mail_id.empty?
101
+ detail = fetch_detail(mail_id)
102
+ merged.merge!(detail) if detail
103
+ end
104
+ Normalize.normalize_email(merged, addr)
64
105
  end
65
106
  end
66
107
  end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+
6
+ module TempmailSdk
7
+ module Providers
8
+ # mail.td 渠道实现(基于 SHA-256 Proof-of-Work 的临时邮箱服务)
9
+ #
10
+ # 流程:
11
+ # 1. GET /api/domains 获取可用域名(过滤 pro_only)
12
+ # 2. 求解 PoW: SHA-256(address + timestamp + nonce) 需满足 difficulty 个前导零位
13
+ # 3. POST /api/accounts 携带 PoW 创建账户 → 返回 JWT + ID
14
+ # 4. GET /api/accounts/{id}/messages?page=1 携带 Bearer JWT 获取邮件
15
+ # token 格式: JSON {"jwt":"...","id":"..."}
16
+ module MailTd
17
+ CHANNEL = "mail-td"
18
+ API_BASE = "https://api.mail.td/api"
19
+ RAND_CHARS = ("a".."z").to_a + ("0".."9").to_a
20
+
21
+ HEADERS = {
22
+ "Accept" => "application/json",
23
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
24
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
25
+ }.freeze
26
+
27
+ module_function
28
+
29
+ # 生成随机小写字母 + 数字字符串
30
+ def random_string(len)
31
+ Array.new(len) { RAND_CHARS.sample }.join
32
+ end
33
+
34
+ # 检查哈希是否满足 difficulty 个前导零位
35
+ def leading_zero_bits?(hash_bytes, difficulty)
36
+ full = difficulty / 8
37
+ remain = difficulty % 8
38
+ (0...full).each { |i| return false if hash_bytes.getbyte(i) != 0 }
39
+ if remain.positive? && full < hash_bytes.bytesize
40
+ mask = (0xFF << (8 - remain)) & 0xFF
41
+ return false if (hash_bytes.getbyte(full) & mask) != 0
42
+ end
43
+ true
44
+ end
45
+
46
+ # 求解 Proof-of-Work
47
+ # 目标:SHA-256(address + timestamp + nonce) 前导零位 >= difficulty
48
+ def solve_pow(address, timestamp, difficulty)
49
+ base = "#{address}#{timestamp}"
50
+ nonce = 0
51
+ while nonce < 100_000_000
52
+ hash = Digest::SHA256.digest("#{base}#{nonce}")
53
+ return nonce.to_s if leading_zero_bits?(hash, difficulty)
54
+
55
+ nonce += 1
56
+ end
57
+ raise "mail-td: PoW 求解超时"
58
+ end
59
+
60
+ # 获取可用域名列表(过滤 pro_only)
61
+ def fetch_domains
62
+ resp = Http.get("#{API_BASE}/domains", headers: HEADERS)
63
+ raise "mail-td: 域名请求 HTTP #{resp.status_code}" unless resp.ok?
64
+
65
+ data = resp.json
66
+ domains = data["domains"] || []
67
+ free = domains.select { |d| d["pro_only"] == false && !d["domain"].to_s.empty? }
68
+ .map { |d| d["domain"] }
69
+ raise "mail-td: 无可用免费域名" if free.empty?
70
+
71
+ free
72
+ end
73
+
74
+ # 创建临时邮箱
75
+ # @return [EmailInfo]
76
+ def generate_email
77
+ domain = fetch_domains.sample
78
+ username = random_string(10)
79
+ address = "#{username}@#{domain}"
80
+ password = random_string(20)
81
+ auth_key = Digest::SHA256.hexdigest(password)
82
+
83
+ addr_lower = address.strip.downcase
84
+ difficulty = 15
85
+ pow_token = ""
86
+
87
+ 4.times do
88
+ timestamp = Time.now.to_i
89
+ nonce = solve_pow(addr_lower, timestamp, difficulty)
90
+ pow_obj = { "t" => timestamp, "n" => nonce, "d" => difficulty }
91
+ pow_obj["token"] = pow_token unless pow_token.empty?
92
+
93
+ resp = Http.post("#{API_BASE}/accounts",
94
+ headers: HEADERS.merge("Content-Type" => "application/json"),
95
+ json: { "address" => address, "auth_key" => auth_key, "pow" => pow_obj })
96
+
97
+ result = begin
98
+ resp.json
99
+ rescue StandardError
100
+ {}
101
+ end
102
+
103
+ if result["status"] == "retry"
104
+ rd = result["required_difficulty"]
105
+ difficulty = rd.is_a?(Numeric) ? rd.to_i : difficulty + 2
106
+ pow_token = result["token"].to_s
107
+ next
108
+ end
109
+
110
+ raise "mail-td: 创建账户 HTTP #{resp.status_code}: #{result['error']}" unless resp.ok?
111
+
112
+ result_addr = result["address"].to_s
113
+ result_token = result["token"].to_s
114
+ result_id = result["id"].to_s
115
+ raise "mail-td: 响应缺少必要字段" if result_addr.empty? || result_token.empty? || result_id.empty?
116
+
117
+ tok = JSON.generate("jwt" => result_token, "id" => result_id)
118
+ return EmailInfo.new(channel: CHANNEL, email: result_addr, token: tok)
119
+ end
120
+
121
+ raise "mail-td: PoW 重试次数超限"
122
+ end
123
+
124
+ # 获取邮件列表
125
+ # @param token [String] JSON {"jwt":"...","id":"..."}
126
+ # @param email [String]
127
+ # @return [Array<Email>]
128
+ def get_emails(token, email)
129
+ raise "mail-td: token 为空" if token.to_s.empty?
130
+
131
+ parsed = JSON.parse(token)
132
+ jwt = parsed["jwt"].to_s
133
+ id = parsed["id"].to_s
134
+ raise "mail-td: token 缺少 jwt 或 id" if jwt.empty? || id.empty?
135
+
136
+ resp = Http.get("#{API_BASE}/accounts/#{id}/messages?page=1",
137
+ headers: HEADERS.merge("Authorization" => "Bearer #{jwt}"))
138
+ raise "mail-td: 邮件请求 HTTP #{resp.status_code}" unless resp.ok?
139
+
140
+ data = resp.json
141
+ messages = data["messages"]
142
+ return [] unless messages.is_a?(Array) && !messages.empty?
143
+
144
+ messages.map do |msg|
145
+ from_addr = msg.dig("from", "address").to_s
146
+ Normalize.normalize_email({
147
+ "id" => msg["id"].to_s,
148
+ "from" => from_addr,
149
+ "to" => email,
150
+ "subject" => msg["subject"].to_s,
151
+ "text" => msg["text"].to_s,
152
+ "html" => msg["html"].to_s,
153
+ "created_at" => msg["created_at"].to_s
154
+ }, email)
155
+ end
156
+ rescue JSON::ParserError
157
+ raise "mail-td: token 格式无效"
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # mailcat.ai 渠道实现
6
+ #
7
+ # 流程:POST /mailboxes 创建邮箱(无 body),返回 email + token
8
+ # GET /inbox 获取邮件列表,需要 Authorization: Bearer {token}
9
+ module MailcatAi
10
+ CHANNEL = "mailcat-ai"
11
+ BASE_URL = "https://api.mailcat.ai"
12
+
13
+ HEADERS = {
14
+ "Accept" => "application/json",
15
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
16
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
17
+ }.freeze
18
+
19
+ module_function
20
+
21
+ # 创建 mailcat.ai 临时邮箱
22
+ # @return [EmailInfo]
23
+ def generate_email
24
+ resp = Http.post("#{BASE_URL}/mailboxes", headers: HEADERS, timeout: 15)
25
+ resp.raise_for_status
26
+ data = resp.json
27
+ email = data.dig("data", "email").to_s.strip
28
+ token = data.dig("data", "token").to_s.strip
29
+ raise "mailcat-ai: 响应缺少 email 或 token" if email.empty? || token.empty?
30
+
31
+ EmailInfo.new(channel: CHANNEL, email: email, token: token)
32
+ end
33
+
34
+ # 获取 mailcat.ai 邮件列表
35
+ # @param token [String] Bearer token
36
+ # @param email [String] 邮箱地址
37
+ # @return [Array<Email>]
38
+ def get_emails(token, email)
39
+ hdrs = HEADERS.merge("Authorization" => "Bearer #{token.to_s.strip}")
40
+ resp = Http.get("#{BASE_URL}/inbox", headers: hdrs, timeout: 15)
41
+ resp.raise_for_status
42
+ data = resp.json
43
+ items = data.is_a?(Hash) ? Array(data["data"]) : []
44
+ items.filter_map do |raw|
45
+ next unless raw.is_a?(Hash)
46
+
47
+ Normalize.normalize_email(raw.merge("to" => email), email)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -62,6 +62,55 @@ module TempmailSdk
62
62
  EmailInfo.new(channel: CHANNEL, email: email, token: email)
63
63
  end
64
64
 
65
+ # 通过详情接口获取单封邮件完整内容
66
+ # GET /api/email_content.php?id={id}
67
+ # 详情响应字段(从前端代码确认):
68
+ # content: 完整 HTML 正文
69
+ # subject / from_addr / date: 邮件元数据
70
+ # attachment: JSON 字符串数组 [{filename, path, size}]
71
+ def fetch_detail(id)
72
+ mid = id.to_s.strip
73
+ return nil if mid.empty?
74
+
75
+ begin
76
+ resp = Http.get("#{BASE}/api/email_content.php?id=#{URI.encode_www_form_component(mid)}",
77
+ headers: HEADERS, timeout: 15)
78
+ return nil if resp.status_code < 200 || resp.status_code >= 300
79
+
80
+ data = resp.body.empty? ? nil : resp.json
81
+ data.is_a?(Hash) ? data : nil
82
+ rescue StandardError
83
+ nil
84
+ end
85
+ end
86
+
87
+ # 解析详情接口的 attachment 字段(JSON 字符串)为附件数组
88
+ def parse_attachments(raw)
89
+ return [] if raw.nil? || !raw.is_a?(String) || raw.strip.empty?
90
+
91
+ begin
92
+ items = JSON.parse(raw)
93
+ rescue JSON::ParserError
94
+ return []
95
+ end
96
+ return [] unless items.is_a?(Array)
97
+
98
+ items.filter_map do |it|
99
+ next unless it.is_a?(Hash)
100
+
101
+ filename = (it["filename"] || "").to_s.strip
102
+ next if filename.empty?
103
+
104
+ entry = { "filename" => filename }
105
+ entry["size"] = it["size"].to_i if it["size"].is_a?(Numeric)
106
+ entry
107
+ end
108
+ end
109
+
110
+ # 获取邮件列表并对每封邮件补拉详情
111
+ # 1. GET /api/emails.php 拉取列表(仅含 description 摘要)
112
+ # 2. 对每封邮件 GET /api/email_content.php?id={id} 拉取详情(含 content 完整 HTML)
113
+ # 3. 详情失败时保留列表 description 作为回退
65
114
  # @param email [String]
66
115
  # @param token [String, nil]
67
116
  # @return [Array<Email>]
@@ -78,12 +127,38 @@ module TempmailSdk
78
127
  return [] unless rows.is_a?(Array)
79
128
 
80
129
  rows.select { |row| row.is_a?(Hash) }.map do |row|
130
+ mail_id = row["id"].to_s
131
+ desc = (row["description"] || "").strip
81
132
  ir = row["isRead"]
133
+ is_read = ir == true || ir == 1
134
+
135
+ from = (row["from_addr"] || "").strip
136
+ subject = (row["subject"] || "").strip
137
+ date = cx_date_to_iso(row["date"] || "")
138
+ html = ""
139
+ attachments = []
140
+
141
+ # 拉取详情覆盖 html/from/subject/date/attachments
142
+ unless mail_id.empty?
143
+ detail = fetch_detail(mail_id)
144
+ if detail
145
+ d_content = detail["content"]
146
+ html = d_content if d_content.is_a?(String) && !d_content.strip.empty?
147
+ d_from = detail["from_addr"]
148
+ from = d_from.strip if d_from.is_a?(String) && !d_from.strip.empty?
149
+ d_subj = detail["subject"]
150
+ subject = d_subj.strip if d_subj.is_a?(String) && !d_subj.strip.empty?
151
+ d_date = detail["date"]
152
+ date = cx_date_to_iso(d_date) if d_date.is_a?(String) && !d_date.strip.empty?
153
+ attachments = parse_attachments(detail["attachment"])
154
+ end
155
+ end
156
+
82
157
  Email.new(
83
- id: row["id"].to_s, from_addr: (row["from_addr"] || "").strip, to: addr,
84
- subject: (row["subject"] || "").strip, text: (row["description"] || "").strip,
85
- html: "", date: cx_date_to_iso(row["date"] || ""),
86
- is_read: ir == true || ir == 1, attachments: []
158
+ id: mail_id, from_addr: from, to: addr,
159
+ subject: subject, text: desc,
160
+ html: html, date: date,
161
+ is_read: is_read, attachments: attachments
87
162
  )
88
163
  end
89
164
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # mailgolem.com 渠道实现
8
+ #
9
+ # 流程:GET / 获取 session cookie + CSRF token → GET /random-email-address 创建邮箱
10
+ # GET / 重新获取 session + CSRF → POST /fetch-emails/{email} 获取邮件列表
11
+ # token 存储 CSRF token 值(收信时重新建立 session)
12
+ module Mailgolem
13
+ CHANNEL = "mailgolem"
14
+ BASE_URL = "https://mailgolem.com"
15
+
16
+ BROWSER_HEADERS = {
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
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif," \
20
+ "image/webp,image/apng,*/*;q=0.8",
21
+ "Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7"
22
+ }.freeze
23
+
24
+ CSRF_RE = /<input[^>]+name="_token"[^>]+id="token"[^>]+value="([^"]*)"/im
25
+
26
+ module_function
27
+
28
+ # 从首页 HTML 提取 CSRF token
29
+ # @param html [String]
30
+ # @return [String]
31
+ def extract_csrf(html)
32
+ m = html.match(CSRF_RE)
33
+ m ? m[1].strip : ""
34
+ end
35
+
36
+ # 访问首页建立 session 并提取 CSRF token
37
+ # @return [String] csrf token
38
+ def fetch_csrf
39
+ resp = Http.get(BASE_URL + "/", headers: BROWSER_HEADERS, timeout: 15)
40
+ resp.raise_for_status
41
+ csrf = extract_csrf(resp.body)
42
+ raise "mailgolem: 未能从首页提取 CSRF token" if csrf.empty?
43
+
44
+ csrf
45
+ end
46
+
47
+ # 创建 mailgolem.com 临时邮箱
48
+ # @return [EmailInfo]
49
+ def generate_email
50
+ csrf = fetch_csrf
51
+ resp = Http.get(
52
+ BASE_URL + "/random-email-address",
53
+ headers: BROWSER_HEADERS.merge("Referer" => BASE_URL + "/"),
54
+ timeout: 15
55
+ )
56
+ resp.raise_for_status
57
+ email = resp.body.to_s.strip
58
+ raise "mailgolem: 获取到的邮箱地址无效" if email.empty? || !email.include?("@")
59
+
60
+ EmailInfo.new(channel: CHANNEL, email: email, token: csrf)
61
+ end
62
+
63
+ # 获取 mailgolem.com 邮件列表
64
+ # @param email [String]
65
+ # @param token [String] 旧 CSRF token(收信时重新建立 session)
66
+ # @return [Array<Email>]
67
+ def get_emails(email, token)
68
+ _ = token
69
+ csrf = fetch_csrf
70
+ form = URI.encode_www_form("_token" => csrf)
71
+ fetch_url = "#{BASE_URL}/fetch-emails/#{URI.encode_uri_component(email)}"
72
+ resp = Http.post(
73
+ fetch_url,
74
+ headers: BROWSER_HEADERS.merge(
75
+ "Content-Type" => "application/x-www-form-urlencoded",
76
+ "X-Requested-With" => "XMLHttpRequest",
77
+ "Accept" => "application/json, text/plain, */*",
78
+ "Referer" => BASE_URL + "/",
79
+ "Origin" => BASE_URL
80
+ ),
81
+ body: form,
82
+ timeout: 15
83
+ )
84
+ resp.raise_for_status
85
+ items = resp.json
86
+ return [] unless items.is_a?(Array)
87
+
88
+ items.filter_map do |item|
89
+ next unless item.is_a?(Hash)
90
+
91
+ Normalize.normalize_email(item.merge("to" => email), email)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -136,13 +136,20 @@ module TempmailSdk
136
136
 
137
137
  # 将邮件摘要与正文/附件展平为 normalize 可识别的字段
138
138
  def flatten_message(summary, recipient_email, text_payload, html_payload, att_payload)
139
+ # 优先读 "text" 键(/text 端点实际返回键),回退兜底 "text/plain"(防御性编程)
140
+ text_content = text_from_payload(text_payload, "text")
141
+ text_content = text_from_payload(text_payload, "text/plain") if text_content.empty?
142
+ # 优先读 "html" 键,回退兜底 "text/html"
143
+ html_content = text_from_payload(html_payload, "html")
144
+ html_content = text_from_payload(html_payload, "text/html") if html_content.empty?
145
+
139
146
  {
140
147
  "id" => (summary["id"] || summary["messageId"] || "").to_s,
141
148
  "from" => first_non_empty(summary["from"], summary["origfrom"]),
142
149
  "to" => summary["to"] || recipient_email,
143
150
  "subject" => summary["subject"] || "",
144
- "text" => text_from_payload(text_payload, "text/plain"),
145
- "html" => text_from_payload(html_payload, "text/html"),
151
+ "text" => text_content,
152
+ "html" => html_content,
146
153
  "date" => to_iso_time(summary["time"] || summary["date"]),
147
154
  "seen" => false,
148
155
  "attachments" => build_attachments(att_payload)
@@ -12,7 +12,7 @@ module TempmailSdk
12
12
  # 2. 本地随机 10 位 [a-z0-9] local part,拼接为 <local>@<域名>
13
13
  # 3. GET /api/mail/messages?to=<email>&page=1&limit=20 -> {"emails": [...], ...}
14
14
  #
15
- # 16 个域名变体渠道(如 16888888.cyou、sbook.pics 等)复用同一后端,
15
+ # 域名变体渠道(如 17666688.shop、sbook.pics 等)复用同一后端,
16
16
  # 仅在 generate_email(domain) 传入固定域名即可,收信逻辑完全一致。
17
17
  module Mailmomy
18
18
  CHANNEL = "mailmomy"