tempmail_sdk 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +116 -0
- data/lib/tempmail_sdk/channels.rb +289 -0
- data/lib/tempmail_sdk/channels_data.rb +289 -0
- data/lib/tempmail_sdk/client.rb +193 -0
- data/lib/tempmail_sdk/config.rb +104 -0
- data/lib/tempmail_sdk/html_utils.rb +42 -0
- data/lib/tempmail_sdk/http.rb +225 -0
- data/lib/tempmail_sdk/logger.rb +38 -0
- data/lib/tempmail_sdk/normalize.rb +193 -0
- data/lib/tempmail_sdk/providers/anonbox.rb +248 -0
- data/lib/tempmail_sdk/providers/anonymmail.rb +92 -0
- data/lib/tempmail_sdk/providers/byom.rb +48 -0
- data/lib/tempmail_sdk/providers/catchmail.rb +110 -0
- data/lib/tempmail_sdk/providers/cleantempmail.rb +49 -0
- data/lib/tempmail_sdk/providers/devmail_uk.rb +58 -0
- data/lib/tempmail_sdk/providers/dropmail.rb +133 -0
- data/lib/tempmail_sdk/providers/dropmail_click.rb +81 -0
- data/lib/tempmail_sdk/providers/dropmail_me.rb +163 -0
- data/lib/tempmail_sdk/providers/duckmail.rb +63 -0
- data/lib/tempmail_sdk/providers/eyepaste.rb +119 -0
- data/lib/tempmail_sdk/providers/fake_email_site.rb +48 -0
- data/lib/tempmail_sdk/providers/fake_legal.rb +109 -0
- data/lib/tempmail_sdk/providers/fmail.rb +87 -0
- data/lib/tempmail_sdk/providers/freecustom.rb +135 -0
- data/lib/tempmail_sdk/providers/getnada.rb +146 -0
- data/lib/tempmail_sdk/providers/gonebox_email.rb +89 -0
- data/lib/tempmail_sdk/providers/guerrillamail.rb +62 -0
- data/lib/tempmail_sdk/providers/guerrillamail_mirrors.rb +81 -0
- data/lib/tempmail_sdk/providers/harakirimail.rb +84 -0
- data/lib/tempmail_sdk/providers/haribu.rb +176 -0
- data/lib/tempmail_sdk/providers/inboxes.rb +110 -0
- data/lib/tempmail_sdk/providers/inboxkitten.rb +90 -0
- data/lib/tempmail_sdk/providers/linshiyou.rb +157 -0
- data/lib/tempmail_sdk/providers/lroid.rb +170 -0
- data/lib/tempmail_sdk/providers/m2u.rb +126 -0
- data/lib/tempmail_sdk/providers/mail10s.rb +59 -0
- data/lib/tempmail_sdk/providers/mail123.rb +82 -0
- data/lib/tempmail_sdk/providers/mail_cx.rb +133 -0
- data/lib/tempmail_sdk/providers/mail_sunls.rb +68 -0
- data/lib/tempmail_sdk/providers/mail_tm.rb +99 -0
- data/lib/tempmail_sdk/providers/mailcatch.rb +91 -0
- data/lib/tempmail_sdk/providers/maildrop.rb +92 -0
- data/lib/tempmail_sdk/providers/maildrop_cc.rb +110 -0
- data/lib/tempmail_sdk/providers/mailforspam.rb +96 -0
- data/lib/tempmail_sdk/providers/mailhole_de.rb +70 -0
- data/lib/tempmail_sdk/providers/mailinator.rb +182 -0
- data/lib/tempmail_sdk/providers/mailmomy.rb +108 -0
- data/lib/tempmail_sdk/providers/mailnesia.rb +141 -0
- data/lib/tempmail_sdk/providers/mffac.rb +101 -0
- data/lib/tempmail_sdk/providers/moakt.rb +296 -0
- data/lib/tempmail_sdk/providers/neighbours.rb +140 -0
- data/lib/tempmail_sdk/providers/neighbours_sh.rb +117 -0
- data/lib/tempmail_sdk/providers/nimail.rb +100 -0
- data/lib/tempmail_sdk/providers/ockito.rb +163 -0
- data/lib/tempmail_sdk/providers/one_sec_mail.rb +46 -0
- data/lib/tempmail_sdk/providers/restmail_net.rb +60 -0
- data/lib/tempmail_sdk/providers/rootsh.rb +167 -0
- data/lib/tempmail_sdk/providers/shitty_email.rb +84 -0
- data/lib/tempmail_sdk/providers/smailpro.rb +103 -0
- data/lib/tempmail_sdk/providers/ta_easy.rb +53 -0
- data/lib/tempmail_sdk/providers/temp_mail_io.rb +67 -0
- data/lib/tempmail_sdk/providers/temp_mail_now.rb +103 -0
- data/lib/tempmail_sdk/providers/temp_mail_org.rb +89 -0
- data/lib/tempmail_sdk/providers/tempemail_co.rb +142 -0
- data/lib/tempmail_sdk/providers/tempemail_info.rb +121 -0
- data/lib/tempmail_sdk/providers/tempemails_net.rb +159 -0
- data/lib/tempmail_sdk/providers/tempfastmail.rb +76 -0
- data/lib/tempmail_sdk/providers/tempgmailer.rb +126 -0
- data/lib/tempmail_sdk/providers/tempgo_email.rb +74 -0
- data/lib/tempmail_sdk/providers/tempinbox.rb +70 -0
- data/lib/tempmail_sdk/providers/tempmail.rb +66 -0
- data/lib/tempmail_sdk/providers/tempmail365.rb +119 -0
- data/lib/tempmail_sdk/providers/tempmail_fish.rb +89 -0
- data/lib/tempmail_sdk/providers/tempmail_fyi.rb +124 -0
- data/lib/tempmail_sdk/providers/tempmail_lol.rb +42 -0
- data/lib/tempmail_sdk/providers/tempmail_lol_v2.rb +57 -0
- data/lib/tempmail_sdk/providers/tempmail_plus.rb +80 -0
- data/lib/tempmail_sdk/providers/tempmailc.rb +80 -0
- data/lib/tempmail_sdk/providers/tempmailpro.rb +89 -0
- data/lib/tempmail_sdk/providers/tempmailten.rb +150 -0
- data/lib/tempmail_sdk/providers/temporam.rb +95 -0
- data/lib/tempmail_sdk/providers/tempp_mails.rb +150 -0
- data/lib/tempmail_sdk/providers/tempy_email.rb +73 -0
- data/lib/tempmail_sdk/providers/ten_minute_mail_net.rb +125 -0
- data/lib/tempmail_sdk/providers/tenminute_one.rb +255 -0
- data/lib/tempmail_sdk/providers/throwawaymail.rb +80 -0
- data/lib/tempmail_sdk/providers/tmail_link.rb +98 -0
- data/lib/tempmail_sdk/providers/uncorreotemporal.rb +84 -0
- data/lib/tempmail_sdk/providers/webmailtemp.rb +108 -0
- data/lib/tempmail_sdk/providers/xkx_me.rb +125 -0
- data/lib/tempmail_sdk/providers/zhujump.rb +256 -0
- data/lib/tempmail_sdk/registry.rb +1097 -0
- data/lib/tempmail_sdk/retry.rb +91 -0
- data/lib/tempmail_sdk/telemetry.rb +134 -0
- data/lib/tempmail_sdk/types.rb +164 -0
- data/lib/tempmail_sdk/version.rb +6 -0
- data/lib/tempmail_sdk/webui.rb +360 -0
- data/lib/tempmail_sdk.rb +76 -0
- metadata +142 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module TempmailSdk
|
|
8
|
+
module Providers
|
|
9
|
+
# Haribu 渠道 — https://haribu.net
|
|
10
|
+
# Tempail 类模式,session cookies 维持会话
|
|
11
|
+
module Haribu
|
|
12
|
+
CHANNEL = "haribu"
|
|
13
|
+
BASE = "https://haribu.net"
|
|
14
|
+
TOK_PREFIX = "haribu1:"
|
|
15
|
+
|
|
16
|
+
HEADERS = {
|
|
17
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
18
|
+
"Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8",
|
|
19
|
+
"Cache-Control" => "no-cache",
|
|
20
|
+
"DNT" => "1",
|
|
21
|
+
"Pragma" => "no-cache",
|
|
22
|
+
"Upgrade-Insecure-Requests" => "1",
|
|
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 Edg/146.0.0.0"
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# 匹配 <input id="eposta_adres" value="xxx@yyy.com">
|
|
28
|
+
EMAIL_INPUT_RE = /<input[^>]+id\s*=\s*["']eposta_adres["'][^>]+value\s*=\s*["']([^"']+)["']/i
|
|
29
|
+
EMAIL_INPUT_RE2 = /<input[^>]+value\s*=\s*["']([^"']+@[^"']+)["'][^>]+id\s*=\s*["']eposta_adres["']/i
|
|
30
|
+
MAIL_ITEM_RE = /<li\s+class\s*=\s*["']mail["'][^>]*>([\s\S]*?)<\/li>/mi
|
|
31
|
+
FROM_RE = /<span\s+class\s*=\s*["']mail_gonderen["'][^>]*>([\s\S]*?)<\/span>/mi
|
|
32
|
+
SUBJECT_RE = /<span\s+class\s*=\s*["']mail_konu["'][^>]*>([\s\S]*?)<\/span>/mi
|
|
33
|
+
DATE_RE = /<span\s+class\s*=\s*["']mail_zaman["'][^>]*>([\s\S]*?)<\/span>/mi
|
|
34
|
+
MAIL_LINK_RE = /href\s*=\s*["']([^"']*(?:mail|read|view)[^"']*)["']/i
|
|
35
|
+
BODY_RE = /<div\s+(?:id|class)\s*=\s*["'](?:mail_icerik|icerik|mail-content|message-body)["'][^>]*>([\s\S]*?)<\/div>/mi
|
|
36
|
+
TAG_RE = /<[^>]+>/
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
def strip_tags(html_str)
|
|
41
|
+
html_str.gsub(TAG_RE, " ").strip
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# 从响应中合并 cookies
|
|
45
|
+
def merge_cookies(prev, resp)
|
|
46
|
+
existing = {}
|
|
47
|
+
prev.to_s.split(";").each do |part|
|
|
48
|
+
part = part.strip
|
|
49
|
+
next unless part.include?("=")
|
|
50
|
+
|
|
51
|
+
k, v = part.split("=", 2)
|
|
52
|
+
existing[k.strip] = v.to_s.strip unless k.strip.empty?
|
|
53
|
+
end
|
|
54
|
+
resp.set_cookies.each do |line|
|
|
55
|
+
pair = line.split(";", 2).first.to_s.strip
|
|
56
|
+
next unless pair.include?("=")
|
|
57
|
+
|
|
58
|
+
k, v = pair.split("=", 2)
|
|
59
|
+
existing[k.strip] = v.to_s.strip unless k.strip.empty?
|
|
60
|
+
end
|
|
61
|
+
existing.keys.sort.map { |k| "#{k}=#{existing[k]}" }.join("; ")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# 编码 session token
|
|
65
|
+
def encode_sess(cookie_hdr)
|
|
66
|
+
payload = JSON.generate("c" => cookie_hdr)
|
|
67
|
+
TOK_PREFIX + Base64.strict_encode64(payload)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 解码 session token
|
|
71
|
+
def decode_sess(token)
|
|
72
|
+
raise "haribu: 无效的会话令牌" unless token.to_s.start_with?(TOK_PREFIX)
|
|
73
|
+
|
|
74
|
+
raw = token[TOK_PREFIX.length..]
|
|
75
|
+
data = JSON.parse(Base64.strict_decode64(raw))
|
|
76
|
+
cookie_hdr = (data["c"] || "").strip
|
|
77
|
+
raise "haribu: 会话令牌中缺少 cookie" if cookie_hdr.empty?
|
|
78
|
+
|
|
79
|
+
cookie_hdr
|
|
80
|
+
rescue ArgumentError, JSON::ParserError
|
|
81
|
+
raise "haribu: 无效的会话令牌"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# 从 HTML 中提取邮箱地址
|
|
85
|
+
def extract_email(html_str)
|
|
86
|
+
m = html_str.match(EMAIL_INPUT_RE) || html_str.match(EMAIL_INPUT_RE2)
|
|
87
|
+
raise "haribu: 未找到邮箱地址" unless m
|
|
88
|
+
|
|
89
|
+
addr = CGI.unescapeHTML(m[1]).strip
|
|
90
|
+
raise "haribu: 邮箱地址无效" unless addr.include?("@")
|
|
91
|
+
|
|
92
|
+
addr
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# 创建临时邮箱
|
|
96
|
+
# @return [EmailInfo]
|
|
97
|
+
def generate_email
|
|
98
|
+
resp = Http.get(BASE, headers: HEADERS, timeout: 15)
|
|
99
|
+
resp.raise_for_status
|
|
100
|
+
raise "haribu: 首页响应为空" if resp.body.empty?
|
|
101
|
+
|
|
102
|
+
email_addr = extract_email(resp.body)
|
|
103
|
+
cookie_hdr = merge_cookies("", resp)
|
|
104
|
+
token = encode_sess(cookie_hdr)
|
|
105
|
+
|
|
106
|
+
EmailInfo.new(channel: CHANNEL, email: email_addr, token: token)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# 获取邮件详情页正文
|
|
110
|
+
def fetch_detail(detail_url, cookie_hdr)
|
|
111
|
+
hdrs = HEADERS.merge("Cookie" => cookie_hdr, "Referer" => BASE)
|
|
112
|
+
resp = Http.get(detail_url, headers: hdrs, timeout: 15)
|
|
113
|
+
return "" unless resp.ok?
|
|
114
|
+
|
|
115
|
+
m = resp.body.match(BODY_RE)
|
|
116
|
+
m ? m[1].strip : ""
|
|
117
|
+
rescue StandardError
|
|
118
|
+
""
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# 获取邮件列表
|
|
122
|
+
# @param token [String]
|
|
123
|
+
# @param email [String]
|
|
124
|
+
# @return [Array<Email>]
|
|
125
|
+
def get_emails(token, email)
|
|
126
|
+
e = (email || "").strip
|
|
127
|
+
raise "haribu: 邮箱地址为空" if e.empty?
|
|
128
|
+
|
|
129
|
+
cookie_hdr = decode_sess(token)
|
|
130
|
+
|
|
131
|
+
# 触发新邮件检查
|
|
132
|
+
begin
|
|
133
|
+
kontrol_hdrs = HEADERS.merge(
|
|
134
|
+
"Cookie" => cookie_hdr,
|
|
135
|
+
"Referer" => BASE,
|
|
136
|
+
"X-Requested-With" => "XMLHttpRequest"
|
|
137
|
+
)
|
|
138
|
+
Http.get("#{BASE}/en/api-kontrol/", headers: kontrol_hdrs, timeout: 15)
|
|
139
|
+
rescue StandardError
|
|
140
|
+
# 忽略
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# GET 首页获取收件箱
|
|
144
|
+
inbox_hdrs = HEADERS.merge("Cookie" => cookie_hdr, "Referer" => BASE)
|
|
145
|
+
resp = Http.get(BASE, headers: inbox_hdrs, timeout: 15)
|
|
146
|
+
resp.raise_for_status
|
|
147
|
+
|
|
148
|
+
items = resp.body.scan(MAIL_ITEM_RE).map(&:first)
|
|
149
|
+
return [] if items.empty?
|
|
150
|
+
|
|
151
|
+
items.each_with_index.filter_map do |content, idx|
|
|
152
|
+
raw = { "id" => "haribu-#{idx}", "to" => e }
|
|
153
|
+
|
|
154
|
+
fm = content.match(FROM_RE)
|
|
155
|
+
raw["from"] = CGI.unescapeHTML(strip_tags(fm[1])).strip if fm
|
|
156
|
+
|
|
157
|
+
sm = content.match(SUBJECT_RE)
|
|
158
|
+
raw["subject"] = CGI.unescapeHTML(strip_tags(sm[1])).strip if sm
|
|
159
|
+
|
|
160
|
+
dm = content.match(DATE_RE)
|
|
161
|
+
raw["date"] = CGI.unescapeHTML(strip_tags(dm[1])).strip if dm
|
|
162
|
+
|
|
163
|
+
lm = content.match(MAIL_LINK_RE)
|
|
164
|
+
if lm
|
|
165
|
+
detail_url = lm[1]
|
|
166
|
+
detail_url = "#{BASE}/#{detail_url.sub(%r{\A/}, '')}" unless detail_url.start_with?("http")
|
|
167
|
+
html_body = fetch_detail(detail_url, cookie_hdr)
|
|
168
|
+
raw["html"] = html_body unless html_body.empty?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
Normalize.normalize_email(raw, e)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# inboxes.com 渠道实现
|
|
6
|
+
module Inboxes
|
|
7
|
+
CHANNEL = "inboxes"
|
|
8
|
+
API_BASE = "https://inboxes.com/api/v2"
|
|
9
|
+
DEFAULT_DOMAIN = "blondmail.com"
|
|
10
|
+
HEADERS = {
|
|
11
|
+
"Accept" => "application/json",
|
|
12
|
+
"Origin" => "https://inboxes.com",
|
|
13
|
+
"Referer" => "https://inboxes.com/",
|
|
14
|
+
"User-Agent" => "Mozilla/5.0"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def random_local
|
|
20
|
+
chars = "abcdefghijklmnopqrstuvwxyz0123456789".chars
|
|
21
|
+
"sdk" + Array.new(16) { chars.sample }.join
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# 获取可用域名列表
|
|
25
|
+
def get_domains
|
|
26
|
+
resp = Http.get("#{API_BASE}/domain", headers: HEADERS, timeout: 15)
|
|
27
|
+
resp.raise_for_status
|
|
28
|
+
data = resp.json
|
|
29
|
+
items = data.is_a?(Hash) ? (data["domains"] || []) : []
|
|
30
|
+
domains = items.select { |i| i.is_a?(Hash) && !i["qdn"].to_s.strip.empty? }
|
|
31
|
+
.map { |i| i["qdn"].to_s.strip }
|
|
32
|
+
raise "inboxes: no domains" if domains.empty?
|
|
33
|
+
|
|
34
|
+
domains
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pick_domain(domains, preferred)
|
|
38
|
+
wanted = preferred.to_s.strip.delete_prefix("@").downcase
|
|
39
|
+
unless wanted.empty?
|
|
40
|
+
domains.each { |d| return d if d.downcase == wanted }
|
|
41
|
+
end
|
|
42
|
+
return DEFAULT_DOMAIN if domains.include?(DEFAULT_DOMAIN)
|
|
43
|
+
|
|
44
|
+
domains.first
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def flatten(raw, recipient)
|
|
48
|
+
{
|
|
49
|
+
"id" => (raw["uid"] || "").to_s,
|
|
50
|
+
"from" => (raw["sf"] || raw["f"] || "").to_s,
|
|
51
|
+
"to" => (raw["ib"] || recipient).to_s,
|
|
52
|
+
"subject" => (raw["s"] || "").to_s,
|
|
53
|
+
"text" => (raw["text"] || "").to_s,
|
|
54
|
+
"html" => (raw["html"] || "").to_s,
|
|
55
|
+
"preview_text" => (raw["ph"] || "").to_s,
|
|
56
|
+
"date" => (raw["cr"] || "").to_s,
|
|
57
|
+
"isRead" => !!raw["ru"],
|
|
58
|
+
"attachments" => raw["at"].is_a?(Array) ? raw["at"] : []
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @param domain [String, nil] 可选指定域名
|
|
63
|
+
# @return [EmailInfo]
|
|
64
|
+
def generate_email(domain = nil)
|
|
65
|
+
domains = get_domains
|
|
66
|
+
selected = pick_domain(domains, domain)
|
|
67
|
+
EmailInfo.new(channel: CHANNEL, email: "#{random_local}@#{selected}")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def fetch_detail(uid)
|
|
71
|
+
return nil if uid.to_s.strip.empty?
|
|
72
|
+
|
|
73
|
+
begin
|
|
74
|
+
resp = Http.get(
|
|
75
|
+
"#{API_BASE}/message/#{URI.encode_www_form_component(uid)}",
|
|
76
|
+
headers: HEADERS, timeout: 15
|
|
77
|
+
)
|
|
78
|
+
return nil unless resp.ok?
|
|
79
|
+
|
|
80
|
+
data = resp.json
|
|
81
|
+
data.is_a?(Hash) ? data : nil
|
|
82
|
+
rescue StandardError
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @param email [String]
|
|
88
|
+
# @return [Array<Email>]
|
|
89
|
+
def get_emails(email)
|
|
90
|
+
address = email.to_s.strip
|
|
91
|
+
raise "inboxes: empty email" if address.empty?
|
|
92
|
+
|
|
93
|
+
resp = Http.get(
|
|
94
|
+
"#{API_BASE}/inbox/#{URI.encode_www_form_component(address)}",
|
|
95
|
+
headers: HEADERS, timeout: 15
|
|
96
|
+
)
|
|
97
|
+
resp.raise_for_status
|
|
98
|
+
data = resp.json
|
|
99
|
+
rows = data.is_a?(Hash) ? (data["msgs"] || []) : []
|
|
100
|
+
return [] unless rows.is_a?(Array)
|
|
101
|
+
|
|
102
|
+
rows.select { |r| r.is_a?(Hash) }.map do |row|
|
|
103
|
+
uid = (row["uid"] || "").to_s.strip
|
|
104
|
+
detail = uid.empty? ? nil : fetch_detail(uid)
|
|
105
|
+
Normalize.normalize_email(flatten(detail || row, address), address)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# InboxKitten 渠道 — https://inboxkitten.com
|
|
6
|
+
module Inboxkitten
|
|
7
|
+
CHANNEL = "inboxkitten"
|
|
8
|
+
API_BASE = "https://inboxkitten.com/api/v1/mail"
|
|
9
|
+
DOMAIN = "inboxkitten.com"
|
|
10
|
+
HEADERS_JSON = { "Accept" => "application/json" }.freeze
|
|
11
|
+
HEADERS_HTML = { "Accept" => "text/html,*/*" }.freeze
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def random_local
|
|
16
|
+
chars = "abcdefghijklmnopqrstuvwxyz0123456789".chars
|
|
17
|
+
"sdk" + Array.new(16) { chars.sample }.join
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 无需请求服务端,随机本地部分即可
|
|
21
|
+
# @return [EmailInfo]
|
|
22
|
+
def generate_email
|
|
23
|
+
EmailInfo.new(channel: CHANNEL, email: "#{random_local}@#{DOMAIN}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def fetch_meta(region, key)
|
|
27
|
+
resp = Http.get(
|
|
28
|
+
"#{API_BASE}/getKey?region=#{URI.encode_www_form_component(region)}&key=#{URI.encode_www_form_component(key)}",
|
|
29
|
+
headers: HEADERS_JSON, timeout: 15
|
|
30
|
+
)
|
|
31
|
+
resp.raise_for_status
|
|
32
|
+
data = resp.json
|
|
33
|
+
data.is_a?(Hash) ? data : {}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fetch_html(region, key)
|
|
37
|
+
resp = Http.get(
|
|
38
|
+
"#{API_BASE}/getHtml?region=#{URI.encode_www_form_component(region)}&key=#{URI.encode_www_form_component(key)}",
|
|
39
|
+
headers: HEADERS_HTML, timeout: 15
|
|
40
|
+
)
|
|
41
|
+
resp.raise_for_status
|
|
42
|
+
resp.body
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def detail_raw(row, recipient)
|
|
46
|
+
storage = row["storage"].is_a?(Hash) ? row["storage"] : {}
|
|
47
|
+
message = row["message"].is_a?(Hash) ? row["message"] : {}
|
|
48
|
+
headers = message["headers"].is_a?(Hash) ? message["headers"] : {}
|
|
49
|
+
envelope = row["envelope"].is_a?(Hash) ? row["envelope"] : {}
|
|
50
|
+
region = storage["region"].to_s.strip
|
|
51
|
+
key = storage["key"].to_s.strip
|
|
52
|
+
raw = {
|
|
53
|
+
"id" => key, "messageId" => key,
|
|
54
|
+
"from" => headers["from"] || envelope["sender"] || "",
|
|
55
|
+
"to" => row["recipient"] || envelope["targets"] || recipient,
|
|
56
|
+
"subject" => headers["subject"] || "", "timestamp" => row["timestamp"]
|
|
57
|
+
}
|
|
58
|
+
return raw if region.empty? || key.empty?
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
meta = fetch_meta(region, key)
|
|
62
|
+
html = fetch_html(region, key)
|
|
63
|
+
raw["from"] = meta["name"] || raw["from"]
|
|
64
|
+
raw["to"] = meta["recipients"] || raw["to"]
|
|
65
|
+
raw["subject"] = meta["subject"] || raw["subject"]
|
|
66
|
+
raw["text"] = HtmlUtils.html_to_text(html)
|
|
67
|
+
raw["html"] = html
|
|
68
|
+
rescue StandardError
|
|
69
|
+
# 忽略详情失败
|
|
70
|
+
end
|
|
71
|
+
raw
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# @param email [String]
|
|
75
|
+
# @return [Array<Email>]
|
|
76
|
+
def get_emails(email)
|
|
77
|
+
local = email.to_s.strip.split("@").first.to_s
|
|
78
|
+
raise "inboxkitten: empty email" if local.empty?
|
|
79
|
+
|
|
80
|
+
resp = Http.get("#{API_BASE}/list?recipient=#{URI.encode_www_form_component(local)}",
|
|
81
|
+
headers: HEADERS_JSON, timeout: 15)
|
|
82
|
+
resp.raise_for_status
|
|
83
|
+
rows = resp.json
|
|
84
|
+
return [] unless rows.is_a?(Array)
|
|
85
|
+
|
|
86
|
+
rows.select { |r| r.is_a?(Hash) }.map { |item| Normalize.normalize_email(detail_raw(item, email), email) }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
|
|
5
|
+
module TempmailSdk
|
|
6
|
+
module Providers
|
|
7
|
+
# linshiyou.com 临时邮 — NEXUS_TOKEN + tmail-emails Cookie,邮件接口返回 HTML 分片
|
|
8
|
+
module Linshiyou
|
|
9
|
+
CHANNEL = "linshiyou"
|
|
10
|
+
ORIGIN = "https://linshiyou.com"
|
|
11
|
+
|
|
12
|
+
DEFAULT_HEADERS = {
|
|
13
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
|
|
14
|
+
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
|
|
15
|
+
"accept-language" => "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
|
16
|
+
"cache-control" => "no-cache",
|
|
17
|
+
"dnt" => "1",
|
|
18
|
+
"pragma" => "no-cache",
|
|
19
|
+
"priority" => "u=1, i",
|
|
20
|
+
"referer" => "#{ORIGIN}/",
|
|
21
|
+
"sec-ch-ua" => '"Chromium";v="146", "Not-A.Brand";v="24", "Microsoft Edge";v="146"',
|
|
22
|
+
"sec-ch-ua-mobile" => "?0",
|
|
23
|
+
"sec-ch-ua-platform" => '"Windows"',
|
|
24
|
+
"sec-fetch-dest" => "empty",
|
|
25
|
+
"sec-fetch-mode" => "cors",
|
|
26
|
+
"sec-fetch-site" => "same-origin"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
RE_LIST_ID = /id="tmail-email-list-([a-f0-9]+)"/i
|
|
30
|
+
RE_DIV = /<div class="([^"]+)"[^>]*>([\s\S]*?)<\/div>/i
|
|
31
|
+
RE_SRCDOC = /\ssrcdoc="([^"]*)"/i
|
|
32
|
+
RE_DOWNLOAD = /href="(\/api\/download\?id=[^"]+)"/i
|
|
33
|
+
RE_TAGS = /<[^>]+>/
|
|
34
|
+
|
|
35
|
+
module_function
|
|
36
|
+
|
|
37
|
+
def strip_tags(s)
|
|
38
|
+
t = s.gsub(RE_TAGS, " ")
|
|
39
|
+
CGI.unescapeHTML(t).split.join(" ")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def pick_div(fragment, class_name)
|
|
43
|
+
fragment.scan(RE_DIV).each do |cls, inner|
|
|
44
|
+
return strip_tags(inner).strip if cls == class_name
|
|
45
|
+
end
|
|
46
|
+
""
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_date(s)
|
|
50
|
+
s = s.to_s.strip
|
|
51
|
+
return "" if s.empty?
|
|
52
|
+
|
|
53
|
+
["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"].each do |fmt|
|
|
54
|
+
begin
|
|
55
|
+
dt = Time.strptime(s, fmt)
|
|
56
|
+
return dt.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
57
|
+
rescue ArgumentError
|
|
58
|
+
next
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
""
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def extract_srcdoc(body_part)
|
|
65
|
+
m = body_part.match(RE_SRCDOC)
|
|
66
|
+
return "" unless m
|
|
67
|
+
|
|
68
|
+
CGI.unescapeHTML(m[1])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# 解析邮件分段
|
|
72
|
+
def parse_segments(raw, recipient)
|
|
73
|
+
raw = raw.to_s.strip
|
|
74
|
+
return [] if raw.empty?
|
|
75
|
+
|
|
76
|
+
out = []
|
|
77
|
+
raw.split("<-----TMAILNEXTMAIL----->").each do |seg|
|
|
78
|
+
seg = seg.strip
|
|
79
|
+
next if seg.empty?
|
|
80
|
+
|
|
81
|
+
parts = seg.split("<-----TMAILCHOPPER----->", 2)
|
|
82
|
+
list_part = parts[0]
|
|
83
|
+
body_part = parts.length > 1 ? parts[1] : ""
|
|
84
|
+
|
|
85
|
+
mid = list_part.match(RE_LIST_ID)
|
|
86
|
+
next unless mid
|
|
87
|
+
|
|
88
|
+
mid_str = mid[1]
|
|
89
|
+
|
|
90
|
+
from_list = pick_div(list_part, "name")
|
|
91
|
+
subject_list = pick_div(list_part, "subject")
|
|
92
|
+
preview_list = pick_div(list_part, "body")
|
|
93
|
+
|
|
94
|
+
from_body = pick_div(body_part, "tmail-email-sender")
|
|
95
|
+
time_body = pick_div(body_part, "tmail-email-time")
|
|
96
|
+
title_body = pick_div(body_part, "tmail-email-title")
|
|
97
|
+
html_body = extract_srcdoc(body_part)
|
|
98
|
+
|
|
99
|
+
from_addr = from_body.empty? ? from_list : from_body
|
|
100
|
+
subject = title_body.empty? ? subject_list : title_body
|
|
101
|
+
text = preview_list.empty? ? strip_tags(html_body) : preview_list
|
|
102
|
+
|
|
103
|
+
attachments = []
|
|
104
|
+
dm = body_part.match(RE_DOWNLOAD)
|
|
105
|
+
if dm
|
|
106
|
+
attachments << EmailAttachment.new(filename: "", url: "#{ORIGIN}#{dm[1]}")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
out << Email.new(
|
|
110
|
+
id: mid_str,
|
|
111
|
+
from_addr: from_addr,
|
|
112
|
+
to: recipient,
|
|
113
|
+
subject: subject,
|
|
114
|
+
text: text,
|
|
115
|
+
html: html_body,
|
|
116
|
+
date: parse_date(time_body),
|
|
117
|
+
is_read: false,
|
|
118
|
+
attachments: attachments
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
out
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# 创建临时邮箱
|
|
125
|
+
# @return [EmailInfo]
|
|
126
|
+
def generate_email
|
|
127
|
+
resp = Http.get("#{ORIGIN}/api/user?user", headers: DEFAULT_HEADERS)
|
|
128
|
+
resp.raise_for_status
|
|
129
|
+
|
|
130
|
+
nexus = resp.cookie_value("NEXUS_TOKEN")
|
|
131
|
+
raise "linshiyou: NEXUS_TOKEN not in Set-Cookie" if nexus.nil? || nexus.empty?
|
|
132
|
+
|
|
133
|
+
email = resp.body.strip
|
|
134
|
+
raise "linshiyou: invalid email in response body" if email.empty? || !email.include?("@")
|
|
135
|
+
|
|
136
|
+
token = "NEXUS_TOKEN=#{nexus}; tmail-emails=#{email}"
|
|
137
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: token)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# 获取邮件列表
|
|
141
|
+
# @param token [String]
|
|
142
|
+
# @param email [String]
|
|
143
|
+
# @return [Array<Email>]
|
|
144
|
+
def get_emails(token, email = "")
|
|
145
|
+
resp = Http.get(
|
|
146
|
+
"#{ORIGIN}/api/mail?unseen=1",
|
|
147
|
+
headers: DEFAULT_HEADERS.merge(
|
|
148
|
+
"Cookie" => token,
|
|
149
|
+
"x-requested-with" => "XMLHttpRequest"
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
resp.raise_for_status
|
|
153
|
+
parse_segments(resp.body, email)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|