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,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# Firetempmail 渠道实现(firetempmail.com)
|
|
8
|
+
#
|
|
9
|
+
# 无认证 REST:建箱无需请求,本地生成 随机词+0-999@<域>(域池与官网一致:
|
|
10
|
+
# offrework.click / service-today.click / jobsdeforyou.sa.com);
|
|
11
|
+
# 读信 GET https://mail.firetempmail.com/mail/get?address=<邮箱 URL 编码>,
|
|
12
|
+
# 必须携带 Header Origin: https://firetempmail.com(否则 403 'Origin not allowed')。
|
|
13
|
+
# 响应形如 {"status":"ok","code":200,"msg":"...","stats":{},"mails":[...]},
|
|
14
|
+
# 邮件字段以 sender/subject/date + content-html/content-text/content-plain 多候选归一。
|
|
15
|
+
module Firetempmail
|
|
16
|
+
CHANNEL = "firetempmail"
|
|
17
|
+
API_BASE = "https://mail.firetempmail.com"
|
|
18
|
+
ORIGIN = "https://firetempmail.com"
|
|
19
|
+
# 官网 JS 中的完整平台域池,顺序与官网一致
|
|
20
|
+
DOMAINS = %w[offrework.click service-today.click jobsdeforyou.sa.com].freeze
|
|
21
|
+
|
|
22
|
+
HEADERS = {
|
|
23
|
+
"Accept" => "application/json",
|
|
24
|
+
"Origin" => ORIGIN,
|
|
25
|
+
"Referer" => "#{ORIGIN}/",
|
|
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
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# 生成 3-6 位小写随机词(模拟官网 1-5 字母词的观感)
|
|
33
|
+
LETTERS = ("a".."z").to_a
|
|
34
|
+
def random_word
|
|
35
|
+
Array.new(rand(3..6)) { LETTERS.sample }.join
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# 创建临时邮箱
|
|
39
|
+
# 建箱无需请求,本地随机生成 随机词+0-999@域名;token 复用完整地址
|
|
40
|
+
# @return [EmailInfo]
|
|
41
|
+
def generate_email
|
|
42
|
+
email = "#{random_word}#{rand(0..999)}@#{DOMAINS.sample}"
|
|
43
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: email)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 读取收件箱
|
|
47
|
+
# GET https://mail.firetempmail.com/mail/get?address=<URL 编码完整邮箱>,必带 Origin 头
|
|
48
|
+
# @param email [String] 邮箱地址
|
|
49
|
+
# @param _token [String] 令牌(本渠道忽略,以邮箱为准)
|
|
50
|
+
# @return [Array<Email>]
|
|
51
|
+
def get_emails(email, _token)
|
|
52
|
+
addr = email.to_s.strip
|
|
53
|
+
raise "firetempmail: 邮箱地址为空" if addr.empty?
|
|
54
|
+
|
|
55
|
+
url = "#{API_BASE}/mail/get?address=#{URI.encode_www_form_component(addr)}"
|
|
56
|
+
resp = Http.get(url, headers: HEADERS, timeout: 15)
|
|
57
|
+
resp.raise_for_status
|
|
58
|
+
data = resp.json
|
|
59
|
+
return [] unless data.is_a?(Hash)
|
|
60
|
+
|
|
61
|
+
# 错误与成功共用同一信封:status 非 ok 时抛错
|
|
62
|
+
status = data["status"].to_s
|
|
63
|
+
if !status.empty? && status != "ok"
|
|
64
|
+
raise "firetempmail: #{data["msg"] || "读信失败"}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
mails = data["mails"]
|
|
68
|
+
return [] unless mails.is_a?(Array)
|
|
69
|
+
|
|
70
|
+
mails.filter_map do |m|
|
|
71
|
+
next unless m.is_a?(Hash)
|
|
72
|
+
|
|
73
|
+
row = {
|
|
74
|
+
"from" => m["sender"] || m["from"] || m["from_address"],
|
|
75
|
+
"to" => addr,
|
|
76
|
+
"subject" => m["subject"] || m["title"],
|
|
77
|
+
"date" => m["date"] || m["received_at"] || m["created_at"],
|
|
78
|
+
"html" => m["content-html"] || m["html"],
|
|
79
|
+
"text" => m["content-text"] || m["content-plain"] || m["text"]
|
|
80
|
+
}
|
|
81
|
+
Normalize.normalize_email(row, addr)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# Flybymail 渠道实现(flybymail.com)
|
|
8
|
+
#
|
|
9
|
+
# POST /api/recipients 建箱(空 JSON body,响应 id/email/createdAt/expiresAt,
|
|
10
|
+
# expiresAt 为毫秒时间戳),GET /api/recipients/{email}/emails 读信
|
|
11
|
+
# (按邮箱地址、非 id 查询,响应 {"emails":[...]})。
|
|
12
|
+
# 信件字段:id/from/to/subject/body(纯文本)/htmlBody(HTML 正文)/preview/time/read。
|
|
13
|
+
module Flybymail
|
|
14
|
+
CHANNEL = "flybymail"
|
|
15
|
+
BASE_URL = "https://flybymail.com"
|
|
16
|
+
|
|
17
|
+
HEADERS = {
|
|
18
|
+
"Content-Type" => "application/json",
|
|
19
|
+
"Accept" => "application/json",
|
|
20
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
|
|
21
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# 创建 flybymail.com 临时邮箱
|
|
27
|
+
# POST /api/recipients(空 JSON body)返回 id/email/createdAt/expiresAt,
|
|
28
|
+
# expiresAt 为毫秒时间戳(约 4 小时),转换为毫秒值供 EmailInfo 统一展示。
|
|
29
|
+
# @return [EmailInfo]
|
|
30
|
+
def generate_email
|
|
31
|
+
resp = Http.post("#{BASE_URL}/api/recipients",
|
|
32
|
+
headers: HEADERS, json: {}, timeout: 15)
|
|
33
|
+
raise "flybymail: 创建邮箱失败 http #{resp.status_code}" unless resp.ok?
|
|
34
|
+
|
|
35
|
+
data = resp.json
|
|
36
|
+
raise "flybymail: 创建响应非对象" unless data.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
id = data["id"].to_s.strip
|
|
39
|
+
email = data["email"].to_s.strip
|
|
40
|
+
raise "flybymail: 创建邮箱响应缺少必要字段" if id.empty? || email.empty? || !email.include?("@")
|
|
41
|
+
|
|
42
|
+
expires_at = data["expiresAt"].to_i.positive? ? data["expiresAt"].to_i : nil
|
|
43
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: id, expires_at: expires_at)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# 获取 flybymail.com 邮件列表
|
|
47
|
+
# GET /api/recipients/{email}/emails(按地址查询)返回 {"emails":[...]}。
|
|
48
|
+
# @param email [String] 邮箱地址
|
|
49
|
+
# @param token [String] 建箱返回的收件人 ID(读信端点按邮箱地址查询,令牌保留)
|
|
50
|
+
# @return [Array<Email>]
|
|
51
|
+
def get_emails(email, token)
|
|
52
|
+
addr = email.to_s.strip
|
|
53
|
+
raise "flybymail: 邮箱地址为空或格式错误" if addr.empty? || !addr.include?("@")
|
|
54
|
+
|
|
55
|
+
uri = "#{BASE_URL}/api/recipients/#{URI.encode_www_form_component(addr)}/emails"
|
|
56
|
+
resp = Http.get(uri, headers: HEADERS, timeout: 15)
|
|
57
|
+
raise "flybymail: 获取邮件列表失败 http #{resp.status_code}" unless resp.ok?
|
|
58
|
+
|
|
59
|
+
data = resp.json
|
|
60
|
+
emails = data.is_a?(Hash) ? data["emails"] : nil
|
|
61
|
+
return [] unless emails.is_a?(Array)
|
|
62
|
+
|
|
63
|
+
emails.filter_map do |raw|
|
|
64
|
+
next unless raw.is_a?(Hash)
|
|
65
|
+
|
|
66
|
+
# id 为数字时按字符串归一,time 为毫秒时间戳时按 timestamp 归一
|
|
67
|
+
row = {
|
|
68
|
+
"id" => raw["id"].to_s.strip,
|
|
69
|
+
"from" => raw["from"],
|
|
70
|
+
"to" => raw["to"],
|
|
71
|
+
"subject" => raw["subject"],
|
|
72
|
+
"text" => raw["body"],
|
|
73
|
+
"html" => raw["htmlBody"],
|
|
74
|
+
"time" => raw["time"],
|
|
75
|
+
"read" => raw["read"],
|
|
76
|
+
"attachments" => raw["attachments"],
|
|
77
|
+
"timestamp" => timestamp_of(raw)
|
|
78
|
+
}
|
|
79
|
+
Normalize.normalize_email(row, addr)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# 归一邮件时间:候选 time/date 字段(time 为毫秒时间戳)
|
|
84
|
+
# @param m [Hash]
|
|
85
|
+
# @return [Object, nil]
|
|
86
|
+
def timestamp_of(m)
|
|
87
|
+
return m["time"] if m.key?("time") && !m["time"].nil?
|
|
88
|
+
|
|
89
|
+
m["date"]
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# GeneratorEmail 渠道实现(generator.email,PHP SSR 网页型)
|
|
8
|
+
#
|
|
9
|
+
# 流程:
|
|
10
|
+
# GET /inbox4/ 首页 SSR:解析 window.SITE_DATA 的 cur_user/cur_domain
|
|
11
|
+
# 快照生成邮箱(邮箱=user@domain),同页 Set-Cookie inbox_ctx
|
|
12
|
+
# 选中该邮箱会话
|
|
13
|
+
# GET /inbox4/(带 inbox_ctx Cookie)收信页:列为 class 含
|
|
14
|
+
# list-group-item2 的行(平台实测真实类为 list-group-item2,
|
|
15
|
+
# 非 list-group-item),内含 from_div_45g45gg / subj_div_45g45gg /
|
|
16
|
+
# time_div_45g45gg 三要素块
|
|
17
|
+
# 限制:本站不提供原文正文,按列表三要素归一。
|
|
18
|
+
# token 语义:{email, domain, user} JSON 快照。
|
|
19
|
+
module GeneratorEmail
|
|
20
|
+
CHANNEL = "generator-email"
|
|
21
|
+
BASE_URL = "https://generator.email"
|
|
22
|
+
INBOX_URL = "#{BASE_URL}/inbox4/"
|
|
23
|
+
|
|
24
|
+
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " \
|
|
25
|
+
"(KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36"
|
|
26
|
+
|
|
27
|
+
USER_RE = /cur_user:"([^"]*)"/
|
|
28
|
+
DOMAIN_RE = /cur_domain:"([^"]*)"/
|
|
29
|
+
# 列表条目与三要素块(平台实测真实类为 list-group-item2,按类名后缀锚定)
|
|
30
|
+
ITEM_RE = /<div[^>]*class="[^"]*list-group-item2[^"]*"[^>]*>(.*?)(?:<\/div>\s*){3}/m
|
|
31
|
+
FROM_RE = /class="[^"]*from_div_45g45gg[^"]*"[^>]*>(.*?)<\/div>/m
|
|
32
|
+
SUBJ_RE = /class="[^"]*subj_div_45g45gg[^"]*"[^>]*>(.*?)<\/div>/m
|
|
33
|
+
TIME_RE = /class="[^"]*time_div_45g45gg[^"]*"[^>]*>(.*?)<\/div>/m
|
|
34
|
+
SCRIPT_RE = /<(script|style)[\s\S]*?<\/\1>/i
|
|
35
|
+
TAG_RE = /<[^>]+>/
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# 请求收件箱渲染页并返回 HTML(邮箱由服务端依据 inbox_ctx Cookie 选择)
|
|
40
|
+
# @return [String]
|
|
41
|
+
def fetch_page
|
|
42
|
+
headers = {
|
|
43
|
+
"User-Agent" => USER_AGENT,
|
|
44
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9," \
|
|
45
|
+
"image/avif,image/webp,*/*;q=0.8",
|
|
46
|
+
"Accept-Language" => "en-US,en;q=0.9",
|
|
47
|
+
"Referer" => "#{BASE_URL}/"
|
|
48
|
+
}
|
|
49
|
+
ctx = @inbox_ctx
|
|
50
|
+
headers["Cookie"] = "inbox_ctx=#{ctx}" if ctx && !ctx.empty?
|
|
51
|
+
resp = Http.get(INBOX_URL, headers: headers, timeout: 20)
|
|
52
|
+
# 收下响应 Set-Cookie 的 inbox_ctx 最新值(同页刷新)
|
|
53
|
+
resp.set_cookies.each do |line|
|
|
54
|
+
kv = line.split(";").first.to_s.strip
|
|
55
|
+
@inbox_ctx = kv.delete_prefix("inbox_ctx=") if kv.start_with?("inbox_ctx=")
|
|
56
|
+
end
|
|
57
|
+
resp.raise_for_status
|
|
58
|
+
resp.body.to_s
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 取正则第一个捕获组,无匹配返回空串
|
|
62
|
+
# @param re [Regexp]
|
|
63
|
+
# @param src [String]
|
|
64
|
+
# @return [String]
|
|
65
|
+
def match_first(re, src)
|
|
66
|
+
m = re.match(src)
|
|
67
|
+
m ? m[1].to_s : ""
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 去标签与脚本/样式块,压缩空白
|
|
71
|
+
# @param s [String]
|
|
72
|
+
# @return [String]
|
|
73
|
+
def strip_tags(s)
|
|
74
|
+
out = s.gsub(SCRIPT_RE, " ")
|
|
75
|
+
out = out.gsub(TAG_RE, " ")
|
|
76
|
+
out.gsub(/\s+/, " ").strip
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# 创建 generator.email 临时邮箱
|
|
80
|
+
# 解析首页 SITE_DATA 快照(cur_user/cur_domain)得到邮箱地址。
|
|
81
|
+
# @return [EmailInfo]
|
|
82
|
+
def generate_email
|
|
83
|
+
src = fetch_page
|
|
84
|
+
user = match_first(USER_RE, src)
|
|
85
|
+
domain = match_first(DOMAIN_RE, src)
|
|
86
|
+
if user.empty? || domain.empty?
|
|
87
|
+
raise "generator-email: 首页未携带邮箱快照(cur_user/cur_domain)"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
email = "#{user}@#{domain}"
|
|
91
|
+
token = JSON.generate({ email: email, domain: domain, user: user })
|
|
92
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: token)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# 获取 generator.email 收件箱
|
|
96
|
+
# 解析收件箱渲染页的列表条目(from/subj/time 三要素);本站不提供
|
|
97
|
+
# 原文正文,SDK 按摘要归一。
|
|
98
|
+
# @param email [String] 邮箱地址
|
|
99
|
+
# @param token [String] 会话凭据 JSON({email, domain, user})
|
|
100
|
+
# @return [Array<Email>]
|
|
101
|
+
def get_emails(email, token)
|
|
102
|
+
sess = JSON.parse(token.to_s)
|
|
103
|
+
raise "generator-email: 会话凭据解析失败(非对象)" unless sess.is_a?(Hash)
|
|
104
|
+
|
|
105
|
+
raise "generator-email: 会话邮箱与查询邮箱不匹配" if sess["email"] != email
|
|
106
|
+
|
|
107
|
+
sess_domain = sess["domain"].to_s
|
|
108
|
+
src = fetch_page
|
|
109
|
+
# 服务端当前渲染邮箱与 token 不一致说明 Cookie 上下文已被切换
|
|
110
|
+
got_domain = match_first(DOMAIN_RE, src)
|
|
111
|
+
if got_domain != sess_domain
|
|
112
|
+
raise "generator-email: 会话域名已切换(token #{sess_domain},服务端 #{got_domain})"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# 列表区域锚定(#email-table ... #markodile 之间)
|
|
116
|
+
list_start = src.index('id="email-table"')
|
|
117
|
+
list_end = src.index('id="markodile"')
|
|
118
|
+
region = if list_start && list_end && list_end > list_start
|
|
119
|
+
src[list_start...list_end]
|
|
120
|
+
else
|
|
121
|
+
""
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
out = []
|
|
125
|
+
src.scan(ITEM_RE) do |raw|
|
|
126
|
+
raw = raw.to_s
|
|
127
|
+
# 跳过列表容器外的候选:要求条目文本来自列表区域
|
|
128
|
+
next if !region.empty? && !region.include?(raw)
|
|
129
|
+
|
|
130
|
+
from = strip_tags(match_first(FROM_RE, raw))
|
|
131
|
+
subject = strip_tags(match_first(SUBJ_RE, raw))
|
|
132
|
+
when_str = strip_tags(match_first(TIME_RE, raw))
|
|
133
|
+
next if from.empty? && subject.empty? && when_str.empty?
|
|
134
|
+
|
|
135
|
+
out << Normalize.normalize_email(
|
|
136
|
+
{ "from" => from, "to" => email, "subject" => subject, "date" => when_str },
|
|
137
|
+
email
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
out
|
|
141
|
+
rescue JSON::ParserError => e
|
|
142
|
+
raise "generator-email: 会话凭据解析失败: #{e.message}"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -32,7 +32,7 @@ module TempmailSdk
|
|
|
32
32
|
SUBJECT_RE = /<span\s+class\s*=\s*["']mail_konu["'][^>]*>([\s\S]*?)<\/span>/mi
|
|
33
33
|
DATE_RE = /<span\s+class\s*=\s*["']mail_zaman["'][^>]*>([\s\S]*?)<\/span>/mi
|
|
34
34
|
MAIL_LINK_RE = /href\s*=\s*["']([^"']*(?:mail|read|view)[^"']*)["']/i
|
|
35
|
-
|
|
35
|
+
BODY_OPEN_RE = /<div\s+(?:id|class)\s*=\s*["'](?:mail_icerik|icerik|mail-content|message-body)["'][^>]*>/mi
|
|
36
36
|
TAG_RE = /<[^>]+>/
|
|
37
37
|
|
|
38
38
|
module_function
|
|
@@ -112,8 +112,34 @@ module TempmailSdk
|
|
|
112
112
|
resp = Http.get(detail_url, headers: hdrs, timeout: 15)
|
|
113
113
|
return "" unless resp.ok?
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
extract_body_html(resp.body)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# 使用栈式深度匹配提取正文 div 的完整内部 HTML,
|
|
119
|
+
# 避免非贪婪正则在嵌套 div 时截断正文。
|
|
120
|
+
def extract_body_html(page)
|
|
121
|
+
m = page.match(BODY_OPEN_RE)
|
|
122
|
+
return "" unless m
|
|
123
|
+
|
|
124
|
+
start = m.end(0)
|
|
125
|
+
pos = start
|
|
126
|
+
depth = 1
|
|
127
|
+
while pos < page.length && depth > 0
|
|
128
|
+
next_open = page.index("<div", pos)
|
|
129
|
+
next_close = page.index("</div>", pos)
|
|
130
|
+
break unless next_close
|
|
131
|
+
|
|
132
|
+
if next_open && next_open < next_close
|
|
133
|
+
depth += 1
|
|
134
|
+
pos = next_open + 4
|
|
135
|
+
else
|
|
136
|
+
depth -= 1
|
|
137
|
+
return page[start...next_close].strip if depth.zero?
|
|
138
|
+
|
|
139
|
+
pos = next_close + 6
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
""
|
|
117
143
|
rescue StandardError
|
|
118
144
|
""
|
|
119
145
|
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# Huskmail 渠道实现(huskmail.xyz,API 域 api.huskmail.space)
|
|
8
|
+
#
|
|
9
|
+
# 流程:
|
|
10
|
+
# POST /v1/accounts 建箱(空 JSON body,响应 id/address/password/token/expiresAt/tier,
|
|
11
|
+
# token 为 JWT)
|
|
12
|
+
# GET /v1/messages 读信(Header Authorization: Bearer <token>,响应 {"messages":[...]})
|
|
13
|
+
# GET /v1/messages/{id} 取单封详情(Bearer)
|
|
14
|
+
# 收信域固定为 @huskmail.xyz。
|
|
15
|
+
module Huskmail
|
|
16
|
+
CHANNEL = "huskmail"
|
|
17
|
+
BASE_URL = "https://api.huskmail.space"
|
|
18
|
+
|
|
19
|
+
HEADERS = {
|
|
20
|
+
"Accept" => "application/json",
|
|
21
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
22
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0"
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# 可行 ID 候选键,按序提取
|
|
26
|
+
ID_KEYS = %w[id Id slug messageId message_id].freeze
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
# 设置 huskmail 请求的通用请求头
|
|
31
|
+
# @param token [String] 认证令牌,非空时携带 Authorization: Bearer
|
|
32
|
+
# @return [Hash]
|
|
33
|
+
def auth_headers(token = "")
|
|
34
|
+
hdrs = HEADERS.dup
|
|
35
|
+
hdrs["Authorization"] = "Bearer #{token}" unless token.to_s.empty?
|
|
36
|
+
hdrs
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# 创建 huskmail(@huskmail.xyz)临时邮箱
|
|
40
|
+
# POST /v1/accounts(空 JSON body)返回 address 与 JWT token
|
|
41
|
+
# @return [EmailInfo]
|
|
42
|
+
def generate_email
|
|
43
|
+
resp = Http.post("#{BASE_URL}/v1/accounts",
|
|
44
|
+
headers: HEADERS.merge("Content-Type" => "application/json"),
|
|
45
|
+
json: {}, timeout: 15)
|
|
46
|
+
resp.raise_for_status
|
|
47
|
+
data = resp.json
|
|
48
|
+
address = data.is_a?(Hash) ? data["address"].to_s.strip : ""
|
|
49
|
+
token = data.is_a?(Hash) ? data["token"].to_s.strip : ""
|
|
50
|
+
raise "huskmail: 创建邮箱响应缺少必要字段" if address.empty? || token.empty?
|
|
51
|
+
|
|
52
|
+
EmailInfo.new(channel: CHANNEL, email: address, token: token,
|
|
53
|
+
expires_at: data["expiresAt"])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# 获取 huskmail 邮件列表
|
|
57
|
+
# GET /v1/messages({"messages":[...]});对每个元素按 id 逐封请求详情合并,
|
|
58
|
+
# 详情失败时以列表摘要归一。
|
|
59
|
+
# @param token [String] JWT 认证令牌
|
|
60
|
+
# @param email [String] 邮箱地址(@huskmail.xyz)
|
|
61
|
+
# @return [Array<Email>]
|
|
62
|
+
def get_emails(token, email)
|
|
63
|
+
tk = token.to_s.strip
|
|
64
|
+
raise "huskmail: token 为空" if tk.empty?
|
|
65
|
+
|
|
66
|
+
resp = Http.get("#{BASE_URL}/v1/messages", headers: auth_headers(tk), timeout: 15)
|
|
67
|
+
resp.raise_for_status
|
|
68
|
+
data = resp.json
|
|
69
|
+
list = data.is_a?(Hash) ? data["messages"] : nil
|
|
70
|
+
return [] unless list.is_a?(Array)
|
|
71
|
+
|
|
72
|
+
addr = email.to_s.strip
|
|
73
|
+
list.filter_map do |m|
|
|
74
|
+
next unless m.is_a?(Hash)
|
|
75
|
+
|
|
76
|
+
Normalize.normalize_email(detail_row(tk, m, addr), addr)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# 从列表元素中提取消息 ID(候选键 id/Id/slug/messageId/message_id)
|
|
81
|
+
# @param row [Hash]
|
|
82
|
+
# @return [String]
|
|
83
|
+
def message_id_of(row)
|
|
84
|
+
ID_KEYS.each do |key|
|
|
85
|
+
val = row[key]
|
|
86
|
+
return val.to_s.strip unless val.nil? || val.to_s.strip.empty?
|
|
87
|
+
end
|
|
88
|
+
""
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# 逐封拉取详情并合并到列表摘要;无 ID 或详情失败时以摘要归一
|
|
92
|
+
# @param token [String] JWT 认证令牌
|
|
93
|
+
# @param summary [Hash] 列表元素摘要
|
|
94
|
+
# @param email [String] 收件人邮箱
|
|
95
|
+
# @return [Hash] 归一化输入行
|
|
96
|
+
def detail_row(token, summary, email)
|
|
97
|
+
id = message_id_of(summary)
|
|
98
|
+
uri = "#{BASE_URL}/v1/messages/#{URI.encode_www_form_component(id)}"
|
|
99
|
+
resp = Http.get(uri, headers: auth_headers(token), timeout: 15)
|
|
100
|
+
merged = summary
|
|
101
|
+
if resp.ok?
|
|
102
|
+
detail = begin
|
|
103
|
+
resp.json
|
|
104
|
+
rescue StandardError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
# 详情缺失字段不覆盖列表摘要(包含 null 的键跳过)
|
|
108
|
+
merged = summary.merge(detail.reject { |_k, v| v.nil? }) if detail.is_a?(Hash)
|
|
109
|
+
end
|
|
110
|
+
to_normalized_row(merged, email)
|
|
111
|
+
rescue StandardError
|
|
112
|
+
to_normalized_row(summary, email)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# 将原始消息 Hash 转换为归一化输入行
|
|
116
|
+
# @param raw [Hash]
|
|
117
|
+
# @param email [String]
|
|
118
|
+
# @return [Hash]
|
|
119
|
+
def to_normalized_row(raw, email)
|
|
120
|
+
{
|
|
121
|
+
"id" => raw["id"] || raw["messageId"] || raw["message_id"],
|
|
122
|
+
"from" => raw["from"],
|
|
123
|
+
"to" => email,
|
|
124
|
+
"subject" => raw["subject"],
|
|
125
|
+
"text" => raw["text"],
|
|
126
|
+
"html" => raw["html"],
|
|
127
|
+
"date" => raw["date"]
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# Internxt 渠道实现(internxt.com/temporary-email,Next.js + OpenNext)
|
|
8
|
+
#
|
|
9
|
+
# 流程:
|
|
10
|
+
# GET /temporary-email 夺取 XSRF-TOKEN Cookie(同气 csrfSecret)
|
|
11
|
+
# GET /api/temp-mail/create-email(头 csrf-token=<XSRF-TOKEN 值>)建箱
|
|
12
|
+
# GET /api/temp-mail/get-inbox?email=&token=(顶层数组)列信
|
|
13
|
+
# GET /api/temp-mail/get-message?email=&token=&messageId= 取单封全文
|
|
14
|
+
# CSRF 要点:csrf-token 头的值必须是罐中 XSRF-TOKEN(每次 API 响应都会
|
|
15
|
+
# 刷新该 Cookie,故每次请求前重取),不是 csrfSecret。
|
|
16
|
+
# token 语义:{address, token} JSON。
|
|
17
|
+
module Internxt
|
|
18
|
+
CHANNEL = "internxt"
|
|
19
|
+
SITE = "https://internxt.com"
|
|
20
|
+
REF = "#{SITE}/temporary-email"
|
|
21
|
+
API_BASE = "#{SITE}/api/temp-mail"
|
|
22
|
+
|
|
23
|
+
USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " \
|
|
24
|
+
"(KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36"
|
|
25
|
+
|
|
26
|
+
# 简单查询编码(取值均为 URL 安全字符)
|
|
27
|
+
REQUIRE_URI = true
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# 收下响应 Set-Cookie:XSRF-TOKEN/csrfSecret 同名覆写模块级状态
|
|
32
|
+
# @param resp [Http::Response]
|
|
33
|
+
def merge_cookies(resp)
|
|
34
|
+
resp.set_cookies.each do |line|
|
|
35
|
+
kv = line.split(";").first.to_s.strip
|
|
36
|
+
name, value = kv.split("=", 2)
|
|
37
|
+
@xsrf = value if name == "XSRF-TOKEN" && value
|
|
38
|
+
@secret = value if name == "csrfSecret" && value
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# 组装 Cookie 请求头(有则携带)
|
|
43
|
+
# @return [String]
|
|
44
|
+
def cookie_header
|
|
45
|
+
parts = []
|
|
46
|
+
parts << "csrfSecret=#{@secret}" if @secret && !@secret.empty?
|
|
47
|
+
parts << "XSRF-TOKEN=#{@xsrf}" if @xsrf && !@xsrf.empty?
|
|
48
|
+
parts.join("; ")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 确保已取得 XSRF-TOKEN:无则 GET /temporary-email 夺取
|
|
52
|
+
# @return [String] 罐中 XSRF-TOKEN 最新值
|
|
53
|
+
def prepare_xsrf
|
|
54
|
+
return @xsrf if @xsrf && !@xsrf.empty?
|
|
55
|
+
|
|
56
|
+
resp = Http.get(REF, headers: {
|
|
57
|
+
"User-Agent" => USER_AGENT,
|
|
58
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9," \
|
|
59
|
+
"image/avif,image/webp,*/*;q=0.8",
|
|
60
|
+
"Accept-Language" => "en-US,en;q=0.9"
|
|
61
|
+
}, timeout: 20)
|
|
62
|
+
resp.raise_for_status
|
|
63
|
+
merge_cookies(resp)
|
|
64
|
+
raise "internxt: 未取得 XSRF-TOKEN Cookie" if @xsrf.to_s.empty?
|
|
65
|
+
|
|
66
|
+
@xsrf
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# 带 CSRF 头请求 internxt 数据接口(GET),返回解析后的 JSON。
|
|
70
|
+
# csrf-token 头取罐中 XSRF-TOKEN 最新值(每次请求前重取,因每个 API
|
|
71
|
+
# 响应都会刷新该 Cookie)。
|
|
72
|
+
# @param path [String] 接口路径
|
|
73
|
+
# @param params [Hash, nil] 查询参数
|
|
74
|
+
# @return [Object]
|
|
75
|
+
def api_get(path, params = nil)
|
|
76
|
+
csrf = prepare_xsrf
|
|
77
|
+
url = "#{API_BASE}#{path}"
|
|
78
|
+
unless params.nil? || params.empty?
|
|
79
|
+
qs = params.map { |k, v| "#{k}=#{v}" }.join("&")
|
|
80
|
+
url = "#{url}?#{qs}"
|
|
81
|
+
end
|
|
82
|
+
headers = {
|
|
83
|
+
"User-Agent" => USER_AGENT,
|
|
84
|
+
"Accept" => "application/json, text/plain, */*",
|
|
85
|
+
"Origin" => SITE,
|
|
86
|
+
"Referer" => REF,
|
|
87
|
+
"csrf-token" => csrf
|
|
88
|
+
}
|
|
89
|
+
cookie = cookie_header
|
|
90
|
+
headers["Cookie"] = cookie unless cookie.empty?
|
|
91
|
+
resp = Http.get(url, headers: headers, timeout: 20)
|
|
92
|
+
merge_cookies(resp)
|
|
93
|
+
resp.raise_for_status
|
|
94
|
+
body = resp.body.to_s.strip
|
|
95
|
+
body.empty? ? nil : JSON.parse(body)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# 创建 internxt.com 临时邮箱
|
|
99
|
+
# 先确保已取得 XSRF-TOKEN,再 GET /api/temp-mail/create-email
|
|
100
|
+
# (带 csrf-token 头),响应 {"address","token"}。
|
|
101
|
+
# @return [EmailInfo]
|
|
102
|
+
def generate_email
|
|
103
|
+
prepare_xsrf
|
|
104
|
+
data = api_get("/create-email")
|
|
105
|
+
unless data.is_a?(Hash)
|
|
106
|
+
raise "internxt: 解析建箱响应失败"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
address = data["address"].to_s.strip
|
|
110
|
+
api_token = data["token"].to_s.strip
|
|
111
|
+
if address.empty? || api_token.empty? || !address.include?("@")
|
|
112
|
+
raise "internxt: 创建邮箱响应缺少必要字段(address/token)"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
token = JSON.generate({ address: address, token: api_token })
|
|
116
|
+
EmailInfo.new(channel: CHANNEL, email: address, token: token)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 从列表元素中提取邮件 ID(字符串形态,与 Go 端 messageIDOf 一致)
|
|
120
|
+
# @param row [Hash]
|
|
121
|
+
# @return [String]
|
|
122
|
+
def message_id_of(row)
|
|
123
|
+
%w[id messageId message_id].each do |key|
|
|
124
|
+
val = row[key]
|
|
125
|
+
return val.to_s.strip unless val.nil? || val.to_s.strip.empty?
|
|
126
|
+
end
|
|
127
|
+
""
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# 获取 internxt.com 收件箱
|
|
131
|
+
# get-inbox 返回顶层数组(列表元素含 id/from/subject/date/seen 等);
|
|
132
|
+
# 逐条 get-message 拉单封全文(响应为单封对象,含 html 渲染全文),
|
|
133
|
+
# 详情失败回退列表摘要。
|
|
134
|
+
# @param email [String] 邮箱地址
|
|
135
|
+
# @param token [String] 会话凭据 JSON(address/token)
|
|
136
|
+
# @return [Array<Email>]
|
|
137
|
+
def get_emails(email, token)
|
|
138
|
+
begin
|
|
139
|
+
sess = JSON.parse(token.to_s)
|
|
140
|
+
rescue JSON::ParserError => e
|
|
141
|
+
raise "internxt: 会话凭据解析失败: #{e.message}"
|
|
142
|
+
end
|
|
143
|
+
raise "internxt: 会话凭据解析失败(非对象)" unless sess.is_a?(Hash)
|
|
144
|
+
|
|
145
|
+
address = sess["address"].to_s.strip
|
|
146
|
+
api_token = sess["token"].to_s.strip
|
|
147
|
+
raise "internxt: 会话凭据缺少必要字段" if address.empty? || api_token.empty?
|
|
148
|
+
|
|
149
|
+
raise "internxt: 会话邮箱与查询邮箱不匹配" if address != email
|
|
150
|
+
|
|
151
|
+
inbox = api_get("/get-inbox", email: address, token: api_token)
|
|
152
|
+
raise "internxt: 解析收件箱响应失败" unless inbox.is_a?(Array)
|
|
153
|
+
|
|
154
|
+
inbox.filter_map do |item|
|
|
155
|
+
next unless item.is_a?(Hash)
|
|
156
|
+
|
|
157
|
+
m = item.dup
|
|
158
|
+
mid = message_id_of(m)
|
|
159
|
+
unless mid.empty?
|
|
160
|
+
begin
|
|
161
|
+
detail = api_get("/get-message",
|
|
162
|
+
email: address, token: api_token, messageId: mid)
|
|
163
|
+
if detail.is_a?(Hash)
|
|
164
|
+
# 列表字段优先,详情仅补齐缺失字段
|
|
165
|
+
detail.each { |k, v| m[k] = v unless m.key?(k) }
|
|
166
|
+
end
|
|
167
|
+
rescue StandardError
|
|
168
|
+
# 详情失败回退列表摘要
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
Normalize.normalize_email(m, email)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|