tempmail_sdk 1.3.4 → 1.3.6

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/tempmail_sdk/channels.rb +52 -27
  4. data/lib/tempmail_sdk/channels_data.rb +52 -27
  5. data/lib/tempmail_sdk/providers/clawdemail.rb +122 -0
  6. data/lib/tempmail_sdk/providers/crazymailing.rb +114 -0
  7. data/lib/tempmail_sdk/providers/duckmail.rb +9 -2
  8. data/lib/tempmail_sdk/providers/firetempmail.rb +86 -0
  9. data/lib/tempmail_sdk/providers/flybymail.rb +93 -0
  10. data/lib/tempmail_sdk/providers/generator_email.rb +146 -0
  11. data/lib/tempmail_sdk/providers/huskmail.rb +132 -0
  12. data/lib/tempmail_sdk/providers/internxt.rb +176 -0
  13. data/lib/tempmail_sdk/providers/linshi_xyz.rb +79 -0
  14. data/lib/tempmail_sdk/providers/mail_tm.rb +9 -2
  15. data/lib/tempmail_sdk/providers/mailmomy.rb +1 -1
  16. data/lib/tempmail_sdk/providers/mailticking.rb +139 -0
  17. data/lib/tempmail_sdk/providers/mtempmail.rb +140 -0
  18. data/lib/tempmail_sdk/providers/nowtempmail.rb +112 -0
  19. data/lib/tempmail_sdk/providers/noxen_de5_net.rb +392 -0
  20. data/lib/tempmail_sdk/providers/nukemail.rb +188 -0
  21. data/lib/tempmail_sdk/providers/nullmail.rb +95 -0
  22. data/lib/tempmail_sdk/providers/shadowmail.rb +174 -0
  23. data/lib/tempmail_sdk/providers/shitpost_email.rb +85 -0
  24. data/lib/tempmail_sdk/providers/smails.rb +128 -0
  25. data/lib/tempmail_sdk/providers/temp_mail_gg.rb +362 -0
  26. data/lib/tempmail_sdk/providers/tempmail100.rb +148 -0
  27. data/lib/tempmail_sdk/providers/tempmail_ee.rb +373 -0
  28. data/lib/tempmail_sdk/providers/tempmailportal.rb +131 -0
  29. data/lib/tempmail_sdk/providers/tempmails_io.rb +89 -0
  30. data/lib/tempmail_sdk/providers/tempmailto.rb +312 -0
  31. data/lib/tempmail_sdk/providers/temporarymail_com.rb +184 -0
  32. data/lib/tempmail_sdk/providers/tenmin_app.rb +100 -0
  33. data/lib/tempmail_sdk/providers/thirty_min_email.rb +90 -0
  34. data/lib/tempmail_sdk/providers/tmpkit.rb +129 -0
  35. data/lib/tempmail_sdk/providers/zerodrop.rb +90 -0
  36. data/lib/tempmail_sdk/registry.rb +223 -107
  37. data/lib/tempmail_sdk/types.rb +13 -0
  38. data/lib/tempmail_sdk/version.rb +1 -1
  39. data/lib/tempmail_sdk.rb +1 -1
  40. metadata +31 -7
  41. data/lib/tempmail_sdk/providers/gonebox_email.rb +0 -89
  42. data/lib/tempmail_sdk/providers/mohmal.rb +0 -208
  43. data/lib/tempmail_sdk/providers/temp_mail_now.rb +0 -103
  44. 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
@@ -25,7 +25,7 @@ module TempmailSdk
25
25
  resp.raise_for_status
26
26
  data = resp.json
27
27
  members = data.is_a?(Array) ? data : (data["hydra:member"] || [])
28
- members.select { |d| d["isActive"] }.map { |d| d["domain"] }
28
+ members.select { |d| d["isActive"] || d["isVerified"] }.map { |d| d["domain"] }
29
29
  end
30
30
 
31
31
  def create_account(address, password)
@@ -48,7 +48,14 @@ module TempmailSdk
48
48
  domains = fetch_domains
49
49
  raise "No available domains" if domains.empty?
50
50
 
51
- address = "#{random_string(12)}@#{domains.sample}"
51
+ generate_email_at_domain(domains.sample)
52
+ end
53
+
54
+ # 为 mail.tm 指定域名的镜像渠道创建账号(跳过随机选域)
55
+ # @param domain [String]
56
+ # @return [EmailInfo]
57
+ def generate_email_at_domain(domain)
58
+ address = "#{random_string(12)}@#{domain}"
52
59
  password = random_string(16)
