tempmail_sdk 1.3.4 → 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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/tempmail_sdk/channels.rb +29 -7
  4. data/lib/tempmail_sdk/channels_data.rb +29 -7
  5. data/lib/tempmail_sdk/providers/clawdemail.rb +122 -0
  6. data/lib/tempmail_sdk/providers/crazymailing.rb +114 -0
  7. data/lib/tempmail_sdk/providers/firetempmail.rb +86 -0
  8. data/lib/tempmail_sdk/providers/flybymail.rb +93 -0
  9. data/lib/tempmail_sdk/providers/generator_email.rb +146 -0
  10. data/lib/tempmail_sdk/providers/huskmail.rb +132 -0
  11. data/lib/tempmail_sdk/providers/internxt.rb +176 -0
  12. data/lib/tempmail_sdk/providers/linshi_xyz.rb +79 -0
  13. data/lib/tempmail_sdk/providers/mailmomy.rb +1 -1
  14. data/lib/tempmail_sdk/providers/mailticking.rb +139 -0
  15. data/lib/tempmail_sdk/providers/mtempmail.rb +140 -0
  16. data/lib/tempmail_sdk/providers/nowtempmail.rb +112 -0
  17. data/lib/tempmail_sdk/providers/noxen_de5_net.rb +392 -0
  18. data/lib/tempmail_sdk/providers/nukemail.rb +188 -0
  19. data/lib/tempmail_sdk/providers/nullmail.rb +95 -0
  20. data/lib/tempmail_sdk/providers/shadowmail.rb +174 -0
  21. data/lib/tempmail_sdk/providers/shitpost_email.rb +85 -0
  22. data/lib/tempmail_sdk/providers/smails.rb +128 -0
  23. data/lib/tempmail_sdk/providers/temp_mail_gg.rb +362 -0
  24. data/lib/tempmail_sdk/providers/tempmail100.rb +148 -0
  25. data/lib/tempmail_sdk/providers/tempmail_ee.rb +373 -0
  26. data/lib/tempmail_sdk/providers/tempmailportal.rb +131 -0
  27. data/lib/tempmail_sdk/providers/tempmails_io.rb +89 -0
  28. data/lib/tempmail_sdk/providers/tempmailto.rb +312 -0
  29. data/lib/tempmail_sdk/providers/temporarymail_com.rb +184 -0
  30. data/lib/tempmail_sdk/providers/tenmin_app.rb +100 -0
  31. data/lib/tempmail_sdk/providers/thirty_min_email.rb +90 -0
  32. data/lib/tempmail_sdk/providers/tmpkit.rb +129 -0
  33. data/lib/tempmail_sdk/providers/zerodrop.rb +90 -0
  34. data/lib/tempmail_sdk/registry.rb +129 -29
  35. data/lib/tempmail_sdk/types.rb +13 -0
  36. data/lib/tempmail_sdk/version.rb +1 -1
  37. data/lib/tempmail_sdk.rb +1 -1
  38. metadata +31 -7
  39. data/lib/tempmail_sdk/providers/gonebox_email.rb +0 -89
  40. data/lib/tempmail_sdk/providers/mohmal.rb +0 -208
  41. data/lib/tempmail_sdk/providers/temp_mail_now.rb +0 -103
  42. data/lib/tempmail_sdk/providers/webmailtemp.rb +0 -108
