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,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # ThrowawayMail 渠道 — https://throwawaymail.app
8
+ # POST /api/mailboxes 创建邮箱,GET /api/mailboxes/{id}/messages 获取邮件
9
+ module Throwawaymail
10
+ CHANNEL = "throwawaymail"
11
+ API_BASE = "https://throwawaymail.app/api"
12
+ HEADERS = { "Accept" => "application/json" }.freeze
13
+
14
+ module_function
15
+
16
+ # @return [EmailInfo]
17
+ def generate_email
18
+ resp = Http.post("#{API_BASE}/mailboxes", headers: HEADERS, timeout: 15)
19
+ resp.raise_for_status
20
+ data = resp.json
21
+ raise "throwawaymail: 创建邮箱响应无效" unless data.is_a?(Hash)
22
+
23
+ mailbox_id = (data["mailbox_id"] || "").strip
24
+ email = (data["address"] || "").strip
25
+ raise "throwawaymail: 创建邮箱响应无效" if mailbox_id.empty? || email.empty? || !email.include?("@")
26
+
27
+ EmailInfo.new(channel: CHANNEL, email: email, token: mailbox_id,
28
+ expires_at: data["expires_at"], created_at: data["created_at"])
29
+ end
30
+
31
+ # @param token [String] mailbox_id
32
+ # @param email [String]
33
+ # @return [Array<Email>]
34
+ def get_emails(token, email)
35
+ mailbox_id = (token || "").strip
36
+ address = (email || "").strip
37
+ raise "throwawaymail: mailbox_id 为空" if mailbox_id.empty?
38
+ raise "throwawaymail: 邮箱地址为空" if address.empty?
39
+
40
+ resp = Http.get("#{API_BASE}/mailboxes/#{URI.encode_www_form_component(mailbox_id)}/messages",
41
+ headers: HEADERS, timeout: 15)
42
+ resp.raise_for_status
43
+ rows = resp.json
44
+ return [] unless rows.is_a?(Array)
45
+
46
+ rows.filter_map do |item|
47
+ next unless item.is_a?(Hash)
48
+
49
+ message_id = (item["message_id"] || "").strip
50
+ detail = nil
51
+ unless message_id.empty?
52
+ begin
53
+ dr = Http.get("#{API_BASE}/mailboxes/#{URI.encode_www_form_component(mailbox_id)}/messages/#{URI.encode_www_form_component(message_id)}",
54
+ headers: HEADERS, timeout: 15)
55
+ detail = dr.json if dr.ok? && dr.json.is_a?(Hash)
56
+ rescue StandardError
57
+ # 忽略
58
+ end
59
+ end
60
+
61
+ src = detail || item
62
+ to_addresses = src["to_addresses"]
63
+ to_val = to_addresses.is_a?(Array) && !to_addresses.empty? ? to_addresses[0] : address
64
+
65
+ raw = {
66
+ "id" => src["message_id"],
67
+ "from_address" => src["from_address"],
68
+ "fromName" => src["from_name"],
69
+ "to" => to_val,
70
+ "subject" => src["subject"] || "",
71
+ "received_at" => src["received_at"],
72
+ "text" => src["text"] || "",
73
+ "html" => src["html"] || ""
74
+ }
75
+ Normalize.normalize_email(raw, address)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # tmail.link 临时邮箱渠道(Django CSRF)
8
+ # GET / 获取首页 HTML,正则提取邮箱地址
9
+ # GET /inbox/{email}/ 获取 csrftoken cookie
10
+ # POST /inbox/{email}/ 获取邮件列表(form: format=json + csrfmiddlewaretoken)
11
+ module TmailLink
12
+ CHANNEL = "tmail-link"
13
+ BASE_URL = "https://tmail.link"
14
+
15
+ EMAIL_RE = /([a-zA-Z0-9._%+\-]+@tmail\.link)/
16
+
17
+ UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
18
+
19
+ module_function
20
+
21
+ def browser_headers
22
+ {
23
+ "User-Agent" => UA,
24
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
25
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8"
26
+ }
27
+ end
28
+
29
+ def post_headers(email, token)
30
+ {
31
+ "User-Agent" => UA,
32
+ "Accept" => "application/json, text/javascript, */*; q=0.01",
33
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
34
+ "X-Requested-With" => "XMLHttpRequest",
35
+ "Content-Type" => "application/x-www-form-urlencoded; charset=UTF-8",
36
+ "Origin" => BASE_URL,
37
+ "Referer" => "#{BASE_URL}/inbox/#{email}/",
38
+ "Cookie" => "csrftoken=#{token}"
39
+ }
40
+ end
41
+
42
+ # @return [EmailInfo]
43
+ def generate_email
44
+ resp = Http.get("#{BASE_URL}/", headers: browser_headers)
45
+ resp.raise_for_status
46
+
47
+ m = resp.body.match(EMAIL_RE)
48
+ raise "tmail-link: 未能从首页提取邮箱地址" unless m
49
+
50
+ email = m[1].strip
51
+ raise "tmail-link: 提取的邮箱地址为空" if email.empty?
52
+
53
+ resp2 = Http.get("#{BASE_URL}/inbox/#{email}/", headers: browser_headers)
54
+ resp2.raise_for_status
55
+
56
+ token = resp2.cookie_value("csrftoken")
57
+ raise "tmail-link: 未能获取 csrftoken" if token.nil? || token.empty?
58
+
59
+ EmailInfo.new(channel: CHANNEL, email: email, token: token)
60
+ end
61
+
62
+ # @param email [String]
63
+ # @param token [String]
64
+ # @return [Array<Email>]
65
+ def get_emails(email, token)
66
+ addr = (email || "").strip
67
+ raise "tmail-link: 邮箱地址为空" if addr.empty?
68
+
69
+ csrf = (token || "").strip
70
+ raise "tmail-link: csrftoken 为空" if csrf.empty?
71
+
72
+ body = "format=json&csrfmiddlewaretoken=#{URI.encode_www_form_component(csrf)}"
73
+ resp = Http.post("#{BASE_URL}/inbox/#{addr}/",
74
+ headers: post_headers(addr, csrf), body: body)
75
+ resp.raise_for_status
76
+
77
+ data = resp.json
78
+ raise "tmail-link: 邮件列表响应格式异常" unless data.is_a?(Hash)
79
+
80
+ messages = data["messages"]
81
+ return [] unless messages.is_a?(Array) && !messages.empty?
82
+
83
+ messages.filter_map do |item|
84
+ next unless item.is_a?(Hash)
85
+
86
+ raw = {
87
+ "id" => (item["key"] || "").to_s,
88
+ "from" => (item["sender"] || "").to_s,
89
+ "to" => addr,
90
+ "subject" => (item["subject"] || "").to_s,
91
+ "date" => (item["date"] || "").to_s
92
+ }
93
+ Normalize.normalize_email(raw, addr)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # UnCorreoTemporal 渠道 — https://uncorreotemporal.com
8
+ # POST /api/v1/mailboxes 创建邮箱,GET /api/v1/mailboxes/{email}/messages 获取邮件
9
+ module Uncorreotemporal
10
+ CHANNEL = "uncorreotemporal"
11
+ API_BASE = "https://uncorreotemporal.com/api/v1"
12
+ HEADERS = {
13
+ "Accept" => "application/json",
14
+ "Origin" => "https://uncorreotemporal.com",
15
+ "Referer" => "https://uncorreotemporal.com/",
16
+ "User-Agent" => "Mozilla/5.0"
17
+ }.freeze
18
+
19
+ module_function
20
+
21
+ def flatten(raw, recipient)
22
+ {
23
+ "id" => raw["id"] || "",
24
+ "from" => raw["from_address"] || "",
25
+ "to" => raw["to_address"] || recipient,
26
+ "subject" => raw["subject"] || "",
27
+ "text" => raw["body_text"] || "",
28
+ "html" => raw["body_html"] || "",
29
+ "date" => raw["received_at"] || "",
30
+ "isRead" => !!raw["is_read"],
31
+ "attachments" => raw["attachments"].is_a?(Array) ? raw["attachments"] : []
32
+ }
33
+ end
34
+
35
+ # @return [EmailInfo]
36
+ def generate_email
37
+ resp = Http.post("#{API_BASE}/mailboxes",
38
+ headers: HEADERS.merge("Content-Type" => "application/json"), timeout: 15)
39
+ resp.raise_for_status
40
+ data = resp.json
41
+ raise "uncorreotemporal: 创建邮箱响应无效" unless data.is_a?(Hash)
42
+
43
+ email = (data["address"] || "").strip
44
+ token = (data["session_token"] || "").strip
45
+ raise "uncorreotemporal: 创建邮箱响应无效" if email.empty? || !email.include?("@") || token.empty?
46
+
47
+ EmailInfo.new(channel: CHANNEL, email: email, token: token, expires_at: data["expires_at"])
48
+ end
49
+
50
+ # @param token [String] session_token
51
+ # @param email [String]
52
+ # @return [Array<Email>]
53
+ def get_emails(token, email)
54
+ session_token = (token || "").strip
55
+ address = (email || "").strip
56
+ raise "uncorreotemporal: session_token 为空" if session_token.empty?
57
+ raise "uncorreotemporal: 邮箱地址为空" if address.empty?
58
+
59
+ resp = Http.get("#{API_BASE}/mailboxes/#{URI.encode_www_form_component(address)}/messages?limit=50",
60
+ headers: HEADERS.merge("X-Session-Token" => session_token), timeout: 15)
61
+ resp.raise_for_status
62
+ data = resp.json
63
+ rows = data.is_a?(Array) ? data : []
64
+
65
+ rows.filter_map do |row|
66
+ next unless row.is_a?(Hash)
67
+
68
+ message_id = (row["id"] || "").to_s.strip
69
+ detail = nil
70
+ unless message_id.empty?
71
+ begin
72
+ dr = Http.get("#{API_BASE}/mailboxes/#{URI.encode_www_form_component(address)}/messages/#{URI.encode_www_form_component(message_id)}",
73
+ headers: HEADERS.merge("X-Session-Token" => session_token), timeout: 15)
74
+ detail = dr.json if dr.ok? && dr.json.is_a?(Hash)
75
+ rescue StandardError
76
+ # 忽略
77
+ end
78
+ end
79
+ Normalize.normalize_email(flatten(detail || row, address), address)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module TempmailSdk
8
+ module Providers
9
+ # webmailtemp.com 临时邮箱渠道
10
+ # GET /api/create 创建邮箱
11
+ # GET /api/check/{username} 获取邮件列表
12
+ module Webmailtemp
13
+ CHANNEL = "webmailtemp"
14
+ BASE_URL = "https://webmailtemp.com"
15
+ TOK_PREFIX = "wmt1:"
16
+
17
+ HEADERS = {
18
+ "Accept" => "application/json",
19
+ "User-Agent" => "Mozilla/5.0"
20
+ }.freeze
21
+
22
+ module_function
23
+
24
+ def encode_token(username, cookie_hdr)
25
+ raw = JSON.generate({ "u" => username, "c" => cookie_hdr })
26
+ "#{TOK_PREFIX}#{Base64.strict_encode64(raw)}"
27
+ end
28
+
29
+ def decode_token(token)
30
+ raise "webmailtemp: 无效的 token" unless token&.start_with?(TOK_PREFIX)
31
+
32
+ data = JSON.parse(Base64.decode64(token[TOK_PREFIX.length..]))
33
+ username = (data["u"] || "").strip
34
+ cookie_hdr = (data["c"] || "").strip
35
+ raise "webmailtemp: token 数据无效" if username.empty? || cookie_hdr.empty?
36
+
37
+ { "username" => username, "cookie" => cookie_hdr }
38
+ end
39
+
40
+ # @return [EmailInfo]
41
+ def generate_email
42
+ resp = Http.get("#{BASE_URL}/api/create", headers: HEADERS, timeout: 15)
43
+ resp.raise_for_status
44
+ data = resp.json
45
+ raise "webmailtemp: 创建邮箱响应无效" unless data.is_a?(Hash)
46
+
47
+ email = (data["email"] || "").strip
48
+ username = (data["username"] || "").strip
49
+
50
+ # 从 set_cookies 提取第一个 cookie
51
+ cookie_hdr = ""
52
+ cookies = resp.set_cookies
53
+ unless cookies.empty?
54
+ pair = cookies.first.split(";").first.to_s.strip
55
+ cookie_hdr = pair unless pair.empty?
56
+ end
57
+
58
+ unless data["success"] && !email.empty? && email.include?("@") && !username.empty? && !cookie_hdr.empty?
59
+ raise "webmailtemp: 创建邮箱响应无效"
60
+ end
61
+
62
+ expires_at = nil
63
+ ttl = data["ttl"]
64
+ if ttl.is_a?(Numeric) && ttl > 0
65
+ expires_at = ((Time.now.to_f + ttl) * 1000).to_i
66
+ end
67
+
68
+ EmailInfo.new(channel: CHANNEL, email: email, token: encode_token(username, cookie_hdr),
69
+ expires_at: expires_at)
70
+ end
71
+
72
+ # @param token [String]
73
+ # @param email [String]
74
+ # @return [Array<Email>]
75
+ def get_emails(token, email)
76
+ address = (email || "").strip
77
+ raise "webmailtemp: 邮箱地址为空" if address.empty?
78
+
79
+ session = decode_token((token || "").strip)
80
+ headers = HEADERS.merge("Cookie" => session["cookie"])
81
+
82
+ resp = Http.get("#{BASE_URL}/api/check/#{URI.encode_www_form_component(session["username"])}",
83
+ headers: headers, timeout: 15)
84
+ resp.raise_for_status
85
+ data = resp.json
86
+
87
+ rows = data.is_a?(Hash) ? data["emails"] : nil
88
+ return [] unless rows.is_a?(Array)
89
+
90
+ rows.filter_map do |row|
91
+ next unless row.is_a?(Hash)
92
+
93
+ raw = {
94
+ "id" => row["id"],
95
+ "from" => row["from"],
96
+ "to" => address,
97
+ "subject" => row["subject"],
98
+ "text" => row["body"],
99
+ "html" => row["html"],
100
+ "date" => row["received_at"] || row["timestamp"],
101
+ "attachments" => row["attachments"]
102
+ }
103
+ Normalize.normalize_email(raw, address)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # xkx.me 临时邮箱渠道(CSRF + session cookie)
8
+ # GET / 获取 CSRF token + session cookie
9
+ # POST /mailbox/create/random 创建邮箱(不跟随重定向,从 Location 提取邮箱)
10
+ # GET /mailbox/{email}/messages 获取邮件列表
11
+ module XkxMe
12
+ CHANNEL = "xkx-me"
13
+ BASE_URL = "https://xkx.me"
14
+
15
+ CSRF_RE = /csrf-token"\s+content="([^"]+)"/
16
+
17
+ HEADERS = {
18
+ "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"
19
+ }.freeze
20
+
21
+ module_function
22
+
23
+ def extract_cookie_header(resp)
24
+ cookies = {}
25
+ resp.set_cookies.each do |line|
26
+ pair = line.split(";").first.to_s.strip
27
+ k, v = pair.split("=", 2)
28
+ cookies[k] = v if k && v
29
+ end
30
+ cookies.sort.map { |k, v| "#{k}=#{v}" }.join("; ")
31
+ end
32
+
33
+ # @return [EmailInfo]
34
+ def generate_email
35
+ # 获取 CSRF token 和 session cookie
36
+ resp = Http.get(BASE_URL, headers: HEADERS, timeout: 15)
37
+ resp.raise_for_status
38
+
39
+ m = resp.body.match(CSRF_RE)
40
+ raise "xkx-me: 无法获取 CSRF token" unless m
41
+
42
+ csrf = m[1]
43
+ cookie_hdr = extract_cookie_header(resp)
44
+
45
+ # POST 创建邮箱(不跟随重定向)
46
+ post_headers = HEADERS.merge(
47
+ "Content-Type" => "application/x-www-form-urlencoded",
48
+ "Referer" => "#{BASE_URL}/"
49
+ )
50
+ post_headers["Cookie"] = cookie_hdr unless cookie_hdr.empty?
51
+
52
+ body = "_token=#{URI.encode_www_form_component(csrf)}"
53
+ resp2 = Http.post("#{BASE_URL}/mailbox/create/random",
54
+ headers: post_headers, body: body, timeout: 15)
55
+
56
+ # 从 Location 头提取邮箱地址(302 重定向)
57
+ location = resp2.header("Location") || ""
58
+ email_match = location.match(/mailbox\/([^\/"'<>\s]+@xkx\.me)/)
59
+ raise "xkx-me: 无法从响应中提取邮箱地址" unless email_match
60
+
61
+ email = email_match[1]
62
+
63
+ EmailInfo.new(channel: CHANNEL, email: email, token: cookie_hdr)
64
+ end
65
+
66
+ # @param token [String]
67
+ # @param email [String]
68
+ # @return [Array<Email>]
69
+ def get_emails(token, email)
70
+ address = (email || "").strip
71
+ raise "xkx-me: 缺少邮箱地址" if address.empty?
72
+
73
+ cookie_str = (token || "").strip
74
+
75
+ headers = HEADERS.merge(
76
+ "Accept" => "application/json",
77
+ "X-Requested-With" => "XMLHttpRequest"
78
+ )
79
+ headers["Cookie"] = cookie_str unless cookie_str.empty?
80
+
81
+ encoded_email = URI.encode_www_form_component(address).gsub("%40", "@")
82
+ resp = Http.get("#{BASE_URL}/mailbox/#{encoded_email}/messages",
83
+ headers: headers, timeout: 15)
84
+ return [] if resp.status_code == 404
85
+
86
+ resp.raise_for_status
87
+ data = resp.json
88
+ return [] unless data
89
+
90
+ message_list = if data.is_a?(Array)
91
+ data
92
+ elsif data.is_a?(Hash)
93
+ if data.key?("messages")
94
+ data["messages"]
95
+ elsif data["message"].is_a?(Hash)
96
+ [data["message"]]
97
+ else
98
+ []
99
+ end
100
+ else
101
+ []
102
+ end
103
+
104
+ return [] unless message_list.is_a?(Array)
105
+
106
+ message_list.filter_map do |msg|
107
+ next unless msg.is_a?(Hash)
108
+
109
+ raw = {
110
+ "id" => (msg["id"] || "").to_s,
111
+ "from" => (msg["from"] || "").to_s,
112
+ "to" => address,
113
+ "subject" => (msg["subject"] || "").to_s,
114
+ "date" => (msg["date"] || "").to_s,
115
+ "html" => (msg["html"] || msg["body"] || "").to_s,
116
+ "text" => (msg["text"] || "").to_s,
117
+ "is_read" => false,
118
+ "attachments" => []
119
+ }
120
+ Normalize.normalize_email(raw, address)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end