53
60
  account = create_account(address, password)
54
61
  token = fetch_token(address, password)
@@ -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"
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # Mailticking 渠道实现(www.mailticking.com,旧域名 temporary-mail.net 的更名站)
6
+ #
7
+ # 实测协议(三类请求均以 application/json 交流,站点在 Cloudflare 后):
8
+ # 1. 建箱 POST /get-mailbox body {"types":["4"]}(4=独立域名,排除 Gmail 别名)
9
+ # 响应 {"success":true,"email":"xxx@domain","activate_token":"..."}
10
+ # 2. 激活 POST /activate-email body {"email":..,"source":"api","activate_token":..}
11
+ # 响应 {"success":true}
12
+ # 3. 列信 POST /get-emails?lang=en body {"email":..,"code":..}
13
+ # 空箱实测响应 {"emails":[],"success":true};空闲超时/被改绑后返回
14
+ # {"success":false,"needNewEmail":true,...},此时返回错误提示换箱。
15
+ #
16
+ # 读信正文无公开端点:GetEmails 只具备列表能力,列表字段名采用多候选提取
17
+ # 策略,命中多少映射多少(Normalize 既有候选字段负责兜底)。
18
+ module Mailticking
19
+ CHANNEL = "mailticking"
20
+ BASE_URL = "https://www.mailticking.com"
21
+
22
+ HEADERS = {
23
+ "Accept" => "application/json",
24
+ "Accept-Language" => "en-US,en;q=0.9",
25
+ "Content-Type" => "application/json",
26
+ "Referer" => "#{BASE_URL}/",
27
+ "Origin" => BASE_URL,
28
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
29
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
30
+ }.freeze
31
+
32
+ module_function
33
+
34
+ # 执行 JSON POST 请求并解析响应;响应非 2xx 或 success=false 时抛错
35
+ # @param path [String] 请求路径
36
+ # @param payload [Hash] JSON 请求体
37
+ # @return [Hash]
38
+ def do_post(path, payload)
39
+ resp = Http.post("#{BASE_URL}#{path}", headers: HEADERS, json: payload, timeout: 15)
40
+ data = resp.json
41
+ unless resp.ok?
42
+ msg = data.is_a?(Hash) ? (data["error"] || data["message"]).to_s : ""
43
+ msg = resp.body.to_s if msg.empty?
44
+ raise "mailticking: http #{resp.status_code}: #{msg}"
45
+ end
46
+ unless data.is_a?(Hash) && data["success"] == true
47
+ msg = data.is_a?(Hash) ? (data["error"] || data["message"]).to_s : ""
48
+ msg = "unknown error" if msg.empty?
49
+ raise "mailticking: 请求失败: #{msg}"
50
+ end
51
+ data
52
+ end
53
+
54
+ # 创建 mailticking 邮箱账号
55
+ # 请求 type=4(独立域名)固定取独立域名邮箱,建箱后立即激活会话;
56
+ # token 必须携带 activate_token(列信协议依赖激活会话),不能为空。
57
+ # @return [EmailInfo]
58
+ def generate_email
59
+ box = do_post("/get-mailbox", { "types" => ["4"] })
60
+ token = box["code"].to_s.strip
61
+ token = box["email"].to_s.strip if token.empty?
62
+ email = box["email"].to_s.strip
63
+ if token.empty? || email.empty?
64
+ raise "mailticking: get-mailbox 返回空 email/activate_token"
65
+ end
66
+
67
+ # 激活邮箱,使后续列信请求与服务器记录的最新会话一致
68
+ do_post("/activate-email", {
69
+ "email" => email,
70
+ "source" => "api",
71
+ "activate_token" => token
72
+ })
73
+
74
+ EmailInfo.new(channel: CHANNEL, email: email, token: token)
75
+ end
76
+
77
+ # 获取 mailticking 邮箱的邮件列表
78
+ # 空箱返回空列表不报错;需要换箱的响应(needNewEmail)返回语义错误。
79
+ # @param email [String] 邮箱地址
80
+ # @param token [String] 建箱返回的激活令牌
81
+ # @return [Array<Email>]
82
+ def get_emails(email, token)
83
+ addr = email.to_s.strip
84
+ tk = token.to_s.strip
85
+ raise "mailticking: 邮箱地址为空" if addr.empty?
86
+ raise "mailticking: activate code 为空" if tk.empty?
87
+
88
+ resp = Http.post("#{BASE_URL}/get-emails?lang=en",
89
+ headers: HEADERS,
90
+ json: { "email" => addr, "code" => tk },
91
+ timeout: 15)
92
+ data = resp.json
93
+ unless resp.ok?
94
+ msg = data.is_a?(Hash) ? (data["error"] || data["message"]).to_s : ""
95
+ msg = resp.body.to_s if msg.empty?
96
+ raise "mailticking: http #{resp.status_code}: #{msg}"
97
+ end
98
+ unless data.is_a?(Hash) && data["success"] == true
99
+ raise "mailticking: 邮箱已过期,请重新建箱" if data.is_a?(Hash) && data["needNewEmail"] == true
100
+
101
+ raise "mailticking: get-emails 失败"
102
+ end
103
+ emails = data["emails"]
104
+ return [] unless emails.is_a?(Array)
105
+
106
+ emails.filter_map do |raw|
107
+ next unless raw.is_a?(Hash)
108
+
109
+ Normalize.normalize_email(migrated_fields(raw), addr)
110
+ end
111
+ end
112
+
113
+ # 尽量映射列表字段到统一字段名
114
+ # 官网首页表格只有 SENDER/SUBJECT/TIME 三列,字段全名缺少可观测证据,
115
+ # 因此对常见字段做多候选提取;未命中的字段留给 Normalize 自己处理。
116
+ # @param raw [Hash]
117
+ # @return [Hash]
118
+ def migrated_fields(raw)
119
+ m = raw.dup
120
+ unless m.key?("from")
121
+ %w[mail_from from_mail from_email sender_address from_address
122
+ send_addr mail_addr address_from ho_from fromname fromS].each do |key|
123
+ v = m[key]
124
+ if v.is_a?(String) && v.strip != ""
125
+ m["from"] = v
126
+ break
127
+ end
128
+ end
129
+ end
130
+ if !m.key?("sender") && m["from"].is_a?(String) && m["from"].strip != ""
131
+ m["sender"] = m["from"]
132
+ end
133
+ m["id"] = m["mail_id"] if !m.key?("id") && m.key?("mail_id")
134
+ m["date"] = m["received_at"] if !m.key?("date") && m.key?("received_at")
135
+ m
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # Mtempmail 渠道实现(mtempmail.com,公共 key 认证)
8
+ #
9
+ # 建箱: POST /api/emails/{apiKey}(空 JSON body)→
10
+ # {"status":true,"data":{"email":"...@domain","domain":"..","ip":"..",
11
+ # "fingerprint":"..","expire_at":"..","created_at":"..","id":..,"email_token":"..."}}
12
+ # 读信: GET /api/messages/{apiKey}/{email} →
13
+ # {"status":true,"mailbox":"..","email_token":"..","messages":[...]}
14
+ # 消息为 mailgun 入站 webhook 风格:to/from 为对象数组,
15
+ # body 为 [{content_type:"text/html",value:".."},...] 分段。
16
+ # 邮箱 24 小时有效(过期时间标注不一致,以服务端为准)。
17
+ module Mtempmail
18
+ CHANNEL = "mtempmail"
19
+ BASE_URL = "https://mtempmail.com"
20
+
21
+ # 公共固定 API key(mtempmail.com 官方提供)
22
+ PUBLIC_KEY = "pub_nRn1hUwpdmZvxQNVWDfoXgKyF7dIm9nRIIIt1qDw"
23
+
24
+ HEADERS = {
25
+ "Accept" => "application/json",
26
+ "User-Agent" => "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
+ }.freeze
29
+
30
+ # 剥离后台拼接的 "• " 前缀(或实收分隔符)
31
+ BULLET_RE = /^[•·]+\s*/.freeze
32
+
33
+ module_function
34
+
35
+ # 清洗主题前导分隔符(后台可能以 "• " 开头拼接微件)
36
+ # @param subject [String]
37
+ # @return [String]
38
+ def clean_subject(subject)
39
+ subject.to_s.strip.sub(BULLET_RE, "")
40
+ end
41
+
42
+ # 拼接正文纯文本(body[].value 按序)
43
+ # @param parts [Array]
44
+ # @return [String]
45
+ def body_text(parts)
46
+ parts.filter_map { |p| p["value"].to_s unless p.nil? || !p.is_a?(Hash) || p["value"].to_s.empty? }
47
+ .join("\n")
48
+ end
49
+
50
+ # 提取首个 text/html 段
51
+ # @param parts [Array]
52
+ # @return [String]
53
+ def body_html(parts)
54
+ parts.each do |p|
55
+ next unless p.is_a?(Hash) && p["content_type"].to_s == "text/html"
56
+
57
+ val = p["value"].to_s
58
+ return val unless val.empty?
59
+ end
60
+ ""
61
+ end
62
+
63
+ # 创建 mtempmail 临时邮箱
64
+ # POST /api/emails/{apiKey}(空 JSON body)
65
+ # @return [EmailInfo]
66
+ def generate_email
67
+ resp = Http.post("#{BASE_URL}/api/emails/#{PUBLIC_KEY}",
68
+ headers: HEADERS.merge("Content-Type" => "application/json"),
69
+ json: {}, timeout: 15)
70
+ resp.raise_for_status
71
+ data = resp.json
72
+ payload = data.is_a?(Hash) ? data["data"] : nil
73
+ email = payload.is_a?(Hash) ? payload["email"].to_s.strip : ""
74
+ if (data.is_a?(Hash) && !data["status"]) || email.empty?
75
+ raise "mtempmail: 建箱响应缺少邮箱"
76
+ end
77
+
78
+ EmailInfo.new(channel: CHANNEL, email: email,
79
+ token: payload["email_token"].to_s,
80
+ expires_at: payload["expire_at"],
81
+ created_at: payload["created_at"])
82
+ end
83
+
84
+ # 读取 mtempmail 收件箱
85
+ # GET /api/messages/{apiKey}/{email};token 元数据仅为校验,不参与请求
86
+ # @param email [String] 邮箱地址
87
+ # @param token [String] email_token(仅校验非空)
88
+ # @return [Array<Email>]
89
+ def get_emails(email, token)
90
+ addr = email.to_s.strip
91
+ raise "mtempmail: 邮箱为空" if addr.empty?
92
+ raise "mtempmail: token 为空" if token.to_s.strip.empty?
93
+
94
+ uri = "#{BASE_URL}/api/messages/#{PUBLIC_KEY}/#{URI.encode_www_form_component(addr)}"
95
+ resp = Http.get(uri, headers: HEADERS, timeout: 15)
96
+ resp.raise_for_status
97
+ data = resp.json
98
+ messages = data.is_a?(Hash) ? data["messages"] : nil
99
+ return [] unless messages.is_a?(Array)
100
+
101
+ messages.filter_map do |m|
102
+ next unless m.is_a?(Hash)
103
+
104
+ to_str = flatten_contact(m["to"])
105
+ row = {
106
+ "id" => m["id"],
107
+ "from" => flatten_contact(m["from"]),
108
+ "to" => to_str.empty? ? addr : to_str,
109
+ "subject" => clean_subject(m["subject"]),
110
+ "date" => m["created_at"] || m["date"]
111
+ }
112
+ body_parts = m["body"]
113
+ if body_parts.is_a?(Array)
114
+ parts = body_parts.select { |p| p.is_a?(Hash) }
115
+ text = body_text(parts)
116
+ row["text"] = text unless text.empty?
117
+ html_part = body_html(parts)
118
+ row["html"] = html_part unless html_part.empty?
119
+ end
120
+ Normalize.normalize_email(row, addr)
121
+ end
122
+ end
123
+
124
+ # 将 mailgun 风格 [{"full":"Sender <a@b.com>"},{...}] 展平为字符串
125
+ # 优先取 full 字段,否则回退 address;空列表返回空串
126
+ # @param contacts [Object]
127
+ # @return [String]
128
+ def flatten_contact(contacts)
129
+ return contacts.to_s unless contacts.is_a?(Array)
130
+ return "" if contacts.empty?
131
+
132
+ first = contacts.first
133
+ return "" unless first.is_a?(Hash)
134
+
135
+ full = first["full"].to_s.strip
136
+ full.empty? ? first["address"].to_s.strip : full
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # NowtempMail 渠道实现(nowtempmail.com)
8
+ #
9
+ # POST /mailbox 建箱(无 body 或 {},响应 token(JWT)/mailbox),
10
+ # GET /messages 读信列表(Header Authorization: Bearer <token>,
11
+ # 响应 {"messages":[...]}),GET /message/{id} 取单封详情(Bearer)。
12
+ # 列表元素按多候选字段归一;详情拉取失败时以列表摘要兜底。
13
+ module Nowtempmail
14
+ CHANNEL = "nowtempmail"
15
+ BASE_URL = "https://nowtempmail.com"
16
+
17
+ HEADERS = {
18
+ "Accept" => "application/json",
19
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
20
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
21
+ }.freeze
22
+
23
+ RETRY_KEYS = %w[id Id slug messageId message_id].freeze
24
+
25
+ module_function
26
+
27
+ # 设置 nowtempmail 请求的通用请求头(可选附带 Bearer token)
28
+ # @param token [String] 认证令牌
29
+ # @return [Hash]
30
+ def auth_headers(token)
31
+ hdrs = HEADERS.dup
32
+ hdrs["Authorization"] = "Bearer #{token}" unless token.to_s.empty?
33
+ hdrs
34
+ end
35
+
36
+ # 创建 nowtempmail.com 临时邮箱
37
+ # POST /mailbox(空 body)返回 token(JWT)与 mailbox 地址。
38
+ # @return [EmailInfo]
39
+ def generate_email
40
+ resp = Http.post("#{BASE_URL}/mailbox",
41
+ headers: auth_headers("").merge("Content-Type" => "application/json"),
42
+ timeout: 15)
43
+ raise "nowtempmail: 创建邮箱失败 http #{resp.status_code}" unless resp.ok?
44
+
45
+ data = resp.json
46
+ raise "nowtempmail: 创建响应非对象" unless data.is_a?(Hash)
47
+
48
+ token = data["token"].to_s.strip
49
+ mailbox = data["mailbox"].to_s.strip
50
+ raise "nowtempmail: 创建邮箱响应缺少必要字段" if token.empty? || mailbox.empty? || !mailbox.include?("@")
51
+
52
+ EmailInfo.new(channel: CHANNEL, email: mailbox, token: token)
53
+ end
54
+
55
+ # 获取 nowtempmail.com 邮件列表
56
+ # 流程:GET /messages 取列表,对每个元素按 id 逐封 GET /message/{id} 合并详情;
57
+ # 详情失败时以列表摘要归一。
58
+ # @param email [String] 邮箱地址
59
+ # @param token [String] 建箱返回的 JWT 认证令牌
60
+ # @return [Array<Email>]
61
+ def get_emails(email, token)
62
+ addr = email.to_s.strip
63
+ resp = Http.get("#{BASE_URL}/messages", headers: auth_headers(token), timeout: 15)
64
+ raise "nowtempmail: 获取邮件列表失败 http #{resp.status_code}" unless resp.ok?
65
+
66
+ data = resp.json
67
+ messages = data.is_a?(Hash) ? data["messages"] : nil
68
+ return [] unless messages.is_a?(Array)
69
+
70
+ messages.filter_map do |m|
71
+ next unless m.is_a?(Hash)
72
+
73
+ detail = get_detail(token, message_id_of(m))
74
+ row = m.dup
75
+ if detail.is_a?(Hash)
76
+ detail.each { |k, v| row[k] = v unless row.key?(k) }
77
+ end
78
+ Normalize.normalize_email(row, addr)
79
+ end
80
+ end
81
+
82
+ # 从列表元素中提取邮件 ID(候选键 id/Id/slug/messageId/message_id)
83
+ # @param row [Hash]
84
+ # @return [String]
85
+ def message_id_of(row)
86
+ RETRY_KEYS.each do |key|
87
+ val = row[key]
88
+ return val.to_s.strip unless val.nil? || val.to_s.strip.empty?
89
+ end
90
+ ""
91
+ end
92
+
93
+ # 获取 nowtempmail.com 单封邮件详情
94
+ # GET /message/{id}(Bearer token),响应为单封对象,含 text/html 等完整字段。
95
+ # @param token [String] JWT
96
+ # @param id [String] 邮件 ID
97
+ # @return [Hash, nil]
98
+ def get_detail(token, id)
99
+ return nil if id.to_s.empty?
100
+
101
+ uri = "#{BASE_URL}/message/#{URI.encode_www_form_component(id)}"
102
+ resp = Http.get(uri, headers: auth_headers(token), timeout: 15)
103
+ return nil unless resp.ok?
104
+
105
+ detail = resp.json
106
+ detail.is_a?(Hash) ? detail : nil
107
+ rescue StandardError
108
+ nil
109
+ end
110
+ end
111
+ end
112
+ end