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,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "cgi"
|
|
5
|
+
require "json"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module TempmailSdk
|
|
9
|
+
module Providers
|
|
10
|
+
# moakt.com 渠道实现(HTML 抓取型)
|
|
11
|
+
#
|
|
12
|
+
# 流程:
|
|
13
|
+
# 1. GET 语言首页,收集初始 Cookie 与可用域名 <option> 列表;
|
|
14
|
+
# 2. POST /{locale}/inbox 创建邮箱(随机域名或指定域名),从 302 响应捕获 tm_session Cookie;
|
|
15
|
+
# 3. GET /{locale}/inbox 解析 #email-address 得到邮箱地址;
|
|
16
|
+
# 4. 会话凭证(locale + 序列化 Cookie 头)经 base64 打包进 token;
|
|
17
|
+
# 5. 收信时列表解析 /{locale}/email/{uuid},正文 GET .../html 解析 .email-body。
|
|
18
|
+
#
|
|
19
|
+
# 多个渠道别名(moakt.cc/moakt.co/moakt.ws/tmail.ws 等)共用此实现,仅固定域名不同。
|
|
20
|
+
module Moakt
|
|
21
|
+
CHANNEL = "moakt"
|
|
22
|
+
ORIGIN = "https://www.moakt.com"
|
|
23
|
+
TOK_PREFIX = "mok1:"
|
|
24
|
+
|
|
25
|
+
# 默认 User-Agent(config.headers 未指定时使用)
|
|
26
|
+
DEFAULT_UA = "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
|
+
|
|
29
|
+
# 页面请求的固定基础头
|
|
30
|
+
DEFAULT_HEADERS = {
|
|
31
|
+
"Accept-Language" => "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
|
|
32
|
+
"Cache-Control" => "no-cache",
|
|
33
|
+
"DNT" => "1",
|
|
34
|
+
"Pragma" => "no-cache",
|
|
35
|
+
"Upgrade-Insecure-Requests" => "1"
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
EMAIL_DIV_RE = /<div\s+id="email-address"\s*>([^<]+)<\/div>/im
|
|
39
|
+
DOMAIN_OPTION_RE = /<option\s+value="([^"]+)">\s*@[^<]+<\/option>/im
|
|
40
|
+
MAIL_DOMAIN_RE = /\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+\z/i
|
|
41
|
+
HREF_EMAIL_RE = /href="(\/[^"]+\/email\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})"/
|
|
42
|
+
TITLE_RE = /<li\s+class="title"\s*>([^<]*)<\/li>/im
|
|
43
|
+
DATE_RE = /<li\s+class="date"[^>]*>[\s\S]*?<span[^>]*>([^<]+)<\/span>/im
|
|
44
|
+
SENDER_RE = /<li\s+class="sender"[^>]*>[\s\S]*?<span[^>]*>([\s\S]*?)<\/span>\s*<\/li>/im
|
|
45
|
+
BODY_RE = /<div\s+class="email-body"\s*>([\s\S]*?)<\/div>/im
|
|
46
|
+
FROM_ADDR_RE = /<([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>/
|
|
47
|
+
TAG_RE = /<[^>]+>/
|
|
48
|
+
|
|
49
|
+
module_function
|
|
50
|
+
|
|
51
|
+
# 解析域名参数,返回 [locale, mail_domain]
|
|
52
|
+
# - 空或含非法字符:语言页取默认 "zh",随机域名
|
|
53
|
+
# - 形如域名:locale="zh",使用指定域名
|
|
54
|
+
# - 其它字符串:视为 locale,随机域名
|
|
55
|
+
def request_parts(domain)
|
|
56
|
+
s = domain.to_s.strip
|
|
57
|
+
return ["zh", ""] if s.empty? || s.match?(/[\/?#\\]/)
|
|
58
|
+
return ["zh", s.sub(/\A@/, "").downcase] if s.match?(MAIL_DOMAIN_RE)
|
|
59
|
+
|
|
60
|
+
[s, ""]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 从首页 HTML 收集服务端提供的可用域名集合
|
|
64
|
+
def parse_server_domains(page)
|
|
65
|
+
page.scan(DOMAIN_OPTION_RE)
|
|
66
|
+
.map { |m| m[0].strip.sub(/\A@/, "").downcase }
|
|
67
|
+
.reject(&:empty?)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# 生成随机本地部分(小写字母 + 数字)
|
|
71
|
+
def random_local(length = 12)
|
|
72
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
73
|
+
Array.new(length) { chars.sample }.join
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 提取邮箱地址的域名部分(小写)
|
|
77
|
+
def email_domain(email)
|
|
78
|
+
_head, sep, domain = email.to_s.rpartition("@")
|
|
79
|
+
sep.empty? ? "" : domain.strip.downcase
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# 将 Cookie 头字符串解析为 name => value 映射
|
|
83
|
+
def parse_cookie_map(hdr)
|
|
84
|
+
map = {}
|
|
85
|
+
hdr.to_s.split(";").each do |part|
|
|
86
|
+
part = part.strip
|
|
87
|
+
next unless part.include?("=")
|
|
88
|
+
|
|
89
|
+
k, v = part.split("=", 2)
|
|
90
|
+
k = k.strip
|
|
91
|
+
map[k] = v.to_s.strip unless k.empty?
|
|
92
|
+
end
|
|
93
|
+
map
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# 从响应的 Set-Cookie 头提取本次新设置的 Cookie
|
|
97
|
+
def cookies_from_response(resp)
|
|
98
|
+
map = {}
|
|
99
|
+
resp.set_cookies.each do |line|
|
|
100
|
+
pair = line.split(";", 2).first.to_s.strip
|
|
101
|
+
next unless pair.include?("=")
|
|
102
|
+
|
|
103
|
+
k, v = pair.split("=", 2)
|
|
104
|
+
k = k.strip
|
|
105
|
+
map[k] = v.to_s.strip unless k.empty?
|
|
106
|
+
end
|
|
107
|
+
map
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# 合并旧 Cookie 头与响应新设置的 Cookie,按 key 排序拼接(保证稳定)
|
|
111
|
+
def merge_cookie_hdr(prev, resp)
|
|
112
|
+
d = parse_cookie_map(prev)
|
|
113
|
+
d.merge!(cookies_from_response(resp))
|
|
114
|
+
d.keys.sort.map { |k| "#{k}=#{d[k]}" }.join("; ")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# 构造页面请求头
|
|
118
|
+
def page_headers(referer, ua)
|
|
119
|
+
DEFAULT_HEADERS.merge(
|
|
120
|
+
"User-Agent" => ua,
|
|
121
|
+
"Accept" => "text/html,application/xhtml+xml,application/xml;q=0.9," \
|
|
122
|
+
"image/avif,image/webp,image/apng,*/*;q=0.8",
|
|
123
|
+
"Referer" => referer
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# 从 config 取 UA,缺省用内置默认值
|
|
128
|
+
def user_agent
|
|
129
|
+
(Config.get_config.headers || {})["User-Agent"] || DEFAULT_UA
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# 将会话(locale + Cookie 头)编码为不透明 token
|
|
133
|
+
def encode_sess(locale, cookie_hdr)
|
|
134
|
+
raw = JSON.generate("l" => locale, "c" => cookie_hdr)
|
|
135
|
+
TOK_PREFIX + Base64.strict_encode64(raw)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# 解码 token,返回 [locale, cookie_hdr]
|
|
139
|
+
def decode_sess(tok)
|
|
140
|
+
raise "moakt: invalid session token" unless tok.to_s.start_with?(TOK_PREFIX)
|
|
141
|
+
|
|
142
|
+
data = Base64.strict_decode64(tok[TOK_PREFIX.length..])
|
|
143
|
+
o = JSON.parse(data)
|
|
144
|
+
loc = o["l"].to_s.strip
|
|
145
|
+
c = o["c"].to_s.strip
|
|
146
|
+
raise "moakt: invalid session token" if loc.empty? || c.empty?
|
|
147
|
+
|
|
148
|
+
[loc, c]
|
|
149
|
+
rescue ArgumentError, JSON::ParserError
|
|
150
|
+
raise "moakt: invalid session token"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# 从收件箱页解析邮箱地址
|
|
154
|
+
def parse_inbox_email(html_s)
|
|
155
|
+
m = html_s.match(EMAIL_DIV_RE)
|
|
156
|
+
raise "moakt: email-address not found" unless m
|
|
157
|
+
|
|
158
|
+
addr = CGI.unescapeHTML(m[1].strip)
|
|
159
|
+
raise "moakt: empty email-address" if addr.empty?
|
|
160
|
+
|
|
161
|
+
addr
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# 去除 HTML 标签,压缩为文本
|
|
165
|
+
def strip_tags(s)
|
|
166
|
+
s.gsub(TAG_RE, " ").strip
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# 从收件箱页提取邮件 UUID 列表(去重、排除删除链接)
|
|
170
|
+
def list_mail_ids(html_s)
|
|
171
|
+
seen = {}
|
|
172
|
+
out = []
|
|
173
|
+
html_s.scan(HREF_EMAIL_RE).each do |cap|
|
|
174
|
+
path = cap[0]
|
|
175
|
+
next if path.include?("/delete")
|
|
176
|
+
|
|
177
|
+
mid = path.split("/").last
|
|
178
|
+
if mid.length == 36 && !seen[mid]
|
|
179
|
+
seen[mid] = true
|
|
180
|
+
out << mid
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
out
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# 解析邮件详情页,返回标准化前的原始 Hash
|
|
187
|
+
def parse_detail(page, mid, recipient)
|
|
188
|
+
from_s = ""
|
|
189
|
+
if (sm = page.match(SENDER_RE))
|
|
190
|
+
inner = CGI.unescapeHTML(sm[1])
|
|
191
|
+
from_s = strip_tags(inner)
|
|
192
|
+
if (em = inner.match(FROM_ADDR_RE))
|
|
193
|
+
from_s = em[1].strip
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
subj = ""
|
|
198
|
+
subj = CGI.unescapeHTML(page.match(TITLE_RE)[1].strip) if page.match?(TITLE_RE)
|
|
199
|
+
|
|
200
|
+
date_s = ""
|
|
201
|
+
date_s = CGI.unescapeHTML(page.match(DATE_RE)[1].strip) if page.match?(DATE_RE)
|
|
202
|
+
|
|
203
|
+
body = ""
|
|
204
|
+
body = page.match(BODY_RE)[1].strip if page.match?(BODY_RE)
|
|
205
|
+
|
|
206
|
+
{
|
|
207
|
+
"id" => mid,
|
|
208
|
+
"to" => recipient,
|
|
209
|
+
"from" => from_s,
|
|
210
|
+
"subject" => subj,
|
|
211
|
+
"date" => date_s,
|
|
212
|
+
"html" => body
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# 创建临时邮箱
|
|
217
|
+
# @param domain [String, nil] 指定接入域名或语言标识
|
|
218
|
+
# @return [EmailInfo]
|
|
219
|
+
def generate_email(domain = nil)
|
|
220
|
+
loc, mail_domain = request_parts(domain)
|
|
221
|
+
base = "#{ORIGIN}/#{loc}"
|
|
222
|
+
inbox = "#{base}/inbox"
|
|
223
|
+
ua = user_agent
|
|
224
|
+
|
|
225
|
+
r1 = Http.get(base, headers: page_headers(base, ua))
|
|
226
|
+
r1.raise_for_status
|
|
227
|
+
cookie_hdr = merge_cookie_hdr("", r1)
|
|
228
|
+
|
|
229
|
+
if mail_domain.empty?
|
|
230
|
+
post_data = { "random" => "1" }
|
|
231
|
+
else
|
|
232
|
+
unless parse_server_domains(r1.body).include?(mail_domain)
|
|
233
|
+
raise "moakt: unsupported domain #{mail_domain}"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
post_data = {
|
|
237
|
+
"setemail" => "",
|
|
238
|
+
"username" => random_local,
|
|
239
|
+
"domain" => mail_domain,
|
|
240
|
+
"preferred_domain" => ""
|
|
241
|
+
}
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# POST /inbox 创建邮箱,从 302 响应捕获 tm_session Cookie(Net::HTTP 默认不跟随重定向)
|
|
245
|
+
r2 = Http.post(
|
|
246
|
+
inbox,
|
|
247
|
+
headers: page_headers(base, ua).merge(
|
|
248
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
|
249
|
+
"Cookie" => cookie_hdr
|
|
250
|
+
),
|
|
251
|
+
body: URI.encode_www_form(post_data)
|
|
252
|
+
)
|
|
253
|
+
cookie_hdr = merge_cookie_hdr(cookie_hdr, r2)
|
|
254
|
+
|
|
255
|
+
unless parse_cookie_map(cookie_hdr).key?("tm_session")
|
|
256
|
+
raise "moakt: missing tm_session cookie"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# GET /inbox 获取邮箱地址
|
|
260
|
+
r3 = Http.get(inbox, headers: page_headers(base, ua).merge("Cookie" => cookie_hdr))
|
|
261
|
+
r3.raise_for_status
|
|
262
|
+
cookie_hdr = merge_cookie_hdr(cookie_hdr, r3)
|
|
263
|
+
|
|
264
|
+
email = parse_inbox_email(r3.body)
|
|
265
|
+
if !mail_domain.empty? && email_domain(email) != mail_domain
|
|
266
|
+
raise "moakt: domain mismatch expected=#{mail_domain} actual=#{email_domain(email)}"
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: encode_sess(loc, cookie_hdr))
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# 收取邮件列表
|
|
273
|
+
# @param email [String]
|
|
274
|
+
# @param token [String]
|
|
275
|
+
# @return [Array<Email>]
|
|
276
|
+
def get_emails(email, token)
|
|
277
|
+
loc, cookie_hdr = decode_sess(token)
|
|
278
|
+
inbox = "#{ORIGIN}/#{loc}/inbox"
|
|
279
|
+
base_ref = "#{ORIGIN}/#{loc}"
|
|
280
|
+
ua = user_agent
|
|
281
|
+
|
|
282
|
+
r = Http.get(inbox, headers: page_headers(base_ref, ua).merge("Cookie" => cookie_hdr))
|
|
283
|
+
r.raise_for_status
|
|
284
|
+
|
|
285
|
+
list_mail_ids(r.body).filter_map do |mid|
|
|
286
|
+
detail = "#{ORIGIN}/#{loc}/email/#{mid}/html"
|
|
287
|
+
refer = "#{ORIGIN}/#{loc}/email/#{mid}"
|
|
288
|
+
rd = Http.get(detail, headers: page_headers(refer, ua).merge("Cookie" => cookie_hdr))
|
|
289
|
+
next unless rd.status_code == 200
|
|
290
|
+
|
|
291
|
+
Normalize.normalize_email(parse_detail(rd.body, mid, email), email)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# Neighbours 渠道实现
|
|
6
|
+
# API: https://neighbours.sh/api/v1
|
|
7
|
+
# 支持多域名选择
|
|
8
|
+
module Neighbours
|
|
9
|
+
CHANNEL = "neighbours"
|
|
10
|
+
API_BASE = "https://neighbours.sh/api/v1"
|
|
11
|
+
HEADERS = {
|
|
12
|
+
"Accept" => "application/json",
|
|
13
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# 生成随机用户名
|
|
19
|
+
def random_local
|
|
20
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
21
|
+
"sdk" + Array.new(16) { chars.sample }.join
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# 从 from/to 结构中提取首个可用地址
|
|
25
|
+
def first_address(value)
|
|
26
|
+
return "" if value.nil?
|
|
27
|
+
return value.strip if value.is_a?(String)
|
|
28
|
+
|
|
29
|
+
if value.is_a?(Array)
|
|
30
|
+
value.each do |item|
|
|
31
|
+
hit = first_address(item)
|
|
32
|
+
return hit unless hit.empty?
|
|
33
|
+
end
|
|
34
|
+
return ""
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if value.is_a?(Hash)
|
|
38
|
+
addr = value["address"].to_s.strip
|
|
39
|
+
return addr unless addr.empty?
|
|
40
|
+
|
|
41
|
+
text = value["text"].to_s.strip
|
|
42
|
+
return text if text.include?("@")
|
|
43
|
+
|
|
44
|
+
return first_address(value["value"])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
value.to_s.strip
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# 请求 JSON,allow_404 时 404 返回 nil
|
|
51
|
+
def request_json(path, allow_404: false)
|
|
52
|
+
resp = Http.get("#{API_BASE}#{path}", headers: HEADERS, timeout: 15)
|
|
53
|
+
return nil if allow_404 && resp.status_code == 404
|
|
54
|
+
|
|
55
|
+
resp.raise_for_status
|
|
56
|
+
data = resp.json
|
|
57
|
+
data.is_a?(Hash) ? data : nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 将邮件映射为标准化中间格式
|
|
61
|
+
def flatten_message(raw, recipient)
|
|
62
|
+
{
|
|
63
|
+
"id" => (raw["uid"] || "").to_s,
|
|
64
|
+
"from" => first_address(raw["from"]),
|
|
65
|
+
"to" => first_address(raw["to"]).then { |v| v.empty? ? recipient : v },
|
|
66
|
+
"subject" => (raw["subject"] || "").to_s,
|
|
67
|
+
"text" => (raw["text"] || raw["snippet"] || "").to_s,
|
|
68
|
+
"html" => (raw["html"] || "").to_s,
|
|
69
|
+
"date" => (raw["date"] || raw["received_at"] || "").to_s,
|
|
70
|
+
"is_read" => false,
|
|
71
|
+
"attachments" => raw["attachments"] || []
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 获取可用域名列表
|
|
76
|
+
def list_domains
|
|
77
|
+
data = request_json("/config/domains")
|
|
78
|
+
domains = []
|
|
79
|
+
if data.is_a?(Hash)
|
|
80
|
+
nested = data["data"]
|
|
81
|
+
if nested.is_a?(Hash)
|
|
82
|
+
(nested["domains"] || []).each do |item|
|
|
83
|
+
s = item.to_s.strip.downcase
|
|
84
|
+
domains << s unless s.empty?
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
raise "neighbours: 域名列表为空" if domains.empty?
|
|
89
|
+
|
|
90
|
+
domains
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# 获取单封邮件详情
|
|
94
|
+
def fetch_detail(address, uid)
|
|
95
|
+
data = request_json("/inbox/#{URI.encode_www_form_component(address)}/#{URI.encode_www_form_component(uid)}", allow_404: true)
|
|
96
|
+
return nil unless data.is_a?(Hash)
|
|
97
|
+
|
|
98
|
+
detail = data["data"]
|
|
99
|
+
detail.is_a?(Hash) ? detail : nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# 创建临时邮箱
|
|
103
|
+
# @param domain [String, nil]
|
|
104
|
+
# @return [EmailInfo]
|
|
105
|
+
def generate_email(domain = nil)
|
|
106
|
+
domains = list_domains
|
|
107
|
+
if domain && !domain.to_s.strip.empty?
|
|
108
|
+
wanted = domain.strip.downcase
|
|
109
|
+
raise "neighbours: 不支持的域名 #{domain}" unless domains.include?(wanted)
|
|
110
|
+
|
|
111
|
+
selected = wanted
|
|
112
|
+
else
|
|
113
|
+
selected = domains.sample
|
|
114
|
+
end
|
|
115
|
+
email = "#{random_local}@#{selected}"
|
|
116
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: email)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 获取邮件列表
|
|
120
|
+
# @param email [String]
|
|
121
|
+
# @return [Array<Email>]
|
|
122
|
+
def get_emails(email)
|
|
123
|
+
address = email.to_s.strip
|
|
124
|
+
raise "neighbours: 邮箱地址为空" if address.empty?
|
|
125
|
+
|
|
126
|
+
data = request_json("/inbox/#{URI.encode_www_form_component(address)}", allow_404: true)
|
|
127
|
+
return [] unless data.is_a?(Hash)
|
|
128
|
+
|
|
129
|
+
rows = data["data"]
|
|
130
|
+
return [] unless rows.is_a?(Array)
|
|
131
|
+
|
|
132
|
+
rows.select { |item| item.is_a?(Hash) }.map do |item|
|
|
133
|
+
uid = item["uid"].to_s.strip
|
|
134
|
+
detail = uid.empty? ? nil : fetch_detail(address, uid)
|
|
135
|
+
Normalize.normalize_email(flatten_message(detail || item, address), address)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# neighbours-sh 渠道实现
|
|
6
|
+
# 公共收件箱模式,任意用户名即可收信,无需注册
|
|
7
|
+
# API: https://neighbours.sh/api/v1
|
|
8
|
+
module NeighboursSh
|
|
9
|
+
CHANNEL = "neighbours-sh"
|
|
10
|
+
API_BASE = "https://neighbours.sh/api/v1"
|
|
11
|
+
DOMAIN = "neighbours.sh"
|
|
12
|
+
HEADERS = {
|
|
13
|
+
"Accept" => "application/json",
|
|
14
|
+
"User-Agent" => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# 生成随机用户名:前缀 sdk + 10 位小写字母数字
|
|
20
|
+
def random_local
|
|
21
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
22
|
+
"sdk" + Array.new(10) { chars.sample }.join
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 从 from/to 结构中提取首个可用地址
|
|
26
|
+
def first_address(value)
|
|
27
|
+
return "" if value.nil?
|
|
28
|
+
return value.strip if value.is_a?(String)
|
|
29
|
+
|
|
30
|
+
if value.is_a?(Array)
|
|
31
|
+
value.each do |item|
|
|
32
|
+
hit = first_address(item)
|
|
33
|
+
return hit unless hit.empty?
|
|
34
|
+
end
|
|
35
|
+
return ""
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if value.is_a?(Hash)
|
|
39
|
+
addr = value["address"].to_s.strip
|
|
40
|
+
return addr unless addr.empty?
|
|
41
|
+
|
|
42
|
+
text = value["text"].to_s.strip
|
|
43
|
+
return text if text.include?("@")
|
|
44
|
+
|
|
45
|
+
return first_address(value["value"])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
value.to_s.strip
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# 请求 JSON,allow_404 时 404 返回 nil
|
|
52
|
+
def request_json(path, allow_404: false)
|
|
53
|
+
resp = Http.get("#{API_BASE}#{path}", headers: HEADERS, timeout: 15)
|
|
54
|
+
return nil if allow_404 && resp.status_code == 404
|
|
55
|
+
|
|
56
|
+
resp.raise_for_status
|
|
57
|
+
data = resp.json
|
|
58
|
+
data.is_a?(Hash) ? data : nil
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# 将邮件详情映射为标准化中间格式
|
|
62
|
+
def flatten_message(detail, recipient)
|
|
63
|
+
{
|
|
64
|
+
"id" => (detail["uid"] || "").to_s,
|
|
65
|
+
"from" => first_address(detail["from"]),
|
|
66
|
+
"to" => first_address(detail["to"]).then { |v| v.empty? ? recipient : v },
|
|
67
|
+
"subject" => (detail["subject"] || "").to_s,
|
|
68
|
+
"text" => (detail["text"] || "").to_s,
|
|
69
|
+
"html" => (detail["html"] || "").to_s,
|
|
70
|
+
"date" => (detail["date"] || "").to_s,
|
|
71
|
+
"is_read" => false,
|
|
72
|
+
"attachments" => detail["attachments"] || []
|
|
73
|
+
}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# 获取单封邮件详情
|
|
77
|
+
def fetch_detail(address, uid)
|
|
78
|
+
data = request_json("/inbox/#{URI.encode_www_form_component(address)}/#{URI.encode_www_form_component(uid)}", allow_404: true)
|
|
79
|
+
return nil unless data.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
detail = data["data"]
|
|
82
|
+
detail.is_a?(Hash) ? detail : nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 创建临时邮箱(公共收件箱,本地生成地址)
|
|
86
|
+
# @return [EmailInfo]
|
|
87
|
+
def generate_email
|
|
88
|
+
email = "#{random_local}@#{DOMAIN}"
|
|
89
|
+
EmailInfo.new(channel: CHANNEL, email: email, token: email)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# 获取邮件列表
|
|
93
|
+
# @param email [String]
|
|
94
|
+
# @return [Array<Email>]
|
|
95
|
+
def get_emails(email)
|
|
96
|
+
address = email.to_s.strip
|
|
97
|
+
raise "neighbours-sh: 缺少邮箱地址" if address.empty?
|
|
98
|
+
|
|
99
|
+
data = request_json("/inbox/#{URI.encode_www_form_component(address)}", allow_404: true)
|
|
100
|
+
return [] unless data
|
|
101
|
+
|
|
102
|
+
rows = data["data"]
|
|
103
|
+
return [] unless rows.is_a?(Array)
|
|
104
|
+
|
|
105
|
+
rows.select { |item| item.is_a?(Hash) }.filter_map do |item|
|
|
106
|
+
uid = item["uid"].to_s.strip
|
|
107
|
+
next if uid.empty?
|
|
108
|
+
|
|
109
|
+
detail = fetch_detail(address, uid)
|
|
110
|
+
next unless detail
|
|
111
|
+
|
|
112
|
+
Normalize.normalize_email(flatten_message(detail, address), address)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TempmailSdk
|
|
4
|
+
module Providers
|
|
5
|
+
# nimail 渠道实现
|
|
6
|
+
# API: https://www.nimail.cn
|
|
7
|
+
# POST 表单 API,无需认证,token 即邮箱地址本身
|
|
8
|
+
module Nimail
|
|
9
|
+
CHANNEL = "nimail"
|
|
10
|
+
BASE_URL = "https://www.nimail.cn"
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# 生成随机邮箱前缀
|
|
15
|
+
def random_local(n = 10)
|
|
16
|
+
chars = ("a".."z").to_a + ("0".."9").to_a
|
|
17
|
+
Array.new(n) { chars.sample }.join
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# 获取 User-Agent
|
|
21
|
+
def user_agent
|
|
22
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# 按优先级从消息字典中提取首个非空值
|
|
26
|
+
def first(msg, *keys)
|
|
27
|
+
keys.each do |key|
|
|
28
|
+
val = msg[key]
|
|
29
|
+
return val.to_s if !val.nil? && val.to_s != ""
|
|
30
|
+
end
|
|
31
|
+
""
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# 创建 nimail.cn 临时邮箱
|
|
35
|
+
# @return [EmailInfo]
|
|
36
|
+
def generate_email
|
|
37
|
+
email = "#{random_local(10)}@nimail.cn"
|
|
38
|
+
|
|
39
|
+
resp = Http.post(
|
|
40
|
+
"#{BASE_URL}/api/applymail",
|
|
41
|
+
headers: {
|
|
42
|
+
"User-Agent" => user_agent,
|
|
43
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
|
44
|
+
"Origin" => BASE_URL,
|
|
45
|
+
"Referer" => "#{BASE_URL}/"
|
|
46
|
+
},
|
|
47
|
+
body: "mail=#{URI.encode_www_form_component(email)}"
|
|
48
|
+
)
|
|
49
|
+
resp.raise_for_status
|
|
50
|
+
|
|
51
|
+
data = resp.json
|
|
52
|
+
unless data.is_a?(Hash) && data["success"] == "true" && data["user"]
|
|
53
|
+
raise "nimail: 创建邮箱失败 #{data.inspect}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
user = data["user"].to_s
|
|
57
|
+
EmailInfo.new(channel: CHANNEL, email: user, token: user)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# 获取邮件列表
|
|
61
|
+
# @param email [String]
|
|
62
|
+
# @return [Array<Email>]
|
|
63
|
+
def get_emails(email)
|
|
64
|
+
addr = email.to_s.strip
|
|
65
|
+
raise "nimail: 缺少邮箱地址" if addr.empty?
|
|
66
|
+
|
|
67
|
+
resp = Http.post(
|
|
68
|
+
"#{BASE_URL}/api/getmails",
|
|
69
|
+
headers: {
|
|
70
|
+
"User-Agent" => user_agent,
|
|
71
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
|
72
|
+
"Origin" => BASE_URL,
|
|
73
|
+
"Referer" => "#{BASE_URL}/"
|
|
74
|
+
},
|
|
75
|
+
body: "mail=#{URI.encode_www_form_component(addr)}&time=0"
|
|
76
|
+
)
|
|
77
|
+
resp.raise_for_status
|
|
78
|
+
|
|
79
|
+
data = resp.json
|
|
80
|
+
return [] unless data.is_a?(Hash) && data["success"] == "true"
|
|
81
|
+
|
|
82
|
+
mail = data["mail"]
|
|
83
|
+
return [] unless mail.is_a?(Array)
|
|
84
|
+
|
|
85
|
+
mail.select { |msg| msg.is_a?(Hash) }.map do |msg|
|
|
86
|
+
row = {
|
|
87
|
+
"id" => first(msg, "id", "time"),
|
|
88
|
+
"from" => first(msg, "from", "sender"),
|
|
89
|
+
"to" => addr,
|
|
90
|
+
"subject" => first(msg, "subject", "title"),
|
|
91
|
+
"text" => first(msg, "text", "content"),
|
|
92
|
+
"html" => first(msg, "html", "content"),
|
|
93
|
+
"date" => first(msg, "date", "time")
|
|
94
|
+
}
|
|
95
|
+
Normalize.normalize_email(row, addr)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|