@@ -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
@@ -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
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module TempmailSdk
6
+ module Providers
7
+ # LinshiXYZ 渠道实现(linshi.xyz)
8
+ #
9
+ # 无建箱请求:本地随机 6 位 hex 前缀(官网 client 即用短 id)+ @linshi.xyz。
10
+ # 读信 GET https://linshi.xyz/api/mails/{前缀},无邮件时返回空数组 [],
11
+ # 有邮件时为邮件对象数组(元素含 headers{from,to,subject,date} 与 html 正文)。
12
+ # 归一化时将 headers 平铺并注入收件人地址;非数组骨架(如 {ok:false})整体失败,
13
+ # 交由上层 fallback 渠道处理。
14
+ module LinshiXyz
15
+ CHANNEL = "linshi-xyz"
16
+ BASE_URL = "https://linshi.xyz"
17
+ DOMAIN = "linshi.xyz"
18
+ HEX_DIGITS = "0123456789abcdef"
19
+
20
+ HEADERS = {
21
+ "Accept" => "application/json",
22
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
23
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
24
+ }.freeze
25
+
26
+ module_function
27
+
28
+ # 生成本地随机 6 位 hex 前缀(与官网 client 相同格式)
29
+ # @return [String]
30
+ def local_name
31
+ Array.new(6) { HEX_DIGITS[rand(HEX_DIGITS.length)] }.join
32
+ end
33
+
34
+ # 创建 linshi.xyz 临时邮箱
35
+ # 无需建箱请求,本地随机 6 位 hex 前缀;token 复用完整地址。
36
+ # @return [EmailInfo]
37
+ def generate_email
38
+ addr = "#{local_name}@#{DOMAIN}"
39
+ EmailInfo.new(channel: CHANNEL, email: addr, token: addr)
40
+ end
41
+
42
+ # 读取 linshi.xyz 收件箱
43
+ # GET /api/mails/{前缀}(URL 编码),响应为邮件对象数组。
44
+ # @param email [String] 完整邮箱地址(前缀@linshi.xyz)
45
+ # @param token [String] 复用完整地址
46
+ # @return [Array<Email>]
47
+ def get_emails(email, token)
48
+ addr = email.to_s.strip
49
+ raise "linshi-xyz: 邮箱地址无效: #{addr.inspect}" if addr.empty? || !addr.include?("@")
50
+
51
+ local = addr.split("@", 2).first.to_s
52
+ resp = Http.get("#{BASE_URL}/api/mails/#{URI.encode_www_form_component(local)}",
53
+ headers: HEADERS, timeout: 15)
54
+ raise "linshi-xyz: 读取收件箱失败 http #{resp.status_code}" unless resp.ok?
55
+
56
+ list = resp.json
57
+ raise "linshi-xyz: 收件箱响应非数组" unless list.is_a?(Array)
58
+
59
+ list.filter_map do |m|
60
+ next unless m.is_a?(Hash)
61
+
62
+ Normalize.normalize_email(normalize_row(m, addr), addr)
63
+ end
64
+ end
65
+
66
+ # 将 linshi.xyz 邮件对象归一化:headers 平铺为顶层字段并注入收件人。
67
+ # @param m [Hash] 原始邮件对象
68
+ # @param email [String] 收件人地址
69
+ # @return [Hash]
70
+ def normalize_row(m, email)
71
+ flat = m.dup
72
+ headers = m["headers"]
73
+ flat.merge!(headers) if headers.is_a?(Hash)
74
+ flat["to"] = email unless flat.key?("to")
75
+ flat
76
+ end
77
+ end
78
+ end
79
+ end
@@ -12,7 +12,7 @@ module TempmailSdk
12
12
  # 2. 本地随机 10 位 [a-z0-9] local part,拼接为 <local>@<域名>
13
13
  # 3. GET /api/mail/messages?to=<email>&page=1&limit=20 -> {"emails": [...], ...}
14
14
  #
15
- # 16 个域名变体渠道(如 16888888.cyou、sbook.pics 等)复用同一后端,
15
+ # 域名变体渠道(如 17666688.shop、sbook.pics 等)复用同一后端,
16
16
  # 仅在 generate_email(domain) 传入固定域名即可,收信逻辑完全一致。
17
17
  module Mailmomy
