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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/tempmail_sdk/channels.rb +29 -7
- data/lib/tempmail_sdk/channels_data.rb +29 -7
- data/lib/tempmail_sdk/providers/altmails.rb +99 -0
- data/lib/tempmail_sdk/providers/apihz.rb +138 -0
- data/lib/tempmail_sdk/providers/awamail.rb +95 -0
- data/lib/tempmail_sdk/providers/best_temp_mail.rb +77 -0
- data/lib/tempmail_sdk/providers/chatgpt_org_uk.rb +129 -0
- data/lib/tempmail_sdk/providers/clawdemail.rb +122 -0
- data/lib/tempmail_sdk/providers/crazymailing.rb +114 -0
- data/lib/tempmail_sdk/providers/disposablemail.rb +125 -0
- data/lib/tempmail_sdk/providers/disposablemail_app.rb +66 -0
- data/lib/tempmail_sdk/providers/email10min.rb +152 -0
- data/lib/tempmail_sdk/providers/emailnator.rb +132 -0
- data/lib/tempmail_sdk/providers/emailtemp_org.rb +131 -0
- data/lib/tempmail_sdk/providers/expressinboxhub.rb +118 -0
- data/lib/tempmail_sdk/providers/fakemail.rb +143 -0
- data/lib/tempmail_sdk/providers/firetempmail.rb +86 -0
- data/lib/tempmail_sdk/providers/flybymail.rb +93 -0
- data/lib/tempmail_sdk/providers/generator_email.rb +146 -0
- data/lib/tempmail_sdk/providers/haribu.rb +29 -3
- data/lib/tempmail_sdk/providers/huskmail.rb +132 -0
- data/lib/tempmail_sdk/providers/internxt.rb +176 -0
- data/lib/tempmail_sdk/providers/linshi_xyz.rb +79 -0
- data/lib/tempmail_sdk/providers/linshiyouxiang_net.rb +85 -0
- data/lib/tempmail_sdk/providers/mail_sunls.rb +42 -1
- data/lib/tempmail_sdk/providers/mail_td.rb +161 -0
- data/lib/tempmail_sdk/providers/mailcat_ai.rb +52 -0
- data/lib/tempmail_sdk/providers/maildrop.rb +79 -4
- data/lib/tempmail_sdk/providers/mailgolem.rb +96 -0
- data/lib/tempmail_sdk/providers/mailinator.rb +9 -2
- data/lib/tempmail_sdk/providers/mailmomy.rb +1 -1
- data/lib/tempmail_sdk/providers/mailtemp_cc.rb +91 -0
- data/lib/tempmail_sdk/providers/mailticking.rb +139 -0
- data/lib/tempmail_sdk/providers/minuteinbox.rb +174 -0
- data/lib/tempmail_sdk/providers/moakt.rb +29 -3
- data/lib/tempmail_sdk/providers/mtempmail.rb +140 -0
- data/lib/tempmail_sdk/providers/mytempmail_cc.rb +76 -0
- data/lib/tempmail_sdk/providers/nowtempmail.rb +112 -0
- data/lib/tempmail_sdk/providers/noxen_de5_net.rb +392 -0
- data/lib/tempmail_sdk/providers/nukemail.rb +188 -0
- data/lib/tempmail_sdk/providers/nullmail.rb +95 -0
- data/lib/tempmail_sdk/providers/openinbox.rb +102 -0
- data/lib/tempmail_sdk/providers/shadowmail.rb +174 -0
- data/lib/tempmail_sdk/providers/shitpost_email.rb +85 -0
- data/lib/tempmail_sdk/providers/smail_pw.rb +156 -0
- data/lib/tempmail_sdk/providers/smails.rb +128 -0
- data/lib/tempmail_sdk/providers/socketio_mail.rb +25 -0
- data/lib/tempmail_sdk/providers/temp_mail_gg.rb +362 -0
- data/lib/tempmail_sdk/providers/tempgbox.rb +118 -0
- data/lib/tempmail_sdk/providers/tempmail100.rb +148 -0
- data/lib/tempmail_sdk/providers/tempmail_cn.rb +111 -0
- data/lib/tempmail_sdk/providers/tempmail_ee.rb +373 -0
- data/lib/tempmail_sdk/providers/tempmailportal.rb +131 -0
- data/lib/tempmail_sdk/providers/tempmails_io.rb +89 -0
- data/lib/tempmail_sdk/providers/tempmailto.rb +312 -0
- data/lib/tempmail_sdk/providers/temporarymail_com.rb +184 -0
- data/lib/tempmail_sdk/providers/tenmin_app.rb +100 -0
- data/lib/tempmail_sdk/providers/tenminutemail_net.rb +183 -0
- data/lib/tempmail_sdk/providers/thirty_min_email.rb +90 -0
- data/lib/tempmail_sdk/providers/tmpkit.rb +129 -0
- data/lib/tempmail_sdk/providers/twentyfourmail_chacuo.rb +83 -0
- data/lib/tempmail_sdk/providers/vip_215.rb +171 -0
- data/lib/tempmail_sdk/providers/zerodrop.rb +90 -0
- data/lib/tempmail_sdk/registry.rb +285 -23
- data/lib/tempmail_sdk/types.rb +13 -0
- data/lib/tempmail_sdk/version.rb +1 -1
- data/lib/tempmail_sdk.rb +1 -1
- metadata +58 -6
- data/lib/tempmail_sdk/providers/gonebox_email.rb +0 -89
- data/lib/tempmail_sdk/providers/temp_mail_now.rb +0 -103
- data/lib/tempmail_sdk/providers/webmailtemp.rb +0 -108
|
@@ -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,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# ClawdEmail 渠道实现(clawdemail.com,API 域 api.clawdemail.com)
|
|
8
|
+
#
|
|
9
|
+
# POST /register 建箱(无验证码,body {"name":"xxx"},响应 success/email/token),
|
|
10
|
+
# GET /inbox 读信列表(Header Authorization: Bearer <token>,
|
|
11
|
+
# 响应 success/email/count/unread/emails[]),GET /email/{id} 取单封详情(Bearer)。
|
|
12
|
+
# 信件保留 30 分钟,仅接收不发送,正文仅纯文本。
|
|
13
|
+
module Clawdemail
|
|
14
|
+
CHANNEL = "clawdemail"
|
|
15
|
+
BASE_URL = "https://api.clawdemail.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
|
+
# 设置 clawdemail 请求的通用请求头(可选附带 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
|
+
# 创建 clawdemail.com 临时邮箱
|
|
37
|
+
# POST /register(body {"name":""})返回 email 与 token。
|
|
38
|
+
# @return [EmailInfo]
|
|
39
|
+
def generate_email
|
|
40
|
+
resp = Http.post("#{BASE_URL}/register",
|
|
41
|
+
headers: auth_headers("").merge("Content-Type" => "application/json"),
|
|
42
|
+
json: { "name" => "" }, timeout: 15)
|
|
43
|
+
raise "clawdemail: 创建邮箱失败 http #{resp.status_code}" unless resp.ok?
|
|
44
|
+
|
|
45
|
+
data = resp.json
|
|
46
|
+
raise "clawdemail: 创建响应非对象" unless data.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
email = data["email"].to_s.strip
|
|
49
|
+
token = data["token"].to_s.strip
|
|
50
|
+
raise "clawdemail: 创建邮箱响应缺少必要字段" if email.empty? || token.empty? || !email.include?("@")
|
|
51
|
+
|
|
52
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: token)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# 获取 clawdemail.com 邮件列表
|
|
56
|
+
# 流程:GET /inbox?limit=50 取列表,对每个元素按 id 逐封 GET /email/{id} 合并详情;
|
|
57
|
+
# 详情失败时以列表摘要归一。
|
|
58
|
+
# @param email [String] 邮箱地址
|
|
59
|
+
# @param token [String] 注册返回的认证令牌
|
|
60
|
+
# @return [Array<Email>]
|
|
61
|
+
def get_emails(email, token)
|
|
62
|
+
addr = email.to_s.strip
|
|
63
|
+
resp = Http.get("#{BASE_URL}/inbox?limit=50",
|
|
64
|
+
headers: auth_headers(token), timeout: 15)
|
|
65
|
+
raise "clawdemail: 获取邮件列表失败 http #{resp.status_code}" unless resp.ok?
|
|
66
|
+
|
|
67
|
+
data = resp.json
|
|
68
|
+
raise "clawdemail: 收件箱响应非对象" unless data.is_a?(Hash)
|
|
69
|
+
|
|
70
|
+
unless data["success"] == true
|
|
71
|
+
raise "clawdemail: 读取收件箱失败: #{data["error"]}"
|
|
72
|
+
end
|
|
73
|
+
emails = data["emails"]
|
|
74
|
+
return [] unless emails.is_a?(Array)
|
|
75
|
+
|
|
76
|
+
emails.filter_map do |m|
|
|
77
|
+
next unless m.is_a?(Hash)
|
|
78
|
+
|
|
79
|
+
detail = get_detail(token, message_id_of(m))
|
|
80
|
+
row = m.dup
|
|
81
|
+
if detail.is_a?(Hash)
|
|
82
|
+
detail.each { |k, v| row[k] = v unless row.key?(k) }
|
|
83
|
+
end
|
|
84
|
+
Normalize.normalize_email(row, addr)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# 从列表元素中提取邮件 ID(候选键 id/Id/slug/messageId/message_id)
|
|
89
|
+
# @param row [Hash]
|
|
90
|
+
# @return [String]
|
|
91
|
+
def message_id_of(row)
|
|
92
|
+
RETRY_KEYS.each do |key|
|
|
93
|
+
val = row[key]
|
|
94
|
+
return val.to_s.strip unless val.nil? || val.to_s.strip.empty?
|
|
95
|
+
end
|
|
96
|
+
""
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# 获取 clawdemail.com 单封邮件详情
|
|
100
|
+
# GET /email/{id}(Bearer token),响应含 email 嵌套对象(from_addr/body_text/received_at)
|
|
101
|
+
# 时提升嵌套对象。
|
|
102
|
+
# @param token [String] 认证令牌
|
|
103
|
+
# @param id [String] 邮件 ID
|
|
104
|
+
# @return [Hash, nil]
|
|
105
|
+
def get_detail(token, id)
|
|
106
|
+
return nil if id.to_s.empty?
|
|
107
|
+
|
|
108
|
+
uri = "#{BASE_URL}/email/#{URI.encode_www_form_component(id)}"
|
|
109
|
+
resp = Http.get(uri, headers: auth_headers(token), timeout: 15)
|
|
110
|
+
return nil unless resp.ok?
|
|
111
|
+
|
|
112
|
+
detail = resp.json
|
|
113
|
+
return nil unless detail.is_a?(Hash)
|
|
114
|
+
|
|
115
|
+
nested = detail["email"]
|
|
116
|
+
nested.is_a?(Hash) ? nested : detail
|
|
117
|
+
rescue StandardError
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# Crazymailing 渠道实现(crazymailing.com)
|
|
8
|
+
#
|
|
9
|
+
# Next.js 全栈站点,API 结构:
|
|
10
|
+
# POST /api/mailbox(空 JSON body)建箱,响应
|
|
11
|
+
# {"mailbox":{"id":"...","address":"...@crazymailing.com","expiresAt":"<RFC3339>"}};
|
|
12
|
+
# GET /api/messages?mailbox=<完整地址 URL 编码> 读信,响应 {"messages":[...]};
|
|
13
|
+
# GET /api/message/{id}/body 取单封正文(完整 HTML 页面)。
|
|
14
|
+
# 请求需携带 Origin/Referer 浏览器形态头。
|
|
15
|
+
module Crazymailing
|
|
16
|
+
CHANNEL = "crazymailing"
|
|
17
|
+
BASE_URL = "https://crazymailing.com"
|
|
18
|
+
|
|
19
|
+
HEADERS = {
|
|
20
|
+
"Content-Type" => "application/json",
|
|
21
|
+
"Accept" => "application/json",
|
|
22
|
+
"Origin" => BASE_URL,
|
|
23
|
+
"Referer" => "#{BASE_URL}/",
|
|
24
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
|
|
25
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
RETRY_KEYS = %w[id Id slug messageId message_id].freeze
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# 创建临时邮箱
|
|
33
|
+
# POST /api/mailbox(空 JSON body);域名由服务端统一分配(当前仅 @crazymailing.com)。
|
|
34
|
+
# @return [EmailInfo]
|
|
35
|
+
def generate_email
|
|
36
|
+
resp = Http.post("#{BASE_URL}/api/mailbox",
|
|
37
|
+
headers: HEADERS, json: {}, timeout: 15)
|
|
38
|
+
raise "crazymailing: 创建邮箱失败 http #{resp.status_code}" unless resp.ok?
|
|
39
|
+
|
|
40
|
+
data = resp.json
|
|
41
|
+
mailbox = data.is_a?(Hash) ? data["mailbox"] : nil
|
|
42
|
+
raise "crazymailing: 创建响应缺少 mailbox" unless mailbox.is_a?(Hash)
|
|
43
|
+
|
|
44
|
+
address = mailbox["address"].to_s.strip
|
|
45
|
+
raise "crazymailing: 创建响应缺少 mailbox.address" if address.empty?
|
|
46
|
+
|
|
47
|
+
EmailInfo.new(channel: CHANNEL, email: address,
|
|
48
|
+
token: mailbox["id"].to_s, expires_at: mailbox["expiresAt"].to_s)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 读取收件箱
|
|
52
|
+
# GET /api/messages?mailbox=<完整地址 URL 编码>,响应 {"messages":[...]};
|
|
53
|
+
# 对每个元素逐封 GET /api/message/{id}/body 拉取正文(失败不阻断列表)。
|
|
54
|
+
# @param email [String] 完整邮箱地址
|
|
55
|
+
# @param token [String] 建箱返回的 mailbox id
|
|
56
|
+
# @return [Array<Email>]
|
|
57
|
+
def get_emails(email, token)
|
|
58
|
+
addr = email.to_s.strip
|
|
59
|
+
raise "crazymailing: 邮箱地址为空" if addr.empty?
|
|
60
|
+
|
|
61
|
+
uri = "#{BASE_URL}/api/messages?mailbox=#{URI.encode_www_form_component(addr)}"
|
|
62
|
+
resp = Http.get(uri, headers: HEADERS, timeout: 15)
|
|
63
|
+
raise "crazymailing: 读取收件箱失败 http #{resp.status_code}" unless resp.ok?
|
|
64
|
+
|
|
65
|
+
data = resp.json
|
|
66
|
+
messages = data.is_a?(Hash) ? data["messages"] : nil
|
|
67
|
+
return [] unless messages.is_a?(Array)
|
|
68
|
+
|
|
69
|
+
messages.filter_map do |m|
|
|
70
|
+
next unless m.is_a?(Hash)
|
|
71
|
+
|
|
72
|
+
row = m.dup
|
|
73
|
+
row["to"] = addr unless row.key?("to")
|
|
74
|
+
id = message_id_of(row)
|
|
75
|
+
unless id.empty?
|
|
76
|
+
# 列表元素为摘要,正文须逐封二拉(失败不阻断)
|
|
77
|
+
html = get_body(id)
|
|
78
|
+
row["html"] = html if !html.empty?
|
|
79
|
+
end
|
|
80
|
+
Normalize.normalize_email(row, addr)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# 从列表元素中提取邮件 ID(候选键 id/Id/slug/messageId/message_id)
|
|
85
|
+
# @param row [Hash]
|
|
86
|
+
# @return [String]
|
|
87
|
+
def message_id_of(row)
|
|
88
|
+
RETRY_KEYS.each do |key|
|
|
89
|
+
val = row[key]
|
|
90
|
+
return val.to_s.strip unless val.nil? || val.to_s.strip.empty?
|
|
91
|
+
end
|
|
92
|
+
""
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# 拉取单封正文(GET /api/message/{id}/body,响应为完整 HTML 页面)
|
|
96
|
+
# @param id [String] 邮件 ID
|
|
97
|
+
# @return [String]
|
|
98
|
+
def get_body(id)
|
|
99
|
+
uri = "#{BASE_URL}/api/message/#{URI.encode_www_form_component(id)}/body"
|
|
100
|
+
resp = Http.get(uri,
|
|
101
|
+
headers: {
|
|
102
|
+
"Accept" => "text/html,application/xhtml+xml,*/*;q=0.8",
|
|
103
|
+
"Origin" => BASE_URL,
|
|
104
|
+
"Referer" => "#{BASE_URL}/",
|
|
105
|
+
"User-Agent" => HEADERS["User-Agent"]
|
|
106
|
+
},
|
|
107
|
+
timeout: 15)
|
|
108
|
+
return "" unless resp.ok?
|
|
109
|
+
|
|
110
|
+
resp.body.to_s
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
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
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# disposablemail.app 渠道实现
|
|
8
|
+
#
|
|
9
|
+
# 纯 REST JSON API,无需认证。
|
|
10
|
+
# 创建收件箱: POST /api/inbox (body: {})
|
|
11
|
+
# 获取邮件: GET /api/inbox/emails?token={token}
|
|
12
|
+
module DisposablemailApp
|
|
13
|
+
CHANNEL = "disposablemail-app"
|
|
14
|
+
API_BASE = "https://disposablemail.app/api"
|
|
15
|
+
|
|
16
|
+
HEADERS = {
|
|
17
|
+
"Accept" => "application/json, text/plain, */*",
|
|
18
|
+
"Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
|
|
19
|
+
"Content-Type" => "application/json",
|
|
20
|
+
"Origin" => "https://disposablemail.app",
|
|
21
|
+
"Referer" => "https://disposablemail.app/",
|
|
22
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
23
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# 创建临时邮箱
|
|
29
|
+
# @return [EmailInfo]
|
|
30
|
+
def generate_email
|
|
31
|
+
resp = Http.post("#{API_BASE}/inbox", headers: HEADERS, body: "{}")
|
|
32
|
+
resp.raise_for_status
|
|
33
|
+
data = resp.json
|
|
34
|
+
address = data["address"].to_s.strip
|
|
35
|
+
token = data["token"].to_s.strip
|
|
36
|
+
raise "disposablemail-app: address 或 token 为空" if address.empty? || token.empty?
|
|
37
|
+
|
|
38
|
+
info = EmailInfo.new(channel: CHANNEL, email: address, token: token)
|
|
39
|
+
info.expires_at = data["expiresAt"].to_s
|
|
40
|
+
info.created_at = data["createdAt"].to_s
|
|
41
|
+
info
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 获取邮件列表
|
|
45
|
+
# @param email [String]
|
|
46
|
+
# @param token [String] API 返回的 token
|
|
47
|
+
# @return [Array<Email>]
|
|
48
|
+
def get_emails(email, token)
|
|
49
|
+
raise "disposablemail-app: token 为空" if token.to_s.empty?
|
|
50
|
+
|
|
51
|
+
get_headers = HEADERS.reject { |k, _| k == "Content-Type" }
|
|
52
|
+
resp = Http.get("#{API_BASE}/inbox/emails?token=#{token}", headers: get_headers)
|
|
53
|
+
resp.raise_for_status
|
|
54
|
+
data = resp.json
|
|
55
|
+
emails_raw = data["emails"]
|
|
56
|
+
return [] unless emails_raw.is_a?(Array) && !emails_raw.empty?
|
|
57
|
+
|
|
58
|
+
emails_raw.filter_map do |raw|
|
|
59
|
+
next unless raw.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
Normalize.normalize_email(raw, email)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|