tempmail_sdk 1.3.3 → 1.3.4

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/lib/tempmail_sdk/providers/altmails.rb +99 -0
  3. data/lib/tempmail_sdk/providers/apihz.rb +138 -0
  4. data/lib/tempmail_sdk/providers/awamail.rb +95 -0
  5. data/lib/tempmail_sdk/providers/best_temp_mail.rb +77 -0
  6. data/lib/tempmail_sdk/providers/chatgpt_org_uk.rb +129 -0
  7. data/lib/tempmail_sdk/providers/disposablemail.rb +125 -0
  8. data/lib/tempmail_sdk/providers/disposablemail_app.rb +66 -0
  9. data/lib/tempmail_sdk/providers/email10min.rb +152 -0
  10. data/lib/tempmail_sdk/providers/emailnator.rb +132 -0
  11. data/lib/tempmail_sdk/providers/emailtemp_org.rb +131 -0
  12. data/lib/tempmail_sdk/providers/expressinboxhub.rb +118 -0
  13. data/lib/tempmail_sdk/providers/fakemail.rb +143 -0
  14. data/lib/tempmail_sdk/providers/haribu.rb +29 -3
  15. data/lib/tempmail_sdk/providers/linshiyouxiang_net.rb +85 -0
  16. data/lib/tempmail_sdk/providers/mail_sunls.rb +42 -1
  17. data/lib/tempmail_sdk/providers/mail_td.rb +161 -0
  18. data/lib/tempmail_sdk/providers/mailcat_ai.rb +52 -0
  19. data/lib/tempmail_sdk/providers/maildrop.rb +79 -4
  20. data/lib/tempmail_sdk/providers/mailgolem.rb +96 -0
  21. data/lib/tempmail_sdk/providers/mailinator.rb +9 -2
  22. data/lib/tempmail_sdk/providers/mailtemp_cc.rb +91 -0
  23. data/lib/tempmail_sdk/providers/minuteinbox.rb +174 -0
  24. data/lib/tempmail_sdk/providers/moakt.rb +29 -3
  25. data/lib/tempmail_sdk/providers/mohmal.rb +208 -0
  26. data/lib/tempmail_sdk/providers/mytempmail_cc.rb +76 -0
  27. data/lib/tempmail_sdk/providers/openinbox.rb +102 -0
  28. data/lib/tempmail_sdk/providers/smail_pw.rb +156 -0
  29. data/lib/tempmail_sdk/providers/socketio_mail.rb +25 -0
  30. data/lib/tempmail_sdk/providers/tempgbox.rb +118 -0
  31. data/lib/tempmail_sdk/providers/tempmail_cn.rb +111 -0
  32. data/lib/tempmail_sdk/providers/tenminutemail_net.rb +183 -0
  33. data/lib/tempmail_sdk/providers/twentyfourmail_chacuo.rb +83 -0
  34. data/lib/tempmail_sdk/providers/vip_215.rb +171 -0
  35. data/lib/tempmail_sdk/registry.rb +163 -1
  36. data/lib/tempmail_sdk/version.rb +1 -1
  37. metadata +30 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c42b7ddf14c615d07554a2d6aa7efb1954cadad4ecef1f052bb6af5fb22f2a8
4
- data.tar.gz: 46ab91c0e8946782a83c60c020feeebf840e625be330839d62c69b1c8f1ab5bc
3
+ metadata.gz: d9633959dc0f47ae3b8b30e6cb69db5e16976773039a186303e47701efd37a6b
4
+ data.tar.gz: 50eed70cbb31235fef5d96ea9d2288cc8d099568bd35946e752875d2c945291d
5
5
  SHA512:
6
- metadata.gz: 175bf0d04d31f7db48739b559af1eba64bdc0c09b51bd18e90124fbf0f05727e4f2586157f714e6c76ee693d3b29725756c89f002fe9a484c4fc7bf969d26ba4
7
- data.tar.gz: 4052cdcfa7f88faf5eff0461a40e427eea368a86ca976fdd7dc25de4900d638f1479401ae98240936ffa489af05cd17446fcec7e4354c9b8debd896c79f2de1c
6
+ metadata.gz: b54a192368aa851e240a34c91c7e8f2b6fc5b0d5587f6091fabd79f54b89bf7f29dc51c13c57987f499c65b570084aa0754838fbe263d7fbc8d564b5ce2e1bc2
7
+ data.tar.gz: f630396b6757e1dd049a931141f946a99c92bcf28a28b3aeb8c78d69767f1b5c383a580cfefdc1ade984cbbaec051173ec82f904dff11ee9b5a8983dec03d7ea
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # altmails.com 渠道实现
8
+ #
9
+ # 流程:
10
+ # 1. GET / 建立 session,从 HTML inline script 提取 CSRF token('_token': 'xxx')
11
+ # 2. GET /random-email-address 获取随机邮箱地址(纯文本响应)
12
+ # 3. 收信:重新建立 session 获取新 CSRF,POST /fetch-emails/{email} 获取邮件列表
13
+ # 4. GET /view/{id} 获取每封邮件正文 HTML
14
+ module Altmails
15
+ CHANNEL = "altmails"
16
+ BASE_URL = "https://tempmail.altmails.com"
17
+
18
+ BROWSER_HEADERS = {
19
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
20
+ "Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
21
+ "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"
22
+ }.freeze
23
+
24
+ CSRF_RE = /'_token'\s*:\s*'([^']+)'/.freeze
25
+
26
+ module_function
27
+
28
+ # 访问首页提取 CSRF token
29
+ # @return [String]
30
+ def fetch_csrf
31
+ resp = Http.get("#{BASE_URL}/", headers: BROWSER_HEADERS)
32
+ resp.raise_for_status
33
+ m = resp.body.match(CSRF_RE)
34
+ raise "altmails: 未能从首页提取 CSRF token" unless m
35
+
36
+ m[1]
37
+ end
38
+
39
+ # 创建临时邮箱
40
+ # @return [EmailInfo]
41
+ def generate_email
42
+ csrf = fetch_csrf
43
+ resp = Http.get("#{BASE_URL}/random-email-address",
44
+ headers: BROWSER_HEADERS.merge("Referer" => "#{BASE_URL}/"))
45
+ resp.raise_for_status
46
+ email = resp.body.strip
47
+ raise "altmails: 邮箱地址无效: #{email.inspect}" if email.empty? || !email.include?("@")
48
+
49
+ EmailInfo.new(channel: CHANNEL, email: email, token: csrf)
50
+ end
51
+
52
+ # 获取邮件列表
53
+ # @param email [String]
54
+ # @param token [String] CSRF token(收信时重新获取新 session)
55
+ # @return [Array<Email>]
56
+ def get_emails(email, token)
57
+ _ = token
58
+ csrf = fetch_csrf
59
+ fetch_url = "#{BASE_URL}/fetch-emails/#{URI.encode_www_form_component(email)}"
60
+ form = "_token=#{URI.encode_www_form_component(csrf)}"
61
+ resp = Http.post(fetch_url,
62
+ headers: {
63
+ "Accept" => "application/json, text/plain, */*",
64
+ "Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
65
+ "Content-Type" => "application/x-www-form-urlencoded",
66
+ "Origin" => BASE_URL,
67
+ "Referer" => "#{BASE_URL}/",
68
+ "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",
69
+ "X-Requested-With" => "XMLHttpRequest"
70
+ },
71
+ body: form)
72
+ resp.raise_for_status
73
+ items = begin
74
+ resp.json
75
+ rescue StandardError
76
+ []
77
+ end
78
+ return [] unless items.is_a?(Array)
79
+
80
+ items.filter_map do |item|
81
+ next unless item.is_a?(Hash)
82
+
83
+ item["to"] = email
84
+ mail_id = item["id"].to_s
85
+ unless mail_id.empty?
86
+ begin
87
+ vr = Http.get("#{BASE_URL}/view/#{URI.encode_www_form_component(mail_id)}",
88
+ headers: BROWSER_HEADERS.merge("Referer" => "#{BASE_URL}/"))
89
+ item["html_body"] = vr.body if vr.ok?
90
+ rescue StandardError
91
+ # 忽略正文获取失败
92
+ end
93
+ end
94
+ Normalize.normalize_email(item, email)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module TempmailSdk
8
+ module Providers
9
+ # apihz(接口盒子)渠道实现
10
+ #
11
+ # 需 id/key 凭据,邮箱有效期 10 分钟。读信须携带创建时的 pwd,
12
+ # 故 token 用 base64(JSON{mail,pwd}) 编码保存。
13
+ module Apihz
14
+ CHANNEL = "apihz"
15
+ BASE_URL = "https://cn.apihz.cn"
16
+ TOK_PREFIX = "apihz1:"
17
+ # 官方公共账号(共享频次),未配置自有 id/key 时使用
18
+ PUBLIC_ID = "88888888"
19
+ PUBLIC_KEY = "88888888"
20
+ DOMAINS = %w[apimail.email apimail.vip].freeze
21
+
22
+ HEADERS = {
23
+ "Accept" => "application/json",
24
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
25
+ "(KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
26
+ }.freeze
27
+
28
+ module_function
29
+
30
+ # 读取 id/key:优先环境变量 APIHZ_ID/APIHZ_KEY,回退官方公共账号
31
+ def credentials
32
+ id = ENV["APIHZ_ID"].to_s.strip
33
+ key = ENV["APIHZ_KEY"].to_s.strip
34
+ id = PUBLIC_ID if id.empty?
35
+ key = PUBLIC_KEY if key.empty?
36
+ [id, key]
37
+ end
38
+
39
+ # 生成随机本地部分
40
+ def random_local(length = 10)
41
+ chars = ("a".."z").to_a + ("0".."9").to_a
42
+ Array.new(length) { chars.sample }.join
43
+ end
44
+
45
+ # 生成 12 位随机密码
46
+ def random_password
47
+ chars = ("A".."Z").to_a + ("a".."z").to_a + ("0".."9").to_a
48
+ Array.new(12) { chars.sample }.join
49
+ end
50
+
51
+ # token 编码:前缀 + base64(JSON{mail,pwd})
52
+ def encode_token(mail, pwd)
53
+ raw = JSON.generate("mail" => mail, "pwd" => pwd)
54
+ TOK_PREFIX + Base64.strict_encode64(raw)
55
+ end
56
+
57
+ # token 解码
58
+ def decode_token(token)
59
+ raise "apihz: 无效 token" unless token.to_s.start_with?(TOK_PREFIX)
60
+
61
+ raw = Base64.strict_decode64(token[TOK_PREFIX.length..])
62
+ o = JSON.parse(raw)
63
+ mail = o["mail"].to_s.strip
64
+ pwd = o["pwd"].to_s.strip
65
+ raise "apihz: 无效 token" if mail.empty? || pwd.empty?
66
+
67
+ [mail, pwd]
68
+ rescue ArgumentError, JSON::ParserError
69
+ raise "apihz: 无效 token"
70
+ end
71
+
72
+ # 创建临时邮箱
73
+ # GET /api/mail/mailcache.php?id=&key=&domain=&name=&pwd=&buytype=0
74
+ # @return [EmailInfo]
75
+ def generate_email
76
+ id, key = credentials
77
+ domain = DOMAINS.sample
78
+ name = random_local
79
+ pwd = random_password
80
+
81
+ url = "#{BASE_URL}/api/mail/mailcache.php?" \
82
+ "id=#{URI.encode_www_form_component(id)}&key=#{URI.encode_www_form_component(key)}" \
83
+ "&domain=#{URI.encode_www_form_component(domain)}" \
84
+ "&name=#{URI.encode_www_form_component(name)}" \
85
+ "&pwd=#{URI.encode_www_form_component(pwd)}&buytype=0"
86
+
87
+ resp = Http.get(url, headers: HEADERS)
88
+ resp.raise_for_status
89
+ data = resp.json
90
+ raise "apihz: 创建邮箱失败 #{data['msg']}" if data["code"] != 200 || data["mail"].to_s.empty?
91
+
92
+ final_pwd = data["pwd"].to_s
93
+ final_pwd = pwd if final_pwd.empty?
94
+
95
+ EmailInfo.new(channel: CHANNEL, email: data["mail"], token: encode_token(data["mail"], final_pwd))
96
+ end
97
+
98
+ # 获取邮件列表
99
+ # GET /api/mail/mailgetlist.php?id=&key=&mail=&pwd=&page=1
100
+ # @param token [String]
101
+ # @return [Array<Email>]
102
+ def get_emails(token)
103
+ mail, pwd = decode_token(token)
104
+ id, key = credentials
105
+
106
+ url = "#{BASE_URL}/api/mail/mailgetlist.php?" \
107
+ "id=#{URI.encode_www_form_component(id)}&key=#{URI.encode_www_form_component(key)}" \
108
+ "&mail=#{URI.encode_www_form_component(mail)}" \
109
+ "&pwd=#{URI.encode_www_form_component(pwd)}&page=1"
110
+
111
+ resp = Http.get(url, headers: HEADERS)
112
+ resp.raise_for_status
113
+ data = resp.json
114
+ return [] if data["code"] != 200
115
+
116
+ list = data["data"]
117
+ return [] unless list.is_a?(Array) && !list.empty?
118
+
119
+ list.map do |msg|
120
+ from_addr = msg["frommail"].to_s
121
+ from_addr = msg["fromname"].to_s if from_addr.empty?
122
+ date_val = msg["time2"].to_s
123
+ date_val = msg["time1"].to_s if date_val.empty?
124
+ Normalize.normalize_email({
125
+ "id" => msg["time1"].to_s,
126
+ "from" => from_addr,
127
+ "to" => mail,
128
+ "subject" => msg["subject"].to_s,
129
+ "text" => msg["content"].to_s,
130
+ "html" => msg["content"].to_s,
131
+ "date" => date_val,
132
+ "isRead" => false
133
+ }, mail)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # awamail.com 渠道实现
8
+ #
9
+ # 流程:
10
+ # 1. POST /welcome/change_mailbox(空 body)不跟随重定向,从 Set-Cookie 提取 awamail_session
11
+ # 2. GET /welcome/get_emails 携带 Cookie 获取邮件列表
12
+ module Awamail
13
+ CHANNEL = "awamail"
14
+ BASE_URL = "https://awamail.com/welcome"
15
+
16
+ HEADERS = {
17
+ "Accept" => "application/json, text/javascript, */*; q=0.01",
18
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
19
+ "Cache-Control" => "no-cache",
20
+ "DNT" => "1",
21
+ "Origin" => "https://awamail.com",
22
+ "Pragma" => "no-cache",
23
+ "Referer" => "https://awamail.com/?lang=zh",
24
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
25
+ "(KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0",
26
+ "X-Requested-With" => "XMLHttpRequest"
27
+ }.freeze
28
+
29
+ module_function
30
+
31
+ # 创建临时邮箱
32
+ # POST /welcome/change_mailbox,从 Set-Cookie 提取 awamail_session
33
+ # @return [EmailInfo]
34
+ def generate_email
35
+ resp = Http.post("#{BASE_URL}/change_mailbox",
36
+ headers: HEADERS.merge("Content-Length" => "0"))
37
+ # 不跟随重定向,直接从响应提取 cookie
38
+ session_cookie = resp.cookie_value("awamail_session")
39
+ raise "awamail: 未能提取 awamail_session cookie" if session_cookie.to_s.empty?
40
+
41
+ cookie_str = "awamail_session=#{session_cookie}"
42
+
43
+ data = begin
44
+ resp.json
45
+ rescue StandardError
46
+ {}
47
+ end
48
+
49
+ email_addr = data.is_a?(Hash) ? data.dig("data", "email_address").to_s : ""
50
+ if email_addr.empty?
51
+ # 若 POST 响应无邮箱,尝试 GET get_emails 获取
52
+ gr = Http.get("#{BASE_URL}/get_emails",
53
+ headers: HEADERS.merge("Cookie" => cookie_str))
54
+ gr.raise_for_status
55
+ gd = gr.json
56
+ email_addr = gd.is_a?(Hash) ? gd.dig("data", "email_address").to_s : ""
57
+ end
58
+
59
+ raise "awamail: 未能获取邮箱地址" if email_addr.empty?
60
+
61
+ info = EmailInfo.new(channel: CHANNEL, email: email_addr, token: cookie_str)
62
+ if data.is_a?(Hash)
63
+ info.expires_at = data.dig("data", "expired_at").to_s
64
+ info.created_at = data.dig("data", "created_at").to_s
65
+ end
66
+ info
67
+ end
68
+
69
+ # 获取邮件列表
70
+ # GET /welcome/get_emails,携带 Cookie
71
+ # @param token [String] awamail_session cookie 字符串
72
+ # @param email [String]
73
+ # @return [Array<Email>]
74
+ def get_emails(token, email)
75
+ resp = Http.get("#{BASE_URL}/get_emails",
76
+ headers: HEADERS.merge(
77
+ "Cookie" => token,
78
+ "X-Requested-With" => "XMLHttpRequest"
79
+ ))
80
+ resp.raise_for_status
81
+ data = resp.json
82
+ return [] unless data.is_a?(Hash) && data["success"]
83
+
84
+ emails_raw = data.dig("data", "emails")
85
+ return [] unless emails_raw.is_a?(Array)
86
+
87
+ emails_raw.filter_map do |raw|
88
+ next unless raw.is_a?(Hash)
89
+
90
+ Normalize.normalize_email(raw, email)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "securerandom"
5
+
6
+ module TempmailSdk
7
+ module Providers
8
+ # best-temp-mail.com 渠道实现
9
+ #
10
+ # 纯 JSON REST API,无需认证,无需 Session。
11
+ # 创建邮箱: POST /api/v3/createEmail body: {"intToken":"<uuid>"}
12
+ # 获取邮件: POST /api/v3/getEmailList body: {"address":"...","id":"...","intToken":"...","update_tag":"..."}
13
+ module BestTempMail
14
+ CHANNEL = "best-temp-mail"
15
+ API_BASE = "https://best-temp-mail.com/api/v3"
16
+
17
+ HEADERS = {
18
+ "Accept" => "application/json, text/plain, */*",
19
+ "Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
20
+ "Content-Type" => "application/json",
21
+ "Origin" => "https://best-temp-mail.com",
22
+ "Referer" => "https://best-temp-mail.com/",
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
+ # @return [EmailInfo]
31
+ def generate_email
32
+ int_token = SecureRandom.uuid
33
+ resp = Http.post("#{API_BASE}/createEmail",
34
+ headers: HEADERS,
35
+ json: { "intToken" => int_token })
36
+ resp.raise_for_status
37
+ data = resp.json
38
+ raise "best-temp-mail: 创建邮箱失败 status=#{data['status']}" \
39
+ if data["status"] != "success" || data.dig("data", "address").to_s.empty?
40
+
41
+ d = data["data"]
42
+ token = JSON.generate("intToken" => int_token, "id" => d["id"].to_s, "update_tag" => d["update_tag"].to_s)
43
+ EmailInfo.new(channel: CHANNEL, email: d["address"], token: token)
44
+ end
45
+
46
+ # 获取邮件列表
47
+ # @param email [String]
48
+ # @param token [String] JSON 编码的 intToken/id/update_tag
49
+ # @return [Array<Email>]
50
+ def get_emails(email, token)
51
+ tkn = JSON.parse(token)
52
+ resp = Http.post("#{API_BASE}/getEmailList",
53
+ headers: HEADERS,
54
+ json: {
55
+ "address" => email,
56
+ "id" => tkn["id"].to_s,
57
+ "intToken" => tkn["intToken"].to_s,
58
+ "update_tag" => tkn["update_tag"].to_s
59
+ })
60
+ resp.raise_for_status
61
+ data = resp.json
62
+ return [] unless data.dig("data", "hasNewEmail")
63
+
64
+ emails_raw = data.dig("data", "emails")
65
+ return [] unless emails_raw.is_a?(Array)
66
+
67
+ emails_raw.filter_map do |raw|
68
+ next unless raw.is_a?(Hash)
69
+
70
+ Normalize.normalize_email(raw, email)
71
+ end
72
+ rescue JSON::ParserError
73
+ raise "best-temp-mail: 无效的 token 格式"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "uri"
5
+
6
+ module TempmailSdk
7
+ module Providers
8
+ # chatgpt.org.uk 渠道实现
9
+ #
10
+ # 流程:
11
+ # 1. GET /api/domains/public 获取可用域名列表(过滤 is_active=1)
12
+ # 2. 本地生成随机用户名,拼接邮箱地址
13
+ # 3. POST /api/inbox-token {"email":"..."} 创建收件箱,返回 inbox JWT 和 gm_sid cookie
14
+ # 4. GET /api/emails?email=... 携带 x-inbox-token 和 gm_sid cookie 获取邮件
15
+ # token 格式: JSON {"gmSid":"...","inbox":"..."}
16
+ module ChatgptOrgUk
17
+ CHANNEL = "chatgpt-org-uk"
18
+ API_BASE = "https://mail.chatgpt.org.uk/api"
19
+ USERNAME_CHARS = ("a".."z").to_a + ("0".."9").to_a
20
+
21
+ HEADERS = {
22
+ "Accept" => "*/*",
23
+ "DNT" => "1",
24
+ "Origin" => "https://mail.chatgpt.org.uk",
25
+ "Referer" => "https://mail.chatgpt.org.uk/zh/",
26
+ "sec-fetch-dest" => "empty",
27
+ "sec-fetch-mode" => "cors",
28
+ "sec-fetch-site" => "same-origin",
29
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
30
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
31
+ }.freeze
32
+
33
+ module_function
34
+
35
+ # 生成随机用户名
36
+ def random_username(length = 10)
37
+ Array.new(length) { USERNAME_CHARS.sample }.join
38
+ end
39
+
40
+ # 获取可用域名列表
41
+ def fetch_domains
42
+ resp = Http.get("#{API_BASE}/domains/public", headers: HEADERS)
43
+ resp.raise_for_status
44
+ data = resp.json
45
+ raise "chatgpt-org-uk: 获取域名失败" unless data["success"]
46
+
47
+ domains = data.dig("data", "domains") || []
48
+ active = domains.select { |d| d["is_active"] == 1 && !d["domain_name"].to_s.empty? }
49
+ .map { |d| d["domain_name"] }
50
+ raise "chatgpt-org-uk: 无可用域名" if active.empty?
51
+
52
+ active
53
+ end
54
+
55
+ # 创建收件箱,返回 [inbox_token, gm_sid]
56
+ def create_inbox(email)
57
+ resp = Http.post("#{API_BASE}/inbox-token",
58
+ headers: HEADERS.merge("Content-Type" => "application/json"),
59
+ json: { "email" => email })
60
+ resp.raise_for_status
61
+ gm_sid = resp.cookie_value("gm_sid").to_s
62
+ data = resp.json
63
+ raise "chatgpt-org-uk: inbox-token 响应缺少 token" \
64
+ unless data["success"] && !data.dig("auth", "token").to_s.empty?
65
+
66
+ [data.dig("auth", "token"), gm_sid]
67
+ end
68
+
69
+ # 创建临时邮箱
70
+ # @return [EmailInfo]
71
+ def generate_email
72
+ domains = fetch_domains
73
+ domain = domains.sample
74
+ email = "#{random_username}@#{domain}"
75
+ inbox, gm_sid = create_inbox(email)
76
+ packed = JSON.generate("gmSid" => gm_sid, "inbox" => inbox)
77
+ EmailInfo.new(channel: CHANNEL, email: email, token: packed)
78
+ end
79
+
80
+ # 获取邮件列表
81
+ # @param email [String]
82
+ # @param token [String] JSON {"gmSid":"...","inbox":"..."}
83
+ # @return [Array<Email>]
84
+ def get_emails(email, token)
85
+ parsed = JSON.parse(token)
86
+ gm_sid = parsed["gmSid"].to_s
87
+ inbox = parsed["inbox"].to_s
88
+ raise "chatgpt-org-uk: inbox token 缺失" if inbox.empty?
89
+
90
+ # gm_sid 丢失时重新创建 session
91
+ if gm_sid.empty?
92
+ inbox, gm_sid = create_inbox(email)
93
+ end
94
+
95
+ fetch_emails_with(email, inbox, gm_sid)
96
+ rescue JSON::ParserError
97
+ raise "chatgpt-org-uk: 无效的 token 格式"
98
+ end
99
+
100
+ # 实际拉取邮件,401/403 时重建 session 重试一次
101
+ def fetch_emails_with(email, inbox, gm_sid)
102
+ encoded = URI.encode_www_form_component(email)
103
+ resp = Http.get("#{API_BASE}/emails?email=#{encoded}",
104
+ headers: HEADERS.merge(
105
+ "Cookie" => "gm_sid=#{gm_sid}",
106
+ "x-inbox-token" => inbox
107
+ ))
108
+ if [401, 403].include?(resp.status_code)
109
+ new_inbox, new_sid = create_inbox(email)
110
+ resp = Http.get("#{API_BASE}/emails?email=#{encoded}",
111
+ headers: HEADERS.merge(
112
+ "Cookie" => "gm_sid=#{new_sid}",
113
+ "x-inbox-token" => new_inbox
114
+ ))
115
+ end
116
+ resp.raise_for_status
117
+ data = resp.json
118
+ raise "chatgpt-org-uk: 获取邮件失败" unless data["success"]
119
+
120
+ emails_raw = data.dig("data", "emails") || []
121
+ emails_raw.filter_map do |raw|
122
+ next unless raw.is_a?(Hash)
123
+
124
+ Normalize.normalize_email(raw, email)
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # disposablemail.com 渠道实现
8
+ #
9
+ # 流程:
10
+ # 1. GET / 获取 PHPSESSID cookie + HTML 中的 CSRF token(CSRF="xxx")
11
+ # 2. GET /index/index?csrf_token={csrf} 创建邮箱,返回 {"email":"user@domain.com"}
12
+ # 3. GET /index/refresh 携带 session cookie 获取邮件列表(捷克语字段: predmet/od/kdy/precteno)
13
+ # 4. GET /email/id/{id} 获取邮件正文 HTML
14
+ # token 存储 "PHPSESSID=xxx" cookie 字符串。
15
+ module Disposablemail
16
+ CHANNEL = "disposablemail"
17
+ BASE_URL = "https://www.disposablemail.com"
18
+
19
+ CSRF_RE = /CSRF="([^"]+)"/.freeze
20
+
21
+ BROWSER_HEADERS = {
22
+ "Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
23
+ "Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
24
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
25
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
26
+ }.freeze
27
+
28
+ AJAX_HEADERS = {
29
+ "Accept" => "application/json, text/javascript, */*; q=0.01",
30
+ "Accept-Language" => "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
31
+ "Referer" => "#{BASE_URL}/",
32
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
33
+ "(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
34
+ "X-Requested-With" => "XMLHttpRequest"
35
+ }.freeze
36
+
37
+ module_function
38
+
39
+ # 提取 Set-Cookie 中的 PHPSESSID
40
+ def extract_session(resp)
41
+ resp.set_cookies.each do |line|
42
+ first = line.split(";", 2).first.to_s.strip
43
+ k, v = first.split("=", 2)
44
+ return "#{k}=#{v}" if k == "PHPSESSID"
45
+ end
46
+ ""
47
+ end
48
+
49
+ # 创建临时邮箱
50
+ # @return [EmailInfo]
51
+ def generate_email
52
+ r1 = Http.get("#{BASE_URL}/", headers: BROWSER_HEADERS)
53
+ r1.raise_for_status
54
+ m = r1.body.match(CSRF_RE)
55
+ raise "disposablemail: 未能从首页提取 CSRF token" unless m
56
+
57
+ csrf = m[1]
58
+ session_cookie = extract_session(r1)
59
+
60
+ r2 = Http.get("#{BASE_URL}/index/index?csrf_token=#{csrf}",
61
+ headers: AJAX_HEADERS.merge(session_cookie.empty? ? {} : { "Cookie" => session_cookie }))
62
+ r2.raise_for_status
63
+
64
+ # 若 index 阶段刷新了 PHPSESSID,改用新的
65
+ new_sess = extract_session(r2)
66
+ session_cookie = new_sess unless new_sess.empty?
67
+
68
+ data = JSON.parse(r2.body)
69
+ email = data["email"].to_s.strip
70
+ raise "disposablemail: 邮箱地址无效: #{email.inspect}" if email.empty? || !email.include?("@")
71
+
72
+ EmailInfo.new(channel: CHANNEL, email: email, token: session_cookie)
73
+ end
74
+
75
+ # 获取邮件列表
76
+ # @param email [String]
77
+ # @param token [String] PHPSESSID cookie 字符串
78
+ # @return [Array<Email>]
79
+ def get_emails(email, token)
80
+ raise "disposablemail: 邮箱地址为空" if email.to_s.strip.empty?
81
+
82
+ headers = AJAX_HEADERS.dup
83
+ headers = headers.merge("Cookie" => token) unless token.to_s.empty?
84
+
85
+ resp = Http.get("#{BASE_URL}/index/refresh", headers: headers)
86
+ resp.raise_for_status
87
+ trimmed = resp.body.strip
88
+ return [] if trimmed == "0" || trimmed.empty? || trimmed == "[]"
89
+
90
+ list = begin
91
+ JSON.parse(trimmed)
92
+ rescue JSON::ParserError
93
+ []
94
+ end
95
+ return [] unless list.is_a?(Array) && !list.empty?
96
+
97
+ list.map do |item|
98
+ mail_id = item["id"].to_s
99
+ html_body = fetch_body(mail_id, token)
100
+ Normalize.normalize_email({
101
+ "id" => mail_id,
102
+ "from" => item["od"].to_s,
103
+ "to" => email,
104
+ "subject" => item["predmet"].to_s,
105
+ "html" => html_body,
106
+ "date" => item["kdy"].to_s,
107
+ "isRead" => item["precteno"] == "precteno"
108
+ }, email)
109
+ end
110
+ end
111
+
112
+ # 获取单封邮件 HTML 正文
113
+ def fetch_body(mail_id, token)
114
+ return "" if mail_id.empty?
115
+
116
+ headers = AJAX_HEADERS.dup
117
+ headers = headers.merge("Cookie" => token) unless token.to_s.empty?
118
+ resp = Http.get("#{BASE_URL}/email/id/#{mail_id}", headers: headers)
119
+ resp.ok? ? resp.body : ""
120
+ rescue StandardError
121
+ ""
122
+ end
123
+ end
124
+ end
125
+ end