18
18
  CHANNEL = "mailmomy"
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TempmailSdk
4
+ module Providers
5
+ # Mailticking 渠道实现(www.mailticking.com,旧域名 temporary-mail.net 的更名站)
6
+ #
7
+ # 实测协议(三类请求均以 application/json 交流,站点在 Cloudflare 后):
8
+ # 1. 建箱 POST /get-mailbox body {"types":["4"]}(4=独立域名,排除 Gmail 别名)
9
+ # 响应 {"success":true,"email":"xxx@domain","activate_token":"..."}
10
+ # 2. 激活 POST /activate-email body {"email":..,"source":"api","activate_token":..}
11
+ # 响应 {"success":true}
12
+ # 3. 列信 POST /get-emails?lang=en body {"email":..,"code":..}
13
+ # 空箱实测响应 {"emails":[],"success":true};空闲超时/被改绑后返回
14
+ # {"success":false,"needNewEmail":true,...},此时返回错误提示换箱。
15
+ #
16
+ # 读信正文无公开端点:GetEmails 只具备列表能力,列表字段名采用多候选提取
17
+ # 策略,命中多少映射多少(Normalize 既有候选字段负责兜底)。
18
+ module Mailticking
19
+ CHANNEL = "mailticking"
20
+ BASE_URL = "https://www.mailticking.com"
21
+
22
+ HEADERS = {
23
+ "Accept" => "application/json",
24
+ "Accept-Language" => "en-US,en;q=0.9",
25
+ "Content-Type" => "application/json",
26
+ "Referer" => "#{BASE_URL}/",
27
+ "Origin" => BASE_URL,
28
+ "User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \
29
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
30
+ }.freeze
31
+
32
+ module_function
33
+
34
+ # 执行 JSON POST 请求并解析响应;响应非 2xx 或 success=false 时抛错
35
+ # @param path [String] 请求路径
36
+ # @param payload [Hash] JSON 请求体
37
+ # @return [Hash]
38
+ def do_post(path, payload)
39
+ resp = Http.post("#{BASE_URL}#{path}", headers: HEADERS, json: payload, timeout: 15)
40
+ data = resp.json
41
+ unless resp.ok?
42
+ msg = data.is_a?(Hash) ? (data["error"] || data["message"]).to_s : ""
43
+ msg = resp.body.to_s if msg.empty?
44
+ raise "mailticking: http #{resp.status_code}: #{msg}"
45
+ end
46
+ unless data.is_a?(Hash) && data["success"] == true
47
+ msg = data.is_a?(Hash) ? (data["error"] || data["message"]).to_s : ""
48
+ msg = "unknown error" if msg.empty?
49
+ raise "mailticking: 请求失败: #{msg}"
50
+ end
51
+ data
52
+ end
53
+
54
+ # 创建 mailticking 邮箱账号
55
+ # 请求 type=4(独立域名)固定取独立域名邮箱,建箱后立即激活会话;
56
+ # token 必须携带 activate_token(列信协议依赖激活会话),不能为空。
57
+ # @return [EmailInfo]
58
+ def generate_email
59
+ box = do_post("/get-mailbox", { "types" => ["4"] })
60
+ token = box["code"].to_s.strip
61
+ token = box["email"].to_s.strip if token.empty?
62
+ email = box["email"].to_s.strip
63
+ if token.empty? || email.empty?
64
+ raise "mailticking: get-mailbox 返回空 email/activate_token"
65
+ end
66
+
67
+ # 激活邮箱,使后续列信请求与服务器记录的最新会话一致
68
+ do_post("/activate-email", {
69
+ "email" => email,
70
+ "source" => "api",
71
+ "activate_token" => token
72
+ })
73
+
74
+ EmailInfo.new(channel: CHANNEL, email: email, token: token)
75
+ end
76
+
77
+ # 获取 mailticking 邮箱的邮件列表
78
+ # 空箱返回空列表不报错;需要换箱的响应(needNewEmail)返回语义错误。
79
+ # @param email [String] 邮箱地址
80
+ # @param token [String] 建箱返回的激活令牌
81
+ # @return [Array<Email>]
82
+ def get_emails(email, token)
83
+ addr = email.to_s.strip
84
+ tk = token.to_s.strip
85
+ raise "mailticking: 邮箱地址为空" if addr.empty?
86
+ raise "mailticking: activate code 为空" if tk.empty?
87
+
88
+ resp = Http.post("#{BASE_URL}/get-emails?lang=en",
89
+ headers: HEADERS,
90
+ json: { "email" => addr, "code" => tk },
91
+ timeout: 15)
92
+ data = resp.json
93
+ unless resp.ok?
94
+ msg = data.is_a?(Hash) ? (data["error"] || data["message"]).to_s : ""
95
+ msg = resp.body.to_s if msg.empty?
96
+ raise "mailticking: http #{resp.status_code}: #{msg}"
97
+ end
98
+ unless data.is_a?(Hash) && data["success"] == true
99
+ raise "mailticking: 邮箱已过期,请重新建箱" if data.is_a?(Hash) && data["needNewEmail"] == true
100
+
101
+ raise "mailticking: get-emails 失败"
102
+ end
103
+ emails = data["emails"]
104
+ return [] unless emails.is_a?(Array)
105
+
106
+ emails.filter_map do |raw|
107
+ next unless raw.is_a?(Hash)
108
+
109
+ Normalize.normalize_email(migrated_fields(raw), addr)
110
+ end
111
+ end
112
+
113
+ # 尽量映射列表字段到统一字段名
114
+ # 官网首页表格只有 SENDER/SUBJECT/TIME 三列,字段全名缺少可观测证据,
115
+ # 因此对常见字段做多候选提取;未命中的字段留给 Normalize 自己处理。
116
+ # @param raw [Hash]
117
+ # @return [Hash]
118
+ def migrated_fields(raw)
119
+ m = raw.dup
120
+ unless m.key?("from")
121
+ %w[mail_from from_mail from_email sender_address from_address
122
+ send_addr mail_addr address_from ho_from fromname fromS].each do |key|
123
+ v = m[key]
124
+ if v.is_a?(String) && v.strip != ""
125
+ m["from"] = v
126
+ break
127
+ end
128
+ end
129
+ end
130
+ if !m.key?("sender") && m["from"].is_a?(String) && m["from"].strip != ""
131
+ m["sender"] = m["from"]
132
+ end
133
+ m["id"] = m["mail_id"] if !m.key?("id") && m.key?("mail_id")
134
+ m["date"] = m["received_at"] if !m.key?("date") && m.key?("received_at")
135
+ m
136
+ end
137
+ end
138
+ end
139
+ end